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!

Side effects of type errors: Lost dev time and increased irritability

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 wasting time on dumb type errors, provides dynamic self-documentation, and makes it far easier for my colleagues to understand the expectations set for the code 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 believe 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 now also co-organizer), where I covered my approach to state management in React applications with TypeScript.

But what about Redux?

https://redux.js.org/

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

  • Depending on your application’s needs, Redux can be overly complex/heavy-handed
  • 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 and why do I care?

I started using the Context API in some applications over Redux this year, and I found it 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,” Rakshit Soral aptly writes 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. You employ a Provider, which is the root source of information to all components in the tree, and you also use a Consumer, whose responsibility involves taking data or functionality from the Provider and feeding it directly to the components that require that information.

First, you’ve got React.createContext, which initializes and passes the context an initial value. In this 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 information, data, functions, etc., that get shared via context.

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

The Consumer in the following example — again, from 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, exactly 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, you can generate static types for your queries as part of the Apollo tooling system.

Adding GraphQL to your app

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

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

Getting a boost with Apollo

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

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

Configuring the Apollo 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, you import ApolloClient, HttpLink, and the InMemoryCache. If you’d prefer to use a GraphQL endpoint other than the default endpoint, which resides on the same host as the client, HttpLink accepts a configuration object.

What this means is, for example, if you’re using a microservice that lives on a different host, you’ll pass in a custom config object for your GraphQL endpoint.

Next, you wrap your root component in ApolloProvider, imported from react-apollo. This gives each component in your application access to GraphQL via Apollo. The example below 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

The package.json scripts I prefer to use 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, again: This is my preferred approach. Everyone should, of course, feel free to configure their package.json scripts however they feel is best!

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 folks have their own lingo 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.

While I explain how to set up React Context with GraphQL and TypeScript in the rest of this article, you can also find the full source code here.

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 my 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

Getting my store ready for the grand opening!

My app already has all the necessary Apollo dependencies installed, and these scripts are 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 modelsfolder 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.tsximport 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 look at the FakerQL docs, they tell me I can 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?

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.

My reasons? 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!

Context API magic!

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 typed as either "US"or "UK".

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

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

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

You’ll also notice that I’m wrapping the component with JawnContext.Provider and its value object, which contains 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.

Moving on to the consumers: This is personal preference here — some folks prefer to create new functions to wrap each consuming component — but I want to set up a reusableWithJawnContext provider here so I can easily compose it with the GraphQL provider and the consuming component whenever 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 my component. React Apollo gives meChildDataProps, the generic type for a component wrapped by ApolloProvider.

Similarly, I need to allow Cart to consume the context.

Here, I’m composing the withJawnContext provider with Cart — typed to JawnState— which gives me access to market, cart, and removeFromCart from context.

And that’s about it! My application 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 for our schema, queries, and mutations
  • The Context API, working together with TypeScript, provides a type-safe, lightweight way to share state and data without drilling down props

A version of this article was originally published on lilydbarrett.com. You can find the full source code for the Jawn Store here.

Additional useful resources:

State management with React Context, TypeScript, and GraphQL was originally published in Hacker Noon on Medium, where people are continuing the conversation by highlighting and responding to this story.

Publication date: 
12/07/2018 - 08:26
Author: 
Share: