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:
-
CamelCase Naming: React events are named
using camelCase (e.g.,
onClick
,onChange
,onSubmit
) instead of lowercase (e.g.,onclick
,onchange
). - JSX Attribute: Event handlers are passed as JSX attributes to elements.
- Function Reference: You pass a function as the event handler, not a string. This function will be executed when the event occurs.
-
SyntheticEvent: React wraps native browser
events with a
SyntheticEvent
object, which is a cross-browser wrapper around the browser's native event. It has the same interface as the browser's native event, includingstopPropagation()
andpreventDefault()
.
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:
- Single Source of Truth: The input's value is always driven by the React state.
- State Management: When the user types or changes the input, an event handler updates the component's state, and the input re-renders with the new value.
- Predictable Behavior: This makes the form data predictable and easy to manipulate, validate, and submit.
Handling Different Input Types
The principle of controlled components applies to all form input elements.
-
Textarea: Use the
value
prop similar to<input type="text">
. -
Checkbox: Use the
checked
prop instead ofvalue
for boolean state. Theevent.target.checked
property gives the new boolean value. -
Select: Use the
value
prop on the<select>
tag. React supports thevalue
prop on the root<select>
tag, which simplifies handling multiple selected options.
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:
-
if
statements: Can be used outside JSX or within the component function. -
Ternary Operator (
condition ? true : false
): Great for inline conditional rendering. -
Logical
&&
(AND operator): Renders a component only if the condition is true, and nothing when it's false.
// 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:
- Data fetching (e.g., making an API call)
- Directly manipulating the DOM (though less common in React)
- Setting up subscriptions or timers
- Logging to the console (for debugging or tracking)
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:
- A function containing the side effect logic.
-
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 DOM Elements: The most common use case is to get a direct reference to a DOM node or a React component instance.
- Mutable Values: Storing any mutable value that needs to persist across renders but doesn't cause a re-render when updated (unlike state). Useful for storing timer IDs, subscriptions, or any value that is not part of the visual output.
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).