New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integrated SSR #83

Merged
merged 12 commits into from Jul 12, 2016

Conversation

Projects
None yet
6 participants
@jbaxleyiii
Member

jbaxleyiii commented Jun 24, 2016

active work on #54

// no changes to client :tada:

// server application code (integrated usage)
import { renderToStringWithData } from "react-apollo/server"

// during request
const markup = await renderToStringWithData(app)


// server application code (custom usage)
import { getDataFromTree } from "react-apollo/server"

// during request
getDataFromTree(app, ).then(({ initialState, store, client }) => {
  // markup with data from requests
  const markup = ReactDOM.renderToString(app);
});

The client does have the option to ignore SSR for particular queries

const WrappedElement = connect({
  mapQueriesToProps: () => ({
    data: { query, ssr: false }, // wont block the SSR
  })
})(Element);

getDataFromTree

The getDataFromTree method takes your react tree and returns an object with initialState, the apollo client (as client) and the redux store as store.
initialState is the hydrated data of your redux store prior to app rendering. Either initialState or store.getState() can be used for server side rehydration.

renderToStringWithData

The renderToStringWithData takes your react tree and returns a promise that resolves to your stringified tree with all data requirements. It also injects a script tag that includes window. __APOLLO_STATE__ which equals the full redux store for hyrdration.

Server notes:

When creating the client on the server, it is best to use ssrMode: true. This prevents unneeded force refetching in the tree walking.

Client notes:

When creating new client, you can pass initialState: __APOLLO_STATE__ to rehydrate which will stop the client from trying to requery data.

@stubailo @tmeasday this needs more tests, but I thinks its pretty close

I'd like to experiment with adding a streaming API so you can stream the html to the client. But I'll probably do this after the API refactor

@jbaxleyiii jbaxleyiii self-assigned this Jun 24, 2016

@zol zol added the in progress label Jun 24, 2016

@Vanuan

This comment has been minimized.

Contributor

Vanuan commented Jun 27, 2016

ssr: true, // block during SSR render

What's this and why it's needed?

@jbaxleyiii

This comment has been minimized.

Member

jbaxleyiii commented Jun 27, 2016

@Vanuan that is an option for each query that tells react-apollo to actually fetch the data during rendering on the server

@Vanuan

This comment has been minimized.

Contributor

Vanuan commented Jun 27, 2016

What's getData for then?

@Vanuan

This comment has been minimized.

Contributor

Vanuan commented Jun 27, 2016

I mean, why can't it be true by default?

@Vanuan

This comment has been minimized.

Contributor

Vanuan commented Jun 27, 2016

So it means that some queries aren't meant to be executed during ssr?

@Vanuan

This comment has been minimized.

Contributor

Vanuan commented Jun 27, 2016

I believe, the most flexible API would be like this:

import {  getQueries, runQueries } from "react-apollo"

let queries = getQueries(app, /* props to be pased as ownProps */);

/*
  filter out queries you don't want on server side
  according to routing or some other logic
*/
queries = ...

runQueries(queries).then((data) => {
  // in redux you dispatch query action
  // store.dispatch(querySucceeded({data}));
  // or update the store somehow else
  // fetched data is saved in the store 
  // in plain React, you'd just provide data to properties
  // or use Apollo's store
  const markup = ReactDOM.renderToString(app);
  // Provider provides stored state to connected components during rendering
}, (err) => {
  // dispatch and render errors
});

Of course, dispatching can be hidden inside apollo, but it would be hard to follow and unclear whether it's dispatched or not.

return queries;
}
const rawQueries = getQueriesFromTree(tree);

This comment has been minimized.

@Vanuan

Vanuan Jun 27, 2016

Contributor

Would be much more flexible if getQueriesFromTree is exported

This comment has been minimized.

@jbaxleyiii

jbaxleyiii Jun 27, 2016

Member

@Vanuan I think we would export it for sure once it is working correctly 👍

This comment has been minimized.

@jbaxleyiii

jbaxleyiii Jun 27, 2016

Member

I think exporting a few options to allow you to build your own rendering process, or just use react-apollo would be best:

