Expressing Data Dependencies in React

One-way data flow is a foundational concept in React (See? It’s on the homepage!). React is very opinionated about how data should flow through a component tree, how that data relates to component state, how to update that state, etc. But it is not very opinionated about how you get that data into the component tree in the first place.

Components depend on data too.

The point of a component-based UI is to provide some degree of encapsulation in this crazy world of increasingly complex web apps. The general goal is for a component to express all of its dependencies — other source modules, libraries / vendor code, even static files like CSS and images. If the component is specific about its dependencies, then we can statically analyze those dependencies to make sure we always include them (and nothing more), and we can make changes to the component without fear that it depends on or is affecting other parts of the system that we’re unaware of.

At least some of your React components will probably have data dependencies. I think of data dependencies as data a component cannot be sure it has (or will receive) when it renders. This data may come from an abstraction layer like a store in Flux, or it might come from an API endpoint directly. The point is that the component needs to specifically request this data from somewhere external to it when it renders — it cannot just rely on the data being passed in.

Take a super simplified example that uses the same pattern outlined in Thinking in React:

var PROFILE = {
name: 'Danny',
interests: ['Books', 'Pizza', 'Space'],
};
class Profile extends React.Component {
render() {
const { profile } = this.props;
return (
<div>
<h1>My name is {profile.name}!</h1>
<p>My interests are: {profile.interests.join(', ')}</p>
</div>
);
}
}
ReactDOM.render(
<Profile profile={PROFILE} />,
document.getElementById('container')
);

This application has a data dependency — it depends on there being a variable called PROFILE in the global scope pointing to some data. This isn’t really a problem in this example because there aren’t any asynchronous dependencies and it’s a comically simple application, but we need a better solution for more realistic web apps. We would want the Profile component to have a way of expressing its dependency on that external data — a way for it to take matters into its own hands.

React points to the componentDidMount method as a place to load initial data via AJAX. This might give you something like:

class Profile extends React.Component {
constructor(props) {
super(props);
this.state = {
profile: {
name: '',
interests: [],
},
};
}
  componentDidMount() {
const { id } = this.props;
api.get(`/api/profile/${id}`).then((response) => {
this.setState({
profile: response,
});
});
}
  render() {
const { profile } = this.state;
return (
<div>
<h1>My name is {profile.name}!</h1>
<p>My interests are: {profile.interests.join(', ')}</p>
</div>
);
}
}
ReactDOM.render(
<Profile id="1" />,
document.getElementById('container')
);

Note: I’m assuming there is some sort of api utility module in the examples. I’m also omitting the imports of things like ReactDOM and other components.

This works fairly well — the component is in charge of fetching the data it needs from the API, which provides isolation. But this component is starting to get a little too smart. It’s mixing data-fetching and presentation concerns. We could re-write it as a container component wrapping a presentational component:

// profile-container.jsx
export default class ProfileContainer extends React.Component {
constructor(props) {
super(props);
this.state = {
profile: {
name: '',
interests: [],
},
};
}
  componentDidMount() {
const { id } = this.props;
api.get(`/api/profile/${id}`).then((response) => {
this.setState({
profile: response,
});
});
}
  render() {
const { profile } = this.state;
return (
<Profile profile={profile} />
);
}
}

// profile.jsx
export default (props) => (
<div>
<h1>My name is {props.profile.name}!</h1>
<p>My interests are: {props.profile.interests.join(', ')}</p>
</div>
);

// app.jsx
ReactDOM.render(
<ProfileContainer id="1" />,
document.getElementById('container')
);

Better, but it still doesn’t really play nice with third parties. This approach is a little too isolated and obscure. The componentDidMount method might not only handle data dependencies, so a third party interested only in data dependencies has no way of determining them. If, for example, you are rendering your components on the server, you need a way to extract these data dependencies — you need to know what to do before the component renders.

The part where I look for guidance from other things that sorta relate to components and data.

You might think to look for advice from other related parts of your stack, like your router and/or your state container. But react-router offers no opinion on how to express data dependencies:

For data loading, you can use the renderProps argument to build whatever convention you want — like adding static load methods to your route components, or putting data loading functions on the routes — it’s up to you.

If you’re using a Flux library like Redux, you might think to check there. It does suggest that data dependencies should go on the route components, but doesn’t really go into the details:

If you use something like React Router, you might also want to express your data fetching dependencies as static fetchData() methods on your route handler components. They may return async actions, so that your handleRender function can match the route to the route handler component classes, dispatch fetchData() result for each of them, and render only after the Promises have resolved. This way the specific API calls required for different routes are colocated with the route handler component definitions. You can also use the same technique on the client side to prevent the router from switching the page until its data has been loaded.

So, you could put “data loading functions on your routes,” as react-router mentioned, but if you’re separating your components into container and presentational components as mentioned above, then you probably have a set of components whose sole job is to ensure that all their child components have the data they need. I think this is the perfect place to express data dependencies!

Seeking Opinions

So, now we know where we want the data dependencies to live, but how exactly should we express them? We essentially just need to provide a way to get a signal that all the data dependencies have been fulfilled. It could be as simple as attaching an array of promise-returning functions to your route components:

