Creating a Flexible Composition Chain for React Components
The more React has evolved, and the more people use it, some patterns have emerged to help us design and build better-organized application code. Here are three specific patterns that provide a foundation for the workflow outlined in this article:
- First is the Container (“smart”) and Presentational (“dumb”) components pattern
- Second is “pure functional React components” pattern
- Finally is “higher-order components” (HOCs)
I will describe a pattern we’ve started using on some of our projects that builds on top of those patterns.
Overview
The base patterns of Container/Presentational split, pure functional components, and higher-order components provide a foundation for assembling components in flexible ways. This enables application developers to serve multiple differing requirements within the app and over time as requirements change.
We’re going to try and solve a few problems with this pattern:
- Load data at the individual component level the page/screen level, based on requirements;
- Handle a loading state when loading data for a component or set of components;
- Handle errors when loading data for a component or set of components;
- Keep presentational components free from handling items 1, 2 and 3;
- Assemble components that handle these issues at the individual component level or at the page/screen level when needed.
The Patterns
NOTE Although the examples use React Native, the pattern is equally applicable to React.
Common Components
The components we create are just example placeholders. You can create customized, nicely styled components for your app. Even better, you could create a custom Loader or Error component for each presentational component. For example, if you have a chart component, your loader might be an animated, chart-like graphic of the same size that sits in place while data is loading. Ditto for error components.
Loading Component
First, let’s create a simple loading component (Loading.js
):
import React from 'react';
import { View, ActivityIndicator } from 'react-native';
export const Loading = () => ( // export as a named export
<View>
<ActivityIndicator />
</View>
);
This component can be used to display a loading indicator for other components while their data loads.
This is a fine component. But we’ve noticed a pretty common pattern that we want to support in this component. We can do that by making it just a little more intelligent.
We want the parent or “wrapping” component of the Loading
component to tell us whether or not loading is happening. So let’s tweak the Loading
component.
We’ll install and import the extremely helpful recompose
package:
npm install recompose --save
And import two HOCs from recompose
:
import { branch, renderComponent } from 'recompose';
We’ll compose the Loading
component with the branch
and renderComponent
HOCs from recompose
enabling the Loading
component to render when a property named loading
is truthy, or pass through to the next component in the composition chain if loading
is falsy.
export default branch(
(props) => props.loading, // if props.loading is 'truthy'...
renderComponent(Loading), // render Loading, else on to the next component
);
There are two exports for the Loading
component, the pure one and the composed one. The pure component is exported as a named component and the composed component as the module’s default export. You could reverse this. It’s a stylistic choice. Just be consistent. Two exports provide the flexibility to use this component in two different ways:
- The composed component when you know you’ll be passing a
loading
property or, - the pure component if you want to use some other logic to decide when to display it.
NOTE The Loading
component doesn’t need a loading
property itself. That property is used by the branch
HOC to decide whether to render the component. After that it isn’t required.
Error Component
Next, we’ll create a presentational component to display errors, if applicable, using the same pattern, except it will look for an error
property (Error.js
):
import React from 'react';
import { View, Text } from 'react-native';
import { branch, renderComponent } from 'recompose';
const Error = ({ error }) => (
<View>
<Text>An Error Occurred: { error }</Text>
</View>
);
Error.propTypes = {
error: React.PropTypes.any.isRequired,
};
export { Error }; // export as a named export
export default branch(
(props) => props.error, // if props.error is 'truthy'...
renderComponent(Error), // render Error, else on to the next component
);
One key difference from our Loading
component is that the Error
component needs an error
property that contains an error of some kind. This could be a simple error message string or an object with more error details. We’re just building a basic pattern here.
Application Components
Now let’s build our application-specific components. We are going to build what we call “UI components” as distinguished from “presentational” components. We can quibble about the names, but basically a presentational component is purely presentational while a UI component brings together the pure presentational component and a component that retrieves the specific data required for the presentational component into a single, composable component of its own.
UI Components
The “UI components” consist of three pieces:
- A data retrieval HOC that queries data specifically for the presentational component.
- A pure presentational component, and
- A composition chain that pulls these and our
Loading
andError
components together into an integrated, composed, reusable component.
First a data-retrieval component (getComponentData.js
):
import { graphql } from 'react-apollo';
import gql from 'graphql-tag';
export default const getComponentData = graphql(gql`
query SomeQuery($x: String!) {
EndPointA(parameterA: $x) {
entity {
fieldA
fieldB
}
}
}
`, {
options: (props) => ({
variables: {
x: props.x,
},
}),
// Using the props option, map the expected data
// properties from the data component into what
// the presentational component expects...
props({ data: { loading, error, EndPointA: { fieldA, fieldB } } }) {
if (loading) {
return { loading };
}
if (error) {
return { error };
}
return {
a: fieldA,
b: fieldB,
};
},
});
Next a pure presentational component (PresentationalComponentA.js
):
import React from 'react';
import { View, Text } from 'react-native';
import { compose, pure } from 'recompose';
import displayLoadingState from './Loading';
import displayError from './Error';
import { getComponentData } from './getComponentData';
const PresentationalComponentA = ({ a, b }) => (
<View>
<Text>
{a}
</Text>
<Text>
{b}
</Text>
</View>
)
// Define your propertyTypes including which ones
// are required. Let React do validation for you!
PresentationalComponentA.propTypes = {
a: React.PropTypes.string.isRequired,
b: React.PropTypes.number,
};
// Export the core pure presentation component as a
// named component which can be used without the
// composition chain defined below.
export { PresentationalComponentA };
Finally, we pull all these together into a composition chain. This can go in its own module but we’ll put this in PresentationalComponentA.js
as its default export. This, again, is a bit of a stylistic choice.
// Export a new HOC, which is a complete composition
// chain, as the default. This component can be used by
// itself this way:
//
// import PresentationalComponentA from './PresentationalComponentA';
//
// <PresentationalComponentA />
export default compose(
getComponentData, // Get the data specifically for this component
displayLoadingState, // If the data is loading, display a loader
displayError, // If there's an error, display an error message
pure, // Prevents the component from updating unless a prop has changed
)(PresentationalComponentA);
This UI component provides a template for other UI components in our application. For brevity, we’ll leave additional components to your imagination. Now it’s time to create a “screen component” that uses…ahem…composes UI components into a single screen in our application.
Screen Components
There are two basic approaches to using the above components. Both approaches have pros and cons, but the pattern we’ve created so far give us the flexibility to compose these UI components in different ways based on our application requirements.
Approach #1: Simple Screen Composition
Here we use each UI component as an independent, stand-alone, component that loads its own data and handles its own loading state and errors. The screen component simply assembles the UI components into a screen and a layout.
import React from 'react';
import { View } from 'react-native';
import PresentationalComponentA from './PresentationalComponentA';
import PresentationalComponentB from './PresentationalComponentB';
const ScreenComponent = () => (
<View>
<PresentationalComponentA />
<PresentationalComponentB />
</View>
);
export default ScreenComponent;
This is very clean and simple.
Notice we import the default exports from our UI components, naming them upon import:
import PresentationalComponentA from './PresentationalComponentA';
import PresentationalComponentB from './PresentationalComponentB';
The default export from the UI components are the composed components that use their own data query component and handle their own loading and error states.
This approach has the following pros and cons.
Pros:
- The screen component doesn’t need to know anything about the data of its child components.
- Each UI component handles its own loading and error states independently.
- The screen can load “progressively,” meaning each UI component loads independently. Faster components load and display quickly for the user, while components that load more slowly won’t hold up the entire screen.
Cons:
- Each UI component requests data separately, making individual requests to the server.
- Each UI component could be displaying their own loading indicator or error creating an undesirable user experience.
This approach may be very suitable in many situations. Two situations this approach might be useful are:
- When you are migrating from one data retrieval approach to a new one (e.g., GraphQL) and you want to do this incrementally, one UI component at a time.
- When you have one UI component whose data retrieval is slower but you want to have some parts of the screen available to users. This is an approach that can improve the perceived performance to users.
But this isn’t the only way we want to construct our application screens. Based on requirements we might want a screen to wait for all of its data to load before displaying anything or present a single error to a user in the event of a problem. This brings us to our next approach.
Approach #2: Integrated Screen Composition
Here we load all of the data required for the screen and all of its sub-components and use each UI component as pure presentational components only. The screen component is now more intelligent and has more responsibility.
First we create a data retrieval component that queries all of the data for all of the components on this screen in a single query (getScreenData.js
):
import { graphql } from 'react-apollo';
import gql from 'graphql-tag';
export const getScreenData = graphql(gql`
query SomeQuery($x: String!) {
EndPointA(parameterA: $x) {
entity {
fieldA
fieldB
}
}
EndPointB {
anotherEntity {
fieldX
fieldY
}
}
}
`, {
options: (props) => ({
variables: {
x: props.x,
},
}),
// Using the props option, map the expected data properties from
// the data component into what the presentational component expects...
props({ data: { loading, error, EndPointA, EndPointB } }) {
if (loading) {
return { loading };
}
if (error) {
return { error };
}
const { entity } = EndPointA;
const { anotherEntity } = EndPointB;
return { entity, anotherEntity };
},
});
Next, we have our complete screen component that is composed with the complete data retrieval component and passes retrieved data to each component. This is the preferred approach in many cases. It reduces the number of requests to your server and brings all the data required for a single screen in at once. In a new application, you will likely start with this approach and, possibly, adopt the previous approach if requirements or performance circumstances change.
import React from 'react';
import { View } from 'react-native';
import { compose, pure } from 'recompose';
import getScreenData from './getScreenData';
import { displayLoadingState } from './Loading';
import { displayError } from './Error';
import { PresentationalComponentA } from './PresentationalComponentA';
import { PresentationalComponentB } from './PresentationalComponentB';
const ScreenComponent = ({ entity, anotherEntity }) => (
<View>
<PresentationalComponentA a={ entity.fieldA } b={ entity.fieldB } />
<PresentationalComponentB x={ anotherEntity.fieldX } y={ anotherEntity.fieldY } />
</View>
);
PresentationalComponentA.propTypes = {
entity: React.PropTypes.shape({
fieldA: React.PropTypes.string.isRequired,
fieldB: React.PropTypes.number,
}).isRequired,
anotherEntity: React.PropTypes.shape({
fieldX: React.PropTypes.string,
fieldY: React.PropTypes.number,
}).isRequired,
};
export default compose(
getScreenData,
displayLoadingState,
displayError,
pure,
)(ScreenComponent);
Here we import the named exports, the pure presentational components, from our UI components:
import { PresentationalComponentA } from './PresentationalComponentA';
import { PresentationalComponentB } from './PresentationalComponentB';
This approach has its own set of pros and cons.
Pros:
- A single request is made for all of the data for all of the components on the screen economizing network bandwidth and server load.
- It displays a single loading indicator or error.
Cons:
- If some parts of the query are slower, the load of the entire screen will be held up while everything loads.
- If there’s an error for any part of the query, the entire screen display an error. This may be undesirable.
Approach #3: Hybrid Screen
In a third “hybrid” approach, left as an exercise for the reader, the screen loads some data for some of its sub-components and leaves other sub-components to handle their own. You might determine that most of the screen can be loaded quickly in a single query, but one sub-component shouldn’t hold everything else up so is left to handle loading, error handling, etc. on its own.
Conclusion
These patterns give the option and flexibility to chose between different approaches based on your requirements. In fact you can make different choices from screen to screen. Even better, you can change your decision if your requirements change over time.