Skip to content
This repository has been archived by the owner on Oct 31, 2024. It is now read-only.

SSR + code-splitting #3

Closed
kukagg opened this issue Mar 31, 2017 · 8 comments
Closed

SSR + code-splitting #3

kukagg opened this issue Mar 31, 2017 · 8 comments

Comments

@kukagg
Copy link

kukagg commented Mar 31, 2017

Hey @leebenson!

Nice starter kit! I have a similar setup with SSR based on react-router@3 and everything works fine. I aim to refactor in the near future and this setup looks great for that. Wondering what your thoughts are on SSR + code-splitting with v4?

@leebenson
Copy link
Owner

leebenson commented Mar 31, 2017

Thanks @kuka.

Code-splitting on the server should actually work out-the-box.

Because we're using Webpack 2 to generate bundles on both the client and the server, any strategy that uses require.ensure() (or import()-based Promises - I'm pushing an update for that this morning) should resolve well on the server, too. The reason I opted for server-side bundling is exactly for that reason - it should be possible (for the most part) to write code in one place, and know that it'll work on the server, too. Same with .css, .json, inline image loading, etc.

The one thing that's currently missing is a built-in HOC for returning a 'loading' component whilst split code hasn't yet returned back from its round-trip, and a recommended pattern in the docs for doing the actual split.

In the meantime, I whipped together this very quick example to demonstrate it working in practice:

Let's say you have this:

src/test.js

export default 'code-splitting for the win!';

And then inside src/app.js, we modify the <Message> component to grab the code-split message, instead of the current GraphQL:

class Message extends React.PureComponent {
  constructor() {
    super();
    this.state = {
      message: '',
    };
  }

  componentDidMount() {
    import('./test').then(mod => {
      this.setState({
        message: mod.default,
      });
    });
  }

  render() {
    return (
      <div>
        <h2>Message from code-splitting: <em>{this.state.message}</em></h2>
      </div>
    );
  }
}

Running npm start will yield the expected result:

screen shot 2017-03-31 at 08 14 56

And you can see the network request that grabbed this asynchronously:

network

Now, the neat this is that you also get this on the server, too - running npm run build-run will confirm:

Browser files generated

Hash: c09946598a0df5285bba2bd58c399409d8f2a767
Version: webpack 2.3.2
Child
Hash: c09946598a0df5285bba
Time: 8738ms
Asset Size Chunks Chunk Names
0.js 143 bytes 0 [emitted] <--- this is the async code-split
browser.js 5.98 kB 1 [emitted] browser
vendor.js 344 kB 2 [emitted] [big] vendor
assets/css/style.css 452 bytes 1 [emitted] browser
browser.js.gz 2.32 kB [emitted]
assets/css/style.css.gz 285 bytes [emitted]
vendor.js.gz 99.9 kB [emitted]

And on the server, too

Hash: 2bd58c399409d8f2a767
Time: 1949ms
      Asset       Size  Chunks             Chunk Names
0.server.js  251 bytes       0  [emitted]   <--- automatically loaded by the server!
  server.js    23.5 kB       1  [emitted]  javascript

Of course, in this example, the <Message> component attempts to set its internal 'message' state after the import() Promise resolves. At this point, the server has already turned the React chain into HTML and fired it back to the screen.

Since import() returns a Promise when the code-split data is available, the solution is to use a lib like redial to decorate components with data requirements, and then await the full chain being ready before rendering into HTML. This would work in the same way that Apollo's getDataFromTree does for GraphQL queries.

In a (soon-ish) future version, I'll add those features to the starter kit, and update src/app.js to show examples of code splitting at work, and how you can simulate synchronous loading for a server environment.

@kukagg
Copy link
Author

kukagg commented Apr 1, 2017

Thank you for a concise reply, Lee!

What you’ve written is going to tremendously help in my and others’ situations.

I consider this closed.

@kukagg kukagg closed this as completed Apr 1, 2017
@leebenson leebenson mentioned this issue Apr 7, 2017
13 tasks
@scf4
Copy link

scf4 commented May 28, 2017

Can you split vendor.js based on the routes which require it?

Edit: actually I see it will already do this. But I'm not quite sure how to get code splitting working on server rendering too, since I call import in componentDidMount?

@leebenson
Copy link
Owner

@scf4 - are you able to share the code for what you're trying to do?

Code splitting should work on the server in exactly the same way (i.e. you can use await import('./someModule'); in both environments), but if you have some kind of DOM-only code you want to load in the client only, you can keep that in componentDidMount and it should be split on the client route only.

@scf4
Copy link

scf4 commented May 28, 2017

@leebenson I'm using this example pretty much: https://gist.github.com/acdlite/a68433004f9d6b4cbc83b5cc3990c194

The server still renders everything else, it just doesn't load the async loaded component. So disabling JS means you can't see the async loaded component.

@leebenson
Copy link
Owner

@scf4 - this actually works in production, but not via the new dev server:

test.js

import React from 'react';

export default () => (<h1>Test!</h1>);

app.js

// ...
function asyncComponent(getComponent) {
  return class AsyncComponent extends React.Component {
    static Component = null;
    state = { Component: AsyncComponent.Component };

    componentWillMount() {
      if (!this.state.Component) {
        getComponent().then(Component => {
          AsyncComponent.Component = Component;
          this.setState({ Component });
        });
      }
    }
    render() {
      const { Component } = this.state;
      if (Component) {
        return <Component {...this.props} />;
      }
      return null;
    }
  };
}

const Test = asyncComponent(() =>
  import('./test').then(module => module.default),
);

// Export a simple component that allows clicking on list items to change
// the route, along with a <Route> 'listener' that will conditionally display
// the <Page> component based on the route name
export default () => (
  <div>
    <Helmet
      title="ReactQL application"
      meta={[{
        name: 'description',
        content: 'ReactQL starter kit app',
      }]} />
    <div className={css.hello}>
      <img src={logo} alt="ReactQL" className={css.logo} />
    </div>
    <hr />
    <GraphQLMessage />
    <hr />
    <ul>
      <li><Link to="/">Home</Link></li>
      <li><Link to="/page/about">About</Link></li>
      <li><Link to="/page/contact">Contact</Link></li>
    </ul>
    <hr />
    <Switch>
      <Route exact path="/" component={Home} />
      <Route path="/page/:name" component={Page} />
      <Route path="/test" component={Test} />
      <Route component={NotFound} />
    </Switch>
    <hr />
    <p>Runtime info:</p>
    <Stats />
    <hr />
    <p>Stylesheet examples:</p>
    <Styles />
  </div>
);

// ...

I'm tracking this in reactql/kit#30

@scf4
Copy link

scf4 commented May 28, 2017

@leebenson Thanks Lee, I'll give that asyncComponent function a try now.

Edit: it worked, thank you 😄

@leebenson
Copy link
Owner

cool @scf4 👍

react-async-component looks like a good way to handle code-splitting. I haven't tried it, but it should work with the ReactQL webpack flow.

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

No branches or pull requests

3 participants