Scratching the surface of composition with React Native and Apollo

When I first stumbled upon Andrew Clark's recompose library I thought awesome, I'm always up for some composition! However, a quick glance at the docs left me feeling like there was a large learning curve ahead of me as I was still just getting comfortable with React Native and GraphQL.

In this post I'll share a few recipes that helped me get started with recompose that had a high degree of impact of the quality of my code. The code examples below are from a project I've been working on called Broce. At a high level, the tech stack is:

  • React Native
  • Expo
  • React Apollo
  • GraphQL backend in Ruby/Rails

On the Menu today

  • Starter: Factor out reusable logic with pure, composable functions
  • Main Course: Factor out fetching remote data from our component all together
  • Dessert: Convert our component to a React PureComponent

Tasting Notes

  • This article assumes you have experience with React and GraphQL
  • Are familiar with or have dappled in composition and higher-order functions

Let's eat!

If you follow the React and Apollo docs you'll quickly end up with a component that looks like the following:

const COMPANY_QUERY = gql`{
  company {
      name
      website
  }
}`;

export default class CompanyScreen extends React.Component {
  render() {
    return (
      <Query query={COMPANY_QUERY}>
        {({ client, loading, error, data }) => {
          if (loading) return <LoadingMask/>;
          if (error) return <ErrorScreen error={error}/>;

          return (
            <ScrollView>
              <CompanyForm company={data.company}/>
            </ScrollView>
          );
        }}
      </Query>
    );
  }
}

This component has a few responsibilities:

  1. It extends a React.Component and is responsible for rendering the component's layout
  2. The company is is wrapped by Apollo's Query component so that it can fetch data from the GraphQL server
  3. It handles the loading and error states for the respective GraphQL query

It's fair to say that Uncle Bob would have an opinion about such a component. We're violating the Single Responsibility Principle a few times over. My main issue with Apollo's Query wrapping component is that couples the concern of the fetching remote data with display logic.

Appetizer

Our first step is to factor away those 2 if conditions that deal with loading and error states. I had been copy and pasting that code around and could easily imagine scenarios where that logic would get more complex (think different error types that warrant different handlers).

We can create 2 plain old javascript constants which leverage recompose's branch function:

export const displayLoadingState = branch(
  (props) => props.data.loading,
  renderComponent(LoadingMask)
);

export const displayErrorState = branch(
  (props) => props.data.error,
  renderComponent(ErrorScreen)
);

The branch function takes 3 arguments. The first is a test function, the second and third arguments are the potential return components if the test functions returns either true or false. Really, it's just another way to go about an if/else condition.

Our test functions look at the component's Apollo provided props and checks if the data.loading or data.error states are set. In that event that the query is loading or returned an error, we call recompose's renderComponent function, passing it our beautifully styled LoadingMask and ErrorScreen components. In the falsey case, we do nothing as we want our CompanyScreen component to render.

A litter further down we'll see how recompose manages to pass the component's props to the test functions above, for now let's just assume magic is real and the props will safely arrive

Main course

Now, let's go about removing that Apollo query logic from our CompanyScreen component.

The react-apollo library offers a HOC function called graphql which will allow us to avoid wrapping our screen components with <Query />. A Higher-Order-Component (HOC) is just a function that takes a component as an argument and returns a new component. All recompose functions are just that, HOC component functions. We'll chain them together shortly.

Introducing Apollo's graphql HOC function will replace <Query query={COMPANY_QUERY}> ... with graphql(COMPANY_QUERY). This will be the first function passed to our composable component chain. Apollo will take and execute that query, returning a new component whose props receive Apollo's data object.

Time to put it all back together, we've managed to factor away a lot of functionality but need to stitch it all back up.

class CompanyScreen extends React.Component<Props> {
  render() {
    const { data } = this.props;

    return (
      <ScrollView>
        <CompanyForm company={data.company}/>
      </ScrollView>
    );
  }
}

export default compose(
  graphql(COMPANY_QUERY),
  displayLoadingState,
  displayErrorState,
)(CompanyScreen);

We can see a lot of code is gone from the CompanyScreen component's render function. At the same time, we've introduced a new default export to this file. We no longer export the CompanyScreen class itself, but rather we export the component that recompose's compose function will create for us.

The call to compose at the bottom of the file will take multiple higher-order components and create a single HOC. This means our resulting CompanyScreen component will have triggered our GraphQL query and Apollo will handle putting the ever important data object onto its props. recompose will also handle chaining the component's props as arguments to each one of the HOC functions passed to compose.

Our CompanyScreen component now only has one concern, rendering a layout in the case of company data having been fetched. Uncle Bob would be proud.

Dessert

For desert we're going to convert our React component into a pure component, as it does not maintain any state. Only the declaration of the CompanyScreen needs to change here. Rather than declaring it as a class we declare it as a function, one that receives and de-structures the props argument.

const CompanyScreen = ({ data: { company } }) => {
  return (
    <ScrollView>
      <CompanyForm company={company}/>
    </ScrollView>
  );
};

export default compose(
  graphql(COMPANY_QUERY),
  displayLoadingState,
  displayErrorState,
)(CompanyScreen);
Luke Mueller