By WebNest Studio Engineering Team
React Tutorial
State and the useState Hook
State is data that a component owns and manages internally, which can change over time in response to user actions, network responses, or timers. Unlike props, which are handed down from a parent, state lives inside the component itself and is completely private to it unless explicitly shared.
The useState hook is the primary way function components create and manage state. Calling useState(initialValue) returns a pair — the current state value and a setter function to update it — and, critically, calling that setter schedules a re-render of the component so the UI stays in sync with the latest data.
Why it matters
State is what makes a component interactive: the value of a counter, whether a menu is open, what has been typed into a field. The rule that state must be changed through its setter, and never modified directly, is the most important thing to understand about React, and the source of the most common bugs.
Declaring State with useState
const [count, setCount] = useState(0); declares a piece of state named count, initialized to 0, along with a function setCount used to update it. The array destructuring syntax is just a convention; useState technically returns a two-element array, and you name both elements yourself.
State Updates Trigger Re-Renders
Calling the setter function (e.g. setCount(count + 1)) tells React that this component's state has changed. React then re-runs the component function to compute new JSX and efficiently updates the real DOM to match. This is the core rendering model of React: state changes in, new UI out, automatically.
State Is Local and Isolated Per Component Instance
Each time a component is rendered as a separate instance (for example, multiple <Counter /> elements on the same page), each instance gets its own independent state. Updating one instance's state does not affect any other instance's state, even though they share the same component definition.
- Never mutate state directly (e.g.
count = count + 1) — always call the setter function, since direct mutation will not trigger a re-render. - When new state depends on the previous state, prefer the updater-function form,
setCount(prev => prev + 1), to avoid stale values in rapid or batched updates. - Initializing useState with an expensive computation should use the lazy initializer form,
useState(() => computeInitial()), so the computation only runs once.
Example
A simple counter component managed entirely with useState
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
function handleIncrement() {
setCount((prev) => prev + 1);
}
return (
<div>
<p>Current count: {count}</p>
<button onClick={handleIncrement}>Increment</button>
</div>
);
}
export default Counter;
Clicking the "Increment" button repeatedly increases the number shown in "Current count: 0" up through 1, 2, 3, and so on, because each click calls setCount and triggers a re-render with the updated value.
Common Mistakes
- Mutating state directly (e.g. "count++" or pushing into a state array) instead of calling the setter function — React will not detect the change and the UI will not update.
- Reading the state variable itself to compute the next value in rapid succession (e.g. calling setCount(count + 1) twice in a row) instead of using the updater function form, which can produce stale results.
- Expecting the state variable to update immediately after calling the setter within the same function — state updates are scheduled and the new value is only available on the next render.
- Creating too many separate useState calls for values that are really one related object, making updates harder to keep in sync.
Exercise
Do this in src/App.jsx of a Vite + React project. Build a counter: a paragraph showing the count, a button that adds one, and a button that resets it to zero. Update the count from its previous value.
Expected result: the page shows "Count: 0"; each click on Add one increases it by one, and Reset returns it to zero.
import { useState } from "react";
export default function App() {
// TODO: declare the count state, starting at 0
return (
<div>
{/* TODO: a paragraph showing "Count: <count>" */}
{/* TODO: a button that adds one */}
{/* TODO: a button that resets to zero */}
</div>
);
}
Show hints
const [count, setCount] = useState(0)gives the current value and the function that changes it.- Passing a function to the setter,
setCount((previous) => previous + 1), updates from the latest value.
Show solution
import { useState } from "react";
export default function App() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount((previous) => previous + 1)}>Add one</button>
<button onClick={() => setCount(0)}>Reset</button>
</div>
);
}
Quick quiz
1. What does useState(0) return?
- A. The number 0
- B. An object with get and set methods
- C. An array holding the current value and a function to update it
- D. A promise
Show answer
C. The two are usually unpacked with array destructuring.
2. Why does count = count + 1 not update the screen?
- A. The syntax is wrong
- B. React re-renders only when the setter function is called
- C. count is a string
- D. State can only decrease
Show answer
B. Assigning to the variable changes nothing that React knows about.
3. Two Counter components are rendered side by side. Do they share their count?
- A. No; each instance of a component has its own state
- B. Yes, always
- C. Only if they have the same key
- D. Only in development
Show answer
A. To share a value, the state is moved up to their common parent.
Interview questions
Why must state never be mutated directly?
Show answer
React decides whether to re-render by comparing the new state with the old by reference. If an object or array in state is modified in place, the reference is unchanged, React sees no difference, and the screen does not update. State must be replaced with a new value: a new array made with spread, map or filter, or a new object made with spread. Immutable updates also make changes easy to trace.
Is setState synchronous?
Show answer
No. Calling the setter schedules a re-render; the state variable in the code that is currently running keeps its old value until the next render. React also batches several updates made in the same event into one render. When the new value depends on the old one, pass an updater function, such as setCount((c) => c + 1), so that each update is applied to the latest value.
Key Points to Remember
- useState(initialValue) returns [currentValue, setterFunction] for a piece of component-local state.
- Calling the setter schedules a re-render so the rendered UI reflects the new state.
- State must never be mutated directly; always update it through its setter function.
- Each component instance has its own independent, isolated state.
Practice in the playground
Write and run your own code for this topic, or start from the exercise or an example. Shows a live preview of plain HTML, CSS and JavaScript. JSX needs a React project on your computer. Your code for each topic is kept in this browser.
Run the examples
Change an input, predict the result, then compare it with the output. Explain why the result changes.
These examples need a project environment on your computer, so they can be copied but not run here. Codelab runs Python, FastAPI backends with SQLite, plain Java 17 and HTML/CSS/JavaScript.
Written by
WebNest Studio Engineering Team
Practical lessons from developers working with Java, Python, React, databases and production systems.
Led by Vansh Duggal, Founder, WebNest Studio · Full-stack & software engineering