i.e:

  1. getQueries, runQueries for a fully custom solution
  2. getData which runs all queries tagged as SSR
  3. a renderWithData that returns back the full SSR payload with the initial state injected into the markup.
}
if (child.props && child.props.children) {
getQueriesFromTree(child.props.children, queries);
}

This comment has been minimized.

@Vanuan

Vanuan Jun 28, 2016

Contributor

Render leaf components here?

} else if (child.type) {
    let leaf = new child.type(child.props, {store, client});
    let newTree = leaf.render();
    getQueriesFromTree(newTree, queries);
}
@jbaxleyiii

This comment has been minimized.

Member

jbaxleyiii commented Jun 28, 2016

@Vanuan this is still very much a WIP

export function getPropsFromChild(child, defaultProps = {}) {
const { props, type } = child;
let ownProps = assign(defaultProps, props);
if (type && type.defaultProps) ownProps = assign(defaultProps, type.defaultProps, props);

This comment has been minimized.

@tmeasday

tmeasday Jun 29, 2016

Contributor

Is this how react does it internally?

key: query,
query: dataRequirements[query],
component: child.type.WrappedComponent,
ownProps,

This comment has been minimized.

@tmeasday

tmeasday Jun 29, 2016

Contributor

Should we be pushing context in here as well so when it renders again after the query is done, it has the correct context?

This comment has been minimized.

@jbaxleyiii

jbaxleyiii Jul 12, 2016

Member

Fixed in the latest code!

if (!Element && typeof child.type === 'function') Element = { type: child.type };
const RenderedComponent = Element && Element.type && new Element.type(ownProps, context);
if (RenderedComponent && RenderedComponent.context) context = RenderedComponent.context;

This comment has been minimized.

@tmeasday

tmeasday Jun 29, 2016

Contributor

Are we overriding the context that's returned from the function? I'm confused about context here. Is it the same thing as getChildContext and co use?

This comment has been minimized.

@jbaxleyiii

jbaxleyiii Jul 12, 2016

Member

Fixed in the latest which pulls context dynamically

@jbaxleyiii

This comment has been minimized.

Member

jbaxleyiii commented Jul 12, 2016

This could use a lot more testing for sure, but I'm going to cut a release so we can start trying it out in some applications. I've added tests for a few tree structures ranging from simple to mildly complex with mixed classes and components.

@jbaxleyiii jbaxleyiii merged commit 2847115 into master Jul 12, 2016

4 checks passed

CLA Author has signed the Meteor CLA.
Details
continuous-integration/travis-ci/pr The Travis CI build passed
Details
continuous-integration/travis-ci/push The Travis CI build passed
Details
coverage/coveralls Coverage increased (+0.8%) to 96.743%
Details
@gauravtiwari

This comment has been minimized.

gauravtiwari commented Jul 12, 2016

@jbaxleyiii Awesome work. I can give it a try 👍

@jbaxleyiii

This comment has been minimized.

Member

jbaxleyiii commented Jul 12, 2016

@gauravtiwari 🎉 thanks so much! I'd use the 0.3.17 version (has a bug fix)

@stubailo stubailo referenced this pull request Jul 12, 2016

Open

Integrate SSR? #1

@gauravtiwari

This comment has been minimized.

gauravtiwari commented Jul 13, 2016

@jbaxleyiii I gave it a try and it seems that I am missing something. Could you a post a minimal setup example for SSR - including component and all (both client and on server) ?

import React, from 'react';
import { connect } from 'react-apollo';

const SomeComponent = (props) => {
    const { data } = props;
    return (
      <div className="nav">
        something to render
      </div>
    );
}

function mapQueriesToProps({ ownProps, state }) {
  return {
    data: `{ someQuery }`
  };
};

const SomeComponentWithData = connect({
  mapQueriesToProps,
})(SomeComponent);

export default SomeComponentWithData;
// Where to pass initial state or props?
 const htmlResult = await renderToStringWithData(SomeComponentWithData);

Is it possible to just use server to hydrate the store on server and then pass it on to client after render? ( Like react default server side rendering. It renders the component on server and then when the client detects the component it binds all component events etc. )

@tmeasday

This comment has been minimized.

Contributor

tmeasday commented Jul 14, 2016

Hey @jbaxleyiii -- worked great for me in this commit: apollographql/saturn@9943e3b for an app which did SSR without queries on the server.

However, for githunt, it just hangs: https://github.com/apollostack/GitHunt/tree/saturn-ssr

I see some output from (I'm guessing) react-apollo:

[0] { shouldForceFetch: false }
[0] -------fetching query { kind: 'Document',
[0]   definitions:
[0]    [ { kind: 'OperationDefinition',
[0]        operation: 'query',
[0]        name: [Object],
[0]        variableDefinitions: [],
[0]        directives: [],
[0]        selectionSet: [Object] } ] }
[0] -------fetching query { kind: 'Document',
[0]   definitions:
[0]    [ { kind: 'OperationDefinition',
[0]        operation: 'query',
[0]        name: [Object],
[0]        variableDefinitions: [Object],
[0]        directives: [],
[0]        selectionSet: [Object] } ] }
@gauravtiwari

This comment has been minimized.

gauravtiwari commented Jul 14, 2016

Hey @jbaxleyiii

So, I tried again and it seems that, for NON-JS environment this won't work (unless the JS environment supports callbacks). The response returned is always empty as the server doesn't wait for callback to be finished.

@jbaxleyiii

This comment has been minimized.

Member

jbaxleyiii commented Jul 14, 2016

@tmeasday I've seen it hang every so often for my stuff locally too. I'll be working on that today / tomorrow but would love some help if you can spare!

@gauravtiwari ah that makes sense! I need to find a way to make the getData sync for a lot of js use cases anyway. Would it work for you then?

@tmeasday

This comment has been minimized.

Contributor

tmeasday commented Jul 14, 2016

@jbaxleyiii I'd love to help although I'm not sure I'll have too much time today. I would say that for githunt it consistently, rather than occassionally hangs. Ping me on slack if I can help though.

@tmeasday

This comment has been minimized.

Contributor

tmeasday commented Jul 15, 2016

@jbaxleyiii Ok, I couldn't help myself. There are two problems that are causing the issue, when I hack around them, it works great!:

  1. If there's a problem in getDataFromTree, the error is silently dropped. We should add a catch handler to the promise here: https://github.com/apollostack/react-apollo/blob/master/src/server.ts#L185

  2. The error comes because this line fails: https://github.com/apollostack/react-apollo/blob/master/src/server.ts#L128

The reason is that (yay, thanks npm!) I end up with 2 copies of apollo-client installed in githunt, as I was npm-linking saturn (so it had its own copy).

So it actually works if you run my saturn-ssr branch of githunt without linking anything (yay!), but I think in general it's risky to check types like that unless you set a peerDependency on apollo-client[1] to ensure you get the "app's version" of apollo-client as a dep.

Oh, and as an aside on deeper calls to getQueriesFromTree you implicitly don't check the type, as you allow the context from the component to override here: https://github.com/apollostack/react-apollo/blob/master/src/server.ts#L123

I'm not sure what the best solution is. One option is to just not check the type :/

[1] In short npm is terrible at helping you with this problem. I could rant for a while about it.

@jbaxleyiii

This comment has been minimized.

Member

jbaxleyiii commented Jul 15, 2016

@tmeasday this is awesome! I can fix those!

@gauravtiwari

This comment has been minimized.

gauravtiwari commented Jul 15, 2016

@jbaxleyiii Yeah, I think so, because rendering react component normally on the server works and returns the response so, as long as the response from react-apollo is synchronous it should work.

let fieldsToNotShip = ['minimizedQuery', 'minimizedQueryString'];
for (let field of fieldsToNotShip) delete initialState[key].queries[queryId][field];
}
initialState = encodeURI(JSON.stringify(initialState));

This comment has been minimized.

@Vanuan

Vanuan Jul 17, 2016

Contributor

I assume encodeURI is wrong? It'll result in
Uncaught SyntaxError: Unexpected token %

@voidale

This comment has been minimized.

voidale commented Sep 25, 2016

Hey guys, anyone had any luck doing this with Meteor?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment