Hi devs! How are you doing?
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:
- Let's create the project:
yarn create vite react-context-api --template react-ts
- Install the dependencies:
cd react-context-api
yarn
- Running the project:
yarn dev
Let's create some directories for our components, hooks and providers.
-
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>
);
};
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;
}
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>
);
};
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;
}
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>
);
};
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;
}
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>
);
};
Form/Form.module.css
.formContainer {
display: flex;
align-items: flex-end;
margin-left: 10px;
}
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>
);
};
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;
}
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>
);
};
FINNALY all components that we will use along the articles are ready!!!
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;
}
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>
);
};
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);
};
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;
}
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>
);
};
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);
};
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>
);
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;
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>
);
};
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>
);
};
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>
);
};
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>
);
};
Now we finnaly (for second time lol) finish second version of the app. BUT using context api.
Let's see on browser the result (if you need just run yarn dev
):
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:
Now Let's see the components render behaviour:
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.
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)