DEV Community

Cover image for State Management on React [Part 1] - Context API
Kevin Toshihiro Uehara
Kevin Toshihiro Uehara

Posted on • Edited on

State Management on React [Part 1] - Context API

Hi devs! How are you doing?

How are you doing meme gif

In this article let's talk a little bit about state management on React. Actually this article will be done in several parts, where I will talk about some technologies and libraries for state management. 🚀

Summary:

Introduction

I hope you guys like of this content, because we are talk about a lot of techs. We are going to create a simple application but we will be able to see the code of the technologies and compare the implementation.

I'll cover these technologies in each article:

We will use different projects but the same components, so in this first part I will create this components that we will use throughout other parts.

BUT, on this first part we will use the Context API where I would like to present one of the context api problems (re-render). We can controll the this re-render but it will be out of the box (like memo hooks etc). I will address this issue further on.

Context API according to the documentation of React:

Context is designed to share data that can be considered “global” for a tree of React components, such as the current authenticated user, theme, or preferred language

Previously the only way to share state in an application was using Redux (on initial of react, when we didn't even have hooks and use state on function components).

Now we can provide the state and share data on our tree of components in a simple way.

Now, how about we create our components and apply the context api? (Remember that this components we will on the another parts of articles. So, I will create all components in this article and in the other articles we go straight to the point for the state manager code)

To use the examples of state management, I will use the feature of dark mode change and a TODO list.

I will use Vite to create the project, using React + typescript, because of the simple boilerplate, setup and optimized by rollup. But it's just for personal preference.

And I will be using yarn

Show me the code

First just install the chrome extension for React:

React DevTools

  • Let's create the project:
yarn create vite react-context-api --template react-ts
Enter fullscreen mode Exit fullscreen mode
  • Install the dependencies:
cd react-context-api
yarn
Enter fullscreen mode Exit fullscreen mode
  • Running the project:
yarn dev
Enter fullscreen mode Exit fullscreen mode

Project Initial Setup

Let's create some directories for our components, hooks and providers.

Image description

  • components it will be used for our default components
  • hooks it will be used for our custom hooks
  • providers it will be used for our Context API provider

On components, let's create our first component called Button on Button/index.tsx and the style using CSS modules Button/Button.module.css

Button/index.tsx

import button from "./button.module.css";

interface ButtonProps {
  label: string;
  onClick?: VoidFunction;
}

export const Button = ({ label, onClick }: ButtonProps) => {
  return (
    <button className={button.btn} onClick={onClick}>
      {label}
    </button>
  );
};

Enter fullscreen mode Exit fullscreen mode

Button/Button.module.css

.btn {
    width: 150px;
    height: 40px;
    background-color: #1e40af;
    border: none;
    border-radius: 5px;
    cursor: pointer;
    color: #fff;
    font-family: Arial, Helvetica, sans-serif;
    font-size: medium;
    margin-left: 4px;
}

.btn:hover {
    background-color: #2563eb;
}
Enter fullscreen mode Exit fullscreen mode

Now, Let's create another button to change the theme (WHY?)
Because I want to show some problem of context api later...

ButtonChangeTheme/index.tsx

import { useTheme } from "../../hooks/useTheme";
import button from "./button.module.css";

interface ButtonChangeThemeProps {
  label: string;
}

export const ButtonChangeTheme = ({ label }: ButtonChangeThemeProps) => {
  return (
    <button className={button.btn} onClick={changeColor}>
      {label}
    </button>
  );
};
Enter fullscreen mode Exit fullscreen mode

ButtonChangeTheme/ButtonChangeTheme.module.css

.btn {
    width: 150px;
    height: 40px;
    background-color: #4c1d95;
    border: none;
    border-radius: 5px;
    cursor: pointer;
    color: #fff;
    font-family: Arial, Helvetica, sans-serif;
    font-size: medium;
    margin-left: 4px;
}

.btn:hover {
    background-color: #7c3aed;
}
Enter fullscreen mode Exit fullscreen mode

Now on context components of our Todo form/list:
Creating the Input component:

Input/index.tsx

import style from "./Input.module.css";

interface InputProps {
  value?: string;
  label: string;
  placeholder?: string;
  onChange?: (evt: React.ChangeEvent<HTMLInputElement>) => void;
}

export const Input = ({ label, placeholder, onChange, value }: InputProps) => {
  return (
    <div className={style.inputContainer}>
      <p className={style.inputLabel}>{label}</p>
      <input
        value={value}
        className={style.input}
        type="text"
        name={label}
        id={label}
        placeholder={placeholder}
        onChange={onChange}
      />
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

Input/Input.module.css

.inputContainer {
    font-family: Arial, Helvetica, sans-serif;
    margin:0;
    padding:0;
    display: flex;
    flex-direction: column;
    width: 20%;
}

.inputLabel {
    font-size: 18px;
    margin-bottom: 4px;
}

.input {
    height: 30px;
    border: 1px solid #000;
    border-radius: 4px;
}
Enter fullscreen mode Exit fullscreen mode

Now creating the FormTodo to integrate our Input and Button component:

Form/index.tsx

import style from "./Form.module.css";
import { Button } from "../Button";
import { Input } from "../Input";
import { useState } from "react";

export const FormTodo = () => {
  const [todo, setTodo] = useState("");

  const handleAddTodo = () => {
    addTodo(todo);
    setTodo("");
  };

  return (
    <div className={style.formContainer}>
      <Input
        value={todo}
        label="Todo"
        onChange={(evt) => setTodo(evt.target.value)}
      />
      <Button label="Adicionar" />
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

Form/Form.module.css

.formContainer {
    display: flex;
    align-items: flex-end;
    margin-left: 10px;
}
Enter fullscreen mode Exit fullscreen mode

Now our list of todos:

ListTodo/index.tsx

import style from "./ListTodo.module.css";

export const ListTodo = () => {

  return (
    <ul>
      {[].map((todo) => (
        <li className={style.item} key={todo.id}>
          <label>{todo.label}</label>
          <i className={style.removeIcon} onClick={() => console.log('we will remove todo item')} />
        </li>
      ))}
    </ul>
  );
};
Enter fullscreen mode Exit fullscreen mode

ListTodo/ListTodo.module.css

.item {
    display: flex;
    align-items: center;
}

.removeIcon::after {
    display: inline-block;
    content: "\00d7"; 
    font-size: 20px;
    margin-top: -2px;
    margin-left: 5px;
}

.removeIcon:hover::after {
    color: red;
    cursor: pointer;
}
Enter fullscreen mode Exit fullscreen mode

Now the last component, just for use the dark mode container. We will modify to use the custom hook to change the style for dark mode.
Let's create the Content simple component:

Content/index.tsx

interface ContentProps {
  text: string;
}

export const Content = ({ text }: ContentProps) => {
  return (
    <div
      style={{
        height: "30vh",
        width: "100vw",
        color: "#111827",
        backgroundColor: "#fff",
      }}
    >
      {text}
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

FINNALY all components that we will use along the articles are ready!!!

Image description

Now finnally, let's create our Context API provider for theme change:

Let's create a file on providers/ called theme/ThemeProvider.tsx and a file with types called theme/types.ts:

providers/theme/types.ts

export interface ThemeProviderState {
  isDark: boolean;
  changeColor?: VoidFunction;
}
Enter fullscreen mode Exit fullscreen mode

providers/theme/ThemeProvider.tsx

import { PropsWithChildren, createContext, useState } from "react";
import { ThemeProviderState } from "./types";

const defaultValues: ThemeProviderState = {
  isDark: false,
};

export const ThemeContext = createContext<ThemeProviderState>(defaultValues);

export const ThemeContextProvider: React.FC<PropsWithChildren> = ({
  children,
}) => {
  const [isDark, setIsDark] = useState<boolean>(false);

  const changeColor = () => {
    setIsDark(!isDark);
  };

  return (
    <ThemeContext.Provider value={{ isDark, changeColor }}>
      {children}
    </ThemeContext.Provider>
  );
};
Enter fullscreen mode Exit fullscreen mode

The theme provider will "provide" the value if theme is dark and a function to change the theme color. Simple, isn’t it?

Just for a better files organization, let's create a new file on hooks called useTheme for our custom hook:

hooks/useTheme.tsx

import { useContext } from "react";
import { ThemeContext } from "../providers/theme/ThemeProvider";

export const useTheme = () => {
  return useContext(ThemeContext);
};
Enter fullscreen mode Exit fullscreen mode

easy peasy!

Now, we can create a new provider for the manage the state of todos. So let's create on providers our TodoProvider with its type.

providers/todo/types.ts

export interface ITodo {
  id: number;
  label: string;
  done: boolean;
}

export interface TodoProviderState {
  todos: ITodo[];
  addTodo: (label: string) => void;
  removeTodo: (id: number) => void;
}
Enter fullscreen mode Exit fullscreen mode

providers/todo/TodoProvider.tsx

import React, {
  PropsWithChildren,
  createContext,
  useCallback,
  useState,
} from "react";
import { ITodo, TodoProviderState } from "./types";

const defaultTodoStateValues: TodoProviderState = {
  todos: [],
  addTodo: () => null,
  removeTodo: () => null,
};

export const TodoContext = createContext<TodoProviderState>(
  defaultTodoStateValues
);

export const TodoProvider: React.FC<PropsWithChildren> = ({ children }) => {
  const [todos, setTodos] = useState<ITodo[]>([]);

  const addTodo = useCallback((label: string) => {
    const newTodo = {
      label,
      id: Math.random() * 100,
      done: false,
    };
    setTodos((todos) => [...todos, newTodo]);
  }, []);

  const removeTodo = (id: number) => {
    const removed = todos.filter((todo) => todo.id !== id);
    setTodos(removed);
  };

  return (
    <TodoContext.Provider value={{ todos, addTodo, removeTodo }}>
      {children}
    </TodoContext.Provider>
  );
};
Enter fullscreen mode Exit fullscreen mode

God bless Context API to provide a easy way to create and manage our states. And again, let's create our custom hook called useTodo on hooks

hooks/useTodo.tsx

import { useContext } from "react";
import { TodoContext } from "../providers/todo/TodoProvider";

export const useTodo = () => {
  return useContext(TodoContext);
};
Enter fullscreen mode Exit fullscreen mode

Now let's use our providers wrapping our app on main.tsx:

main.tsx

import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.tsx";
import "./index.css";
import { ThemeContextProvider } from "./providers/theme/ThemeProvider.tsx";
import { TodoProvider } from "./providers/todo/TodoProvider.tsx";
import { ButtonChangeTheme } from "./components/ButtonChangeTheme/index.tsx";
import { Content } from "./components/Content/index.tsx";

ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
  <React.StrictMode>
    <ThemeContextProvider>
      <ButtonChangeTheme label="Change theme" />
      <Content text="Hello World!" />
      <TodoProvider>
        <App />
      </TodoProvider>
    </ThemeContextProvider>
  </React.StrictMode>
);
Enter fullscreen mode Exit fullscreen mode

And change the App.tsx to just use the FormTodo and ListTodo components:

src/App.tsx

import { FormTodo } from "./components/FormTodo";
import { ListTodo } from "./components/ListTodo";

function App() {
  return (
    <>
      <FormTodo />
      <ListTodo />
    </>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Now we need just replace in some components to use our custom hooks:

First, on ButtonChangeTheme component just add the custom hook useTheme and call on button the changeColor function:

components/ButtonChangeTheme/index.tsx

import { useTheme } from "../../hooks/useTheme";
import button from "./button.module.css";

interface ButtonChangeThemeProps {
  label: string;
}

export const ButtonChangeTheme = ({ label }: ButtonChangeThemeProps) => {
  const { changeColor } = useTheme();
  return (
    <button className={button.btn} onClick={changeColor}>
      {label}
    </button>
  );
};
Enter fullscreen mode Exit fullscreen mode

on FormTodo adding the useTodo to add a todo item:

components/FormTodo/index.tsx

import style from "./Form.module.css";
import { Button } from "../Button";
import { Input } from "../Input";
import { useState } from "react";
import { useTodo } from "../../hooks/useTodo";

export const FormTodo = () => {
  const [todo, setTodo] = useState("");
  const { addTodo } = useTodo();

  const handleAddTodo = () => {
    addTodo(todo);
    setTodo("");
  };

  return (
    <div className={style.formContainer}>
      <Input
        value={todo}
        label="Todo"
        onChange={(evt) => setTodo(evt.target.value)}
      />
      <Button label="Adicionar" onClick={handleAddTodo} />
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

On ListTodo component use the useTodo to list all todos items (remember that we create with empty array):

components/ListTodo/index.tsx

import { useTodo } from "../../hooks/useTodo";
import style from "./ListTodo.module.css";

export const ListTodo = () => {
  const { removeTodo, todos } = useTodo();

  return (
    <ul>
      {todos.map((todo) => (
        <li className={style.item} key={todo.id}>
          <label>{todo.label}</label>
          <i className={style.removeIcon} onClick={() => removeTodo(todo.id)} />
        </li>
      ))}
    </ul>
  );
};
Enter fullscreen mode Exit fullscreen mode

And for last, let's change the Content component that will use the useTheme to change the background color using css-in-js:

import { useTheme } from "../../hooks/useTheme";

interface ContentProps {
  text: string;
}

export const Content = ({ text }: ContentProps) => {
  const { isDark } = useTheme();

  return (
    <div
      style={{
        height: "30vh",
        width: "100vw",
        color: isDark ? "#fff" : "#111827",
        backgroundColor: isDark ? "#111827" : "#fff",
      }}
    >
      {text}
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

Now we finnaly (for second time lol) finish second version of the app. BUT using context api.

Sponge bob gif tired

Let's see on browser the result (if you need just run yarn dev):

Image description

Simple app, isn’t it?
But now we can see all integrations working with providers and custom hooks...

That's all folks!

HAAAA I'm kidding. 🤡
Let's talk about some problem of context api:

Trade offs

Open the devtools and enable the highlight of react components render:

Enable highlight react render component on devtools

Now Let's see the components render behaviour:

Image description

We can see that when we update the theme, all components bellow are re-render. And you can think: Just use a Memo! And yeah! It's a solution. But imagine when you have a lot of complex providers and components. Maybe can be harder to maintain the re-renders.
And I consider a solution out of the box for state manager of context api.

The re-render just happens because on main.tsx we are wrapping all components using the ThemeProvider. So when the some state of Provider changes, all children components will re-render.

Image description

Conclusion

Context API offers a simple and incredible way for we manage our global state of our application (and is native of react, we don't need to install any dependency), but we need to be careful with renderings and break into small providers to avoid the complex render unnecessary components.

This first part I used to explain the native solution to manage our state on React Applications.

In the next parts, I will introduce some librarys that will provide the same global state management, but we will see different implementations, devX and fix the problem of re-render. (Spoiler) Let's build using Redux with redux toolkit (recent library) that will make implementation easier.

Remember, that all components that we created here, we will use in the next articles. So I will do not repeat the component creations, I will assume that you know what component we will change.

Now it's true! We really finish this first part of article!
That's all folks!

I hope you enjoyed it and added some knowledge. See you in the next parts

Some references:

Top comments (0)