State management with React Context, TypeScript, and GraphQL

Tired of debugging type errors in your state? Want up-to-date documentation for your React apps? Read on!

When I first encountered TypeScript, I felt a decent amount of despair: Why did I have to write what felt like more boilerplate code? When using it with React, why did I have to determine the type of every single React prop, and the request and response objects for async calls? And what the hell were intersection and union types?

After spending time working with TypeScript, however, I quickly fell in love with it. It saves me from debugging dumb type errors, provides dynamic documentation, and makes it far easier for my colleagues to understand the expectations set for a particular application at first glance.

My interest in TypeScript grew as I explored different approaches to state management in React applications. I’m especially excited about React’s new Context API, which I think can be very powerful, especially when combined with GraphQL and TypeScript.

In October, my excitement composed itself into a talk for the Boston TypeScript meetup (for which I’m also co-organizer), where I covered my approach to React applications with TypeScript.

But what about Redux?

But before we get into all that: We need to talk about Redux. It’s well-established, arguably the default state management pattern by now. So why don’t we just use it?

  • It could be overly complex/heavy-handed for your application
  • You may not want to use the entire Redux ecosystem, including action creators, reducers, etc.
  • You wind up with tons of boilerplate code

What’s the Context API?

I started using the Context API in some applications over Redux this year, and I found it really elegant and fast to implement. The Context API is “an upgraded version of old concept of context in React which allow[s] components to share data outside the parent-child relationship,” writes Rakshit Soral in “Everything You Need to Know About React’s Context API.

My own short definition?

The Context API is a way to share state and avoid prop drilling (as of React 16.3). What it all boils down to is a clean, pretty way to share information across your application without sending it down through components that don’t care about it.

My diagram of the Context API may be over-simplified, though you’ll see there’s really not too much more to it than this. We’ve got a Provider, which is the root source of information to all components in the tree, and we’ve got a Consumer, which is responsible for taking all that info from the Provider and feeding it directly to the components that require that info.

First, we’ve got React.createContext, which initializes and passes the context an initial value. In this code example from the Context API docs, React.createContext returns an object with a Provider and Consumer.

const {Provider, Consumer} = React.createContext(defaultValue);

The Provider in the below code (also from the docs) accepts a value prop that represents the info, data, functions, etc., that get shared via context.

<MyContext.Provider value={/* some value */}>

The Consumer here (again, via the docs) wraps a function that takes in a value from the Provider and returns JSX in the form of components that are privy to the Provider‘s information.

<MyContext.Consumer>
  {value => /* render something based on the context value */}
</MyContext.Consumer>

What’s GraphQL and why do I care?

GraphQL, like React, was created by Facebook. Unlike REST, GraphQL uses just ONE single endpoint that allows you to fetch data via multiple queries at once. It allows you to request only the data you want, when you want it.


https://graphql.org/

As you can see above, GraphQL also has a built-in type system that helps provide dynamic API self-documentation as the API grows and evolves. Even better, we can generate static types for our queries as part of the Apollo tooling system.

Adding GraphQL to your app

$ npm install --save apollo-boost react-apollo graphql

Apollo Boost gives us a bunch of packages right out of the box.

  • apollo-client is a caching GraphQL client that we can use with React (as well as Angular and other frameworks)
  • apollo-cache-inmemory is a standard, in-memory cache recommended for use with apollo-client
  • apollo-link-http fetches GraphQL results from a GraphQL endpoint over an HTTP connection
  • graphql-tag exports the gql function, which allows us to write easily parseable strings for our queries and mutations

react-apollo contains bindings for using apollo-client with React ,and graphql is merely Facebook’s reference implementation of GraphQL.

Configuring the client

Here’s an example from the React Apollo docs:

import { ApolloClient } from 'apollo-client';
import { HttpLink } from 'apollo-link-http';
import { InMemoryCache } from 'apollo-cache-inmemory';

const client = new ApolloClient({
  // By default, this client will send queries to the
  // `/graphql` endpoint on the same host
  // Pass the configuration option { uri: YOUR_GRAPHQL_API_URL } to the `HttpLink` to connect
  // to a different host
  link: new HttpLink(),
  cache: new InMemoryCache(),
});

Here, we’re importing ApolloClient, HttpLink, and the InMemoryCache. HttpLink accepts a configuration object with the GraphQL endpoint URL rathe than the default — so, if you’re using a microservice, you’d pass in a custom config object for your endpoint.

