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

Refactor ssr #313

Merged
merged 3 commits into from
Nov 13, 2016
Merged

Refactor ssr #313

merged 3 commits into from
Nov 13, 2016

Conversation

tmeasday
Copy link
Contributor

@tmeasday tmeasday commented Nov 7, 2016

Refactored SSR to use a slightly more principled walkTree method.

Tests pass, some points of interest:

  1. I got rid of returning the context from the SSR functions. This seemed kind of undefined (what was it? The lowest context we saw in the tree? Should we have been just picking the first ApolloProvider child context we saw?). All in all it seemed a bit hacky, and the user will have access to an ApolloClient anyway, so can't they just get the store themselves?
  • this will require a docs change.
  1. I am still assigning instance.setState. This seems to not throw an exception. How does this fit your understanding @rewop? I was surprised by this but we need to do something like this to allow people to call setState in componentWillMount (or pursue a different method of rendering[1]).

  2. @jbaxleyiii I wonder if rather than passing { element, context } around (and calling element.type.fetchData, a static method), we could not just pass the instance around, and call instance.fetchData? It would make the code a little simpler in server.js, but also it would make graphql.js a bit simpler too. Is there a reason to not instanciate the element on the server before running the query?

[1] I feel like ultimately we should be trying to use React's rendering infrastructure, as although this is no doubt quicker, it feels really brittle and wouldn't be surprised if there's lots of things you can do that break it.

@tmeasday
Copy link
Contributor Author

tmeasday commented Nov 7, 2016

I believe this will fix #237 and #250

@rattrayalex
Copy link

Is there a reason to not instantiate the element on the server before running the query?

I'm just a bystander with little context, but I imagine performance would be one reason – especially if users defined any heavy constructor methods, but even with standard object/class instantiation, that could add up with an entire react component tree.

@tmeasday
Copy link
Contributor Author

tmeasday commented Nov 7, 2016

I'm just a bystander with little context, but I imagine performance would be one reason – especially if users defined any heavy constructor methods, but even with standard object/class instantiation, that could add up with an entire react component tree.

Sure, but we have to create the instances at some point to render the subtree. Also in this case we are talking about graphql instances anyway, and we know a lot about their behaviour!

// (we can't do the default React thing as we aren't mounted "properly"
// however, we don't need to re-render as well only support setState in
// componentWillMount, which happens *before* render).
instance.setState = (newState) => {

Choose a reason for hiding this comment

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

Asking out of curiosity, why is it necessary to call setState? I don't really see any relevant usages of setState within the react-apollo codebase

Feel free not to answer if it's complicated 😄

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We don't call set state, but it's totally possible that any component we are "rendering" will rely on calling in it componentWillMount. I think there were some issues about this (and that's why there's a test for it).

Choose a reason for hiding this comment

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

Ahhh, I see. That makes sense, I believe.

I imagine another option might be to run Component.flushState() or whatever it's called, just before Component.componentWillMount() ? But I don't necessarily see that as more elegant.

Again, thanks for taking the time to explain!


// Tell walkTree to not recurse inside this component; we will
// wait for the query to execute before attempting it.
return false;

Choose a reason for hiding this comment

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

Again, asking out of curiosity, no need to respond (I just find this to be a really interesting project):

Won't this prevent queries on child components from executing?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

True, but it will eventually get rendered when the query is ready. The assumption is that you have something like

@graphql(...)
const X = ({ loading }) => {
  if (loading) {
    // A. there's no nested calls to `graphql` inside this render tree
    return <Loading/>;
  } else {
    // B. there could be anything in here
  }
}

We don't bother rendering the A. subtree as we are going to await the query setup by graphql and then render X with the results (and loading: false). There are probably pathological cases you can think of where this is inefficient, but I think it seems a reasonable assumption and is "eventually consistent" in any case.

Choose a reason for hiding this comment

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

Hmm, is Component.fetchData recursive then? What if the B subtree has a @graphql(...) of its own?

Thanks very much for taking the time to explain!

Copy link
Contributor Author

@tmeasday tmeasday Nov 7, 2016

Choose a reason for hiding this comment

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

getQueriesFromTree is recursive, so B's @graphqls will get picked up after X's query is loaded.

We have to wait for X's query though, as any sub-queries may rely on the results.

Thanks very much for taking the time to explain!

No problem! Happy to talk about it while I still remember ;)

