How To Use React Context (V19 and Typescript)

No more prop drilling!


By Mike Cronin

React context is the perfect tool for managing those little bits of state that just pop up everywhere. Like getting the current user or dealing with shopping cart data. It's great when you don't want to prop drill 9 components deep, but also don't want to bring in the big guns like Redux, or some 3rd party manager like Zustand.

What about React 19? It's finally here, and luckily the changes for context are super small. For now, I'm going to assume you aren't running 19 yet, and will include the updates alongside the previous defaults.

Full GitHub with working code

TL;DR: The Code

Make your actual context:

Then make the provider component with whatever data and functions you need:

Then wrap your app up with that provider.


And finally, use it in a component.


If any of that was confusing, keep reading below for full explanations of everything, and a bit about how to test your context components.

What is context?

Context lets you have global values and functions that can be accessed from any child. It's the only built-in "global store" that React has, and a lot of other libraries use it under the hood. It's a lot like Redux, the biggest difference is just complexity: context is smaller and simpler. Redux is all about an immutable store with actions and reducers to intricately manipulate your data, whereas context is more like a floating component that can talk to any other component.

Setup

We're just going to use Vite to create a basic TS React project, and we're only going to have a few components. It's only App, Parent, and Child, and the point of them is to show that when Child eventually uses context, it doesn't trigger any re-renders of the parents. Here's the starting code:

And here's the file structure we're working with. Now let's make those context files!

src/
  components/
    Child.tsx
    Parent.tsx
  contexts/
    ExampleContext/
      index.ts
      ExampleContextProvider.tsx
  App.tsx
  main.tsx

Creating a context

Contexts are meant to be relatively small and focused, so it's common to have a few, but we'll stick with one for now. I don't have a ton of creativity, so we're going to call ours ExampleContext (it's convention that contexts are capitalized). There are 2 parts:

  • The context itself: this is what gets imported into any component that wants to use it
  • The context component: This is the provider that actually defines what the context value is

You can combine them into one file, but then the Vite hot module lint rule will yell at you. I like to split them out into an index file and then the provider component. Feels like a nice separation of concerns to me.

The index file

All this file contains is the type and createContext


Careful, here's where TS can trip you up. createContext forces you to pass in a default value, but it will always be written over by the provider component (more on that during the testing section). You can do some hullabaloo with null and then a bunch of as modifiers in your code, but that feels so clunky to me. I like the approach shown here since the typing works out of the box, and I can define my true default values in the provider component.

The provider component

Next, we need to import that context (and its typing) into the wrapper component. This is where all the logic really goes, but as you'll see: context is basically just a component with state.

Nothing too crazy here. I do want to call out that right now we have to use ExampleContext.Provider, but as of v19, you can just do ExampleContext. Also be sure to set the type of context explicitly, this will ensure your types always stay true to your actual context values.

You can also make your own mini-redux by using useReducer instead of useState, and have more in depth functions and values. But, for the point of the tutorial, this is all we need. I do encourage you to keep your contexts "dumb" though. Things like async behaviors and complex functions are better left to custom hooks that use the context.

Always use props.children

One last piece to call out before we move on: YOU MUST USE children OR ELSE YOU WILL OBLITERATE PERFORMANCE. Some tutorials just slap the context provider in another component. Never do that. That would trigger every child component in the entire tree to re-render every time you change anything. That's just prop drilling by another name.

Adding the context to the app

Believe it or not, that was the hard part. Now all we need to do is add that wrapper component into our main app.

By the way, you can absolutely nest contexts, and this is the file you would do that in.

Typically, your context provider will go in the root of your app, that way every component will have access to it. You can have a context provider lower in your project, but then you'll have to be careful to make sure that the components that need it are children to avoid weird behavior.

Anyway, let's finally use our context!

Using context in a component

Modify the Child component to pull in the context using the useContext hook. Remember, the components themselves import the actual context from the index, not the wrapper component.


The biggest difference with the useContext hook compared to something like useState is that it returns an object to destructure instead of an array. Note that the object that gets returned is the values we put into the value prop in our provider component.

Also, this is the other React 19 change. In the future, you'll use the use API instead of useContext. It behaves identically in this case, so it's an easy swap.

And that's pretty much it when it comes to using context in your apps! It's just a floating store with values and functions that you can access through the context hook/API. You can play around with this code, and notice that even though the state is changing in the context provider, the App and Parent components never re-render.

Testing a context component

The last little hiccup with context is making sure you test it properly. Good news: it's not that hard. You just need to wrap whatever components you're testing in some kind of provider. Here I'm demonstrating using the actual provider component we made, as well as making a new provider with custom values.


Honestly, I would not recommend using a custom context provider like the second example. Instead, mimic whatever user actions you need in the test to get the state how you want it. But, the option is there if you need it and know what you're doing.

Be warned, if you don't use some kind of provider, the component won't crash, it'll simply use whatever default context value you gave in the initial setup. This is almost never what you want since we used a {} which will be missing values and functions. So always be sure to add a provider in your tests. To make things easier, it's common to have custom render helpers in your tests that do things like add routers and contexts.

You're all set!

Alright, that is more than enough to get you started with context. Why not give it a shot before you reach for a third party library on your next project? Even if you do replace it later as your app grows, it can be helpful to build your own systems instead of always using someone else's, it teaches you a lot about things you can usually ignore.

Happy coding everyone,

Mike

Join the conversation on Reddit to leave a comment!