Ibrahima Ndaw JavaScript enthusiast, full-stack developer, and blogger who also dabbles in UI/UX design.

How to use React Context with TypeScript

7 min read 2089

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:

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.


More great articles from LogRocket:


// 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.

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

The to-do provider we built

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>
  );
}

Example of theming with React Context

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.

Full visibility into 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 is like a DVR for web and mobile apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

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 — .

Writing a lot of TypeScript? Watch the recording of our recent TypeScript meetup to learn about writing more readable code.

TypeScript brings type safety to JavaScript. There can be a tension between type safety and readable code. Watch the recording for a deep dive on some new features of TypeScript 4.4.

Ibrahima Ndaw JavaScript enthusiast, full-stack developer, and blogger who also dabbles in UI/UX design.

4 Replies to “How to use React Context with TypeScript”

  1. Your example gives me errors while working in Typescript.
    All in the return ” after “value”

    Any suiggestions?

Leave a Reply