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:

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:

  1. Load data at the individual component level the page/screen level, based on requirements;
  2. Handle a loading state when loading data for a component or set of components;
  3. Handle errors when loading data for a component or set of components;
  4. Keep presentational components free from handling items 1, 2 and 3;
  5. 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:

  1. The composed component when you know you’ll be passing a loading property or,
  2. 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:

  1. A data retrieval HOC that queries data specifically for the presentational component.
  2. A pure presentational component, and
  3. A composition chain that pulls these and our Loading and Error 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…ahemcomposes 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:

  1. 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.
  2. 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.

Let's stay connected. Join our monthly newsletter to receive updates on events, training, and helpful articles from our team.