|||

Video Transcript

X

Introducing React Refetch

Heroku has years of experience operating our world-class platform, and we have developed many internal tools to operate it along the way; however, with the introduction of Heroku Private Spaces, much of the infrastructure was built from the ground up and we needed new tools to operate this new platform. At the center of this, we built a new operations console to give ourselves a bird's eye view of the entire system, be able to drill down into issues in a particular space, and everything in between.

The operations console is a single-page React application with a reverse proxy on the backend to securely access data from a variety of sources. The console itself started off from a mashup of a few different applications, all of which happened to be using React, but all three were using different methods of loading data into the components. One was manually loading data into component state with jQuery, one was using mixins to do basically the same thing, and one was using classic Flux to load data into stores via actions. We obviously needed to standardize on a way to load data, but we weren't really happy with any of our existing methods. Loading data into state made components smarter and more mutable than they needed to be, and these problems only became worse with more data sources. We liked the general idea of unidirectional flow and division of responsibility that the Flux architecture introduced, but it also brought a lot of boilerplate and complexity with it.

Looking around for alternatives, Redux was the Flux-like library du jour, and it did seem very promising. We loved how the React Redux bindings used pure functions to select state from the store and higher-order functions to inject and bind that state and actions into otherwise stateless components. We started to move down the path of standardizing on Redux, but there was something that felt wrong about loading and reducing data into the global store only to select it back out again. This pattern makes a lot of sense when an application is actually maintaining client-side state that needs to be shared between components or cached in the browser, but when components are just loading data from a server and rendering it, it can be overkill.

Furthermore, we realized that all of our application's state was already represented in the URL. We decided to embrace this. For something like an operations console, this is a really important property. This means that if an engineer is diagnosing an issue, he or she can send the URL to a colleague who can load it up and see the same thing. Of course, this is nothing new -- this is how URLs are supposed to work by locating unique resources; however, this has been lost to a certain degree with single page applications and the shift to moving application state to the browser. Using something like React Router, it becomes very easy to keep URLs front and center, maintain state in dynamic parameters, and pass them down to components as props.

With the application's state represented in the URL, all we needed to do was translate those props into requests to fetch the actual data from backend services. To do this, we built a new library called React Refetch. Similar to the React Redux bindings, components are wrapped in a connector and given a pure function to select data that is injected as props when the component mounts. The difference is that instead of selecting the data from the global store, React Refetch pulls the data from remote servers. The other notable difference is that because the data is automatically fetched when the component mounts, there's no need to manually fire off actions to get the data into the store in the first place. In fact, there is no store at all. All state is maintained as immutable props, which are ultimately controlled by the URL. When the URL changes, the props change, which recalculates the requests, new data is fetched, and it is reinjected into the components. All of this is done simply and declaratively with no stores, no callbacks, no actions, no reducers, no switch statements -- just a function that maps props to requests.

This is best shown with an example. Let's say we have the following route (note, this example uses React Router and ES6 syntax, but these are not requirements):

<Route path="/users/:userId" component={Profile}/>

This passes down the userId param as a prop into the Profile component:

import React, { Component, PropTypes } from 'react'

export default class Profile extends Component {
  static propTypes = {
    params: PropTypes.shape({
      userId: PropTypes.string.isRequired,
    }).isRequired
  }

  render() {
    // TODO
  }
}

Now we know which user to load, but how do we actually load the data from the server? This is where React Refetch comes in. We simply define a pure function to map the props to a request:

(props) => ({
   userFetch: `/api/users/${props.params.userId}`
})

This is then wrapped up in connect() and we pass in our component:

export default connect((props) => ({
  userFetch: `/api/users/${props.params.userId}`
}))(Profile)

When Profile mounts, the request will automatically be calculated and the data fetched from the server. As soon as the request is fired off, the userFetch prop is injected into the component. This prop is a PromiseState, which is a composable representation of the Promise of the data at a particular point in time. While the request is still in flight, it is pending, but once the response is received, it will be either fulfilled or rejected. This makes it easy to reason about and render these different states as an atomic unit rather than a group of variables loosely connected with some naming convention. Now, we can fill in our render() function now like this:

render() {
  const { userFetch } = this.props 

  if (userFetch.pending) {
    return <LoadingAnimation/>
  } else if (userFetch.rejected) {
    return <Error error={userFetch.reason}/>
  } else if (userFetch.fulfilled) {
    return <User user={userFetch.value}/>
  }
}

If we want to display a different user, just change the URL in the browser. With React Router, this can be done with either a <Link/> or programmatically with history.pushState(). Of course manual changes to the URL work too. This will trigger the userId prop to change, React Refetch will recalculate the request, fetch new data from the server, and inject a new userFetch in the component. In this new world, state changes look like this:

react-refetch data flow

This is the simplest use case of React Refetch, but it demonstrates the basic flow. The library also supports many other options such as composing multiple data sources, chaining requests, periodically refreshing data, custom headers and methods, lazy loading data, and even writing data to the server. Several of these features leverage "fetch functions" which allow the mappings to be calculated in response to user actions. Instead of requests being fired off immediately or when props change, the functions are bound to the props and can be called later with additional arguments. This is a powerful feature that provides data control to the component while still maintaining one-way data flow.

While building the operations console, we experienced a lot of trial and error learning how best to load remote data into React. Architectures like Flux and Redux can be wonderful if an application requires complex client-side state; however, if components just need to load data from a handful of sources and have no need to maintain that state in the browser after it renders, React Refetch can be a simpler alternative. React Refetch is available through npm as react-refetch, and many more examples are shown in the project's readme.

Originally published: December 16, 2015

Browse the archives for engineering or all blogs Subscribe to the RSS feed for engineering or all blogs.