You update state in React, then immediately try to read the new value or fire a network request that depends on it — and it uses the old value. This happens because state updates are asynchronous and batched. The fix is to run your logic after the update commits, using a callback. How you do that depends on whether you are in a class component or a functional component.
Quick answer. In a React class component, pass a function as the second argument to setState — it runs after the state updates and the component re-renders. Functional components have no such callback: use a useEffect hook with the state value in its dependency array to run code after useState updates.
Why doesn't setState update the state immediately?
setState does not return a Promise, and it does not synchronously change this.state. React queues the change and applies it on the next render, often batching several updates together for performance. So this pattern is a common bug:
// ❌ Reads the OLD speed, not the value you just set
this.setState({ speed: 80 });
console.log(this.state.speed); // still the previous value
this.checkSpeed(); // runs against stale stateBecause the read runs before React has committed the update, you get the previous value. The same trap exists with the useState setter in functional components. To reliably run code once the new state is live, you need a callback.
How do you use the setState callback in a class component?
this.setState() accepts an optional second argument: a callback function that React runs after the state has been updated and the component has re-rendered. This is the correct place for follow-up work such as an API call, validation, or logging.
Here is a complete example — a small speed checker that generates a challan (a fine) when the speed goes over the limit:
import React from "react";
class ChallanGenerator extends React.Component {
state = { speed: 0 };
// Runs only AFTER speed has been committed to state
checkSpeed = () => {
if (this.state.speed > 70) {
// safe to make the API call — state is up to date
console.log("Over limit — generating challan for", this.state.speed);
} else {
console.log("Within limit:", this.state.speed);
}
};
handleChange = (event) => {
const value = Number(event.target.value);
// second argument is the callback
this.setState({ speed: value }, this.checkSpeed);
};
render() {
return (
<div>
<p>Challan Generator</p>
<input
type="number"
value={this.state.speed}
placeholder="miles per hour"
onChange={this.handleChange}
/>
</div>
);
}
}
export default ChallanGenerator;Because checkSpeed is passed as the callback, it always sees the latest speed — never a stale value. This is the classic, reliable answer to "run this once the state has actually changed."
Does useState have a callback in functional components?
No. The useState setter does not accept a second callback argument the way this.setState does. If you pass one, React ignores it (and warns you in development). The React team left it out deliberately, because useEffect and useLayoutEffect cover the same need more cleanly.
So the equivalent of a setState callback in a functional component is a useEffect that lists the state value in its dependency array.
How do you run code after useState updates?
Set the state as usual, then put the follow-up logic inside a useEffect whose dependency array contains that piece of state. React runs the effect after the render caused by the update, so you always get the fresh value:
import React, { useEffect, useState } from "react";
function ChallanGenerator() {
const [speed, setSpeed] = useState(0);
const updateSpeed = (event) => {
setSpeed(Number(event.target.value));
};
// Acts like a setState callback: runs after `speed` updates
useEffect(() => {
if (speed === 0) return;
if (speed > 70) {
console.log("Over limit — generating challan for", speed);
} else {
console.log("Within limit:", speed);
}
}, [speed]); // dependency: re-run only when speed changes
return (
<div>
<p>Challan Generator</p>
<input
type="number"
value={speed}
placeholder="miles per hour"
onChange={updateSpeed}
/>
</div>
);
}
export default ChallanGenerator;The dependency array [speed] is what makes this behave like a callback: the effect fires after every render where speed changed, and skips renders where it didn't. Leave the array off entirely and the effect runs after every render, which is rarely what you want.
When should you use useEffect vs useLayoutEffect?
Reach for useEffect by default — it runs after the browser paints, so it won't block rendering. Use useLayoutEffect only when your follow-up logic reads or mutates the DOM (measuring an element, adjusting scroll position) and you need it to happen before the user sees the frame. For side effects like API calls, logging, or validation, useEffect is the right choice.
How do you compute new state from the previous state?
If your new value depends on the current one, don't read state directly inside the setter — pass an updater function instead. This avoids stale-value bugs when several updates are batched together:
// Class component
this.setState((prev) => ({ count: prev.count + 1 }), this.afterIncrement);
// Functional component
setCount((prev) => prev + 1);The updater form (prev => ...) guarantees you're building on the latest state, and in a class component you can still attach a callback as the second argument.
Class component vs functional component: quick comparison
| Need | Class component | Functional component |
|---|---|---|
| Update state | this.setState({...}) | setState(value) |
| Run code after update | Second argument to setState | useEffect(fn, [state]) |
| Use previous state | setState(prev => ...) | setState(prev => ...) |
| DOM-reflecting work | callback in setState | useLayoutEffect(fn, [state]) |
New React code should default to functional components and hooks — that's the pattern the React team recommends and maintains going forward. The class-based setState callback remains fully supported for existing codebases.
Building a React front end and need experienced engineers who already know these patterns? You can hire vetted remote React developers through Codersera and extend your team quickly.
FAQ
What is a callback function in JavaScript?
A callback is a function passed as an argument to another function, to be run ("called back") at a later point — either synchronously or asynchronously. In React, the function you pass as the second argument to setState is a callback that React runs after the state update completes.
What is the difference between a function and a callback?
Every callback is a function, but a callback is specifically a function you hand to another function so it can invoke it later. A regular function you call yourself; a callback is called for you by the code you passed it to — here, by React once the state has updated.
Are callbacks asynchronous?
Not inherently. A callback runs whenever the receiving function decides to run it — that can be synchronous or asynchronous. React's setState callback is effectively asynchronous because React defers it until after the state update and re-render commit.
Does useState support a callback like setState?
No. The useState setter ignores a second callback argument. To run code after a functional-component state update, use a useEffect hook with that state value in its dependency array.
Why is setState asynchronous?
React batches state updates and applies them on the next render for performance, so this.state does not change the instant you call setState. That's why reading state right after calling setState gives you the old value, and why the callback (or useEffect) exists.