The fundamentals of useState are straightforward โ declare state, call the setter, React re-renders. But as your MERN Blog components grow more complex, several subtle patterns and pitfalls become important. In this lesson you will master the functional updater form for safe sequential updates, lazy initialisation for expensive initial values, how to decide between multiple state variables and one grouped object, and how to recognise and fix the most common useState bugs: stale closures, direct mutation, and unnecessary re-renders caused by state that should not be state.
The Functional Updater Form
// When new state depends on old state โ always use the functional form
// This guarantees you always act on the latest state value,
// even if multiple updates are batched together
// โโ Counter โ safe increment โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
setCount(prev => prev + 1); // โ uses latest queued value
// โโ Toggle โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
setIsOpen(prev => !prev); // โ
// โโ Add to list โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
setPosts(prev => [...prev, newPost]); // โ
// โโ Remove from list โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
setPosts(prev => prev.filter(p => p._id !== deletedId)); // โ
// โโ Update item in list โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
setPosts(prev => prev.map(p =>
p._id === updatedPost._id ? { ...p, ...updatedPost } : p
)); // โ
// โโ Object with one field updated โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
setUser(prev => ({ ...prev, name: 'Jane' })); // โ
setState(prev => newValue)) is not just for sequential updates โ it is also the correct pattern when your state update logic runs inside a closure that may capture a stale value. If your update function is defined inside a useEffect or a timer callback, the functional form ensures it always reads the current state value rather than the stale snapshot captured when the closure was created.setState(newValue)) is only appropriate when the new value is completely independent of the previous value โ for example, resetting a form to an empty string or setting a flag to a specific known value.useEffect dependency array, covered in depth in Chapter 17.Lazy Initialisation
// โโ Problem: expensive computation runs on every render โโโโโโโโโโโโโโโโโโโโโโโ
const [posts, setPosts] = useState(JSON.parse(localStorage.getItem('posts')) || []);
// JSON.parse is called every render, even though useState only uses it on mount
// โโ Solution: lazy initialiser โ pass a function, not a value โโโโโโโโโโโโโโโโโ
const [posts, setPosts] = useState(() => {
// This function runs ONLY on the first render
const saved = localStorage.getItem('posts');
return saved ? JSON.parse(saved) : [];
});
// Lazy initialisation use cases:
// โ Reading from localStorage or sessionStorage
// โ Heavy computation to derive the initial value
// โ Creating a complex initial object
// โ Parsing URL search params for initial filter state
const [filters, setFilters] = useState(() => {
const params = new URLSearchParams(window.location.search);
return {
tag: params.get('tag') || '',
sort: params.get('sort') || 'createdAt',
page: parseInt(params.get('page') || '1', 10),
};
});
Multiple Variables vs One Object
// โโ Rule: group state that always changes together โโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// โ Separate variables: independent values that rarely update together
const [searchQuery, setSearchQuery] = useState('');
const [currentPage, setCurrentPage] = useState(1);
const [selectedTag, setSelectedTag] = useState('');
// Each can be updated independently without touching the others
// โ One object: values that are always set/reset together
const [asyncState, setAsyncState] = useState({ data: null, loading: false, error: null });
// When a fetch starts: loading: true, error: null
// When it succeeds: data: result, loading: false, error: null
// When it fails: error: msg, loading: false, data: null
// These three always change together โ makes sense as one object
const setLoading = () => setAsyncState({ data: null, loading: true, error: null });
const setSuccess = (data) => setAsyncState({ data, loading: false, error: null });
const setError = (error) => setAsyncState({ data: null, loading: false, error });
// โโ Anti-pattern: artificial grouping โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const [state, setState] = useState({ isOpen: false, count: 0, name: '' });
// These are unrelated โ updating isOpen requires spreading count and name
// Separate variables are cleaner here
Identifying Unnecessary State
// Ask: "Can this be computed from existing state?"
// If yes โ compute it, do not store it
function PostListPage() {
const [posts, setPosts] = useState([]);
const [query, setQuery] = useState('');
const [filter, setFilter] = useState('all');
// โ WRONG โ derived state:
// const [filteredPosts, setFilteredPosts] = useState([]);
// const [postCount, setPostCount] = useState(0);
// โ
CORRECT โ derived values computed at render time:
const filteredPosts = posts
.filter(p => filter === 'all' || p.published === (filter === 'published'))
.filter(p => p.title.toLowerCase().includes(query.toLowerCase()));
const postCount = filteredPosts.length; // always in sync
const hasResults = filteredPosts.length > 0; // always in sync
const isEmpty = filteredPosts.length === 0 && !loading; // always in sync
return (
<div>
<p>{postCount} posts found</p>
<PostList posts={filteredPosts} />
</div>
);
}
The Stale Closure Bug โ and the Fix
// โโ The bug: handler captures stale state value โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function AutoSaveEditor() {
const [content, setContent] = useState('');
useEffect(() => {
const timer = setInterval(() => {
// content is captured from the initial render โ always ''!
console.log('Auto-saving:', content); // stale closure bug
localStorage.setItem('draft', content);
}, 5000);
return () => clearInterval(timer);
}, []); // empty deps โ timer never re-created, content stays stale
return <textarea value={content} onChange={e => setContent(e.target.value)} />;
}
// โโ Fix 1: Add content to the dependency array โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
useEffect(() => {
const timer = setInterval(() => {
localStorage.setItem('draft', content); // always fresh
}, 5000);
return () => clearInterval(timer);
}, [content]); // re-creates timer when content changes
// โโ Fix 2: Use a ref to hold the latest value โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const contentRef = useRef(content);
useEffect(() => { contentRef.current = content; }, [content]);
useEffect(() => {
const timer = setInterval(() => {
localStorage.setItem('draft', contentRef.current); // always latest via ref
}, 5000);
return () => clearInterval(timer);
}, []); // timer never re-created, but reads latest via ref
Common Mistakes
Mistake 1 โ Using useState for values that do not need to trigger re-renders
โ Wrong โ storing a timeout ID or a subscription reference in state:
const [timerId, setTimerId] = useState(null);
setTimerId(setTimeout(fn, 1000)); // triggers re-render unnecessarily
โ Correct โ use useRef for mutable values that should not cause re-renders:
const timerRef = useRef(null);
timerRef.current = setTimeout(fn, 1000); // no re-render โ
Mistake 2 โ Initialising state with a prop and expecting it to stay in sync
โ Wrong โ state initialised from a prop does not update when the prop changes:
function EditModal({ post }) {
const [title, setTitle] = useState(post.title); // one-time init
// If post.title changes in the parent โ title state does NOT update!
}
โ Correct โ use the prop directly if it is read-only, or use useEffect to sync:
useEffect(() => { setTitle(post.title); }, [post.title]); // syncs when prop changes โ
Mistake 3 โ Not resetting form state when the component is reused
โ Wrong โ form retains previous values when the parent passes a new post to edit:
function PostEditForm({ post }) {
const [title, setTitle] = useState(post.title);
// When parent passes a different post โ title still shows the old post's title
}
โ Correct โ add a key to force remount when the identity changes:
<PostEditForm key={post._id} post={post} />
// Changing post._id forces React to unmount + remount โ fresh state โ
Quick Reference
| Pattern | Code |
|---|---|
| Functional updater | setState(prev => newValue) |
| Toggle | setFlag(prev => !prev) |
| Lazy initialisation | useState(() => expensiveComputation()) |
| Group related state | useState({ loading: false, data: null, error: null }) |
| Update grouped field | setState(prev => ({ ...prev, field: value })) |
| Force remount | <Component key={uniqueId} /> |
| Non-rendering mutable ref | const ref = useRef(initialValue) |