How to use React Context

H
andrewgbliss
a month ago

This article will cover how to use React context and easily use state within our components.

. . .

Github repo

https://github.com/EntryLevelDeveloperTraining/todos

What is React Context?

React context is a way to use state management without having to pass down props to each child component. You can declare context at the top level higher order component and then use that state in any of the children of that component making state management easily accessible and your components cleaner.

Making a store

Let's create a Store.js file with this contents:

import React, { useContext, createContext, useState } from "react";

const Context = createContext({
  todos: [],
});

const Provider = (props) => {
  const { children } = props;
  const [todos, setTodos] = useState([
    {
      id: 0,
      text: "feed the dog",
      completed: false,
    },
    {
      id: 1,
      text: "go shopping",
      completed: false,
    },
    {
      id: 2,
      text: "hang glide",
      completed: false,
    },
  ]);
  const addTodo = (text) => {
    const nextId =
      todos.length > 0 ? Math.max(...todos.map((t) => t.id)) + 1 : 0;
    const newTodo = {
      id: nextId,
      text,
      completed: false,
    };
    setTodos([...todos, newTodo]);
  };
  const removeTodo = (id) => {
    const newTodos = todos.filter((t) => t.id !== id);
    setTodos(newTodos);
  };
  const toggleTodo = (id) => {
    const foundTodo = todos.find((t) => t.id === id);
    if (foundTodo) {
      foundTodo.completed = !foundTodo.completed;
    }
    const newTodos = todos.map((t) => {
      if (t.id === id) {
        return foundTodo;
      }
      return t;
    });
    setTodos(newTodos);
  };
  return (
    <Context.Provider value={{ todos, addTodo, removeTodo, toggleTodo }}>
      {children}
    </Context.Provider>
  );
};

export const useTodos = () => useContext(Context);

export const withProvider = (Component) => {
  return (props) => {
    return (
      <Provider>
        <Component {...props} />
      </Provider>
    );
  };
};

This is the context and the provider. To use context you must also create a provider. Here is how to create the context.

Create Context

import React, { createContext } from "react";
const Context = createContext({
  todos: [],
});

React gives us a function to create a context. You then pass it any type of data you want to store as state you want to use throughout your components.

Using the withProvider

export const withProvider = (Component) => {
  return (props) => {
    return (
      <Provider>
        <Component {...props} />
      </Provider>
    );
  };
};

This withProvider function will take in a component and wrap that component with the Provider component. Doing this will enable everything within the provider to have access to the context.

Now in our Todos component we can wrap the whole thing in the provider and use context anywhere in our components.

import React from "react";
import Box from "@material-ui/core/Box";
import Grid from "@material-ui/core/Grid";
import TodoInput from "./components/TodoInput";
import TodoList from "./components/TodoList";
import { withProvider } from "./store/Store";

const Todos = () => {
  return (
    <Box p={2}>
      <Grid container direction="column">
        <Grid item>
          <TodoInput />
        </Grid>
        <Grid item>
          <TodoList />
        </Grid>
      </Grid>
    </Box>
  );
};

export default withProvider(Todos);

We don't have to pass props down to each child component.

Custom Context Hook

Usually you would use the function useContext, but here we will export a custom hook called useTodos that uses the normal useContext function.

export const useTodos = () => useContext(Context);

Doing this will enable us to use the todos context anywhere in our components.

Helper functions

We also setup some helper function called addTodo, removeTodo, and toggleTodo. By having this in the context we can use any of these functions, that change the todos state, anywhere in our components.

TodoList with Context

Here is the updated version of the TodoList component using Context instead of Props.

import React, { useState, useMemo } from "react";
import List from "@material-ui/core/List";
import ListItem from "@material-ui/core/ListItem";
import ListItemSecondaryAction from "@material-ui/core/ListItemSecondaryAction";
import ListItemText from "@material-ui/core/ListItemText";
import Checkbox from "@material-ui/core/Checkbox";
import IconButton from "@material-ui/core/IconButton";
import Button from "@material-ui/core/Button";
import DeleteIcon from "@material-ui/icons/Delete";
import Box from "@material-ui/core/Box";
import { useTodos } from "../store/Store";

const TodoList = () => {
  const [filter, setFilter] = useState("all");
  const { todos, toggleTodo, removeTodo } = useTodos();
  const filteredTodos = useMemo(() => {
    if (filter === "all") {
      return todos;
    } else if (filter === "completed") {
      return todos.filter((t) => t.completed);
    } else if (filter === "not_completed") {
      return todos.filter((t) => !t.completed);
    }
  }, [todos, filter]);
  return (
    <>
      <List>
        {filteredTodos.map((todo) => {
          return (
            <ListItem key={todo.id}>
              <ListItemText primary={todo.text} />
              <ListItemSecondaryAction>
                <Checkbox
                  checked={todo.completed}
                  onClick={() => toggleTodo(todo.id)}
                />
                <IconButton onClick={() => removeTodo(todo.id)}>
                  <DeleteIcon />
                </IconButton>
              </ListItemSecondaryAction>
            </ListItem>
          );
        })}
      </List>
      <Box pr={1} component="span">
        <Button
          variant="contained"
          color="secondary"
          onClick={() => setFilter("all")}
        >
          All
        </Button>
      </Box>
      <Box pr={1} component="span">
        <Button
          variant="contained"
          color="secondary"
          onClick={() => setFilter("completed")}
        >
          Completed
        </Button>
      </Box>
      <Box component="span">
        <Button
          variant="contained"
          color="secondary"
          onClick={() => setFilter("not_completed")}
        >
          Due
        </Button>
      </Box>
    </>
  );
};

export default TodoList;

This will use the useTodos context, now we can use removeTodo and toggleTodo from the context. So anytime they click on delete it will call the context helper function and update everywhere the todos are used. We also bring in useMemo and cache the filtered todos, anytime the todos change it will recalculate and return a new filtered list of todos.

TodoInput with Context

We can also now update the TodoInput component to use Context.

import React, { useState } from "react";
import { makeStyles } from "@material-ui/core";
import Grid from "@material-ui/core/Grid";
import TextField from "@material-ui/core/TextField";
import Button from "@material-ui/core/Button";
import Box from "@material-ui/core/Box";
import { useTodos } from "../store/Store";

const useStyles = makeStyles((theme) => ({
  textField: {
    width: 400,
    [theme.breakpoints.down("xs")]: {
      width: 200,
    },
  },
}));

const TodoInput = (props) => {
  const { addTodo } = useTodos();
  const classes = useStyles();
  const [newTodo, setNewTodo] = useState("");
  const onClick = () => {
    addTodo(newTodo);
    setNewTodo("");
  };
  return (
    <>
      <Grid container>
        <Grid item>
          <TextField
            className={classes.textField}
            label="What do you want to do?"
            variant="outlined"
            size="small"
            value={newTodo}
            onChange={(e) => setNewTodo(e.target.value)}
          />
        </Grid>
        <Grid item>
          <Box pl={1}>
            <Button
              disabled={newTodo.length === 0}
              variant="contained"
              color="primary"
              onClick={onClick}
            >
              Add Todo
            </Button>
          </Box>
        </Grid>
      </Grid>
    </>
  );
};

export default TodoInput;

This makes it easy to add a new todo to the context. It will then update the context and the TodoList component with the new todo.

Conclusion

Context is a way to clean up your code so you don't have to pass down props to every child component. Sometimes you won't need to go this far, but it is nice to have all your state in one place that is easily accessible in components.


H
andrewgbliss
a month ago