Learnwizy Technologies Logo

Learnwizy Technologies

Class 17: Interactivity, Forms, and Side Effects

React applications come alive through user interaction. This class covers how to handle events, manage form inputs, render content conditionally, and perform "side effects" using the useEffect Hook.


React Event Handling

Interactivity in web applications relies heavily on responding to user actions, such as clicks, form submissions, or keyboard inputs. In React, event handling is similar to handling events in plain HTML, but with some key differences in syntax and approach.

Key characteristics of React event handling:

Handling a Click Event

Let's create a simple button that logs a message on click:

// src/components/MyButton.jsx
function MyButton() {
  const handleClick = () => {
    alert("Button clicked!");
    console.log('Button was clicked!');
  };

  return <button onClick={handleClick}>Click Me</button>;
}

export default MyButton;

In this example, the handleClick function is defined and then passed as the onClick prop to the button. When the button is clicked, React will invoke handleClick.

Passing Arguments to Event Handlers

Sometimes you need to pass extra arguments to your event handler function. You can do this using an arrow function or bind.

// src/components/ItemList.jsx
function ItemList() {
  const items = ['Apple', 'Banana', 'Cherry'];

  const handleDelete = (item) => {
    console.log(`Deleting item: ${item}`);
    // In a real app, you'd update state or make an API call here
  };

  return (
    <ul>
      {items.map((item) => (
        <li key={item}>
          {item}
          <button onClick={() => handleDelete(item)}>Delete</button>
        </li>
      ))}
    </ul>
  );
}

export default ItemList;

Here, for each item in the list, an arrow function is used to call handleDelete and pass the specific item as an argument.


Controlled Components for Forms

In HTML, form elements like <input>, <textarea>, and <select> typically maintain their own state and update it based on user input. In React, "controlled components" are the preferred way to handle form input. A controlled component is an input form element whose value is controlled by React state.

Key concepts for controlled components:

Handling Different Input Types

The principle of controlled components applies to all form input elements.

Handling Multiple Form Inputs

When you have many input elements, it can become repetitive to write a separate change handler for each one. You can handle multiple inputs with a single event handler by adding a name attribute to each input and using it to update the corresponding state property.

// src/components/MyForm.jsx
import { useState } from "react";

function MyForm() {
    const [formData, setFormData] = useState({
    username: "",
    email: "",
    password: "",
    gender: "male",
    isAgreed: false,
    });

    const handleChange = (event) => {
    const { name, value } = event.target;
    setFormData((prevData) => ({
        ...prevData,
        [name]: value, // Use computed property name to update the correct field
    }));
    };

    const handleSubmit = (event) => {
    event.preventDefault(); // Prevent default browser form submission
    console.log("Form Submitted:", { formData });
    alert(
        `Form Submitted:\nName: ${formData.username}\nEmail: ${formData.email}\nGender: ${formData.gender}\nAgreed: ${formData.isAgreed}`,
    );
    // Reset form
    setFormData({
        username: "",
        email: "",
        password: "",
        gender: "male",
        isAgreed: false,
    });
    };

    return (
    <form onSubmit={handleSubmit}>
        <div>
        <h2>Controlled Form</h2>
        <label>
            Username: 
            <input
            type="text"
            name="username"
            value={formData.username}
            onChange={handleChange}
            />
        </label>
        <div>
            <label>
            Email: 
            <input
                type="email"
                name="email"
                value={formData.email}
                onChange={handleChange}
            />
            </label>
        </div>
        <div>
            <label>
            Password: 
            <input
                type="password"
                name="password"
                value={formData.password}
                onChange={handleChange}
            />
            </label>
        </div>
        <div>
            <label>
            Gender: 
            <select
                name="gender"
                value={formData.gender}
                onChange={handleChange}
            >
                <option value="male">Male</option>
                <option value="female">Female</option>
                <option value="other">Other</option>
            </select>
            </label>
        </div>
        <label>
            <input
            type="checkbox"
            name="isAgreed"
            checked={formData.isAgreed}
            onChange={handleChange}
            />
             I agree to the terms
        </label>
        </div>
        <button type="submit">Submit</button>
    </form>
    );
}

export default MyForm;

Here, the handleChange function uses the name attribute of the input element (e.g., "username", "email") to dynamically update the corresponding property in the formData state object.


Conditional Rendering

Conditional rendering in React allows you to render different elements or components based on certain conditions. This is essential for displaying dynamic UI elements, such as showing or hiding content, displaying loading indicators, or showing different parts of a form based on user input.

Common ways to implement conditional rendering:

// src/components/WarningBanner.jsx
import { useState } from 'react';

