React Hook Form - Simple Todo List
Chris
Posted on August 1, 2021
Today we'll play around with React Hook Form library and build a simple to-do list project.
Creating a form using React is straightforward. But things start to get more tricky when the form requires multiple inputs/validations, responsive UI, validation, and external data. Luckily React Hook Form is one of many libraries that improve the developer experience when creating web forms. The library promises to make it easier for developers to add form validation and build performant forms.
So let us test out the React Hook Form library by building a simple to-do list project. This quick guide will not go over styling/CSS but instead focus on building out the components. Feel free to clone and play around with the finished project here.
File structure
The image above illustrates how our file structure will look like, so feel free to remove any additional files that come included after creating a new react app.
Styling
The styling is quite long and will take too much space on this page. So feel free to copy/past the styling from the project's repo into the app.css
file.
And make sure to import the stylesheet by adding the code below into index.js
.
import React from 'react';
import './styles/app.css';
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
Building our Components
For the structure of our project, we will have our parent component, app.js
. And two child components, TaskList.js
and NewTask.js
. So let's get started with the app.js
.
Parent Component - App.js
import { useState } from 'react';
import NewTaskForm from './components/NewTaskForm';
import TaskList from './components/TaskList';
const defaultTasks = [
{ id: 1, completed: false, label: 'buy pickle' },
{ id: 2, completed: true, label: 'buy ketchup' },
];
const uniqueId = () => Math.floor(Math.random() * Date.now());
export default function App() {
const [tasks, setTasks] = useState(defaultTasks);
const completeTaskHandler = (taskId) => {
const updatedTasks = tasks.map((task) => {
const completed = !task.completed;
return task.id === taskId ? { ...task, completed } : task;
});
setTasks(updatedTasks);
};
const deleteTaskHandler = (taskId) => {
setTasks(tasks.filter(({ id }) => taskId !== id));
};
const newTaskHandler = (label) => {
const newTask = {
id: uniqueId(),
completed: false,
label,
};
setTasks([...tasks, newTask]);
};
return (
<div className="container">
<NewTaskForm newTaskHandler={newTaskHandler} />
<TaskList
tasks={tasks}
completeTaskHandler={completeTaskHandler}
deleteTaskHandler={deleteTaskHandler}
/>
</div>
);
}
First, we will import our child components
and the useState
hook. Then as the name implies, our defaultTasks
variable will store our default tasks. Each task will require an id, completed, and label property. Since we need a unique id for each task, we will create a helper function called uniqueId
to generate an id.
Now let's use the useState
hook to store all of our tasks. And create three separate functions for creating, deleting, and marking a task as complete. Lastly, we will return our JSX containing our child components. While making sure we provide the required properties for each component
Child Component #1 - TaskList.js
export default function TaskList({
tasks,
completeTaskHandler,
deleteTaskHandler,
}) {
tasks.sort((a, b) => a.completed - b.completed);
return (
<div>
{tasks.map(({ label, completed, id }) => (
<div key={id} className={`task ${completed && 'task--completed'}`}>
<button
className="task__complete-button"
onClick={() => completeTaskHandler(id)}
/>
<p className="task__label">{label}</p>
<button
className="task__delete-button"
onClick={() => deleteTaskHandler(id)}
>
🗑
</button>
</div>
))}
</div>
);
}
The TaskList
component will use object destructuring to use the props provided by the parent component. And the 'sort' method will be called on our tasks array to display the uncompleted tasks at the top and the completed tasks at the bottom. Finally, we will iterate through each task to create our HTML elements.
Child Component #2 - NewTaskForm.js
import { useForm } from 'react-hook-form';
export default function NewTaskForm({ newTaskHandler }) {
const { register, handleSubmit, reset, formState, clearErrors } = useForm({
shouldUnregister: true,
defaultValues: { label: '' },
});
const onSubmit = (data) => {
newTaskHandler(data.label);
reset();
clearErrors();
};
const errors = Object.values(formState.errors);
}
We will now import the useForm
hook from the React Hook Form library, which takes optional arguments. The shouldUnregister
will be set to true
to unregister input during unmount. And for the defaultValues
property, we will set the default value for the task label input.
The useForm
hook returns an object containing information about our form and helper functions to manipulate our form. Therefore destructuring assignment is used to access the register
, handleSubmit
, reset
, formState
, and clearErrors
property.
Next, an onSubmit
function is created to handle the form submission. First, the function will trigger the newTaskHandler
while passing down the new task label from our form data. Then reset
will reset the input values in our form. And finally, clearErrors
as the name states will clear out all the form errors.
return (
<form className="new-task-form" onSubmit={handleSubmit(onSubmit)}>
<label htmlFor="task">New Task</label>
<input
id="task"
{...register('label', {
required: 'task cannot be blank',
validate: {
lessThanTwenty: (v) =>
v.length <= 20 || 'Task cannot be longer than 20 characters.',
},
})}
/>
<ul className="error-messages">
{errors.map((error) => (
<li>{error.message}</li>
))}
</ul>
<button type="submit">add</button>
</form>
);
The last step will be to return the JSX. The React Hook Form's handleSubmit
function is passed down to the form's onSubmit
property; notice we also provide the onSubmit
callback function to hadleSubmit
as well.
For the input element, we will use the React Hook Form's register
function. The first argument will be the name of the input, label
. And the second argument is a configuration object. In our case, we will only set the validation settings, the field cannot be blank, and the field length cannot be longer than twenty. The last step is to use the spread operator to give the input
access to all the properties provided by React Hook Form.
Here is how the final code for the NewTaskForm
should look.
import { useForm } from 'react-hook-form';
export default function NewTaskForm({ newTaskHandler }) {
const { register, handleSubmit, reset, formState, clearErrors } = useForm({
shouldUnregister: true,
defaultValues: { label: '' },
});
const onSubmit = (data) => {
newTaskHandler(data.label);
reset();
clearErrors();
};
const errors = Object.values(formState.errors);
return (
<form className="new-task-form" onSubmit={handleSubmit(onSubmit)}>
<label htmlFor="task">New Task</label>
<input
id="task"
{...register('label', {
required: 'task cannot be blank',
validate: {
lessThanTwenty: (v) =>
v.length <= 20 || 'Task cannot be longer than 20 characters.',
},
})}
/>
{errors.length > 0 && (
<ul className="error-messages">
{errors.map((error) => (
<li>{error.message}</li>
))}
</ul>
)}
<button type="submit">add</button>
</form>
);
}
Posted on August 1, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.