Unit 4 - Notes

INT252

Unit 4: Working with Forms

In modern single-page applications (SPAs), forms are the primary mechanism for user interaction and data collection. Unlike traditional HTML forms, React introduces a specific paradigm for managing form data, focusing on state management and event handling to create dynamic, interactive user interfaces.


1. Adding Forms in React

While React forms use standard HTML elements (<form>, <input>, <label>, <button>), the syntax requires JSX compliance.

Key Syntax Differences from HTML

  • htmlFor instead of for: To associate a label with an input, use htmlFor (since for is a reserved JavaScript keyword).
  • Self-Closing Tags: All input tags must be self-closed (e.g., <input /> not <input>).
  • CamelCase Attributes: Attributes usually follow camelCase (e.g., tabIndex, autoComplete).

Basic Structure

JSX
function SimpleForm() {
  return (
    <form>
      <label htmlFor="username">Username:</label>
      <input type="text" id="username" name="username" />
      
      <button type="submit">Register</button>
    </form>
  );
}


2. Controlled vs. Uncontrolled Components

React offers two distinct approaches to handling form data. Understanding the distinction is critical for architectural decisions.

A. Controlled Components (The Recommended Way)

In a controlled component, React state (useState) is the "single source of truth."

  1. The input's value is set by the React state.
  2. Changes to the input trigger an event handler that updates the state.

Flow of Data:
User types → onChange fires → setState updates state → Component re-renders → Input shows new state value.

JSX
import React, { useState } from 'react';

function ControlledInput() {
  const [email, setEmail] = useState('');

  const handleChange = (e) => {
    setEmail(e.target.value); // Update state immediately
  };

  return (
    <input 
      type="text" 
      value={email}       // Controlled by state
      onChange={handleChange} // Triggers state update
    />
  );
}

B. Uncontrolled Components

Uncontrolled components act like traditional HTML forms. The DOM handles the data, not React state. React accesses the values only when necessary (usually on submit) using Refs.

Mechanism:

  • Use the useRef hook to create a reference to the DOM node.
  • Access ref.current.value to get the data.

JSX
import React, { useRef } from 'react';

function UncontrolledInput() {
  const nameInputRef = useRef(null);

  const handleSubmit = (e) => {
    e.preventDefault();
    alert('Name: ' + nameInputRef.current.value);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input type="text" ref={nameInputRef} />
      <button type="submit">Submit</button>
    </form>
  );
}

Comparison Table

Feature Controlled Component Uncontrolled Component
Source of Truth React State The DOM
Data Access Available at all times (real-time) Available only on query/submit
Validation Instant validation possible Validation typically on submit
Code Volume Higher (requires handlers) Lower (closer to HTML)
Use Case Complex forms, instant feedback, dynamic inputs Simple forms, file uploads, integrating non-React libs

3. Handling Forms (Managing Multiple Inputs)

Handling multiple inputs individually with separate useState hooks is inefficient. The standard pattern involves using a single state object and a generic change handler.

The Computed Property Name Syntax

We use [e.target.name] to dynamically update the specific key in the state object corresponding to the input's name attribute.

JSX
import React, { useState } from 'react';

function MultiInputForm() {
  const [formData, setFormData] = useState({
    username: '',
    email: '',
    role: 'user'
  });

  const handleChange = (e) => {
    const { name, value } = e.target;
    
    setFormData(prevData => ({
      ...prevData,       // Spread existing state to preserve other fields
      [name]: value      // Dynamic key assignment
    }));
  };

  return (
    <form>
      <input 
        name="username" 
        value={formData.username} 
        onChange={handleChange} 
        placeholder="Username"
      />
      <input 
        name="email" 
        value={formData.email} 
        onChange={handleChange} 
        placeholder="Email"
      />
      <select name="role" value={formData.role} onChange={handleChange}>
        <option value="user">User</option>
        <option value="admin">Admin</option>
      </select>
    </form>
  );
}


4. Submitting Forms

Submitting a form in React involves intercepting the browser's default behavior and processing the data (usually sending it to an API).

Key Steps:

  1. Attach an onSubmit handler to the <form> element (not the button).
  2. Prevent Default: Use event.preventDefault() to stop the browser from reloading the page (standard HTML behavior).
  3. Process data or call an API.

JSX
const handleSubmit = async (e) => {
  e.preventDefault(); // Crucial step
  
  try {
    // Simulation of API call
    console.log("Submitting data:", formData);
    await submitToAPI(formData); 
    
    // Optional: Reset form after success
    setFormData({ username: '', email: '' });
  } catch (error) {
    console.error("Submission failed");
  }
};

