Creating custom React hooks for a simple To-do list app
Franco D'Alessio
April 01, 2020 • 8 min. readIn this post I’m going to do a refactor to a simple To-do list app. The goal is to create some custom hooks so you can see how simple it is.
If you don’t know what hooks are and how awesome they are, it’s a good idea to check the official documentation first
But… Why bother in the first place? 😕
Coding a custom hook provides us with two very clear benefits. First, we can extract common functionality to be reused by several components. We do that with components, so it makes sense to do the same with functions.
And secondly, we get rid of a lot of boilerplate code in our components, which make them cleaner and easier to read.
This two benefits will become clearer when you see the example ✍️
Let’s start with a simple To-do list app ✔️
I’ve coded a very simple app. It allows the user to:
- Type a to-do item and add it to the list
- See the list
- Mark a to-do as completed/uncompleted by clicking on it
- Removing a to-do from the list by clicking the red X button next to it
See it in action in this Codesandbox.
And here is the code:
import React from "react";
import "./TodoApp.css";
export default function TodoApp() {
const [todo, setTodo] = React.useState("");
const [todos, setTodos] = React.useState([]);
const handleChange = e => {
setTodo(e.target.value);
};
const addTodo = () => {
setTodos([
...todos,
{
id: todos.length + 1,
text: todo,
completed: false
}
]);
};
const onSubmit = e => {
e.preventDefault();
if (todo === "") return;
addTodo();
setTodo("");
};
const removeTodo = todoId => {
const updatedTodos = todos.filter(todo => todo.id !== todoId);
setTodos(updatedTodos);
};
const toggleTodo = todoId => {
const updatedTodos = todos.map(todo => {
return todo.id === todoId
? { ...todo, completed: !todo.completed }
: todo;
});
setTodos(updatedTodos);
};
return (
<div className="container">
<form onSubmit={onSubmit}>
<label htmlFor="todo">Todo text</label>
<br />
<input
id="todo"
className="todo-input"
onChange={handleChange}
value={todo}
/>
<button type="submit" className="add-btn">
Add
</button>
</form>
<div>
<ul>
{todos.map(todo => (
<li key={todo.id}>
<span
className={todo.completed ? "todo-completed" : undefined}
onClick={() => toggleTodo(todo.id)}
>
{todo.text}
</span>
<span
className="delete-btn"
onClick={() => removeTodo(todo.id)}
>
x
</span>
</li>
))}
</ul>
</div>
</div>
);
}
Now that we’re ready, let’s start building some custom hooks! 🚀
A custom hook to control the input field
Let’s start with a simple one. It’s a good idea to create a hook to manage the state of the input field we have. Why? Because it will be highly reusable; every other component that has a form with input fields will be able to benefit from it.
Right now we’re controlling the input field with this piece of state:
const [todo, setTodo] = React.useState("");
When the value is changed (the user types something) we’re calling this function:
And onSubmit, after the new to-do is added to the list, we’re cleaning the input field:
setTodo("");
In every component that has an input field, we would need to have these three things. Keep them in mind because we’re going to use these 3 things in our hook.
Creating the new useInput hook
First, start by creating a new folder hooks (just to keep things organized) and create a new file called useInput.js.
The name of every official hook start with the word use, so follow that rule when creating a custom one
You’ve seen that React’s useState hook returns two things: the value and a function to update it. However, it’s not necessary that a hook returns just 2 things.
In fact, we’re making our hook return 3 things:
- The value
- A function to handle the change (update the value)
- A function to reset the value (clean the input)
As for the parameters, our hook function only needs one thing: the initial value.
This is the skeleton of our hook:
function useInput(initialValue) {
// Code goes here
return [state, handleChange, reset];
}
export default useInput;
Now we have to add the body of our function. But actually we’ve already done it! Remember, we’re just extracting the logic from our component.
So we’re going to use the 3 things we enumerated before (and I asked you to keep in mind 😉). The final hook should look like this:
import { useState } from "react";
function useInput(initialValue) {
const [state, setState] = useState(initialValue);
const handleChange = e => {
setState(e.target.value);
};
const reset = () => {
setState("");
};
return [state, handleChange, reset];
}
export default useInput;
So now we can go ahead and add the hook to our component! 🚀 That includes importing it:
import useInput from './hooks/useInput';
Actually calling it:
const [todo, setTodo, resetTodo] = useInput(“”);
And also get rid of our handleChange function and replace some of the code to use the two functions our hook provides. The component should now look like this:
import React from "react";
import useInput from "./hooks/useInput";
import "./TodoApp.css";
export default function TodoApp() {
const [todo, setTodo, resetTodo] = useInput("");
const [todos, setTodos] = React.useState([]);
const addTodo = () => {
setTodos([
...todos,
{
id: todos.length + 1,
text: todo,
completed: false
}
]);
};
const onSubmit = e => {
e.preventDefault();
if (todo === "") return;
addTodo();
resetTodo();
};
const removeTodo = todoId => {
const updatedTodos = todos.filter(todo => todo.id !== todoId);
setTodos(updatedTodos);
};
const toggleTodo = todoId => {
const updatedTodos = todos.map(todo => {
return todo.id === todoId
? { ...todo, completed: !todo.completed }
: todo;
});
setTodos(updatedTodos);
};
return (
<div className="container">
<form onSubmit={onSubmit}>
<label htmlFor="todo">Todo text</label>
<br />
<input
id="todo"
className="todo-input"
onChange={setTodo}
value={todo}
/>
<button type="submit" className="add-btn">
Add
</button>
</form>
<div>
<ul>
{todos.map(todo => (
<li key={todo.id}>
<span
className={todo.completed ? "todo-completed" : undefined}
onClick={() => toggleTodo(todo.id)}
>
{todo.text}
</span>
<span
className="delete-btn"
onClick={() => removeTodo(todo.id)}
>
x
</span>
</li>
))}
</ul>
</div>
</div>
);
}
Better, right? It’s cleaner; not a huge change because our hook is small, but remember that we’ll be able to use this hook in every other component that has an input field 🎉
Let’s build a bigger hook 💪
Ok, so now that we’ve seen how to create a custom hook and how easy it is, let’s create a bigger one.
Our app is really small and doesn’t have a lot of functionality, but it does share a common thing with almost every application, big or small. Can you guess what?
Exactly, it uses a list. Have you ever coded a React application without using an array list? 🤔
It would be great if we could create a hook to handle arrays. That would be useful not only for other components in this application, but also for virtually any other application.
Creating the new useArray hook
We have 3 actions involving our to-do list:
- We can add a to-do
- We can remove a to-do
- We can toggle the completed status of a to-do
Let’s move all that logic to a hook called useArray. As this hook is going to return several functions (plus the list itself) and may even grow in the future, I’m not going to do it return an array as the previous hook. That would mean destructuring a lot of things and wouldn’t look nice.
Instead, I’m going to make our function return an object, containing all we need. There’s no rule saying that you must make your hook return an array, so this is not a problem.
As with the previous hook, let’s begin with the skeleton. We know that we will be receiving the initial value for the list itself and that we’re returning that alongside with the 3 methods we mentioned:
function useArray(initialList) {
const [list, setList] = useState(initialList);
return {
list,
addItem: () => {},
removeItem: () => {},
toggleItem: () => {},
};
}
export default useArray;
Now we just need to extract those 3 functions from our component and add them to this new hook, which will end up looking like this:
import React, { useState } from "react";
function useArray(initialList) {
const [list, setList] = useState(initialList);
return {
list,
addItem: newItemText => {
setList([
...list,
{
id: list.length + 1,
text: newItemText,
completed: false
}
]);
},
removeItem: itemId => {
const updatedItems = list.filter(item => item.id !== itemId);
setList(updatedItems);
},
toggleItem: itemId => {
const updatedItems = list.map(item => {
return item.id === itemId
? { ...item, completed: !item.completed }
: item;
});
setList(updatedItems);
}
};
}
export default useArray;
Now for the big moment 🥁 Let’s get rid of all these functions on the component and replace them with our brand new hook:
import React from "react";
import useInput from "./hooks/useInput";
import "./TodoApp.css";
import useArray from "./hooks/useArray";
export default function TodoApp() {
const [todo, setTodo, resetTodo] = useInput("");
const todos = useArray([]);
const onSubmit = e => {
e.preventDefault();
if (todo === "") return;
todos.addItem(todo);
resetTodo();
};
return (
<div className="container">
<form onSubmit={onSubmit}>
<label htmlFor="todo">Todo text</label>
<br />
<input
id="todo"
className="todo-input"
onChange={setTodo}
value={todo}
/>
<button type="submit" className="add-btn">
Add
</button>
</form>
<div>
<ul>
{todos.list.map(todo => (
<li key={todo.id}>
<span
className={todo.completed ? "todo-completed" : undefined}
onClick={() => todos.toggleItem(todo.id)}
>
{todo.text}
</span>
<span
className="delete-btn"
onClick={() => todos.removeItem(todo.id)}
>
x
</span>
</li>
))}
</ul>
</div>
</div>
);
}
Now that’s a lot cleaner, right? 😎 And of course, it works the same as before.
We got rid of a lot of code! Our component is definitely easier to read now. And the best part, once again: we’ll be able to use our hook in any other component that handles a list like this.
So… What do you think? 😃
What’s your opinion on custom hooks? Have you ever used it? If you haven’t yet, hopefully you will after reading this!
I hope it was useful and please forgive me (and let me know 🙏) if there is any error in the code.
Thanks for reading ❤️