Choose a reason for hiding this comment

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

Ahhh, of course. Somehow I missed the fact that there are two separate recursions happening: one to find all top-level graphql nodes, and one to find/execute their child queries once the first have completed... although, of course, you had clearly stated that.

We have to wait for X's query though, as any sub-queries may rely on the results.

Of course. Presumably in the future some introspection could check whether this is true, and batch the queries together if not.

Hah, well I'm glad to get your thoughts on the public record while we can then! 😄

let fieldsToNotShip = ['minimizedQuery', 'minimizedQueryString'];
for (let field of fieldsToNotShip) delete apolloState.queries[queryId][field];
}
.then(() => ReactDOM.renderToString(component));

Choose a reason for hiding this comment

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

Wow, what a massive cleanup!

Are you planning to keep initialState as a return value?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, see point 1. in the original PR comment; it was a bit of a hack IMO

Choose a reason for hiding this comment

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

Ahh, I see...

the user will have access to an ApolloClient anyway, so can't they just get the store themselves

Yes, that sounds much better anyway 😄

@rattrayalex
Copy link

This PR seems like a major step forward in the simplicity and readability of this functionality!

@jbaxleyiii
Copy link
Contributor

@tmeasday awesome work! I'm going to try and review this today!

@rewop
Copy link

rewop commented Nov 12, 2016

@tmeasday sorry I was away on holiday. This PR is great indeed. As for my issue the problems was setting props and context to the instance of the component because the properties could not be modified. setState was not an issue for me.

So this should fix my scenario.

@rewop
Copy link

rewop commented Nov 12, 2016

I feel like ultimately we should be trying to use React's rendering infrastructure, as although this is no doubt quicker, it feels really brittle and wouldn't be surprised if there's lots of things you can do that break it.

Totally agree with this.

@rattrayalex
Copy link

I feel like ultimately we should be trying to use React's rendering infrastructure, as although this is no doubt quicker, it feels really brittle and wouldn't be surprised if there's lots of things you can do that break it.

Speaking as a (potential) user of this library, perf is likely to be a top consideration in whether to adopt Apollo for SSR. I don't know very much about the tradeoffs at hand, so I wouldn't take my "opinion" on the matter too seriously. But If tests can be written to combat the brittleness and detect breakages, that'd be much preferred to a slower-but-more-"correct" approach.

Writing some benchmarks might be helpful in making that call as well (though again, I don't have the context necessary to tell how helpful that'd be).

@jbaxleyiii
Copy link
Contributor

@tmeasday this looks awesome. I'm great to merge it if you are! (Feel free to merge and release if you want!)

@tmeasday
Copy link
Contributor Author

I think it's a step forward in any case, so I am going to merge it. I guess we'll hear about problems as it goes live ;)

@tmeasday tmeasday merged commit 8cb978b into master Nov 13, 2016
@tmeasday
Copy link
Contributor Author

@jbaxleyiii I wonder what your thoughts are on

I got rid of returning the context from the SSR functions. This seemed kind of undefined (what was it? The lowest context we saw in the tree? Should we have been just picking the first ApolloProvider child context we saw?). All in all it seemed a bit hacky, and the user will have access to an ApolloClient anyway, so can't they just get the store themselves?

@jbaxleyiii
Copy link
Contributor

@tmeasday it was a hack 👍 I think its great

@jbaxleyiii
Copy link
Contributor

Also, thank you SO much for the fantastic work with the new SSR renderer!

@tmeasday
Copy link
Contributor Author

Ok, I'll update the docs etc to reflect the change.

@calebmer calebmer deleted the refactor-ssr branch March 1, 2017 18:28
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.

4 participants