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=>
{({ client, loading, error, data }) => {
if (loading) return <LoadingMask/>;
if (error) return <ErrorScreen error=/>;
return (
<ScrollView>
<CompanyForm company=/>
</ScrollView>
);
}}
</Query>
);
}
}
This component has a few responsibilities:
- It extends a
React.Component
and is responsible for rendering the component's layout - The
CompanyScreen
's render element is wrapped by Apollo'sQuery
component so that it can fetch data from the GraphQL server - 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 it couples the concern of 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=> ...
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.
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 = this.props;
return (
<ScrollView>
<CompanyForm 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 = ( ) => {
return (
<ScrollView>
<CompanyForm company=/>
</ScrollView>
);
};
export default compose(
graphql(COMPANY_QUERY),
displayLoadingState,
displayErrorState,
)(CompanyScreen);
Back to Explore Focused Lab