Skip to content
This repository has been archived by the owner on Apr 13, 2023. It is now read-only.

Integrated SSR #83

Merged
merged 12 commits into from
Jul 12, 2016
Merged

Integrated SSR #83

merged 12 commits into from
Jul 12, 2016

Conversation

jbaxleyiii
Copy link
Contributor

@jbaxleyiii 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
@Vanuan
Copy link
Contributor

Vanuan commented Jun 27, 2016

ssr: true, // block during SSR render

What's this and why it's needed?

@jbaxleyiii
Copy link
Contributor Author

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

@Vanuan
Copy link
Contributor

Vanuan commented Jun 27, 2016

What's getData for then?

@Vanuan
Copy link
Contributor

Vanuan commented Jun 27, 2016

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

@Vanuan
Copy link
Contributor

Vanuan commented Jun 27, 2016

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

@Vanuan
Copy link
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);
Copy link
Contributor

@Vanuan Vanuan Jun 27, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be much more flexible if getQueriesFromTree is exported

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@jbaxleyiii
Copy link
Contributor Author

@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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this how react does it internally?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jbaxleyiii
Copy link
Contributor Author

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.

@gauravtiwari
Copy link

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

@jbaxleyiii
Copy link
Contributor Author

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

@gauravtiwari
Copy link

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
Copy link
Contributor

Hey @jbaxleyiii -- worked great for me in this commit: https://github.com/apollostack/saturn/commit/9943e3b72e625bf70efc76f594ca4f0de8c4bef9 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
Copy link

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
Copy link
Contributor Author

@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
Copy link
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
Copy link
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
Copy link
Contributor Author

@tmeasday this is awesome! I can fix those!

@gauravtiwari
Copy link

@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));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@markudevelop
Copy link

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

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants