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

How to use React Context with TypeScript

5 min read 1431

React Context Typescript

TypeScript has become increasingly popular due to numerous benefits recently added to the code, such as static type-checking robustness, understandability, and type interface.

And 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. Let’s dive in.

Prerequisites

To get the most out of this tutorial, you need a basic understanding of React and TypeScript. If you’re new to web development, you can start with this React TypeScript tutorial to get familiar with these technologies.

Setting up

To create a new app, I 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 enable TypeScript when using Create React App, you need to add the flag --template typescript, otherwise the app will support only JavaScript.

Next, let’s structure the project as follows:

├── src
|  ├── components
|  |  ├── AddTodo.tsx
|  |  └── Todo.tsx
|  ├── containers
|  |  └── Todos.tsx
|  ├── context
|  |  └── todoContext.tsx
|  ├── App.tsx
|  ├── index.css
|  ├── index.tsx
|  ├── react-app-env.d.ts
|  └── type.d.ts
├── tsconfig.json
├── package.json
└── yarn.lock

Here, there are two files to underline:

  • The context/todoContext.tsx file that serves as a context for the project
  • The type.d.ts file that contains the TypeScript Types. The extension .d.ts allows using the types in other files without importing them

With this in place, we can now get our hands dirty and code something meaningful.

We made a custom demo for .
No really. Click here to check it out.

Create the to-do type

TypeScript Type allows you to define what a variable or function should expect as a value in order to help the compiler catch errors before runtime.

type.d.ts

interface ITodo {
  id: number
  title: string
  description: string
  status: boolean
}

type ContextType = {
  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 ContextType 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";

export const TodoContext = React.createContext(null);

const TodoProvider: React.FC = ({ children }) => {
  const [todos, setTodos] = React.useState<ITodo[]>([
    {
      id: 1,
      title: "post 1",
      description: "this is my first description",
      status: false
    },
    {
      id: 2,
      title: "post 2",
      description: "this is my second description",
      status: true
    }
  ]);

Here, we start by creating a new context and set its type to match ContextType or null. The latter is using here to initialize the context with a null value. 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 (
    
      {children}
    
  );
};

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'

export const TodoContext = React.createContext(null)

const TodoProvider: React.FC = ({ 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 (
    
      {children}
    
  )
}

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

components/AddTodo.tsx

import * as React from 'react'
import { TodoContext } from '../context/todoContext'

const AddTodo: React.FC = () => {
  const { saveTodo } = React.useContext(TodoContext) as ContextType
  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

Here, we have a form component that allows handling 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.

Note that I use typecasting on the useContext hook to avoid TypeScript throwing errors because the context will be null at the beginning.

components/Todo.tsx

import * as React from 'react'

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 to-do 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 { TodoContext } from '../context/todoContext'
import Todo from '../components/Todo'

const Todos = () => {
  const { todos, updateTodo } = React.useContext(TodoContext) as ContextType
  return (
    <>
      {todos.map((todo: ITodo) => (
        
      ))}
    </>
  )
}

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 to the Todo component the object to show. 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

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:

Todo Provider React Context
Here’s a preview of your to-do app

Great! With that final touch, we have now finished building a to-do app using React Context and TypeScript.

You can find the finished project 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 difficult 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 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 — .

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

One Reply to “How to use React Context with TypeScript”

Leave a Reply