DEV Community

Cover image for React Hook Form - Simple Todo List
Chris
Chris

Posted on • Updated on

React Hook Form - Simple Todo List

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

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

react-hook-project-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')
);
Enter fullscreen mode Exit fullscreen mode

Building our Components

react-hook-form-project-structure

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
Enter fullscreen mode Exit fullscreen mode

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>
  );
}

Enter fullscreen mode Exit fullscreen mode

Top comments (0)