Hooks: useState and useEffect
What Are Hooks?
Hooks are JavaScript functions introduced in React 16.8 that unlock capabilities previously available only in class components. With hooks, you can add state, side effects, and lifecycle behaviour directly inside function components — no class required.
Before hooks, a component that needed to track a counter value and respond to user input looked very different. You had to write a class, define a constructor, bind methods, and manage this. Hooks collapse all that boilerplate into a handful of lines.
import { useState, useEffect } from 'react';
function Greeting() {
const [name, setName] = useState('World');
return <h1>Hello, {name}!</h1>;
}
// name === 'World'
This tutorial walks through the two most important hooks — useState and useEffect — with plenty of runnable examples. By the end you’ll know how to declare state, run side effects at the right time, and clean up after yourself.
Rules of Hooks
Hooks are regular JavaScript functions, but React imposes two hard rules around how you call them:
1. Only call hooks at the top level. Never call useState or useEffect inside loops, conditions, or nested functions. React relies on call order to associate each hook with the correct component instance. If the order changes between renders, React loses track of state.
This is invalid:
function Broken() {
if (condition) {
const [x, setX] = useState(0); // Wrong — inside a condition
}
return null;
}
Always call hooks at the top level of your component function, before any early returns.
2. Only call hooks from React functions. Call useState and useEffect from function components or custom hooks — never from regular JavaScript functions, event handlers, or class components.
// Correct — called from a function component
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
// count === 0
// Wrong — calling a hook from an event handler
function handleClick() {
const [x, setX] = useState(0); // This breaks React's rules
}
A third practical rule: name your custom hooks with the use prefix. This unlocks React’s linting rules and makes it clear the function follows hook rules.
useState — Declaring and Updating State
useState declares a piece of state in your component. It takes one argument: the initial state value. It returns an array with exactly two elements — the current state value and a setter function.
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
// count === 0
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
The array destructuring is just a convention — useState returns an array so you can name the two elements whatever you want. The first element is always the current state, and the second is always a function that updates it.
Updating State
Pass the new value directly to the setter:
const [name, setName] = useState('');
setName('Alice');
// name === 'Alice'
setName('Bob');
// name === 'Bob'
Functional Updates
When the next state depends on the previous state, always use the functional form of the setter:
const [count, setCount] = useState(0);
// Functional update — safe when next state depends on previous
setCount(prev => prev + 1);
// count === 1
setCount(prev => prev + 1);
// count === 2
Why does this matter? React schedules state updates. If you call setCount(count + 1) rapidly, the value of count inside the callback might be stale. The functional form always receives the most recent state value, so you avoid closure bugs.
Object and Array State
State can hold any JavaScript value: strings, numbers, booleans, objects, and arrays.
const [user, setUser] = useState({ name: '', age: 0 });
// Replace the whole object
setUser({ name: 'Alice', age: 30 });
// user.name === 'Alice'
// Partial update — always spread the previous object
setUser(prev => ({ ...prev, age: 31 }));
// user.age === 31
const [items, setItems] = useState([]);
// Add an item — create a new array
setItems(prev => [...prev, 'new item']);
// items === ['new item']
// Remove an item — filter returns a new array
setItems(prev => prev.filter(item => item !== 'old item'));
// items === ['new item']
React state should be treated as immutable. Never mutate user.name directly — always call the setter with a new object or array. This is how React detects when to re-render.
useEffect — Side Effects with the Dependency Array
useEffect runs a side effect after your component renders. A side effect is anything that reaches outside the component: fetching data from an API, attaching an event listener, starting a timer, or updating the document title.
import { useEffect } from 'react';
function MyComponent() {
useEffect(() => {
// This runs after every render by default
document.title = 'Hello from useEffect';
});
return <div>Hello</div>;
}
The second argument is the dependency array. It controls exactly when the effect runs:
// Runs once on mount — equivalent to componentDidMount in a class
useEffect(() => {
console.log('Component mounted');
}, []);
// Runs whenever `userId` changes between renders
useEffect(() => {
console.log('userId changed:', userId);
}, [userId]);
// Runs after every render — no dependency array at all
useEffect(() => {
console.log('Something rendered');
});
Think of the dependency array as a list of “reasons to re-run this effect.” If none of the values in the array changed since last render, React skips the effect entirely.
Cleanup Functions in useEffect
Many effects need to clean up after themselves — stopping timers, removing event listeners, or cancelling network requests. Return a function from your effect to handle this:
import { useState, useEffect } from 'react';
function Stopwatch() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setSeconds(prev => prev + 1);
}, 1000);
// Cleanup: runs when the component unmounts
// or before the effect re-runs
return () => clearInterval(id);
}, []); // Empty array — this effect runs once on mount
return <p>Elapsed: {seconds}s</p>;
}
// seconds === 0
The cleanup function runs in two situations:
- Before the component unmounts (to prevent memory leaks)
- Before the effect re-executes when its dependencies change (to reset before re-running)
Skipping cleanup is one of the most common sources of bugs in React applications. A timer started in an effect without cleanup keeps running even after the component disappears, causing stale updates and performance problems.
Common Patterns
Data Fetching
Fetching data is one of the most frequent uses of useEffect. Always include a cleanup flag to avoid setting state on an unmounted component:
import { useState, useEffect } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
let cancelled = false;
fetch(`https://api.example.com/users/${userId}`)
.then(res => res.json())
.then(data => {
if (!cancelled) setUser(data);
});
return () => { cancelled = true; };
}, [userId]);
if (!user) return <p>Loading...</p>;
return <h1>{user.name}</h1>;
}
// user === null
Event Listeners
Attach listeners inside useEffect and clean them up when done:
import { useState, useEffect } from 'react';
function WindowSize() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
function handleResize() {
setWidth(window.innerWidth);
}
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return <p>Window width: {width}px</p>;
}
// width === 1024
Timers
Timers started in effects must be cleared on cleanup:
import { useState, useEffect } from 'react';
function DelayCounter() {
const [value, setValue] = useState(0);
useEffect(() => {
const id = setTimeout(() => {
setValue(v => v + 1);
}, 1000);
return () => clearTimeout(id);
}, [value]);
return <p>Value: {value} (auto-increments every second)</p>;
}
// value === 0
Subscriptions
Online status, WebSocket connections, and similar subscriptions follow the same pattern:
import { useState, useEffect } from 'react';
function OnlineStatus() {
const [isOnline, setIsOnline] = useState(navigator.onLine);
useEffect(() => {
function handleOnline() { setIsOnline(true); }
function handleOffline() { setIsOnline(false); }
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
return <p>{isOnline ? 'Online' : 'Offline'}</p>;
}
// isOnline === true
Form Inputs with useState
Tracking user input is a primary use case for useState:
import { useState } from 'react';
function SearchBox() {
const [query, setQuery] = useState('');
return (
<div>
<input
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="Search..."
/>
<p>You typed: {query}</p>
</div>
);
}
// query === ''
Toggle State
Simple boolean toggles are a great first use of useState:
import { useState } from 'react';
function Toggle() {
const [on, setOn] = useState(false);
return (
<button onClick={() => setOn(prev => !prev)}>
{on ? 'ON' : 'OFF'}
</button>
);
}
// on === false
useState vs useEffect — When to Use Which
The distinction matters:
| Situation | Hook to use |
|---|---|
| Track a value that changes over time (counter, input, toggle) | useState |
| Run code as a side effect (fetch, timer, event listener) | useEffect |
| Next state depends on the previous state | Functional update setState(prev => ...) |
| React to a change in props or state | useEffect with dependency array |
A good mental model: if the result needs to appear in the render, it belongs in useState. If you need to synchronise with something outside React — the network, the DOM, a timer — it belongs in useEffect.
You will sometimes see code that uses useEffect to update state that could be computed during render. This creates an extra render cycle and is usually unnecessary. Ask yourself: can this value be derived from existing state or props? If yes, you probably don’t need a separate state variable.
Summary
Hooks let function components own state and side effects. useState holds data, useEffect performs actions. The two work together: useEffect can read state and props, and trigger updates via setters, while useState initialises values that useEffect reacts to.
The key rules to remember: call hooks only at the top level of your component, and only from function components or custom hooks. Always include cleanup for timers, listeners, and subscriptions to prevent memory leaks and stale updates.
Once you internalise these patterns, you’ll find that most React components can be expressed as a handful of useState declarations, one or two useEffect calls with proper cleanup, and a render function that turns state into UI.
See Also
- React: Intro and Setup — get React running in your project
- React: Components and JSX — understand the component model before adding hooks
- React: Event Handling — handle user interactions in function components