The First useState Hook
Till now, our click handler can run some code, maybe print something to console, whatever. But logging "clicked" doesn't change anything on screen. If we actually want the component to remember something between renders, and then ask React to render again when that thing changes, we need state.
And this is what useState gives us.
import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
{count}
</button>
);
}The button starts with 0. Click it once, we ask React to make the next count 1, React runs Counter again, and now the button shows 1.
Pretty small piece of code. There's actually quite a lot happening in those few lines though, so let's not just memorize const [something, setSomething] = useState(...) and move on. I want to understand what each value is doing.
Why can't we just use a normal variable?
First thing you might try is completely reasonable. We need a count, JavaScript has variables, so... use a variable.
function Counter() {
let count = 0;
function handleClick() {
count += 1;
}
return <button onClick={handleClick}>{count}</button>;
}Looks okay at first.
When you click, handleClick runs and count += 1 really does change that variable. So if count was 0, it becomes 1. JavaScript is doing exactly what you asked.
But the button still says 0.
Why?
Because changing a normal JavaScript variable doesn't tell React, "hey, something changed, run this component again". React has no reason to render just because some local variable got assigned another value.
And there's another problem. Suppose the component does render again for some unrelated reason, maybe its parent rendered. React calls Counter() again from the top, and what does this line do?
let count = 0;Yep. New function call, new local variable, back to zero.
So a normal variable is missing two things we need here. We need the value to survive from one render to another, and when we change it, we need some way to request another render.
That's state.
React keeps state for a component while that component stays at the same place in the rendered tree. Calling its state setter requests another render with the new state.
Importing useState
useState comes from the react package, so first we import it.
import { useState } from "react";Then inside our component:
function Counter() {
const statePair = useState(0);
// ...
}That 0 is the initial value. We're saying this piece of state should start at zero.
What comes back from useState()? An array containing two things. First item is the current state value, second item is the function we use when we want to update that state.
We could technically write this:
const statePair = useState(0);
const count = statePair[0];
const setCount = statePair[1];Nobody wants to keep writing that, so we use normal JavaScript array destructuring:
const [count, setCount] = useState(0);Nothing React-specific about the destructuring syntax itself. React just returns the two values, JavaScript pulls them out for us.
So count is the value for this render, and setCount is the function we call to request another value.
Why count and setCount?
You don't actually have to use those names.
This is valid JavaScript and valid React:
const [banana, updateBanana] = useState(0);React doesn't care.
Your future self probably will.
Usually if the value is called count, we call the setter setCount. If it's query, we use setQuery. If it's isOpen, then setIsOpen.
const [count, setCount] = useState(0);
const [query, setQuery] = useState("");
const [isOpen, setIsOpen] = useState(false);It's just convention, but it's a good one because you can immediately see which setter belongs to which state value.
Booleans also read nicer when the name itself sounds like a condition.
const [showDetails, setShowDetails] = useState(false);Then later:
{showDetails && <LessonDetails />}That reads pretty naturally. Show the details if showDetails is true.
Calling the setter
Now let's actually change the count.
function handleClick() {
setCount(count + 1);
}Suppose this render has count === 0. Then count + 1 gives us 1, and setCount(1) requests an update with 1 as the next state value.
Notice I said next value.
Calling setCount() does not reach back into the currently running function and replace the local count variable. That particular render already has its count.
So this can surprise you at first:
function handleClick() {
setCount(count + 1);
console.log(count);
}If count was 0 when this handler was created, that log still sees 0.
The new value comes with the next render.
Also, the setter doesn't return the new state.
const result = setCount(count + 1);result is undefined.
So don't try doing something with the return value. Call the setter, React queues the update, and you'll receive the state on the next render.
What about directly assigning to count?
count = count + 1;Well, ours is declared with const anyway, so JavaScript won't allow that. But even if you had another mutable variable, changing it wouldn't update React's stored state.
When you want to update this state, use the setter React gave you.
Calling a setter queues an update. The count variable inside the function that's already running does not suddenly change.
Let's follow one click properly
Take the whole component again:
function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
}
return <button onClick={handleClick}>{count}</button>;
}First render, React reaches useState(0). There's no existing state for this Hook yet, so React starts it with 0. For this render, count becomes 0, and setCount is the setter React gives us for that state.
Then we return a button displaying count, so the button shows 0. The handleClick function created during this render can also see this render's count, which is still 0.
Now you click.
The browser runs handleClick(), and inside it we calculate:
count + 1Which gives 1.
Then:
setCount(1);requests another render.
React calls Counter() again. The function starts from the first line again, because yes, function components really are functions and React calls them again. But this time useState(0) doesn't reset our count to zero. React already has state stored for that Hook, so it gives this render the current value, 1.
Now count is 1, we return another button description, and the text becomes 1.
So the sequence is:
first render -> count is 0
click -> setCount(1)
next render -> count is 1The Counter() function call itself doesn't stay alive forever storing variables somewhere. React keeps the state, then gives the current value back to each render.
Each Counter gets its own state
Now what if we render two of them?
function App() {
return (
<main>
<Counter />
<Counter />
</main>
);
}We've got one Counter function in our source code, but React has two rendered Counter instances in the tree.
And each one gets its own state.
Click the first one and you can have:
first Counter -> count 1
second Counter -> count 0The second Counter doesn't care that the first one changed.
This also tells us something useful about where state lives. React isn't storing one global count against the Counter function itself. If it did that, both counters would share the same number, which they obviously don't.
React associates state with the component's place in the rendered tree.
If React keeps the same component at the same position, its state can be preserved. If that component gets removed, replaced with another component type, or recreated in another identity, React can discard the old state.
We'll get much deeper into state preservation later. For now just remember that two <Counter /> usages can have two completely separate counts.
One component can use useState more than once
You're not limited to one state value either.
Maybe a lesson panel needs to remember whether it's open, and also which tab is selected.
function LessonDetails() {
const [isOpen, setIsOpen] = useState(false);
const [tab, setTab] = useState("notes");
// ...
}React needs some way to know which state belongs to which useState() call every time this component renders.
And the way Hooks work depends on the calls staying in the same order.
On one render React sees the first useState as the isOpen state, and the second one as the tab state. On the next render it expects the same order again.
first useState call -> isOpen
second useState call -> tabWhich finally explains one of those React rules you've probably seen before and maybe wondered, "why does React care where I call a function?"
Well, now we know.
The Rules of Hooks
Hooks need to be called at the top level of your component, not conditionally, not inside loops, and not from random nested functions that may or may not run.
For example, don't do this:
function LessonDetails({ available }) {
if (available) {
const [isOpen, setIsOpen] = useState(false);
}
// ...
}Suppose available is true on one render. React sees that Hook call.
Next render available becomes false. Now that Hook call disappears.
If you had more Hooks after it, all of their positions would move around. React can't reliably associate the stored state with the same Hook calls anymore.
So call the Hook first and do the conditional logic after:
function LessonDetails({ available }) {
const [isOpen, setIsOpen] = useState(false);
if (!available) {
return null;
}
// ...
}Same reason you don't put useState inside a loop:
lessons.map(() => {
const [open, setOpen] = useState(false);
});The number of lessons could change, so now your Hook call count changes too.
If every lesson needs its own open state, make a LessonItem component and let every rendered LessonItem call useState at its own top level.
Hooks need to be called in the same order every time a component renders.
React's Hooks ESLint rules catch a lot of these mistakes before you even run the app, so yeah, don't disable those rules because they're being "annoying". They're saving you from much more annoying bugs.
Don't call useState inside a click handler either
This is also wrong:
function handleClick() {
const [count, setCount] = useState(0);
}The click handler runs later, after rendering. React isn't currently going through the component's Hook calls at that point, so this gives you an invalid Hook call.
Declare the state while React is rendering the component:
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
}Then the handler can use the count and setCount values created for that render.
Why doesn't useState(0) reset to zero every render?
This line runs every time React calls the component:
const [count, setCount] = useState(0);So you might reasonably ask, if 0 is sitting right there, why doesn't every render reset the state?
Because React uses that value when this state is being initialized. After the state already exists, React gives you its stored value instead.
You can see this more clearly with props:
function Counter({ initialCount }) {
const [count, setCount] = useState(initialCount);
// ...
}Suppose initialCount is 10 when the Counter first appears. State starts at 10.
Later the parent changes initialCount to 50.
Does count automatically become 50?
No.
initialCount was used to initialize the state. It isn't a permanent connection between the prop and the state value.
This causes a lot of bugs when someone copies a prop into state expecting the state to automatically follow later prop changes. It won't.
If the component should always display whatever the prop currently says, you may not need local state in the first place.
Expensive initial values
Sometimes getting the initial state requires actual work.
Maybe you have a function that reads and parses some saved lesson data:
function createInitialLessons() {
return readSavedLessons();
}You could write:
const [lessons, setLessons] = useState(createInitialLessons());But notice the ().
JavaScript calls createInitialLessons() before useState() gets its argument, so this function runs every time the component renders. React only needs that result for initialization, but your calculation still happened.
Instead you can pass the function itself:
const [lessons, setLessons] = useState(createInitialLessons);Now React can call that initializer when the state needs initializing instead of you calling it on every render.
This is called lazy initialization.
Don't start doing it for everything though.
const [count] = useState(() => 0);Sure, it works. But calculating 0 wasn't exactly expensive work. Just write:
const [count] = useState(0);Initializer functions should also be pure. In development Strict Mode, React may call an initializer twice and ignore one of the results. If your initializer modifies something outside itself, that can expose some very confusing behavior.
What can state contain?
Pretty much normal JavaScript values.
useState(0);
useState("");
useState(false);
useState(null);
useState([]);
useState({ query: "", status: "all" });Numbers, strings, booleans, arrays, objects, null, all fine.
React isn't checking whether your status string makes sense for your business logic. That's your job.
Arrays and objects need a little more care when updating them because we normally replace them instead of mutating the existing value directly. We'll cover that properly later rather than stuffing object state into our first useState lesson.
Functions are also a slightly special case. React treats a function passed to useState as an initializer, and a function passed directly to a state setter as an updater. So if you genuinely want the function itself to be the stored value, you need to wrap it.
Most beginner state doesn't need callable values anyway, so don't worry about that one yet.
Don't store information you can calculate
Suppose all we need is whether some details are open:
const [isOpen, setIsOpen] = useState(false);Then the button label can come from isOpen:
const label = isOpen ? "Hide details" : "Show details";We don't need this:
const [isOpen, setIsOpen] = useState(false);
const [label, setLabel] = useState("Show details");Now you've stored the same fact twice in two different forms. Every time isOpen changes, you also need to remember to update label.
Forget once and you can end up with isOpen === true while the label still says "Show details".
Much easier to store the one value that can independently change, then calculate the other value from it.
Let's build one properly
Okay, enough tiny Counters. Let's make a small details control.
function ChapterDetails() {
const [isOpen, setIsOpen] = useState(false);
function handleClick() {
setIsOpen((open) => !open);
}
return (
<section>
<button
type="button"
aria-expanded={isOpen}
onClick={handleClick}
>
{isOpen ? "Hide details" : "Show details"}
</button>
{isOpen && <p>This chapter contains six lessons.</p>}
</section>
);
}We've got one state value, isOpen, starting as false. The button text comes from it, aria-expanded comes from it, and whether the paragraph exists also comes from it.
Click the button and we call:
setIsOpen((open) => !open);This version gives React an updater function. React passes the current queued state into open, and we return the opposite value.
false becomes true. Next click, true becomes false.
When the next state depends on the previous state, this updater form is generally the one you want.
Now open React DevTools and click the button a few times. You'll see the state switching between false and true, while the same component stays there in the tree.
And that's really your first useState.
React stores a value for this component, your current render receives that value, the event handler calls its setter, and React renders the component again with the next value.
You don't modify the rendered button text yourself. You don't search the DOM for the paragraph and manually insert or remove it either. You change the state, the component runs again, and its returned UI now describes the next screen.
For one click, the flow is pretty simple:
component renders with current state
â
button handler is created for that render
â
user clicks
â
handler calls the setter
â
React renders again with the next state
â
React DOM updates whatever actually changedIf you can follow that sequence without treating setCount() as some magic variable mutation, you've got the main idea behind useState.