What is useState?
The useState Hook is a special function in React that lets function components have their own state — so they can “remember” values and update the UI automatically whenever those values change.
Before hooks, only class components could have state. Now, any functional component can track and update information!
How Does useState Work?
Import from React:
import { useState } from 'react';Initialise state:
const [value, setValue] = useState(initialValue);valueis the current state.setValueis a function to update that state.initialValueis the starting value (used only on the first render).
Update state:
When you use
setValue(newValue):It schedules a re-render for the component.
The component redraws with the updated value.
<div class="infographic-usestatehow" id="uh">
<style>
.infographic-usestatehow {
--ink: #1a1a1a;
--ink-soft: #5c5c5c;
--paper: #ffffff;
--paper-alt: #f0ece4;
--accent: #2563eb;
--accent-deep: #1942a8;
--accent-soft: #e3ebfd;
--line: #ddd6c9;
--radius: 8px;
--display-font: 'Fraunces', Georgia, serif;
--body-font: 'Inter', system-ui, sans-serif;
--mono-font: 'IBM Plex Mono', monospace;
max-width: 880px;
margin: 0 auto;
box-sizing: border-box;
background: transparent;
color: var(--ink);
font-family: var(--body-font);
}
.infographic-usestatehow[data-theme="dark"] {
--ink: #e8e6e1;
--ink-soft: #9a968c;
--paper: #13171f;
--paper-alt: #1c212b;
--accent: #5b8def;
--accent-deep: #8fb4ff;
--accent-soft: #20293f;
--line: #2c313d;
}
.infographic-usestatehow * { box-sizing: border-box; min-width: 0; }
.uh-eyebrow {
font-family: var(--mono-font); font-size: 0.72rem; letter-spacing: 0.1em;
text-transform: uppercase; color: var(--accent); margin: 0 0 0.45rem;
display: flex; align-items: center; justify-content: center; gap: 0.4rem;
}
.uh-eyebrow::before { content: ""; width: 14px; height: 2px; background: var(--accent); display: inline-block; }
.uh-title {
font-family: var(--display-font); font-size: clamp(1.5rem, 4vw, 2.3rem); text-align: center;
margin: 0 0 0.5rem; line-height: 1.14; letter-spacing: -0.01em;
}
.uh-sub { color: var(--ink-soft); font-size: 0.95rem; text-align: center; margin: 0 0 1.8rem; }
/* ===== anatomy code strip ===== */
.uh-strip-wrap { display: flex; justify-content: center; margin-bottom: 2.2rem; }
.uh-strip {
background: #0d0d10; border: 1px solid #24252d; border-radius: var(--radius);
padding: 0.9rem 1.3rem; font-family: var(--mono-font); font-size: 0.95rem; color: #6b7280;
text-align: center; max-width: 100%; overflow-x: auto; white-space: nowrap;
}
.uh-part {
cursor: pointer; border-radius: 5px; padding: 0.15rem 0.35rem; margin: 0 -0.05rem;
transition: background 0.2s, color 0.2s, opacity 0.2s;
}
.uh-part:hover { background: rgba(255,255,255,0.08); }
.uh-part:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.uh-part.pt-count { color: #e06c75; }
.uh-part.pt-setter { color: #61afef; }
.uh-part.pt-hook { color: #c678dd; }
.uh-part.pt-initial { color: #56b6c2; }
.uh-part.is-active { background: var(--accent); color: #fff !important; }
.uh-strip.has-active .uh-part:not(.is-active) { opacity: 0.35; }
/* ===== diagram ===== */
.uh-diagram { display: flex; flex-direction: column; align-items: center; margin-bottom: 1rem; }
.uh-box {
width: 100%; max-width: 340px; border: 1.5px solid var(--line); border-radius: var(--radius);
background: var(--paper-alt); padding: 0.9rem 1.1rem; text-align: center; position: relative;
transition: border-color 0.25s, box-shadow 0.25s, transform 0.25s;
}
.uh-box.is-active { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.uh-box.is-pulse { animation: uh-pulse 1.5s ease; }
@keyframes uh-pulse { 0% { transform: scale(1); } 45% { transform: scale(1.035); border-color: var(--accent); } 100% { transform: scale(1); } }
.uh-box-icon { width: 26px; height: 26px; margin: 0 auto 0.4rem; }
.uh-box-label { font-size: 0.68rem; font-weight: 700; letter-spacing: 0.05em; text-transform: uppercase; color: var(--ink-soft); margin-bottom: 0.25rem; }
.uh-box-value { font-family: var(--mono-font); font-size: 1.1rem; color: var(--ink); }
.uh-arrow { width: 18px; height: 22px; margin: 0.15rem 0; }
.uh-rerender-badge {
position: absolute; top: -11px; right: 10px; font-family: var(--mono-font); font-size: 0.6rem; font-weight: 700;
color: #fff; background: var(--accent); border-radius: 100px; padding: 0.15rem 0.5rem;
opacity: 0; transform: translateY(3px) scale(0.9); transition: opacity 0.2s, transform 0.2s;
}
.uh-rerender-badge.is-shown { opacity: 1; transform: translateY(0) scale(1); }
.uh-returns-row { display: flex; gap: 0.7rem; width: 100%; max-width: 340px; }
.uh-returns-row .uh-box { max-width: none; flex: 1; }
/* ===== live demo ===== */
.uh-demo { display: flex; flex-direction: column; align-items: center; gap: 0.6rem; margin: 1.6rem 0 1rem; }
.uh-demo-label {
font-family: var(--mono-font); font-size: 0.68rem; letter-spacing: 0.1em; text-transform: uppercase; color: var(--ink-soft);
}
.uh-demo-btn {
font: inherit; font-weight: 600; font-size: 0.85rem; color: #fff; background: var(--accent);
border: none; border-radius: 100px; padding: 0.6rem 1.4rem; cursor: pointer;
box-shadow: 0 6px 16px -4px rgba(37,99,235,0.45); transition: transform 0.15s;
}
.uh-demo-btn:hover { transform: translateY(-1px); }
.uh-demo-btn:active { transform: scale(0.96); }
.uh-demo-btn:focus-visible { outline: 2px solid var(--accent-deep); outline-offset: 2px; }
.uh-caption {
text-align: center; font-size: 0.9rem; color: var(--ink); min-height: 2.4em; display: flex;
align-items: center; justify-content: center; max-width: 480px; margin: 0 auto 1.6rem; line-height: 1.45;
}
.uh-takeaway {
padding: 0.95rem 1.2rem; background: var(--accent-soft); border-left: 3px solid var(--accent);
border-radius: 4px; font-size: 0.9rem; color: var(--ink); line-height: 1.5;
max-width: 620px; margin: 0 auto;
}
.uh-takeaway b { color: var(--accent-deep); }
@media (prefers-reduced-motion: reduce) {
.infographic-usestatehow *, .infographic-usestatehow *::before, .infographic-usestatehow *::after {
transition-duration: 0.001ms !important; animation-duration: 0.001ms !important;
}
}
</style>
<div class="uh-eyebrow">React Hooks</div>
<h1 class="uh-title">How useState Works</h1>
<p class="uh-sub">Click a piece below, or press +1 to watch the cycle run.</p>
<div class="uh-strip-wrap">
<div class="uh-strip" id="uhStrip">
<span class="tok-punc">const [ </span><span class="uh-part pt-count" data-part="count" tabindex="0" role="button" aria-pressed="false">count</span><span class="tok-punc">, </span><span class="uh-part pt-setter" data-part="setter" tabindex="0" role="button" aria-pressed="false">setCount</span><span class="tok-punc"> ] = </span><span class="uh-part pt-hook" data-part="hook" tabindex="0" role="button" aria-pressed="false">useState</span><span class="tok-punc">( </span><span class="uh-part pt-initial" data-part="initial" tabindex="0" role="button" aria-pressed="false">0</span><span class="tok-punc"> );</span>
</div>
</div>
<div class="uh-diagram">
<div class="uh-box" id="uhBoxHook" data-part="hook">
<svg class="uh-box-icon" viewBox="0 0 26 26" aria-hidden="true"><circle cx="13" cy="13" r="12" fill="none" stroke="var(--accent)" stroke-width="1.8"/><path d="M9 13a4 4 0 1 1 4 4" fill="none" stroke="var(--accent)" stroke-width="1.8" stroke-linecap="round"/><circle cx="9" cy="13" r="1.4" fill="var(--accent)"/></svg>
<div class="uh-box-label">Called on every render</div>
<div class="uh-box-value">useState(0)</div>
</div>
<svg class="uh-arrow" viewBox="0 0 18 22" aria-hidden="true"><line x1="9" y1="0" x2="9" y2="16" stroke="var(--ink-soft)" stroke-width="1.6"/><path d="M4 13 L9 20 L14 13" fill="none" stroke="var(--ink-soft)" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>
<div class="uh-box" id="uhBoxMemory" data-part="initial">
<span class="uh-rerender-badge" id="uhRerenderBadge">↻ re-render</span>
<svg class="uh-box-icon" viewBox="0 0 26 26" aria-hidden="true"><rect x="4" y="6" width="18" height="15" rx="2" fill="var(--accent-soft)" stroke="var(--accent)" stroke-width="1.6"/><line x1="4" y1="11" x2="22" y2="11" stroke="var(--accent)" stroke-width="1.6"/></svg>
<div class="uh-box-label">React remembers</div>
<div class="uh-box-value" id="uhMemoryValue">count = 0</div>
</div>
<svg class="uh-arrow" viewBox="0 0 18 22" aria-hidden="true"><line x1="9" y1="0" x2="9" y2="16" stroke="var(--ink-soft)" stroke-width="1.6"/><path d="M4 13 L9 20 L14 13" fill="none" stroke="var(--ink-soft)" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>
<div class="uh-returns-row">
<div class="uh-box" id="uhBoxCount" data-part="count">
<div class="uh-box-label">count</div>
<div class="uh-box-value" id="uhCountValue">0</div>
</div>
<div class="uh-box" id="uhBoxSetter" data-part="setter">
<div class="uh-box-label">setCount</div>
<div class="uh-box-value">fn()</div>
</div>
</div>
</div>
<div class="uh-demo">
<div class="uh-demo-label">Try it</div>
<button class="uh-demo-btn" id="uhDemoBtn" type="button">setCount(count + 1)</button>
</div>
<p class="uh-caption" id="uhCaption">Click any part of the code above to see what it does.</p>
<div class="uh-takeaway"><b>useState doesn't reset</b> like a normal variable would — React keeps the value in its own memory between renders, and calling the setter is the only way to change it.</div>
</div>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Fraunces:wght@500;600&family=Inter:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet">
<script>
(function () {
var root = document.getElementById('uh');
if (!root) return;
/* --- dark mode detection: body attribute > cookie > postMessage > light --- */
function applyDark(dark) {
if (dark) root.setAttribute('data-theme', 'dark');
else root.removeAttribute('data-theme');
}
var bodyAttr = document.body.getAttribute('data-darkmode');
var dark;
if (bodyAttr === 'true') dark = true;
else if (bodyAttr === 'false') dark = false;
else {
var match = document.cookie.match(/(?:^|;\s*)darkMode=([^;]+)/);
dark = match ? match[1] === 'true' : false;
}
applyDark(dark);
window.addEventListener('message', function (e) {
if (e.data && e.data.type === 'darkMode') applyDark(!!e.data.value);
});
/* --- anatomy click-to-explain --- */
var strip = root.querySelector('#uhStrip');
var parts = root.querySelectorAll('.uh-part');
var boxes = root.querySelectorAll('.uh-box');
var caption = root.querySelector('#uhCaption');
var defaultCaption = 'Click any part of the code above to see what it does.';
var explain = {
count: 'The current value \u2014 read fresh from React\u2019s memory on every render.',
setter: 'Call this to update the value and tell React to re-render.',
hook: 'This line runs again every single time the component renders.',
initial: 'Only used on the very first render \u2014 after that, React ignores it.'
};
var activePart = null;
function setActivePart(key) {
activePart = (activePart === key) ? null : key;
strip.classList.toggle('has-active', !!activePart);
parts.forEach(function (p) {
var match = p.getAttribute('data-part') === activePart;
p.classList.toggle('is-active', match);
p.setAttribute('aria-pressed', match ? 'true' : 'false');
});
boxes.forEach(function (b) {
b.classList.toggle('is-active', !!activePart && b.getAttribute('data-part') === activePart);
});
caption.textContent = activePart ? explain[activePart] : defaultCaption;
}
parts.forEach(function (p) {
p.addEventListener('click', function () { setActivePart(p.getAttribute('data-part')); });
p.addEventListener('keydown', function (e) {
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setActivePart(p.getAttribute('data-part')); }
});
});
/* --- live re-render cycle demo --- */
var demoBtn = root.querySelector('#uhDemoBtn');
var memoryBox = root.querySelector('#uhBoxMemory');
var memoryValue = root.querySelector('#uhMemoryValue');
var countBox = root.querySelector('#uhBoxCount');
var countValue = root.querySelector('#uhCountValue');
var hookBox = root.querySelector('#uhBoxHook');
var rerenderBadge = root.querySelector('#uhRerenderBadge');
var count = 0;
var running = false;
demoBtn.addEventListener('click', function () {
if (running) return;
running = true;
demoBtn.disabled = true;
count = count + 1;
memoryBox.classList.add('is-pulse');
memoryValue.textContent = 'count = ' + count;
setTimeout(function () { memoryBox.classList.remove('is-pulse'); }, 1500);
setTimeout(function () {
rerenderBadge.classList.add('is-shown');
hookBox.classList.add('is-pulse');
}, 900);
setTimeout(function () {
countBox.classList.add('is-pulse');
countValue.textContent = count;
}, 2000);
setTimeout(function () {
hookBox.classList.remove('is-pulse');
countBox.classList.remove('is-pulse');
rerenderBadge.classList.remove('is-shown');
demoBtn.disabled = false;
running = false;
}, 3500);
});
})();
</script>Practical Example: Counter
Class Component:
class Counter extends React.Component {
constructor() {
super();
this.state = { count: 0 };
this.increment = this.increment.bind(this);
}
increment() {
this.setState({ count: this.state.count + 1 });
}
render() {
return <button onClick={this.increment}>
Count: {this.state.count}
</button>
}
}Functional Component with useState:
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
);
}No classes, binding, or
thiskeyword needed!Just state and set function, declared in one line.
Notice boilerplate: constructor, binding,
this.state,this.setState.
Breaking Down the Syntax
Array destructuring:
The hook returns a pair —
[stateVariable, setFunction]— which you can name however you like.Initial value:
The
initialValueis set only once (on the first render); later renders use the updated state.Supported data types:
You can store anything: numbers, strings, arrays, objects, or functions.
Key Rules of Hooks
Call hooks at the top level of your component —
Not inside loops, conditional statements, or nested functions.
Only use hooks in React function components or inside your own custom hooks.
Why Prefer useState Over Class State?
Cleaner and shorter code — Less boilerplate, easier to read.
No
thisconfusion — No method binding or worrying about context.Logic organization — You can group stateful logic right where it’s used.
Reusability — Easily extract logic as custom hooks for sharing between components.
Key Benefits
Modern React — useState is a foundation for React’s functional programming style.
Automatic UI updates — Changes to state always update the visible UI.
Flexible and powerful — Great for anything from simple counters to complex, interactive interfaces.