How to Beat useState
Overload with useReducer
The Trouble with Too Much useState
In React, we often start by using useState
for everything. It's like our go-to tool for keeping track of stuff that changes in our app. But when our app starts getting bigger and we're juggling lots of these useState
hooks, things can get messy real fast. It's like trying to cook in a kitchen where every single ingredient is out on the counter. Chaos, right?
Why Too Many useState
Can Be a Headache
- It Gets Complicated: Imagine you’re trying to keep track of a bunch of stuff that all depends on each other. Updating them separately can turn your code into a tangled mess.
- Slowdowns: If you’re updating a bunch of these states separately, your app might start lagging because it’s working overtime.
- Things Can Get Weird: Managing related stuff with separate
useState
hooks can sometimes make your app act up in ways you didn't expect. - Repeating Yourself: You might end up writing the same kind of state management logic in more than one place, which is a pain to update and keep track of.
Enter useReducer
useReducer
is like hiring an organizer for your app's state management. It's especially great when the changes in one part of your app depend a lot on changes in another.
How useReducer
Helps
- It Makes State Updates Predictable: With
useReducer
, all your updates go through one place, so you always know what's happening and when. - Better Performance: Because it’s more efficient, your app runs smoother, even with complex state logic.
- Grows with You:
useReducer
is awesome for managing state that's shared across components or is just plain complicated. - Easier to Keep Track: Putting all your state logic in one spot makes your code cleaner and your life easier when it’s time to change things.
- Simpler Testing: Testing your state logic is straightforward because you can focus on the reducer function, seeing how different actions change your app’s state.
Using useReducer
: A Before-and-After Story
Before: A simple example with useState
Let’s say you’ve got a simple app where you can add todos, mark them as done, and filter them. Starting with useState
, you've got hooks for todos, hooks for filters, and your code is starting to look like this:
"use client";
import React, { useState, KeyboardEvent } from "react";
type Todo = {
id: number;
text: string;
completed: boolean;
}
const TodoAppWithState: React.FC = () => {
const [todos, setTodos] = useState<Todo[]>([]);
const [filter, setFilter] = useState<"all" | "active" | "completed">("all");
const addTodo = (text: string): void => {
const newTodo: Todo = { id: Date.now(), text, completed: false };
setTodos((currentTodos) => [...currentTodos, newTodo]);
};
const toggleTodo = (id: number): void => {
setTodos((currentTodos) =>
currentTodos.map((todo) =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
};
const setVisibilityFilter = (
filter: "all" | "active" | "completed"
): void => {
setFilter(filter);
};
const getVisibleTodos = (): Todo[] => {
switch (filter) {
case "completed":
return todos.filter((t) => t.completed);
case "active":
return todos.filter((t) => !t.completed);
default:
return todos;
}
};
const visibleTodos = getVisibleTodos();
return (
<div>
<h2>Todos</h2>
<input
type="text"
placeholder="Add todo"
className="text-black"
onKeyDown={(e: KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter" && e.currentTarget.value.trim()) {
addTodo(e.currentTarget.value);
e.currentTarget.value = "";
}
}}
/>
<div>
<button onClick={() => setVisibilityFilter("all")}>All</button>
<button onClick={() => setVisibilityFilter("active")}>Active</button>
<button onClick={() => setVisibilityFilter("completed")}>
Completed
</button>
</div>
<ul>
{visibleTodos.map((todo) => (
<li
key={todo.id}
onClick={() => toggleTodo(todo.id)}
style={{ textDecoration: todo.completed ? "line-through" : "none" }}
>
{todo.text}
</li>
))}
</ul>
</div>
);
};
export default TodoAppWithState;
After: Neat and Tidy with useReducer
Now let’s switch to useReducer
. Instead of having the state logic all over the place, it's all in one place. You've got a clear set of actions for adding todos, toggling them, and filtering them. Your code is cleaner, easier to read, and best of all, easier to manage.
"use client";
import React, { useReducer, KeyboardEvent } from "react";
type Todo = {
id: number;
text: string;
completed: boolean;
}
type Filter = "all" | "active" | "completed";
type State = {
todos: Todo[];
filter: Filter;
}
type Action =
| { type: "add"; text: string }
| { type: "toggle"; id: number }
| { type: "filter"; filter: Filter };
function todosReducer(state: State, action: Action): State {
switch (action.type) {
case "add":
return {
...state,
todos: [
...state.todos,
{ id: Date.now(), text: action.text, completed: false },
],
};
case "toggle":
return {
...state,
todos: state.todos.map((todo) =>
todo.id === action.id ? { ...todo, completed: !todo.completed } : todo
),
};
case "filter":
return {
...state,
filter: action.filter,
};
default:
return state;
}
}
const TodoAppWithReducer: React.FC = () => {
const [{ todos, filter }, dispatch] = useReducer(todosReducer, {
todos: [],
filter: "all",
});
const addTodo = (text: string): void => dispatch({ type: "add", text });
const toggleTodo = (id: number): void => dispatch({ type: "toggle", id });
const setVisibilityFilter = (filter: Filter): void =>
dispatch({ type: "filter", filter });
const getVisibleTodos = (): Todo[] => {
switch (filter) {
case "completed":
return todos.filter((t) => t.completed);
case "active":
return todos.filter((t) => !t.completed);
default:
return todos;
}
};
const visibleTodos = getVisibleTodos();
return (
<div>
<h2>Todos</h2>
<input
type="text"
placeholder="Add todo"
onKeyDown={(e: KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter" && e.currentTarget.value.trim()) {
addTodo(e.currentTarget.value);
e.currentTarget.value = "";
}
}}
/>
<div>
<button onClick={() => setVisibilityFilter("all")}>All</button>
<button onClick={() => setVisibilityFilter("active")}>Active</button>
<button onClick={() => setVisibilityFilter("completed")}>
Completed
</button>
</div>
<ul>
{visibleTodos.map((todo) => (
<li
key={todo.id}
onClick={() => toggleTodo(todo.id)}
style={{ textDecoration: todo.completed ? "line-through" : "none" }}
>
{todo.text}
</li>
))}
</ul>
</div>
);
};
export default TodoAppWithReducer;
Wrapping Up
Starting with useState
is fine when your app is small. But as it grows, you'll feel the pain. That's where useReducer
comes in, cleaning up the mess and keeping things orderly. It's like going from a cluttered desk to a well-organized workspace where everything is just where you need it. So, when your app starts to get complicated, remember useReducer
is your friend for keeping your state management clean and manageable.
Happy coding!