Next, we wrap our root component in ApolloProvider, imported from react-apollo. This gives each component in our application access to GraphQL via Apollo. This example is also from the React Apollo docs:

import { ApolloProvider } from 'react-apollo';

ReactDOM.render(
  <ApolloProvider client={client}>
    <MyRootComponent />
  </ApolloProvider>,
  document.getElementById('root'),
);

Generating types with Apollo

$ npm i --save apollo-codegen

My package.json scripts are below:

"introspect": "apollo-codegen introspect-schema GRAPHQL_ENDPOINT --output PATH_TO_SCHEMA_FILE",
// this fetches the schema and saves it in our project
"generate": "apollo-codegen generate GLOB_PATH_TO_QUERY_FILES --schema PATH_TO_SCHEMA_FILE --target typescript --output PATH_TO_GENERATED_TYPES_FILE --add-typename --tag-name gql",
// this generates type interfaces from our schema
"typegen": "npm run introspect && npm run generate"

In my introspect script, I’m calling apollo codegen introspect-schema with my endpoint and requesting GraphQL to output my schema files to a specified file.

My generate script looks at my auto-generated schema file and my queries and mutations and generates types for my queries and mutations.

And, finally, my typegen script combines those two aforementioned scripts.

I run npm run typegen and I’m good to go with my GraphQL types!

Please note: This is my preferred approach. Everyone should, of course, feel free to configure their package.json scripts however they wish!

Demo time

I drank way too much coffee the other day and decided I wanted to rebuild and rebrand Amazon.

Thankfully, I decided to start small.

My partner just moved to Philadelphia, and they have their own words for various things down there. Like this one:

Jawn: noun, chiefly in eastern Pennsylvania, used to refer to a thing, place, person, or event that one need not or cannot give a specific name to.

My Jawn Store MVP should eventually display a list of products with their prices and give me the ability to add things to my cart. I should also be able to remove items from my cart and see the updated total instantly.

For my prototype, I’m using Faker.js, a terrific library for generating fake data. Faker.js hosts a FakerQL endpoint, allowing me to get fake data from a GraphQL endpoint. It offers me the following types to query:

  • Post
  • Product
  • User
  • Todo

For my purposes, since I’m running a store, I’ll be fetching data via FakerQL for products to sell.

My app also uses the following technologies:

  • TypeScript with Parcel.js, a bundler which supports TS right out of the box
  • React’s Context API

Setting up my GraphQL client

My app already has all the necessary Apollo dependencies installed, and these scripts included in my package.json:

"scripts": {
   "test": "npm run test",
   "dev": "parcel ./index.html",
   "introspect": "apollo-codegen introspect-schema https://fakerql.com/graphql --output ./data/models/index.json",
   "generate": "apollo-codegen generate ./data/**/*.ts --schema ./data/models/index.json --target typescript --output ./data/models/index.ts --add-typename --tag-name gql",
   "typegen": "npm run introspect && npm run generate",
   "build": "tsc"
}

You’ll notice the use of the FakerQL endpoint and a path to a data folder where I’m both auto-generating schema models and setting up my query types.

And here’s the actual structure for my data folder:

- data
    - formatters
    - models
    - queries

My formatters are functions for calculating prices in different countries (already implemented). When I run my introspect script, Apollo will output the schema into an index.json file in my models folder. All files in the models folder will wind up being auto-generated.

When I run my generate script, Apollo will look at my queries, in conjunction with the endpoint schema, and output the types onto an index.ts file in my models folder.

Next, I need to create an instance of ApolloClient so I can use its capabilities.

// ./index.tsx
import React from "react";
import { ApolloProvider } from "react-apollo";
import { ApolloClient } from "apollo-client";
import { HttpLink } from "apollo-link-http";
import { InMemoryCache } from "apollo-cache-inmemory";

const client = new ApolloClient({
   link: new HttpLink({
      uri: "https://fakerql.com/graphql",
      // Remember, we only need ONE endpoint!
   }),
   cache: new InMemoryCache(),
});

class App extends React.Component {
   public render () {
      // App contents
   }
}

Just like in the example we saw before, we’re using ApolloClient, HttpLink, and the InMemoryCache. I’m passing in a URI configuration object with the FakerQL endpoint.

I’m also ensuring that the root component is wrapped in ApolloProvider, and that all components in the tree can therefore take advantage of GraphQL.

Let’s get down to business: I need a query to fetch all the products via FakerQL. I prefer to have a file for each query in my data folder.

