Skip to content

Form Basics in React

Forms are essential for collecting user input. In React, the most common way to handle form inputs is through controlled components. A controlled component is an input element whose value is controlled by React state.

Controlled Components Pattern

With controlled components:

  1. The value of the input element is stored in a React state variable.
  2. The onChange event listener on the input updates this state variable whenever the input value changes.
  3. The input element's value prop is explicitly set to the state variable.

This creates a "single source of truth" for the input's value, making it easy to validate, manipulate, and submit the data.

Handling Input Changes (onChange)

The onChange event handler is crucial for controlled inputs. It fires every time the value of the input element changes (e.g., when a user types a character). Inside the handler, you'll call the state setter function to update the state with the new input value.

Example from TaskList.tsx for newTaskTitle:

<input
  type="text"
  placeholder="Add a new task..."
  value={newTaskTitle} // Value is controlled by state
  onChange={(e) => setNewTaskTitle(e.target.value)} // Update state on change
  // ... other props
/>

Handling Form Submission (onSubmit)

When a user submits a form (e.g., by clicking a submit button or pressing Enter in an input field), the onSubmit event handler on the <form> element is triggered.

It's very important to call event.preventDefault() inside the onSubmit handler. This stops the browser's default behavior of reloading the page, which is typically undesirable in a single-page application (or a Next.js application that handles routing itself).

Our TaskList.tsx already uses this concept for the "Add Task" functionality, though it's attached to a button click rather than a full <form> element. Let's wrap the input and button in a <form> element to demonstrate onSubmit.

  1. Update components/TaskList.tsx to use a <form> tag:

    components/TaskList.tsx (Update this file)

    // components/TaskList.tsx
    // Updated to use a <form> element with onSubmit for task input.
    'use client';
    
    import { useState, useEffect, FormEvent } from 'react'; // Import FormEvent for type safety
    
    interface Task {
      id: string;
      title: string;
      completed: boolean;
    }
    
    export default function TaskList() {
      const [tasks, setTasks] = useState<Task[]>([]);
      const [newTaskTitle, setNewTaskTitle] = useState<string>('');
    
      useEffect(() => {
        const initialTasks: Task[] = [
          { id: '1', title: 'Learn Next.js Routing', completed: true },
          { id: '2', title: 'Build Task Input Form', completed: false },
          { id: '3', title: 'Implement Task Completion', completed: false },
        ];
        setTasks(initialTasks);
      }, []);
    
      // Modified addTask function to handle form submission event
      const addTask = (event: FormEvent) => {
        event.preventDefault(); // Prevent default form submission behavior (page reload)
    
        if (newTaskTitle.trim() === '') {
          console.log('Task title cannot be empty.');
          return;
        }
        const newTask: Task = {
          id: crypto.randomUUID(), // Use a more robust unique ID for new tasks
          title: newTaskTitle,
          completed: false,
        };
        setTasks([...tasks, newTask]);
        setNewTaskTitle('');
      };
    
      const toggleTaskCompletion = (id: string) => {
        setTasks(
          tasks.map((task) =>
            task.id === id ? { ...task, completed: !task.completed } : task
          )
        );
      };
    
      const deleteTask = (id: string) => {
        setTasks(tasks.filter((task) => task.id !== id));
      };
    
      return (
        <div className="w-full max-w-2xl mx-auto p-6 bg-white rounded-lg shadow-xl border border-gray-200">
          <h3 className="text-3xl font-bold text-gray-800 mb-6">Your Tasks</h3>
    
          {/* Task Input Form - now a proper <form> element */}
          <form onSubmit={addTask} className="flex flex-col md:flex-row gap-4 mb-8">
            <input
              type="text"
              placeholder="Add a new task..."
              value={newTaskTitle}
              onChange={(e) => setNewTaskTitle(e.target.value)}
              className="flex-grow p-3 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 outline-none"
            />
            <button
              type="submit" // Set type to submit for form submission
              className="px-6 py-3 bg-blue-600 text-white rounded-md shadow-md hover:bg-blue-700 transition duration-300 transform hover:scale-105"
            >
              Add Task
            </button>
          </form>
    
          {/* Task List */}
          {tasks.length === 0 ? (
            <p className="text-center text-gray-500 italic">No tasks yet. Start adding some!</p>
          ) : (
            <ul className="space-y-4">
              {tasks.map((task) => (
                <li
                  key={task.id}
                  className="flex items-center justify-between p-4 bg-gray-50 border border-gray-200 rounded-md shadow-sm transition duration-200 hover:bg-gray-100"
                >
                  <div className="flex items-center">
                    <input
                      type="checkbox"
                      checked={task.completed}
                      onChange={() => toggleTaskCompletion(task.id)}
                      className="form-checkbox h-5 w-5 text-blue-600 rounded mr-3 cursor-pointer"
                    />
                    <span
                      className={`text-lg font-medium ${
                        task.completed ? 'line-through text-gray-500' : 'text-gray-900'
                      }`}
                    >
                      {task.title}
                    </span>
                  </div>
                  <button
                    onClick={() => deleteTask(task.id)}
                    className="ml-4 px-3 py-1 bg-red-500 text-white rounded-md text-sm shadow-sm hover:bg-red-600 transition duration-300"
                  >
                    Delete
                  </button>
                </li>
              ))}
            </ul>
          )}
        </div>
      );
    }
    

Summary

What we've done:

  • We've wrapped the task input and button in a <form> tag.
  • We've added an onSubmit handler to the form, which calls addTask and event.preventDefault().
  • We've changed the button's type to submit.
  • We updated the id generation to use crypto.randomUUID() for more robust unique IDs.

To verify form submission:

  1. Ensure your development server is running (npm run dev).
  2. Navigate to http://localhost:3000/dashboard.
  3. Type a task and press Enter. The task should be added without a page reload. You can also click the "Add Task" button.

This module has provided you with a solid understanding of Client Components, basic state management with useState and useEffect, and how to handle forms in React. This is the foundation for creating interactive UIs that respond to user input. In the next module, we will explore more advanced data fetching techniques using Server Components and learn about different rendering strategies in Next.js.