// In JSX:
// <form onSubmit={handleSubmit}> ...


5. Form Validation

Validation ensures the data collected is clean, secure, and usable. Validation usually happens in two places:

  1. Client-side (React): For user experience (instant feedback).
  2. Server-side: For security (never trust the client).

We focus here on Client-side validation.

Validation Logic

Create a function that checks the formData against rules (regex, length checks, required fields) and returns an object containing error messages.

JAVASCRIPT
const validate = (values) => {
  const errors = {};
  const regex = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/i; // Simple email regex

  if (!values.username) {
    errors.username = "Username is required!";
  }
  if (!values.email) {
    errors.email = "Email is required!";
  } else if (!regex.test(values.email)) {
    errors.email = "This is not a valid email format!";
  }
  if (values.password.length < 4) {
    errors.password = "Password must be more than 4 characters";
  }
  
  return errors;
};


6. Error Checking and Displaying Errors

Displaying errors involves managing an errors state object and conditionally rendering messages in the UI.

Implementation Steps

  1. State for Errors: Create a state variable const [formErrors, setFormErrors] = useState({}).
  2. Trigger Validation: Run validation inside handleSubmit or useEffect dependent on formData.
  3. Blocking Submission: If Object.keys(errors).length > 0, do not submit.
  4. Conditional Rendering: Display the error message next to the input field.

Comprehensive Example: Full Flow

Below is a complete example integrating Handling, Validation, Error Display, and Submission.

JSX
import React, { useState, useEffect } from "react";

function RegistrationForm() {
  const initialValues = { username: "", email: "", password: "" };
  const [formValues, setFormValues] = useState(initialValues);
  const [formErrors, setFormErrors] = useState({});
  const [isSubmit, setIsSubmit] = useState(false);

  // 1. Handle Input Change
  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormValues({ ...formValues, [name]: value });
  };

  // 2. Handle Submission
  const handleSubmit = (e) => {
    e.preventDefault();
    const errors = validate(formValues);
    setFormErrors(errors);
    setIsSubmit(true);
  };

  // 3. Submit only if no errors
  useEffect(() => {
    if (Object.keys(formErrors).length === 0 && isSubmit) {
      console.log("Form Submitted Successfully:", formValues);
      // API Call would go here
    }
  }, [formErrors, isSubmit, formValues]);

  // 4. Validation Logic
  const validate = (values) => {
    const errors = {};
    if (!values.username) {
      errors.username = "Username is required";
    }
    if (!values.email) {
      errors.email = "Email is required";
    } else if (!values.email.includes("@")) {
      errors.email = "Invalid email address";
    }
    if (!values.password) {
      errors.password = "Password is required";
    } else if (values.password.length < 6) {
      errors.password = "Password must be 6 characters or more";
    }
    return errors;
  };

  return (
    <div className="container">
      <h1>Login Form</h1>
      <form onSubmit={handleSubmit}>
        {/* Username Field */}
        <div className="form-group">
          <label>Username</label>
          <input
            type="text"
            name="username"
            value={formValues.username}
            onChange={handleChange}
            // Add red border class if error exists
            className={formErrors.username ? "input-error" : ""}
          />
          {/* 5. Display Error */}
          <p className="error-text">{formErrors.username}</p>
        </div>

        {/* Email Field */}
        <div className="form-group">
          <label>Email</label>
          <input
            type="text"
            name="email"
            value={formValues.email}
            onChange={handleChange}
            className={formErrors.email ? "input-error" : ""}
          />
          <p className="error-text">{formErrors.email}</p>
        </div>

        {/* Password Field */}
        <div className="form-group">
          <label>Password</label>
          <input
            type="password"
            name="password"
            value={formValues.password}
            onChange={handleChange}
            className={formErrors.password ? "input-error" : ""}
          />
          <p className="error-text">{formErrors.password}</p>
        </div>

        <button type="submit">Submit</button>
      </form>
    </div>
  );
}

export default RegistrationForm;

Specific Input Nuances

  • Textarea: In HTML, <textarea>Value</textarea>. In React, it uses a value attribute: <textarea value={state} />.
  • Select: Instead of putting selected on an <option>, React puts a value on the root <select> tag.
  • Checkbox: Uses checked attribute (boolean) instead of value. When handling changes, access e.target.checked instead of e.target.value.

JSX
// Checkbox Example
<input 
  type="checkbox" 
  checked={isSubscribed} 
  onChange={(e) => setIsSubscribed(e.target.checked)} 
/>