function WarningBanner() {
  const [showWarning, setShowWarning] = useState(true);

  const toggleWarning = () => {
    setShowWarning(!showWarning);
  };

  return (
    <div>
      <button onClick={toggleWarning}>
        {showWarning ? 'Hide Warning' : 'Show Warning'}
      </button>
      {showWarning && <div className="warning">Warning: This is a test!</div>}
    </div>
  );
}

export default WarningBanner;

The && operator works because in JavaScript, if the first operand is true, it returns the second operand. If the first operand is false, it short-circuits and returns false (which React treats as nothing to render).


Side Effects

In React, "side effects" are operations that affect something outside the scope of the component's rendering. Examples include:

Side effects are generally undesirable during the component's rendering phase because they can lead to unpredictable behavior or performance issues. React provides the useEffect Hook to handle side effects in function components.

The useEffect Hook

The useEffect Hook allows you to perform side effects after every render. It takes two arguments:

  1. A function containing the side effect logic.
  2. A optional dependency array. If this array is provided, the effect will only re-run if any of the values in the array change between renders.
    • No Array: The effect runs after every render. Useful for effects that need to re-run based on any state or prop changes.
    • Empty Array []: The effect runs only once after the initial render, and never again.
    • Array with values [prop1, state2]: Effect runs on mount and whenever any value in the dependency array changes. The cleanup function runs before the effect re-runs and on unmount.

Example: Let's add a useEffect to our Counter component to log the count:

// src/components/Counter.jsx
import { useState, useEffect } from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  // This effect runs after every render where 'count' has changed
  useEffect(() => {
    console.log(`Count has changed to: ${count}`);
  }, [count]);

  const increment = () => {
    setCount(count + 1);
  };

  const decrement = () => {
    setCount(count - 1);
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment</button>
      <button onClick={decrement}>Decrement</button>
    </div>
  );
}

export default Counter;

With this useEffect, every time the count state updates (and thus the component re-renders), a message will be logged to the browser's console showing the new count. This is a basic example, but useEffect is incredibly powerful for handling more complex scenarios like data fetching, setting up event listeners, and more, which we will explore in later classes.

Cleanup Function in useEffect

Some side effects require cleanup to prevent memory leaks or unwanted behavior. For example, clearing timers, unsubscribing from event listeners, or canceling network requests.

useEffect allows you to return a function, which React will execute when the component unmounts, or before the effect re-runs due to a dependency change.

// src/components/TimerComponent.jsx
import { useState, useEffect } from 'react';

function TimerComponent() {
  const [count, setCount] = useState(0);
  const [isActive, setIsActive] = useState(false);

  useEffect(() => {
    let interval = null;
    if (isActive) {
      interval = setInterval(() => {
        setCount((prevCount) => prevCount + 1);
      }, 1000);
    }
    // Cleanup function
    return () => {
      if (interval) {
        clearInterval(interval);
      }
    };
  }, [isActive]); // Re-run effect only when isActive changes

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setIsActive(!isActive)}>
        {isActive ? 'Pause' : 'Start'}
      </button>
    </div>
  );
}

export default TimerComponent;

Here, the timer effect only starts or stops when isActive changes, thanks to the [isActive] dependency array.


useRef Hook

The useRef hook returns a mutable ref object whose .current property is initialized to the passed argument (initialValue). The returned ref object will persist for the full lifetime of the component.

Common use cases for useRef:

Accessing a DOM Element with useRef

Let's create an input field and automatically focus it when the component mounts:

// src/components/FocusInput.jsx
import { useRef, useEffect } from 'react';

function FocusInput() {
  const inputRef = useRef(null); // Initialize ref with null

  useEffect(() => {
    // Current property of the ref object will hold the DOM node
    if (inputRef.current) {
      inputRef.current.focus();
    }
  }, []); // Empty dependency array means this effect runs once on mount

  return (
    <div>
      <label>
        Auto-focused Input:
        <input type="text" ref={inputRef} /> {/* Attach the ref to the input */}
      </label>
    </div>
  );
}

export default FocusInput;

By attaching ref={inputRef} to the <input> element, React will set the .current property of inputRef to the actual DOM element once it's rendered. We then use useEffect to focus it.

Storing a Mutable Value with useRef

// src/components/CounterWithRef.jsx
import { useRef, useState, useEffect } from "react";

function CounterWithRef() {
  const [count, setCount] = useState(0);
  const renders = useRef(0); // This value won't trigger re-renders

  // This effect runs after every render
  useEffect(() => {
    renders.current = renders.current + 1; // Mutate the .current property
  });

  return (
    <div>
      <p>Count: {count}</p>
      <p>Component has rendered {renders.current} times.</p>
      <button onClick={() => setCount(count + 1)}>Increment Count</button>
    </div>
  );
}

export default CounterWithRef;

Here, renders.current is updated on every render. Since updating a ref's .current property does not cause a re-render, this is an efficient way to track a value that changes but doesn't need to be immediately reflected in the UI (or only reflected when another state change occurs).