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.
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.
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:
v0.13
or v0.14
For browsers that don't natively support Promises, use ES6 Promises.
react-resolver
$ npm install --save react-resolver
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) { ... });
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)
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"));
});
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!
If you'd like to contribute to this project, all you need to do is clone this project and run:
$ npm install
$ npm test
If you have questions or issues, please open an issue!