Editor’s note: This post was updated 15 February 2022 to add a theming example, improve and update the references to the React Context API, and remove outdated information.
TypeScript has become increasingly popular after numerous improvements and additions were made to the code, such as robust static type checking, understandability, and type inferences. When TypeScript is used with React, it offers improved developer experience and more predictability to the project.
In this guide, we will learn how to use TypeScript with React Context by building a to-do app from scratch. To get the most out of this tutorial, you need a basic understanding of React and TypeScript.
In this post, we’ll cover:
- What is the React Context API?
- Setting up
- Create the to-do type
- Create the context
- Create the components and consume the context
- Provide the context
- Theming with the Context API
Let’s dive in.
What is the React Context API?
The React Context API was introduced in React v16 as a way to share data in a component tree without needing to pass props down at every level.
The Context API is ideal for data that is considered “global” but not large or complex enough for a dedicated state manager like Redux or MobX, such as the user’s current language, current theme, or even data from a multi-step form before being sent to an API.
Setting up the app
To demonstrate React Context, we’ll build a to-do app that uses the Context API for managing tasks on the list, and also for theming.
We will use Create React App in order to have a modern configuration with no hassle, but you are welcome to set up a new app from scratch using Webpack.
Begin by opening your terminal and running the following command:
npx create-react-app react-context-todo --template typescript
To easily create a TypeScript project with CRA, you need to add the flag --template typescript
, otherwise the app will only support JavaScript.
Next, let’s structure the project as follows:
src ├── @types │ └── todo.d.ts ├── App.tsx ├── components │ ├── AddTodo.tsx │ └── Todo.tsx ├── containers │ └── Todos.tsx ├── context │ └── todoContext.tsx ├── index.tsx ├── react-app-env.d.ts └── styles.css
Here, there are two files to underline:
- The
context/todoContext.tsx
file, which exports the created context for the to-do functionality, and its provider - The
todo.d.ts
in@types
file contains the type definitions for parts of the app that concern the to-do list implementation
Having dedicated type definition files is a best practice because it improves the structure of your project. The declared types can either be used by reference without importing them, or explicitly by importing them into another file (though they have to be exported first). Ideally, we’d want to import the types so we don’t pollute the global namespace.
With this in place, we can now get our hands dirty and code something meaningful.
Create the to-do type
TypeScript types allow you to define what a variable or function should expect as a value in order to help the compiler catch errors before runtime.
// @types.todo.ts export interface ITodo { id: number; title: string; description: string; status: boolean; } export type TodoContextType = { todos: ITodo[]; saveTodo: (todo: ITodo) => void; updateTodo: (id: number) => void; };
As you can see, the interface ITodo
defines the shape of a to-do object. Next, we have the type TodoContextType
that expects an array of to-dos and the methods to add or update a to-do.
Create the context
React Context allows you to share and manage state across your components without passing down props. The context will provide the data to just the components that need to consume it.
// context/todoContext.tsx import * as React from 'react'; import { TodoContextType, ITodo } from '../@types/todo'; export const TodoContext = React.createContext<TodoContextType | null>(null); const TodoProvider: React.FC<React.ReactNode> = ({ children }) => { const [todos, setTodos] = React.useState<ITodo[]>([ { id: 1, title: 'post 1', description: 'this is a description', status: false, }, { id: 2, title: 'post 2', description: 'this is a description', status: true, }, ]);
Here, we start by creating a new context and set its type to match TodoContextType
or null
. We set the default value to null
temporarily when creating the context; the intended values will be assigned on the provider. Next, we create the component TodoProvider
that provides the context to the component consumers. Here, I initialize the state with some data to have todos
to work.
// context/todoContext.tsx const saveTodo = (todo: ITodo) => { const newTodo: ITodo = { id: Math.random(), // not really unique - but fine for this example title: todo.title, description: todo.description, status: false, } setTodos([...todos, newTodo]) } const updateTodo = (id: number) => { todos.filter((todo: ITodo) => { if (todo.id === id) { todo.status = true setTodos([...todos]) } }) }
The function saveTodo
will create a new to-do based on the interface ITodo
and then append the object to the array of to-dos. The next function, updateTodo
, will look for the id of the to-do passed as a parameter in the array of to-dos and then update it.
// context/todoContext.tsx return ( <TodoContext.Provider value={{ todos, saveTodo, updateTodo }}> {children} </TodoContext.Provider> ); }; export default TodoProvider;
Next, we pass the values to the context to make them consumable for the components.
// context/todoContext.tsx import * as React from 'react'; import { TodoContextType, ITodo } from '../@types/todo'; export const TodoContext = React.createContext<TodoContextType | null>(null); const TodoProvider: React.FC<React.ReactNode> = ({ children }) => { const [todos, setTodos] = React.useState<ITodo[]>([ { id: 1, title: 'post 1', description: 'this is a description', status: false, }, { id: 2, title: 'post 2', description: 'this is a description', status: true, }, ]); const saveTodo = (todo: ITodo) => { const newTodo: ITodo = { id: Math.random(), // not really unique - but fine for this example title: todo.title, description: todo.description, status: false, }; setTodos([...todos, newTodo]); }; const updateTodo = (id: number) => { todos.filter((todo: ITodo) => { if (todo.id === id) { todo.status = true; setTodos([...todos]); } }); }; return <TodoContext.Provider value={{ todos, saveTodo, updateTodo }}>{children}</TodoContext.Provider>; }; export default TodoProvider;
With this, we are now able to consume the context. So, let’s create the components in the next section.
Create the components and consume the context
Below, we have a form component that allows us to handle data entered by the user using the useState
Hook. Once we get the form data, we use the function saveTodo
, pulled from the context object, to add a new to-do.
// components/AddTodo.tsx import * as React from 'react'; import { TodoContext } from '../context/todoContext'; import { TodoContextType, ITodo } from '../@types/todo'; const AddTodo: React.FC = () => { const { saveTodo } = React.useContext(TodoContext) as TodoContextType; const [formData, setFormData] = React.useState<ITodo | {}>(); const handleForm = (e: React.FormEvent<HTMLInputElement>): void => { setFormData({ ...formData, [e.currentTarget.id]: e.currentTarget.value, }); }; const handleSaveTodo = (e: React.FormEvent, formData: ITodo | any) => { e.preventDefault(); saveTodo(formData); }; return ( <form className="Form" onSubmit={(e) => handleSaveTodo(e, formData)}> <div> <div> <label htmlFor="name">Title</label> <input onChange={handleForm} type="text" id="title" /> </div> <div> <label htmlFor="description">Description</label> <input onChange={handleForm} type="text" id="description" /> </div> </div> <button disabled={formData === undefined ? true : false}>Add Todo</button> </form> ); }; export default AddTodo;
Note that I use typecasting on the useContext
Hook to prevent TypeScript from throwing errors because the context will be null
at the beginning.
// components/Todo.tsx import * as React from 'react'; import { ITodo } from '../@types/todo'; type Props = { todo: ITodo; updateTodo: (id: number) => void; }; const Todo: React.FC<Props> = ({ todo, updateTodo }) => { const checkTodo: string = todo.status ? `line-through` : ''; return ( <div className="Card"> <div className="Card--text"> <h1 className={checkTodo}>{todo.title}</h1> <span className={checkTodo}>{todo.description}</span> </div> <button onClick={() => updateTodo(todo.id)} className={todo.status ? `hide-button` : 'Card--button'}> Complete </button> </div> ); }; export default Todo;
As you can see here, we have a presentational component that shows a single to-do. It receives the todo
object and the function to update it as parameters that need to match the Props
type defined above.
More great articles from LogRocket:
- Don't miss a moment with The Replay, a curated newsletter from LogRocket
- Learn how LogRocket's Galileo cuts through the noise to proactively resolve issues in your app
- Use React's useEffect to optimize your application's performance
- Switch between multiple versions of Node
- Discover how to animate your React app with AnimXYZ
- Explore Tauri, a new framework for building binaries
- Advisory boards aren’t just for executives. Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
// containers/Todos.tsx import * as React from 'react'; import { TodoContextType, ITodo } from '../@types/todo'; import { TodoContext } from '../context/todoContext'; import Todo from '../components/Todo'; const Todos = () => { const { todos, updateTodo } = React.useContext(TodoContext) as TodoContextType; return ( <> {todos.map((todo: ITodo) => ( <Todo key={todo.id} updateTodo={updateTodo} todo={todo} /> ))} </> ); }; export default Todos;
This component shows the list of to-dos when the page loads. It pulls the todos
and the function updateTodo
from the to-do context. Next, we loop through the array and pass the object we want to display to the Todo
component.
With this step forward, we are now able to provide the to-do context in the App.tsx
file to finish up building the app. So, let’s use the context provider in the next part.
Provide the context
// App.tsx import * as React from 'react' import TodoProvider from './context/todoContext' import Todos from './containers/Todos' import AddTodo from './components/AddTodo' import './styles.css' export default function App() { return ( <TodoProvider> <main className='App'> <h1>My Todos</h1> <AddTodo /> <Todos /> </main> </TodoProvider> ) }
Here, we import the TodoProvider
component that wraps the consumers of the to-do context. That said, we can now access the todos
array and the function to add or update a to-do using the useContext
Hook in other components.
With this, we can now open the project on the terminal and run the following command:
yarn start
Or
npm start
If everything works, you’ll be able to see this in the browser:
http://localhost:3000
Theming with the Context API
In this section, we’ll take another look at one more application of the Context API: theming.
// @types/theme.d.ts export type Theme = 'light' | 'dark'; export type ThemeContextType = { theme: Theme; changeTheme: (theme: Theme) => void; }; };
We create the types necessary for implementing theming: Theme
specifies the possible theme modes and ThemeContextType
specifies the properties that will be available in the theme context as we consume it.
Next, we create a themeContext.tsx
file exports the raw theme context and its provider.
// context/themeContext.tsx import * as React from 'react'; import { Theme, ThemeContextType } from '../@types/theme'; export const ThemeContext = React.createContext<ThemeContextType | null>(null); const ThemeProvider: React.FC<React.ReactNode> = ({ children }) => { const [themeMode, setThemeMode] = React.useState<Theme>('light'); return ( <ThemeContext.Provider value={{ theme: themeMode, changeTheme: setThemeMode }}> {children} </ThemeContext.Provider> ); }; export default ThemeProvider;
After creating the context, we create a ThemeWrapper
component that will both consume it and also toggle the theme on the app.
// components/ThemeWrapper.tsx import React from 'react'; import { ThemeContextType, Theme } from '../@types/theme'; import { ThemeContext } from '../context/themeContext'; const ThemeWrapper: React.FC = ({ children }) => { const { theme, changeTheme } = React.useContext(ThemeContext) as ThemeContextType; const handleThemeChange = (event: React.ChangeEvent<HTMLSelectElement>) => { changeTheme(event.target.value as Theme); }; return ( <div data-theme={theme}> <select name="toggleTheme" onChange={handleThemeChange}> <option value="light">Light</option> <option value="dark">Dark</option> </select> {children} </div> ); }; export default ThemeWrapper;
With ThemeWrapper
ready, we can now import it into App.tsx
and test it out.
// App.tsx import * as React from 'react'; import TodoProvider from './context/todoContext'; import ThemeProvider from './context/themeContext'; import Todos from './containers/Todos'; import AddTodo from './components/AddTodo'; import ThemeWrapper from './components/ThemeWrapper'; import './styles.css'; export default function App() { return ( <ThemeProvider> <TodoProvider> <ThemeWrapper> <main className="App"> <h1>My Todos</h1> <AddTodo /> <Todos /> </main> </ThemeWrapper> </TodoProvider> </ThemeProvider> ); }
Once the data-theme
attribute is modified by the change handler of the select element, the prewritten CSS file takes of the rest.
You can find the source code here.
Conclusion
TypeScript is a great language that makes our code better. In this tutorial, we’ve learned how to use TypeScript with React Context. Hopefully, it helps you with your next project. Thanks for reading.
LogRocket: Full visibility into your production React apps
Debugging React applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket combines session replay, product analytics, and error tracking – empowering software teams to create the ideal web and mobile product experience. What does that mean for you?
Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay problems as if they happened in your own browser to quickly understand what went wrong.
No more noisy alerting. Smart error tracking lets you triage and categorize issues, then learns from this. Get notified of impactful user issues, not false positives. Less alerts, way more useful signal.
The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.
Modernize how you debug your React apps — start monitoring for free.
LogRocket: Full visibility into your web and mobile apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.
In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.
Try it for free.
For something closer to real life I would recommend using this pattern for context, it checks for initial context value and has more ergonomic type safety https://gist.github.com/JLarky/5a1642abd8741f2683a817f36dd48e78
I think there is a mistake here. Because you never expose your provider with
“
Checked yout gist, it’s neat 👍
Your example gives me errors while working in Typescript.
All in the return ” after “value”
Any suiggestions?