// data/queries/JAWN_QUERY.ts
import gql from "graphql-tag";
export default gql`
query FindJawnProducts {
  // The FakerQL docs tell me I can query "allProducts" and get a list of products back
  // I'm also specifying the fields I want returned for each Product: id, name, price
  allProducts {
     id
     name
     price
  }
}`;

Here, I’m using gql to drop my query into an easily readable string. When I looked at the FakerQL docs, they told me I could query allProducts and specify the above fields — among others — to be returned for each product.

When I run npm run typegen, here are the types that get generated:

export interface FindJawnProducts_allProducts {
   __typename: "Product";
   id: string;
   name: string;
   price: string;
}

export interface FindJawnProducts {
   allProducts: (FindJawnProducts_allProducts | null)[] | null;
}

FindJawnProducts_allProducts represents the type for an individual project or item, and FindJawnProducts is the type for an array or list of products in our store. These types will be useful for setting up our context and typing components that wind up taking advantage of this data.

Before I get our components using data from GraphQL, I stop to ask myself: What other information do I want besides the product details fetched from FakerQL?

And, as it turns out, I want to support two different markets: the U.S. and the U.K.

In order to provide the correct calculations for product prices, I need my components to be aware of my market. In this case, I’ll pass the market down as a prop into the root component.

class App extends React.Component {
   public render () {
        const { market } = this.props;
        return (
            <ApolloProvider client={client}>
                    <Container fluid>
                        <Row>
                            <Col xs={12} sm={6}>
                                <JawnList market={market}/>
                            </Col>
                            <Col xs={12} sm={6}>
                                <Cart market={market}/>
                            </Col>
                        </Row>
                    </Container>
            </ApolloProvider>
        );
    }
}

const HotApp = hot(module)(App);

render(<HotApp market="US" />, document.getElementById("root"));

But I don’t want to drill props down from the root component just to provide awareness about my market.

I also have two components — JawnList and Cart — that potentially need to know about the products I’m fetching from my API, but I don’t want to pass that data down as a prop, either.

Reason being? Prop drilling can get incredibly messy as your application increases in size. My MVP could grow into a much bigger app, and I don’t want to wind up passing details down through components that don’t care about them.

Enter the Context API!

I create a file called JawnContext.tsx, where I define and create my context for the application:

This is where the Apollo-generated types will start to come in handy. Cart will be an array of FakerQL products. addToCart will take in a FakerQL product as an argument and add it to the Cart. removeFromCart will do exactly what it sounds like. And, finally, the market can be either US or UK.

Then React.createContext works its magic! (null, by the way, is the required default value for the context).

Next, let’s hook up my context to my root component.

You’ll notice that App takes JawnState — the context type — as a type, since one of the component’s props is market, which I now want to derive from context.

You’ll also notice that I’ve included JawnContext.Provider and its value object, which reflects the values of each of the context properties — the implementations of addToCart and removeFromCart, the market passed into the root, and the current state of the cart.

This is personal preference here — some folks prefer to create new functions for consuming context for each consuming component — but I’d like to set up a WithJawnContext provider here so I can reuse and compose it with the GraphQL provider and the consuming component when necessary.

Here, my Props extend JawnState, the type for the context, and the function accepts a React component as a child. It’s then returning a child, wrapped by JawnContext.Consumer, which spreads the given props and context state within it.

To allow JawnList to successfully consume my context in a type-safe fashion, I need to define JawnListType as a child that combines attributes from the JawnState context and GraphQL’s autogenerated data type, FindJawnProducts.

This gives me access to the data from my GraphQL endpoint, as well as the market and addToCart from my context.

At the bottom of the above code, you’ll see I’ve created a function to make the necessary GraphQL query for the product data. I’m composing that with the withJawnContext provider and the component. React Apollo gives us ChildDataProps, the generic type for a component wrapped by ApolloProvider.

I also need to allow Cart to consume the context.

CartState is also a type of JawnState, and I’m composing the withJawnContext provider with Cart, which gives me access to market, cart, and removeFromCart from context.

And that’s about it! My application now allows users to add and remove items from their carts and view updated total prices — and I get to avoid prop-drilling across my application. I win!

Takeaways

  • Apollo helps us query a single endpoint and generate GraphQL types
  • The Context API, working together with TypeScript, provides a type-safe, lightweight way to share state and data without drilling props down

You can find the full source code here.

Additional useful resources:


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s