React Resolver https://img.shields.io/npm/v/react-resolver.svg

React Resolver https://img.shields.io/npm/v/react-resolver.svg

Async-rendering & data-fetching for universal React applications.

React Resolver lets you define data requirements per-component and will handle the nested, async rendering on both the server & client for you.


Why?

React's rendering is synchronous by design. As a result, rendering on the server is left as an exercise for the rest of the community to figure out.

There are some cludgy solutions for this, most of which require having fat handlers components controllers at the top of your application that are responsible for marshalling data for all components under them.

For a non-trivial application, this means mixing concerns between your specialized components and the controllers, which is conceptually difficult and programmatically annoying to maintain.

How?

The simplest example is a pure, client-side only application. Afterwards, we can change a few lines to turn this into a universal application.

If you're the visual type, view the examples:

Dependencies

  • React v0.13 or v0.14

For browsers that don't natively support Promises, use ES6 Promises.

1. Install react-resolver

$ npm install --save react-resolver

2. Decorate Components with Data

Suppose you have the following Stargazer component to render a Github user's profile:

export default class Stargazer extends React.Component {
  static propTypes = {
    user: React.PropTypes.object.isRequired,
  }

  render() {
    /* Render profile from this.props.user */
  }
}

In 2014, you would use componentWillMount to fire off an AJAX request for the profile, then use setState to trigger a re-render with the data.

This won't work on the server-side, and it's annoying to test.

According to most universal boilerplates, we'd put static fetchData() function in our component for a middleware or library to handle, rendering the component when data comes back.

This only works for fat controllers at the top of your application, usually defined by a React Router <Route>.

Instead, let's decorate it:

import { resolve } from "react-resolver";

// Assuming this _is_ from <Route> component matching `/users/ericclemmons`
@resolve("user", function(props) {
  return axios
    .get(`https://api.github.com/users/${props.params.user}`)
    .then((response) => response.data)
  ;
})
export default class Stargazer extends React.Component {

Or, if ES7 decorators aren't your bag:

class Stargazer extends React.Component {
  ...
}

export default resolve(Stargazer)("user", function(props) { ... });

3. Render on the Client

Again, if we're only rendering on the client, we can render like normal:

import React from "react";

Router.run(routes, location, (Root) => {
  React.render(<Root />, document.getElementById("app"));
});

The End. (Unless you want to see how to build a universal application)

4. Resolve on the Client

React Resolver handles bootstrapping server-rendered markup via Resolver.render:

import { Resolver } from "react-resolver";

Router.run(routes, location, (Root) => {
  // To preserver context, you have to pass a render function!
  Resolver.render(() => <Root />, document.getElementById("app"));
});

5. Resolve on the Server

The server will look very familiar to the client-side version. The difference being, Resolver.resolve will async render the application, fetch all data, & return a <Resolved /> component ready for React.render, as well as the data needed to bootstrap the client:

import { Resolver } from "react-resolver";

Router.create({ location, routes }).run((Handler, state) => {
  Resolver
    .resolve(() => <Handler {...state} />) // Pass a render function for context!
    .then(({ Resolved, data }) => {
      res.send(`
        <!DOCTYPE html>
        <html>
          <body>
            <div id="app">${React.render(<Resolved />)}</div>

            <script src="/client.min.js" async defer></script>
            <script>
              window.__REACT_RESOLVER_PAYLOAD__ = ${JSON.stringify(data)}
            </script>
          </body>
        </html>
      `)
    })
    .catch((error) => res.status(500).send(error)) // Just in case!
  ;
});

Enjoy writing universal apps with clarity around data requirements!

Development

If you'd like to contribute to this project, all you need to do is clone this project and run:

$ npm install
$ npm test

Authors

License

Collaboration

If you have questions or issues, please open an issue!