rapscallion icon indicating copy to clipboard operation
rapscallion copied to clipboard

Implement data-fetching for server rendering

Open mhaagens opened this issue 7 years ago • 19 comments

This is more of a question, but would it be possible to implement something like this into Rapscallion? https://github.com/rexhome7326/sync-will-mount

Tested it and it works, lets you await a promise in ComponentWillMount() before continuing render. With React Router 4's new API data fetching is a little more cumbersome, so a lot of people are looking for simpler solutions to server side data fetching.

mhaagens avatar Mar 02 '17 09:03 mhaagens

Something like this (in traverse.js)? Just tried it out and it seems to work. Lifted the code from sync-will-mount, can probably change it to use async/await etc...

function evalComponent (seq, node, context) {
  const componentContext = getContext(node.type, context);

  if (!(node.type.prototype && node.type.prototype.isReactComponent)) {
    const instance = node.type(node.props, componentContext);
    const childContext = getChildContext(node.type, instance, context);
    traverse(seq, instance, childContext);
    return;
  }

  // eslint-disable-next-line new-cap
  const instance = new node.type(node.props, componentContext);

  let res = null;
  let promise = null;

  if (typeof instance.componentWillMount === "function") {
    instance.setState = syncSetState;
    res = instance.componentWillMount();
  }

  if (res && res.then) {
    promise = res
  }

  let done = false;

  if(promise){
    promise.then(() => {done = true;});
  } else {
    done = true;
  }
  
  require('deasync').loopWhile(function(){return !done;});

  if (done) {
    const childContext = getChildContext(node.type, instance, context);
    traverse(seq, instance.render(), childContext);
  }

}

mhaagens avatar Mar 02 '17 10:03 mhaagens

Feels like this would be a good candidate to explore optional Rapscallion plugins/middleware. It's a very workflow specific thing to have to have in the main library.

abritinthebay avatar Mar 15 '17 00:03 abritinthebay

It is non-standard but there is definitely general demand for a feature like this. For example, the apollo client currently traverses a react tree before every render to resolve data dependencies (if that could be done during SSR it would introduce a great performance boost to many graphql apps). Other libraries have introduced functionality to solve the same issue.

I think the approach that is really emerging as a convention within the community is to extend the component contract with getInitialProps. Both next.js (https://github.com/zeit/next.js/#fetching-data-and-component-lifecycle) and inferno (https://infernojs.org/docs/api/inferno-server) are doing this.

@stubailo @jbaxleyiii might be able to offer some thoughts on this from the perspective of a library author.

djgrant avatar Mar 15 '17 09:03 djgrant

Thanks for pointing me to getInitialProps @djgrant! That's exactly what I was looking for. I'm adding this feature request into the queue.

divmain avatar Mar 15 '17 10:03 divmain

Oh don't get me wrong - it's a cool feature. Was just questioning if it should be a core feature. Feels very... extend-y. That's usually the domain of plugins, especially when it's not core to the features of a project.

Of course that depends on project goals/architecture choices.

abritinthebay avatar Mar 15 '17 20:03 abritinthebay

@abritinthebay I'm open to exposing a generic plugin interface. My concern with that, however, is that it will be difficult to implement in a way that does not hurt performance. If you have thoughts on how to accomplish that, I'd be eager to hear them. In any event, the implementation will be as non-specific as we can manage without hurting perf.

divmain avatar Mar 15 '17 21:03 divmain

True, though I'd argue the same is probably in this case - it's all going to have hot-path impact no matter what.

abritinthebay avatar Mar 15 '17 21:03 abritinthebay

Yeah I think our approach is something similar to getInitialProps, but you can put it anywhere in the tree. I think it's definitely desirable that SSR is possible without requiring a static mapping of URL -> data fetch.

stubailo avatar Mar 16 '17 03:03 stubailo

@divmain @abritinthebay How about allowing a visitor function (something similar to https://github.com/ctrlplusb/react-tree-walker) to be attached to the renderer?

render(<App />)
  .attachVisitor(function visitor(element, instance, context) {
    if (instance.getInitialProps) {
      // returning a promise pauses traversal until promise resolves
      return instance.getInitialProps().then(initialProps => {
         instance.props = initialProps
      })
    }
  })
  .toPromise()

That would provide a flexible generic interface, and common visitors such as getInitialPropsVisitor could be packaged for developer convenience.

cc. @ctrlplusb

djgrant avatar Mar 16 '17 21:03 djgrant

That seems a reasonable approach. I'll look into whether this approach adds any performance overhead. It should be possible to add this without impacting perf for those not using the feature...

divmain avatar Mar 17 '17 02:03 divmain

Hey all!

My promise-based API is now officially released for react-tree-walker. Seems to work well, but like @divmain states, it would be interesting to do some performance overhead testing on this.

I also released react-async-bootstrapper which is an abstraction on top of react-tree-walker and essentially provides what you guys are after, except it looks for a member called asyncBootstrap rather than getInitialProps.

I am using this with great success within react-async-component, which has been applied to the next branch of my server side rendering starter kit, react-universally.

Sorry, that is a mess of links. 😀

ctrlplusb avatar Apr 04 '17 10:04 ctrlplusb

Maybe my own attempt at solving this issue can be helpful here: https://github.com/elierotenberg/react-prepare

Instead of hacking the React lifecycle functions, I use a specially marked higher-order component, which is detected during the recursive traversal of the tree. I think separating the preparation step from the rendering step is usually a good idea, and I could implement a stream-friendly API so that it works nice with raspscallion.

elierotenberg avatar Jul 26 '17 10:07 elierotenberg

Also, https://github.com/elierotenberg/react-traverse performs deep components output rewriting, using higher order magic.

Sorry for the plugs, but I think this can shed a light on various approaches to tackle this problem.

elierotenberg avatar Jul 26 '17 10:07 elierotenberg

Nice! Thanks for sharing @elierotenberg.

I think separating the preparation step from the rendering step is usually a good idea

This means traversing the React tree twice. In my experiments that was always slower than resolving async work while rendering.

If we go ahead with a plugin approach (e.g. https://github.com/FormidableLabs/rapscallion/issues/51#issuecomment-287202896) then you could write a plugin that conforms to your projects' APIs.

djgrant avatar Jul 26 '17 13:07 djgrant

Will fiber land and change everything?

ctrlplusb avatar Jul 26 '17 20:07 ctrlplusb

https://github.com/facebook/react/issues/10294#issuecomment-318184274

🤤

ctrlplusb avatar Jul 26 '17 21:07 ctrlplusb

To be clear “asynchronous” Fiber features are not related to server side rendering. They’re related to client-side updates. The new server side renderer is not even using Fiber, and is written as standalone renderer with a simple while loop.

gaearon avatar Jul 26 '17 22:07 gaearon

@gaearon Thanks for the clarification! Is a server side renderer that is capable of doing async work during rendering likely to fall on the React roadmap in the future?

djgrant avatar Jul 26 '17 22:07 djgrant

It might. @sebmarkbage was looking into this.

gaearon avatar Jul 26 '17 23:07 gaearon