React Context

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();Code language: JavaScript (javascript)

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;Code language: JavaScript (javascript)

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>
);Code language: JavaScript (javascript)

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;Code language: JavaScript (javascript)

Refactoring the Todo app using Context

Let’s refactor the Todo app and use the React context:

React Context

In this diagram:

  • The App component will access the getTodos function from the context to fetch all the todo items.
  • The TodoList component will access the todos state to display all todo items.
  • The TodoShow component will access the changeTodo and removeTodo functions.
  • The TodoCreate component will access the createTodo function 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 };Code language: JavaScript (javascript)

How it works.

First, import the createContext and useState functions from the react library:

import { createContext, useState } from 'react';Code language: JavaScript (javascript)

Second, create a new Context object called TodosContext using the createContext function:

const TodosContext = createContext();Code language: JavaScript (javascript)

Third, define the Provider component:

const Provider = ({ children }) => {
   // ...
}Code language: JavaScript (javascript)

Fourth, create a state for the Provider component using the useState function:

const [todos, setTodos] = useState([]);Code language: JavaScript (javascript)

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 };Code language: JavaScript (javascript)

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>
);Code language: JavaScript (javascript)

Finally, export the TodosContext using the default export and Provider component using named export:

export default TodosContext;
export { Provider };Code language: JavaScript (javascript)

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>
);Code language: JavaScript (javascript)

How it works.

First, import the Provider component from the context:

import { Provider } from './context/todos';Code language: JavaScript (javascript)

Second, wrap the App component with the Provider component:

root.render(
  <Provider>
    <App />
  </Provider>
);Code language: JavaScript (javascript)

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;
Code language: JavaScript (javascript)

How it works.

First, import the TodosContext object:

import TodosContext from './context/todos';Code language: JavaScript (javascript)

Second, access the getTodos function from the TodosContext using the useContext() function:

const { getTodos } = useContext(TodosContext);Code language: JavaScript (javascript)

Third, call the getTodos function within the useEffect hook:

useEffect(() => {
  getTodos();
}, []);Code language: JavaScript (javascript)

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;Code language: JavaScript (javascript)

How it works.

First, import the TodosContext:

import TodosContext from '../context/todos';Code language: JavaScript (javascript)

Second, import the useContext function:

import { useContext } from 'react';Code language: JavaScript (javascript)

Third, access the todos array from the TodosContext:

const { todos } = useContext(TodosContext);Code language: JavaScript (javascript)

Finally, pass each todo item to the TodoShow component:

const renderedTodos = todos.map((todo) => {
  return <TodoShow key={todo.id} todo={todo} />;
});Code language: JavaScript (javascript)

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;Code language: JavaScript (javascript)

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';Code language: JavaScript (javascript)

Second, access the removeTodo and changeTodo functions from the context:

const { removeTodo, changeTodo } = useContext(TodosContext);Code language: JavaScript (javascript)

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;Code language: JavaScript (javascript)

How it works.

First, import the TodosContext object:

import TodosContext from './context/todos';Code language: JavaScript (javascript)

Second, access the createTodo function from the TodosContext using the useContext() function:

const { createTodo } = useContext(TodosContext);Code language: JavaScript (javascript)

Third, call the createTodo function to create a new todo item:

const handleSubmit = (e) => {
  e.preventDefault();
  createTodo(todo);
  setTodo('');
};Code language: JavaScript (javascript)

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.
Was this tutorial helpful ?