Our goals on server are:
ReactDOM.renderToString())The main problem here is that, if we have several connected 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.
@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:
Static fetchData on router level component (how we are currently doing it)
// 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:
connect functionRouter 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
Being 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 blockRender: true or something like that applied to connect would be helpful.
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 initialState to the apollo-client before it starts up.
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 renderToString would be called on a tree that can sync get all of the data because it has already been requested. It would inject a <script apollo-data>/* base64 encoded data */</script> into the html that the ApolloProvider would read when it is running createClient on the client side.
alternatively,
createClientcould take a promise so async cache lookup would be easier
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.
For ex: on server or client if, data is already provided through some kinda of prop.
<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 PostClient on client as it's globally set and use it on client side mounting of the component. Props could be accessed, for example on rails: https://github.com/reactjs/react-rails/blob/master/lib/react/rails/component_mount.rb#L25
or in express:
https://github.com/mjackson/react-stdio/blob/master/server.js#L33
@jbaxleyiii Another example, here a redux store is exposed to window or globalobject as available, https://github.com/shakacode/react_on_rails/blob/master/node_package/src/StoreRegistry.js and then used on both client and server. Same could be done with instantiated client object.
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:
This 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>
....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!
Most helpful comment
regarding serialization of data, why not to simply use:
I believe everybody uses this way of serialization when it comes to passing data from server to a client in a markup.