// profile-container.jsx
class ProfileContainer extends React.Component {
render() {
const { data = {name: '', interests: []} } = this.props;
return <Profile profile={data} />
}
}
ProfileContainer.dependencies = [
({id}) => api.get(`/api/profile/${id}`),
];
export default ProfileContainer;

// profile.jsx
export default (props) => (
<div>
<h1>My name is {props.profile.name}!</h1>
<p>My interests are: {props.profile.interests.join(', ')}</p>
</div>
);

// routes.js
function someSortOfRouteHandler(Component, props) {
const deps = Component.dependencies.map(dep => dep(props));
Promise.all(deps).then(results => {
render(<Component {...props} data={results[0]} />);
});
}

Now we know that ProfileContainer has a property called dependencies that contains the set of promise-returning functions that we need to resolve to render the presentational Profile component. We can fulfill these data dependencies in a route change event handler or something similar. Isolated and analyzable. Yay!

But I wanted to know if the React ecosystem in general had opinions about expressing component data dependencies. So I did the lazy thing and asked Dan Abramov and Ryan Florence about it on Twitter:

Ryan pointed me in the right direction and Mark Dalgleish helped me the rest of the way with the related projects section of his README:

Such a good idea.

I ended up with this list:

Some of the libraries are targeted at specific contexts or technologies — for example, AsyncProps is for React Router, GroundControl is for React Router and Redux, Relay depends on a GraphQL server, and React Async is to be used with Observables.

But as far as I can tell, the libraries differ most in their opinion of what the component(s) should look like, which changes how exactly they wire the data in (if at all).

Some only focus on providing a set of component-level hooks that you can use to a) define some stuff that needs to happen for the component to render (probably a promise-returning function), b) trigger that stuff, and c) know when that stuff is complete. This was the approach in the example I proposed above, where all we’re really doing is specifying an array of promise-returning functions and letting the surrounding code (in the router or wherever we’re rendering that container) handle the process of ensuring those actions are called. It’s conceptually similar to (though less powerful than) Mark Dalgleish’s Redial:

// profile-container.jsx
import { provideHooks } from 'redial';
class ProfileContainer extends React.Component {
render() {
const { data = {name: '', interests: []} } = this.props;
return <Profile profile={data} />
}
}
const hooks = {
fetch: ({ id }) => api.get(`/api/profile/${id}`),
};
export default provideHooks(hooks)(ProfileContainer);

// profile.jsx
export default (props) => (
<div>
<h1>My name is {props.profile.name}!</h1>
<p>My interests are: {props.profile.interests.join(', ')}</p>
</div>
);

// routes.js
import { trigger } from 'redial';

function someSortOfRouteHandler(Component, props) {
trigger('fetch', Component, props).then(results => {
render(<Component {...props} data={results[0]} />);
})
}

This approach encourages — but does not necessarily require — you to write container and presentational components and leaves the specifics of populating the state or props to you. It’s a little less magical but more flexible, which might be useful if you have, say, a complex state container in your application.

The majority of the libraries allow you to write almost exclusively presentational components by abstracting away the container components. You specify which props should be populated from which external sources and then they take care of everything for you— you can just rely on those props magically being populated on the client and server. They differ mostly in how they expect the dependencies to be expressed — whether it’s just an API URL, a promise-returning function, or a GraphQL fragment.

For example, our Profile component could be written with Eric Clemmonsreact-resolver like this:

// profile.jsx
import { resolve } from 'react-resolver';
const Profile = (props) => (
<div>
<h1>My name is {props.profile.name}!</h1>
<p>My interests are: {props.profile.interests.join(', ')}</p>
</div>
);
export default resolve('profile',
({id}) => api.get(`/api/profile/${id}`)
)(Profile);

Or Heroku’s react-refetch like this:

// profile.jsx
import { connect } from 'react-refetch'

const Profile = (props) => {
const { profile } = props;
if (!profile.value) {
return <div>Loading!</div>;
}
return (
<div>
<h1>My name is {profile.value.name}!</h1>
<p>My interests are: {profile.value.interests.join(', ')}</p>
</div>
);
};

export default connect(({id}) => ({
profile: `/api/profile/${id}`,
}))(Profile)

These libraries essentially let you define a property on the component’s props object, and explain how that property should be populated. They handle the wiring of data dependencies into props for you, so you don’t have to think about writing container components or keeping your presentational components pure.

Which library or approach to use, as with everything, largely depends on your specific requirements. But, as the Relay team points out, your requirements are probably not that unique:

In our experience, the overwhelming majority of products want one specific behavior: fetch all the data for a view hierarchy while displaying a loading indicator, and then render the entire view once the data is ready.

The Heroku team also hints at what to think about when coming up with a data dependency strategy:

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.

Though the details will vary, all the libraries and approaches here basically encourage you to abstract your data dependencies into container (or higher order) components in a way that lets external code determine when they have been resolved, while keeping your presentational components pure — a pattern that I think is extremely powerful.


Thanks to Ryan Florence (ryanflorence) and Mark Dalgleish (markdalgleish) for pointing me in the right direction; Dan Abramov (dan_abramov) and michael chan (chantastic) for really evangelizing the container/presentational component approach; and all the authors of the libraries mentioned above for giving your code to the world!

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.