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 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 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?
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>
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.
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.
$ 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
gql
function, which allows you to write easily parseable strings for our queries and mutationsreact-apollo
contains bindings for using apollo-client
with React, and graphql
is just Facebook’s reference implementation of GraphQL.
Here’s an example from the React Apollo docs:
import
const client = new
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
ReactDOM.render( <ApolloProvider client={client}> <MyRootComponent /> </ApolloProvider>, document.getElementById('root'),);
$ 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!
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:
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:
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 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.tsximport
const client = new
class
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
export
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
export
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
const HotApp = hot(module)(App);render(<HotApp market="US"
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.
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!
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: