-
Notifications
You must be signed in to change notification settings - Fork 787
Server-side rendering: Create an API for async data fetching #54
Comments
@wizardzloy I'm thinking this will be my focus late this week / next week. I've got a ton of thoughts on it + react-router integration. Just curious, what does your stack look like? React / React Router / Meteor? |
It's react + react-router + react-apollo. So far the code looks like this: const express = require('express');
const React = require('react');
const ReactDomServer = require('react-dom/server');
const { createMemoryHistory, match, RouterContext } = require('react-router');
const ApolloClient = require('apollo-client').default;
const { ApolloProvider } = require('react-apollo');
const App = require('./components/App').default;
const { getRoutes } = require('./routes');
const app = express();
const mockGraphQLNetworkInterface = {
query: () => Promise.resolve({ data: { list: [ 'apple', 'orange' ] }, errors: null })
};
const apolloClient = new ApolloClient({
networkInterface: mockGraphQLNetworkInterface
});
// Server-Side-Rendering
app.use((req, res) => {
const history = createMemoryHistory(req.path);
const routes = getRoutes();
// Find the correct route
match({ history, routes, location: req.url }, (err, redirectLocation, renderProps) => {
if (err) {
return res.status(500).send(err.message);
}
if (redirectLocation) {
return res.redirect(302, redirectLocation.pathname + redirectLocation.search);
}
if (!renderProps) {
return res.status(404).send('Not found');
}
const app = (
<ApolloProvider client={apolloClient} >
<RouterContext {...renderProps}/>
</ApolloProvider>
);
const html = ReactDomServer.renderToString(app);
res.send(html);
});
});
app.listen(8484, 'localhost', (err) => {
if (err) {
console.log(err);
return;
}
console.log('listening on http://127.0.0.1:8484')
}); @jbaxleyiii Could you, please, share your ideas regarding this topic? |
Okay this is going to be a brain dump: Possible SSR / rehydration methodsStatic // component
import { Component } from 'react';
import { connect } from 'react-apollo';
class Category extends Component {
static fetchData = (props, location) => {
return props.query({
query: gql`
query getCategory($categoryId: Int!) {
category(id: $categoryId) {
name
color
}
}
`,
variables: {
categoryId: 5,
},
});
}
render() {
// markup
}
}
function mapQueriesToProps({ ownProps, state }) {
return {
category: {
query: gql`
query getCategory($categoryId: Int!) {
category(id: $categoryId) {
name
color
}
}
`,
variables: {
categoryId: 5,
},
forceFetch: false,
returnPartialData: true,
},
};
};
const CategoryWithData = connect({
mapQueriesToProps,
})(Category);
// from router tree
<Route path="categories" component={CategoryWithData}/>
// our router addon which populates the store with data
// this is called during render, but before rendering the app to a string
function fetchComponentData(renderProps, reduxStore) {
const componentsWithFetch = renderProps.components
// Weed out 'undefined' routes.
.filter(component => !!component)
// Only look at components with a static fetchData() method
.filter(component => component.fetchData);
if (!componentsWithFetch.length) {
return;
}
// Call the fetchData() methods, which lets the component dispatch possibly
// asynchronous actions, and collect the promises.
const promises = componentsWithFetch
.map(component => component.fetchData(reduxStore.getState, reduxStore.dispatch, renderProps));
// Wait until all promises have been resolved.
Promise.awaitAll(promises);
} Issues with above:
Router based queries (from apollostack/react-router-apollo draft) import React from 'react';
import { render } from 'react-dom';
import { Link, browserHistory } from 'react-router';
import { Router, Route, applyRouterMiddleware } from 'react-router';
import apolloMiddleware from 'react-router-apollo';
import ApolloClient from 'apollo-client';
const client = new ApolloClient();
const App = React.createClass({/*...*/})
const About = React.createClass({/*...*/})
// etc.
const Users = React.createClass({
render() {
return (
<div>
<h1>Users</h1>
<div className="master">
<ul>
{/* use Link to route around the app */}
{this.state.users.map(user => (
<li key={user.id}><Link to={`/user/${user.id}`}>{user.name}</Link></li>
))}
</ul>
</div>
<div className="detail">
{this.props.children}
</div>
</div>
)
}
})
const User = React.createClass({
render() {
return (
<div>
<h2>{this.props.currentUser.user.name}</h2>
{/* etc. */}
</div>
)
}
})
const userQueries = ({ location, params }) => ({
currentUser: {
query: `
query getUserData ($id: ID!) {
user(id: $id) {
emails {
address
verified
}
name
}
}
`,
variables: {
id: params.userId,
},
}
})
const userMutations = ({ location, params }) => ({
addProfileView: (date) => {
mutation: `
mutation addProfileView($route: String!, $id: ID!, date: $String!) {
addProfileView(route: $route, id: $id, date: $date)
}
`,
variables: {
route: location.pathname,
id: params.userId,
date,
},
},
});
// Declarative route configuration (could also load this config lazily
// instead, all you really need is a single root route, you don't need to
// colocate the entire config).
render((
<Router
history={browserHistory}
client={client}
render={applyRouterMiddleware(apolloMiddleware)}
>
<Route path="/" component={App}>
<Route path="about" component={About}/>
<Route path="users" component={Users}>
<Route
path="/user/:userId"
component={User}
queries={userQueries}
mutations={userMutations}
onEnter={function(nextState, replace, callback){
const now = new Date();
this.mutations.addProfileView(now);
}}
/>
</Route>
<Route path="*" component={NoMatch}/>
</Route>
</Router>
), document.body) Issues with above:
Tree analyzation + react-router integration import { Component } from 'react';
import { connect } from 'react-apollo';
class Category extends Component {
render() {
// markup
}
}
function mapQueriesToProps({ ownProps, state }) {
return {
category: {
query: gql`
query getCategory($categoryId: Int!) {
category(id: $categoryId) {
name
color
}
}
`,
variables: {
categoryId: 5,
},
forceFetch: false,
returnPartialData: true,
ssr: true // tell the client to block during SSR
},
};
};
const CategoryWithData = connect({
mapQueriesToProps,
})(Category);
// router
import { apolloSSRMiddleware } from "react-router-apollo";
render((
<Router
history={browserHistory}
client={client}
render={applyRouterMiddleware(apolloSSRMiddleware)}
>
<Route path="/" component={App}>
<Route path="about" component={About}/>
<Route path="users" component={Users}>
<Route path="/user/:userId" component={User} />
</Route>
<Route path="*" component={NoMatch}/>
</Route>
</Router>
), document.body) Issues with above:
Best of all worlds could be combining tree analyzation with the react-router middleware Other thoughtsBeing able to combine queries into one big request would be awesome. Some data in the tree may require props from parent data from queries. If this is the case, we would need to expose a way to tell the tree to get all of the data from this query before working your way down the tree further. Something along the lines of We will also need to ship a way to add the data as a string (we base64 encode it currently) to a script tag, then parse it on the client side before starting the client. This ships all of the redux store over to the client. I've already added a way to pass Regardless of how we do this, it will have to be a client and server supported system. Since the apollo-client is passed into the react tree, we either have to provide a non-react tree based way of doing it, OR do something like this: // application code
import { ApolloProvider, getData } from "react-apollo"
import ApolloClient from "apollo-client"
const app = (
<ApolloProvider
createClient={(initialState) => {
return new ApolloClient({
initialState,
})
}}
>
<App />
</ApolloProvider>
)
// server code
html = ReactDOMServer.renderToString(getData(app));
// alternatively, if a user just wants SSH (server side hydration)
const data = getData(app);
// add to initial markup somehow
<script apollo-data>/* base64 encoded data */</script>
// Ideally `getData` would take an argument that would allow you to specify the serialization of your data
// then within ApolloProvider on the client you could de-serialization how you want If we did the above, on the server the initialState would be empty (you could do a sync request to pull an initial state from cache too) and the client would be generated and the store executed before the render. Then
SO, there are quite a few ways we can do it. I'm sure there are more / better ways than even what I have listed. My overall goal though is this:
Thoughts? cc @wizardzloy @stubailo @johnthepink @helfer @gauravtiwari |
regarding serialization of data, why not to simply use: // server
window.__APOLLO_STATE__ = JSON.serialize(store.getState())
// client
const client = new ApolloClient({
initialState: window.__APOLLO_STATE__
}) I believe everybody uses this way of serialization when it comes to passing data from server to a client in a markup. |
We can probably make that the default. We obfuscate our data for SEO reasons so having that option is nice |
@jbaxleyiii, I guess colocation of data is very important, because that would make the client pluggable with almost all front-end frameworks or libraries and it will also help in caching. One of thing that's quite unique with apollo client is that it can work with any framework or library, which is very important and USP. @jbaxleyiii you already mentioned all points that are necessary to make SSR painless. If we make the data store fully interoperable and independent then it would be easy to transact with store from any framework or library, in the end it's all Javascript. How about we just set the result object straightaway with props? Because, props are already available during runtime, on server. Instead of passing JSON blobs, how about we share the instance of apollo client on server and client and then set the store accordingly.
<div data-props='{"key": "value"}'></div> and we have access to this prop on client or server. // somewhere on server to be globally available or on client
const PostClient = new ApolloClient({ initialState: props }); //just set the data to result object
or
PostClient.setInitialState(props)
window.PostComponent = PostComponent; // some react component
window.PostClient = PostClient; // globally available
window.serverRenderComponent({ component: PostComponent, props: props, client: PostClient }); and then during rendering: import ReactDOMServer from 'react-dom/server';
import createReactElement from './createReactElement';
export default function serverRenderComponent(options) {
const { component, props, client } = options;
const reactElement = createReactElement(component, props);
htmlResult = ReactDOMServer.renderToStaticMarkup(
<ApolloProvider client={PostClient} children={reactElement} />
);
return htmlResult;
} And then we can access same or in express: https://github.com/mjackson/react-stdio/blob/master/server.js#L33 |
@jbaxleyiii Another example, here a redux store is exposed to https://github.com/shakacode/react_on_rails/blob/master/spec/dummy/client/app/startup/serverRegistration.jsx#L40 (registration to window object) |
Okay so it sounds like we can tackle this in multiple steps:
Per Component PrefillingThis should be pretty simple overall because it is primarily a client side action. Given a prop (detailed below), react-apollo's connect should dispatch that data to the store on mount. It can pass the data through to the child component as well so both the client and the store have the correct data when the app is instansiated. // given query object
const mapQueriesToProps = () => ({
category: {
query: gql`
query getCategory($categoryId: Int!) {
category(id: $categoryId) {
name
color
}
}
`,
variables: {
categoryId: 5,
},
ssr: true // tell the client to block during SSR
},
})
// passed props
const data = {
category: {
category: {
name: "sample",
color: "blue"
}
}
} Props as stored in the DOM <div data-ssr-props='{ "category": { "category": { "name": "sample", "color": "blue" } } }'></div> Minimial SSR API....still in development / thought |
https://github.com/iamdustan/tiny-react-renderer a nice explanation of how to start building your own custom React renderer. In case, we would need to create one for SSR async query resolving |
First round of this is shipped in #83. If anyone can try this in an app and file bugs that would be great! |
Our goals on server are:
ReactDOM.renderToString()
)The main problem here is that, if we have several
connect
ed components in a tree, we have to use some sort of waterfall fetching - child component cannot fetch its data until all its parents fetch their queries because there might be different components rendered depending on the props received from the queries. That means, we cannot statically analyze the entire tree in advance and fetch all data in one run, which could negatively affect performance.Need to investigate how
relay
addresses the same issue and maybe come up with our own solution here.The text was updated successfully, but these errors were encountered: