useState and useEffect
Workplace Context
As you build more sophisticated React applications, you’ll find that managing component state and handling side effects become increasingly complex. For instance, you might need to update state based on its previous value in rapid succession, manage forms with interconnected fields, or interact with browser APIs and external data sources. A deeper understanding of useState
’s nuances and the capabilities of useEffect
for managing side effects like data fetching, subscriptions, or manual DOM manipulations after rendering, are crucial for creating robust, performant, and maintainable React components. This lesson provides that deeper understanding, enabling you to tackle these common yet advanced scenarios effectively.
Learning Objectives
By the end of this lesson, learners will be able to:
- Explain and utilize functional updates with
useState
for reliable state transitions. - Understand how React batches state updates and its implications.
- Implement complex state management patterns using
useState
for objects and arrays, ensuring immutability. - Describe the purpose and fundamental syntax of the
useEffect
hook for managing side effects. - Utilize the
useEffect
dependency array to control when effects are re-executed. - Implement cleanup functions within
useEffect
to prevent memory leaks and unwanted behavior. - Identify and implement common
useEffect
patterns such as running effects once on mount or responding to prop/state changes.
Deep Dive into useState
In React Fundamentals, you were introduced to the useState
hook as the primary way to add state to functional components. Let’s explore some of its more advanced aspects.
Recap: useState
Basics
Quickly, useState
is a Hook that lets you add React state to function components.
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0); // count is the state, setCount is the updater
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
You call it with an initial state, and it returns an array containing the current state value and a function to update that state.
Functional Updates
When your new state depends on the previous state, you can pass a function to the state setter. This function will receive the previous state as its argument and should return the new state. This is known as a functional update.
Why are functional updates important?
- Correctness with Asynchronous Updates: State updates in React can be asynchronous and batched for performance. If you update state multiple times in a single event handler based on the current state variable directly, you might be working with a stale value. Functional updates ensure you’re always using the most recent state.
- State Batching: React often groups multiple state updates into a single re-render to improve performance. Functional updates are crucial when an update relies on the value from a previous update within the same batch.
Syntax:
setCount(prevState => prevState + newValue);
State Batching
React’s performance strategy includes batching state updates. This means that when you call setState
multiple times within a single synchronous event handler (like a button click), React doesn’t re-render the component for each setState
call. Instead, it “batches” them together and performs a single re-render at the end of the event.
Since React 18, automatic batching is more extensive and consistent, applying to updates inside of timeouts, promises, native event handlers, or any other event. This generally improves performance out-of-the-box. Functional updates are still best practice when the next state depends on the previous one, ensuring correctness regardless of batching behavior.
Complex State Management with useState
useState
isn’t limited to primitive values like numbers or strings. You can also manage objects and arrays. However, it’s crucial to remember the principle of immutability: never modify state directly. Always create a new copy of the object or array with the changes.
Managing Objects
When updating an object in state, use the spread syntax (...
) to copy the existing properties and then override the ones you want to change.
Managing Arrays
Similarly, for arrays, use immutable operations:
- Adding:
setMyArray(prevArray => [...prevArray, newItem])
- Removing:
setMyArray(prevArray => prevArray.filter(item => item.id !== itemIdToRemove))
- Updating:
setMyArray(prevArray => prevArray.map(item => item.id === itemIdToUpdate ? { ...item, property: newValue } : item))
The useEffect
Hook
While useState
allows components to remember things, many applications need to perform actions that go beyond managing state directly within the component’s rendering logic. These are called side effects.
What are Side Effects?
Side effects are operations that interact with the “outside world” from within your component. Common examples include:
- Data fetching from an API
- Setting up or tearing down subscriptions (e.g., to timers, WebSockets, browser events)
- Manually changing the DOM (though this should be rare in React)
- Logging to the console
- Setting the document title
These actions shouldn’t block rendering and often need to happen after React has updated the DOM.
Introduction to useEffect
The useEffect
Hook lets you perform side effects in function components. You pass it a function (the “effect”), and React will run this function after performing the DOM updates.
Basic Syntax:
useEffect(() => { /* Your effect code here */ }, [dependencies]);
- The first argument is a function that contains the side effect logic.
- The second argument (optional) is the dependency array.
The Dependency Array
The dependency array is crucial for controlling when your effect re-runs.
-
No Dependency Array (Runs after every render):
- Example:
useEffect(() => { console.log('Component rendered or updated'); });
- If you omit the dependency array, the effect runs after the initial render and after every subsequent re-render of the component.
- This is often a source of bugs (like infinite loops if the effect itself triggers a state update that causes a re-render). Use with caution.
- Example:
-
Empty Dependency Array
[]
(Runs once after initial render):- Example:
useEffect(() => { console.log('Component did mount (initial render)'); }, []);
- This tells React to run the effect only once, after the initial render. This is common for:
- Initial data fetching.
- Setting up subscriptions that should last the lifetime of the component.
- Example:
-
Dependency Array with Values
[propA, stateB]
(Runs when dependencies change):- Example:
useEffect(() => { console.log(`Value of propA or stateB changed: ${propA}, ${stateB}`); }, [propA, stateB]);
- The effect will run after the initial render and then again only if any of the values in the dependency array have changed between renders. React uses
Object.is
comparison to check for changes. - This is vital for effects that depend on certain props or state to avoid unnecessary re-execution.
- Example:
Terminal
Always include all values from your component scope (props, state, functions) that are used inside the useEffect
callback in its dependency array. If you omit a dependency, your effect might capture stale values, leading to bugs. ESLint plugins (like eslint-plugin-react-hooks
) often help enforce this.
Cleanup Functions
Some side effects need to be “cleaned up” when the component unmounts or before the effect runs again. For example:
- Clearing timers (
clearInterval
,clearTimeout
) - Unsubscribing from event listeners (
window.removeEventListener
) - Canceling API requests or closing WebSocket connections
To perform cleanup, you can return a function from your useEffect
callback. This cleanup function will be executed:
- Before the component unmounts.
- Before the effect runs again (if its dependencies have changed).
Syntax:
useEffect(() => {
// Effect setup
const timerId = setInterval(() => {
console.log('Timer tick');
}, 1000);
// Cleanup function
return () => {
clearInterval(timerId);
console.log('Timer cleaned up');
};
}, []); // Empty array: runs once on mount, cleans up on unmount
In TimerComponent
, if isActive
changes, the previous interval is cleared before a new one (or none) is set up. If ShowHideTimer
hides TimerComponent
, its cleanup function will also run.
Common useEffect
Patterns (Introduction)
While Lesson 2 will dive deeper, here are a few common patterns:
-
Fetching Data on Mount:
useEffect(() => { fetch('https://api.example.com/data') .then(response => response.json()) .then(data => console.log(data)); }, []); // Empty array: fetch once
(We’ll improve this with loading/error states later.)
-
Subscribing to Browser Events:
useEffect(() => { const handleResize = () => console.log('Window resized'); window.addEventListener('resize', handleResize); return () => { window.removeEventListener('resize', handleResize); }; }, []);
-
Updating Document Title:
useEffect(() => { document.title = \`Current Page: \${pageName}\`; }, [pageName]);
Activity 1: Form with State Dependencies
Task: Build a form component (RegistrationForm
) with the following fields:
username
(text input)password
(password input)confirmPassword
(password input)email
(email input, optional)subscribeToNewsletter
- checkbox, only enabled if
email
is provided and valid - basic validation like includes ’@’ is sufficient
- checkbox, only enabled if
Requirements:
- Manage form state using
useState
. You can choose individual state variables or a single state object. - The
confirmPassword
field should show an error message (e.g., a simple<p>
tag) if its value doesn’t match thepassword
field. This check should happen as the user types. - The
subscribeToNewsletter
checkbox should only be enabled if theemail
field contains text and includes an ”@” symbol. - Implement functional updates for state if one field’s validation depends on another.
- On submit,
console.log
the form data (if valid).
Completed Example:
Activity 2: Data Fetching Component with useEffect
Task: Create a component (PostFetcher
) that fetches a single post from the JSONPlaceholder API (https://jsonplaceholder.typicode.com/posts/1
) when it mounts.
Requirements:
- Use
useState
to manage:post
(to store the fetched post data, initiallynull
orundefined
).loading
(boolean, initiallytrue
).error
(to store any error message, initiallynull
).
- Use
useEffect
to fetch the data when the component mounts.- Set
loading
tofalse
after the fetch completes (successfully or with an error). - If successful, store the fetched post in the
post
state. - If there’s an error, store the error message in the
error
state.
- Set
- Render the UI:
- If
loading
istrue
, display “Loading post…”. - If
error
is present, display the error message. - If
post
data is available, display itstitle
andbody
.
- If
- Make it interactive:
- Add a button to fetch a different post (e.g., by changing an ID in state, which
useEffect
should then depend on).
- Add a button to fetch a different post (e.g., by changing an ID in state, which
Completed Example:
Loading post...
Knowledge Check
When using useState
and updating state based on its previous value, why is it recommended to use a functional update (e.g., setCount(prevCount => prevCount + 1)
)?
- Select an answer to view feedback.
What does an empty dependency array ([]
) in a useEffect
hook signify?
- Select an answer to view feedback.
When should you provide a cleanup function in useEffect
?
- Select an answer to view feedback.
If you have useEffect(() => { console.log(myProp); }, [])
, and myProp
changes, will the console log the new value?
- Select an answer to view feedback.
Summary
In this lesson, we took a deeper look at the useState
hook, emphasizing functional updates for reliable state transitions and immutable patterns for managing complex state like objects and arrays. We then introduced useEffect
, a powerful hook for handling side effects such as data fetching, subscriptions, and manual DOM changes. You learned how the dependency array controls when effects run and the importance of cleanup functions for preventing memory leaks and managing resources. These concepts are foundational for building more dynamic and robust React applications.
References
- React Docs:
Additional Resources
- Thinking in React (touches on state management)
- A Complete Guide to useEffect
- Immutability in React and Redux (ignore the Redux sections)