Summary: in this tutorial, you will learn about React context and how to use it to share state across your entire React app.
Introduction to React Context #
React context is a feature in React that allows you to share the state across your entire React app (or part of it) without passing props down through every level of the component tree.
React context is useful when many components at different nesting levels must access a piece of state.
Here’s a step-by-step guide to implementing React context.
Step 1. Create a context #
First, create a context using the createContext() function from the react library:
// MyContext.js
import { createContext } fronm 'react';
const MyContext = createContext();Step 2. Create a Provider component #
The provider component will use the context object and provide the shared state to its child components.
// MyProvider.js
import { useState } from 'react';
import MyContext from './MyContext';
const MyProvider = ({ children }) => {
const [state, setState] = useState();
const shared = {state, setState };
return (
<MyContext.Provider value={shared}>
{children}
</MyContext.Provider>
);
};
export default MyProvider;In this example, the Provider component will share the object (shared) that includes the state and setState function to its child components.
Besides the state and setState function, you can share any functions by placing them in the shared object.
Step 3. Use the Provider component in your App #
Wrap the component tree within the Provider component to make the context object available to all child components:
// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import MyProvider from './MyProvider';
const el = document.querySelector('#root');
const root = ReactDOM.createRoot(el);
root.render(
<MyProvider>
<App />
</MyProvider>
);In this example, the App components and all of its child components will be able to access the MyContext object shared by the Provider component.
Step 4. Consume the Context in a component #
The following illustrates how to consume (or use or access) the MyContext object from a child component of the Provider component:
import { useContext } from 'react';
import MyContext from './MyContext';
const MyComponent = () => {
const { state, setState } = useContext(MyContext);
return (
<div>
<p>{state.someValue}</p>
<button onClick={() => setState({ ...state, someValue: 'New Value' })}>
Update Value
</button>
</div>
);
};
export default MyComponent;Refactoring the Todo app using Context #
Let’s refactor the Todo app and use the React context:
In this diagram:
- The App component will access the
getTodosfunction from the context to fetch all the todo items. - The TodoList component will access the
todosstate to display all todo items. - The TodoShow component will access the
changeTodoandremoveTodofunctions. - The TodoCreate component will access the
createTodofunction to create a new todo item.
1) Creating a React context #
First, create a new directory called context within the src directory.
Second, create a todos.js file that will store the context object and Provider component:
import { createContext, useState } from 'react';
const TodosContext = createContext();
const Provider = ({ children }) => {
const [todos, setTodos] = useState([]);
const getTodos = async () => {
const url = 'http://localhost:5000/todos';
try {
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error('Network response was not ok');
}
const storedTodos = await response.json();
// Update the state
if (storedTodos) setTodos(storedTodos);
} catch (error) {
console.error('Error during GET request:', error);
}
};
const createTodo = async (title) => {
// call an API to create a new todo
const url = 'http://localhost:5000/todos';
try {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
title: title,
completed: false,
}),
});
if (!response.ok) {
throw new Error('Network response was not ok');
}
const responseData = await response.json();
const newTodo = { ...responseData };
// add the new todo to the list
const updatedTodos = [...todos, newTodo];
setTodos(updatedTodos);
} catch (error) {
console.error('Error creating a todo:', error);
}
};
const removeTodo = async (id) => {
// Delete the todo
const url = `http://localhost:5000/todos/${id}`;
try {
const response = await fetch(url, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error('Network response was not ok');
}
// Update the state
const updatedTodos = todos.filter((todo) => todo.id !== id);
setTodos(updatedTodos);
} catch (error) {
console.error('Error during DELETE request:', error);
throw error;
}
};
const changeTodo = async (id, newTitle, completed = false) => {
// Update todo
const url = `http://localhost:5000/todos/${id}`;
const data = { title: newTitle, completed };
try {
const response = await fetch(url, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
if (!response.ok) {
throw new Error('Network response was not ok');
}
const responseData = await response.json();
// Update the state
const updatedTodos = todos.map((todo) => {
if (todo.id === id) {
return { ...todo, ...responseData };
}
return todo;
});
setTodos(updatedTodos);
} catch (error) {
console.error('Error during PUT request:', error);
throw error;
}
};
const shared = { todos, getTodos, createTodo, removeTodo, changeTodo };
return (
<TodosContext.Provider value={shared}>{children}</TodosContext.Provider>
);
};
export default TodosContext;
export { Provider };How it works.
First, import the createContext and useState functions from the react library:
import { createContext, useState } from 'react';Second, create a new Context object called TodosContext using the createContext function:
const TodosContext = createContext();Third, define the Provider component:
const Provider = ({ children }) => {
// ...
}Fourth, create a state for the Provider component using the useState function:
const [todos, setTodos] = useState([]);Fifth, move the getTodos, createTodo, removeTodo, and changeTodo functions from the App component to the Provider component.
Sixth, define an object to be shared across components that state and functions:
const shared = { todos, getTodos, createTodo, removeTodo, changeTodo };Seventh, pass the shared object to the value prop of the Provider component and return the Provider component:
return (
<TodosContext.Provider value={shared}>{children}</TodosContext.Provider>
);Finally, export the TodosContext using the default export and Provider component using named export:
export default TodosContext;
export { Provider };Use the Provider component #
Modify the index.js to wrap the App component with the Provider component in the index.js file:
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { Provider } from './context/todos';
const el = document.querySelector('#root');
const root = ReactDOM.createRoot(el);
root.render(
<Provider>
<App />
</Provider>
);How it works.
First, import the Provider component from the context:
import { Provider } from './context/todos';Second, wrap the App component with the Provider component:
root.render(
<Provider>
<App />
</Provider>
);The App component and all child components will be able to access the TodosContext object provided by the Provider component.
Modifying App component #
Change the App component to the following:
import TodoList from './components/TodoList';
import TodoCreate from './components/TodoCreate';
import { useEffect, useContext } from 'react';
import TodosContext from './context/todos';
import './App.css';
const App = () => {
const { getTodos } = useContext(TodosContext);
useEffect(() => {
getTodos();
}, []);
return (
<main className="main">
<h1>
React Todo <span>Streamline Your Day, the React Way!</span>
</h1>
<TodoList />
<TodoCreate />
</main>
);
};
export default App;How it works.
First, import the TodosContext object:
import TodosContext from './context/todos';Second, access the getTodos function from the TodosContext using the useContext() function:
const { getTodos } = useContext(TodosContext);Third, call the getTodos function within the useEffect hook:
useEffect(() => {
getTodos();
}, []);Modifying TodoList component #
Modify the TodoList component that uses the TodosContext:
import TodoShow from './TodoShow';
import TodosContext from '../context/todos';
import { useContext } from 'react';
const TodoList = () => {
const { todos } = useContext(TodosContext);
const renderedTodos = todos.map((todo) => {
return <TodoShow key={todo.id} todo={todo} />;
});
return <ul className="todo-list">{renderedTodos}</ul>;
};
export default TodoList;How it works.
First, import the TodosContext:
import TodosContext from '../context/todos';Second, import the useContext function:
import { useContext } from 'react';Third, access the todos array from the TodosContext:
const { todos } = useContext(TodosContext);Finally, pass each todo item to the TodoShow component:
const renderedTodos = todos.map((todo) => {
return <TodoShow key={todo.id} todo={todo} />;
});Modifying TodoShow component #
Change the TodoShow.js component to use the TodosContext:
import { useState } from 'react';
import TodoEdit from './TodoEdit';
import EditIcon from '../edit.svg';
import DeleteIcon from '../delete.svg';
import { useContext } from 'react';
import TodosContext from '../context/todos';
const TodoShow = ({ todo }) => {
const [showEdit, setShowEdit] = useState(false);
const { removeTodo, changeTodo } = useContext(TodosContext);
const handleDelete = (e) => {
removeTodo(todo.id);
};
const handleEdit = (e) => {
setShowEdit(!showEdit);
};
const handleSubmit = (id, title) => {
changeTodo(id, title);
setShowEdit(false);
};
const handleDoubleClick = (e) => {
e.preventDefault();
changeTodo(todo.id, todo.title, !todo.completed);
setShowEdit(false);
};
if (showEdit)
return (
<li className="todo">
<TodoEdit todo={todo} onSubmit={handleSubmit} />
</li>
);
else {
return (
<li className="todo" onDoubleClick={handleDoubleClick}>
<p className={todo.completed ? 'completed' : 'open'}>{todo.title}</p>
<div className="actions">
<button onClick={handleDelete}>
<img src={DeleteIcon} title="Delete" />
</button>
<button onClick={handleEdit}>
<img src={EditIcon} title="Edit" />
</button>
</div>
</li>
);
}
};
export default TodoShow;How it works.
First, import the useContext function from the react library and TodosContext object from the todos.js module:
import { useContext } from 'react';
import TodosContext from '../context/todos';Second, access the removeTodo and changeTodo functions from the context:
const { removeTodo, changeTodo } = useContext(TodosContext);Modify the TodoCreate component #
Modify the TodoCreate component to use the TodosContext object:
import { useState, useContext } from 'react';
import TodosContext from '../context/todos';
const TodoCreate = () => {
const [todo, setTodo] = useState('');
const { createTodo } = useContext(TodosContext);
const handleSubmit = (e) => {
e.preventDefault();
createTodo(todo);
setTodo('');
};
const handleChange = (e) => {
setTodo(e.target.value);
};
return (
<form onSubmit={handleSubmit} className="todo-create">
<input
type="text"
name="todo"
id="todo"
placeholder="Enter a todo"
value={todo}
onChange={handleChange}
/>
</form>
);
};
export default TodoCreate;How it works.
First, import the TodosContext object:
import TodosContext from './context/todos';Second, access the createTodo function from the TodosContext using the useContext() function:
const { createTodo } = useContext(TodosContext);Third, call the createTodo function to create a new todo item:
const handleSubmit = (e) => {
e.preventDefault();
createTodo(todo);
setTodo('');
};Download the project source code #
Download the React Todo App using Context
Summary #
- Use React context to share the state across your entire React app.
Thank you for your feedback!