React provides a declarative way to manage user interface (UI) interactions, allowing developers to describe how the UI should look based on different states instead of directly manipulating individual elements. This approach enhances clarity, maintainability, and scalability in your applications. Below, we’ll explore the key concepts from the official documentation on Reacting to Input with State.
In declarative programming, you describe what the UI should look like for each state, while in imperative programming, you give explicit instructions on how to achieve that state.
When designing a component, you first identify all the possible visual states it can be in:
Create a mockup of these states to visualize how the component should look without implementing any logic.
export default function Form({ status = "empty" }) {
if (status === "success") {
return <h1>That's right!</h1>;
}
return (
<>
<h2>City quiz</h2>
<p>
In which city is there a billboard that turns air into drinkable water?
</p>
<form>
<textarea />
<br />
<button>Submit</button>
</form>
</>
);
}
State updates can be triggered by:
useState
You will represent these visual states in memory using React’s useState
hook. Start with the essential pieces of state required for your component:
const [answer, setAnswer] = useState("");
const [error, setError] = useState(null);
const [status, setStatus] = useState("typing"); // Possible values: 'typing', 'submitting', 'success'
It’s crucial to minimize the number of state variables to avoid paradoxes. For example, instead of managing isEmpty
and isTyping
separately, derive their values from a single answer
state:
answer.length === 0
, the form is empty; if not, it’s typing.answer
, error
, and status
.The final step is to connect event handlers that will update the state based on user interactions:
import { useState } from "react";
export default function Form() {
const [answer, setAnswer] = useState("");
const [error, setError] = useState(null);
const [status, setStatus] = useState("typing");
if (status === "success") {
return <h1>That's right!</h1>;
}
async function handleSubmit(e) {
e.preventDefault();
setStatus("submitting");
try {
await submitForm(answer); // Assume this function makes an API call
setStatus("success");
} catch (err) {
setStatus("typing");
setError(err);
}
}
function handleTextareaChange(e) {
setAnswer(e.target.value);
}
return (
<>
<h2>City quiz</h2>
<p>
In which city is there a billboard that turns air into drinkable water?
</p>
<form onSubmit={handleSubmit}>
<textarea
value={answer}
onChange={handleTextareaChange}
disabled={status === "submitting"}
/>
<br />
<button disabled={status === "empty" || status === "submitting"}>
Submit
</button>
{status === "error" && (
<p className="Error">Good guess but a wrong answer. Try again!</p>
)}
</form>
</>
);
}
handleSubmit
function manages the submission logic, updating the state based on the success or failure of the submission.handleTextareaChange
function updates the answer state as the user types.React’s declarative approach to UI programming allows developers to describe the various states of a component and how to transition between them seamlessly. By managing inputs and states effectively, you can build interactive and user-friendly applications with minimal complexity. For further details, refer to the official documentation on Reacting to Input with State.
React provides a declarative way to manipulate the UI. Instead of manipulating individual pieces of the UI directly, you describe the different states that your component can be in, and switch between them in response to user input. This is similar to how designers think about the UI.
When you design UI interactions, you probably think about how the UI changes in response to user actions. Consider a form that lets the user submit an answer:
In imperative programming, the above corresponds directly to how you implement interaction. You have to write the exact instructions to manipulate the UI depending on what just happened. Here’s another way to think about this: imagine riding next to someone in a car and telling them turn by turn where to go.
In a car driven by an anxious-looking person representing JavaScript, a passenger orders the driver to execute a sequence of complicated turn-by-turn navigations. They don’t know where you want to go; they just follow your commands. (And if you get the directions wrong, you end up in the wrong place!) It’s called imperative because you have to “command” each element, from the spinner to the button, telling the computer how to update the UI.
Manipulating the UI imperatively works well enough for isolated examples, but it gets exponentially more difficult to manage in more complex systems. Imagine updating a page full of different forms like this one. Adding a new UI element or a new interaction would require carefully checking all existing code to make sure you haven’t introduced a bug (for example, forgetting to show or hide something).
React was built to solve this problem. In React, you don’t directly manipulate the UI—meaning you don’t enable, disable, show, or hide components directly. Instead, you declare what you want to show, and React figures out how to update the UI. Think of getting into a taxi and telling the driver where you want to go instead of telling them exactly where to turn. It’s the driver’s job to get you there, and they might even know some shortcuts you haven’t considered!
You’ve seen how to implement a form imperatively above. To better understand how to think in React, you’ll walk through reimplementing this UI in React below:
useState
First, visualize all the different “states” of the UI the user might see:
Just like a designer, you’ll want to create “mocks” for the different states before you add logic.
You can trigger state updates in response to two kinds of inputs:
For the form you’re developing, you will need to change state in response to a few different inputs:
useState
Next, represent the visual states of your component in memory with useState
. Simplicity is key; you want as few “moving pieces” as possible.
Start with the essential state variables:
const [answer, setAnswer] = useState("");
const [error, setError] = useState(null);
const [status, setStatus] = useState("typing"); // 'typing', 'submitting', or 'success'
Avoid duplication in the state content so you’re only tracking what is essential. Spending a little time on refactoring your state structure will make your components easier to understand.
Create event handlers that update the state. Below is the final form, with all event handlers wired up:
import { useState } from "react";
export default function Form() {
const [answer, setAnswer] = useState("");
const [error, setError] = useState(null);
const [status, setStatus] = useState("typing"); // 'typing', 'submitting', or 'success'
const handleSubmit = async (e) => {
e.preventDefault();
setStatus("submitting");
setError(null);
try {
await submitForm(answer);
setStatus("success");
} catch (err) {
setError(err);
setStatus("typing");
}
};
return (
<>
<h2>City quiz</h2>
<p>
In which city is there a billboard that turns air into drinkable water?
</p>
<form onSubmit={handleSubmit}>
<textarea
value={answer}
onChange={(e) => setAnswer(e.target.value)}
disabled={status === "submitting"}
/>
<br />
<button disabled={answer.length === 0 || status === "submitting"}>
Submit
</button>
{error && <p className="Error">{error.message}</p>}
</form>
</>
);
}
function submitForm(answer) {
return new Promise((resolve, reject) => {
setTimeout(() => {
const shouldError = answer.toLowerCase() !== "lima";
if (shouldError) {
reject(new Error("Good guess but a wrong answer. Try again!"));
} else {
resolve();
}
}, 1500);
});
}
Although this code is longer than the original imperative example, it is much less fragile. Expressing all interactions as state changes lets you later introduce new visual states without breaking existing ones, and change what should be displayed in each state without changing the logic of the interaction itself.
useState
.Structuring state effectively can greatly impact a component’s maintainability and debuggability. Here’s a guide to help you make informed choices when organizing state.
When writing a component with state, consider the following principles:
Group Related State: If multiple state variables change together, merge them into a single variable to ensure consistency.
Example: Instead of separate x and y coordinates:
const [position, setPosition] = useState({ x: 0, y: 0 });
Avoid Contradictions in State: Prevent scenarios where multiple state variables can conflict. A unified status variable can help.
Improvement:
Instead of isSending
and isSent
, use a single status
variable with possible states: ‘typing’, ‘sending’, ‘sent’.
Avoid Redundant State: Calculate values from existing props or state rather than storing derived data.
Example: Instead of tracking fullName
:
const fullName = firstName + " " + lastName;
Avoid Duplication in State: Eliminate duplicate data across state variables to simplify updates and prevent inconsistencies.
Improvement: Instead of storing selectedItem
directly, keep just the selectedId
and derive the item as needed.
Avoid Deeply Nested State: Simplify updates by flattening state structures, reducing complexity.
Example: Instead of a nested object for a travel plan, use a flat structure with IDs and a mapping to places.
Use a single state object when multiple values are interdependent:
const [position, setPosition] = useState({ x: 0, y: 0 });
Unify conflicting states:
const [status, setStatus] = useState("typing");
Calculate values on render:
const fullName = firstName + " " + lastName;
Use identifiers instead of duplicating entire objects:
const [selectedId, setSelectedId] = useState(0);
const selectedItem = items.find((item) => item.id === selectedId);
Flatten your state to simplify updates:
const initialTravelPlan = {
0: { id: 0, title: "(Root)", childIds: [1, 2] },
1: { id: 1, title: "Earth", childIds: [] },
};
Sometimes, you want the state of two components to always change together. To achieve this, remove state from both components, move it to their closest common parent, and then pass it down via props. This process is known as lifting state up, and it’s one of the most common tasks in React development.
In this example, a parent Accordion
component renders two separate Panel
components. Each Panel
has a boolean isActive
state that determines whether its content is visible.
The initial implementation allows each panel to operate independently. Here’s how it looks:
import { useState } from "react";
function Panel({ title, children }) {
const [isActive, setIsActive] = useState(false);
return (
<section className="panel">
<h3>{title}</h3>
{isActive ? (
<p>{children}</p>
) : (
<button onClick={() => setIsActive(true)}>Show</button>
)}
</section>
);
}
export default function Accordion() {
return (
<>
<h2>Almaty, Kazakhstan</h2>
<Panel title="About">
With a population of about 2 million, Almaty is Kazakhstan's largest
city.
</Panel>
<Panel title="Etymology">
The name comes from <span lang="kk-KZ">алма</span>, the Kazakh word for
"apple".
</Panel>
</>
);
}
Pressing the button on one panel does not affect the other, as they maintain their independent state.
Now, let’s modify the design so that only one panel is expanded at a time. When one panel expands, the other should collapse.
The first step is to remove the isActive
state from the Panel
component and pass it down from the parent instead.
Change the Panel
component to accept isActive
as a prop:
function Panel({ title, children, isActive }) {
return (
<section className="panel">
<h3>{title}</h3>
{isActive ? (
<p>{children}</p>
) : (
<button onClick={() => setIsActive(true)}>Show</button>
)}
</section>
);
}
Now, let’s move to the Accordion
component and pass hardcoded values to the Panel
components.
export default function Accordion() {
return (
<>
<h2>Almaty, Kazakhstan</h2>
<Panel title="About" isActive={true}>
With a population of about 2 million, Almaty is Kazakhstan's largest
city.
</Panel>
<Panel title="Etymology" isActive={true}>
The name comes from <span lang="kk-KZ">алма</span>, the Kazakh word for
"apple".
</Panel>
</>
);
}
Next, we need the Accordion
component to keep track of which panel is active. We’ll use a number to represent the active panel index.
import { useState } from "react";
export default function Accordion() {
const [activeIndex, setActiveIndex] = useState(0);
return (
<>
<h2>Almaty, Kazakhstan</h2>
<Panel
title="About"
isActive={activeIndex === 0}
onShow={() => setActiveIndex(0)}
>
With a population of about 2 million, Almaty is Kazakhstan's largest
city.
</Panel>
<Panel
title="Etymology"
isActive={activeIndex === 1}
onShow={() => setActiveIndex(1)}
>
The name comes from <span lang="kk-KZ">алма</span>, the Kazakh word for
"apple".
</Panel>
</>
);
}
Panel
ComponentThe Panel
component’s button should now use the onShow
prop to trigger state changes in the parent:
function Panel({ title, children, isActive, onShow }) {
return (
<section className="panel">
<h3>{title}</h3>
{isActive ? <p>{children}</p> : <button onClick={onShow}>Show</button>}
</section>
);
}
By lifting state up, you coordinate multiple components effectively, ensuring that only one panel is active at any time.
State in React is isolated between components. React manages state based on the component’s position in the UI tree, determining when to preserve or reset state across re-renders.
In React, state isn’t just contained within the component; it’s managed by React itself. Each piece of state is linked to a specific component by its location in the render tree.
import { useState } from "react";
export default function App() {
const counter = <Counter />;
return (
<div>
{counter}
{counter}
</div>
);
}
function Counter() {
const [score, setScore] = useState(0);
const [hover, setHover] = useState(false);
let className = "counter";
if (hover) {
className += " hover";
}
return (
<div
className={className}
onPointerEnter={() => setHover(true)}
onPointerLeave={() => setHover(false)}
>
<h1>{score}</h1>
<button onClick={() => setScore(score + 1)}>Add one</button>
</div>
);
}
When rendered, the tree looks like this:
<div>
)
<Counter>
)
score
: 0)<Counter>
)
score
: 0)In this scenario, even though there is only one <Counter />
component being reused, each instance maintains its own independent state.
React will keep a component’s state as long as it remains rendered at the same position in the UI tree. For example, if a component is conditionally rendered and removed from the tree, its state will be lost.
import { useState } from "react";
export default function App() {
const [showB, setShowB] = useState(true);
return (
<div>
<Counter />
{showB && <Counter />}
<label>
<input
type="checkbox"
checked={showB}
onChange={(e) => {
setShowB(e.target.checked);
}}
/>
Render the second counter
</label>
</div>
);
}
When a component is added back into the render tree, it initializes its state from scratch.
score = 0
).React preserves a component’s state when it is consistently rendered at the same position in the UI tree. Even if the component receives different props, its state remains intact.
import { useState } from "react";
export default function App() {
const [isFancy, setIsFancy] = useState(false);
return (
<div>
{isFancy ? <Counter isFancy={true} /> : <Counter isFancy={false} />}
<label>
<input
type="checkbox"
checked={isFancy}
onChange={(e) => {
setIsFancy(e.target.checked);
}}
/>
Use fancy styling
</label>
</div>
);
}
isFancy
is true or false, the state of <Counter />
does not reset because it remains at the same position in the render tree.The critical point to remember is that it’s the position of the component in the UI tree that determines whether state is preserved, not how you structure your JSX.
import { useState } from "react";
export default function App() {
const [isFancy, setIsFancy] = useState(false);
if (isFancy) {
return (
<div>
<Counter isFancy={true} />
<label>
<input
type="checkbox"
checked={isFancy}
onChange={(e) => {
setIsFancy(e.target.checked);
}}
/>
Use fancy styling
</label>
</div>
);
}
return (
<div>
<Counter isFancy={false} />
<label>
<input
type="checkbox"
checked={isFancy}
onChange={(e) => {
setIsFancy(e.target.checked);
}}
/>
Use fancy styling
</label>
</div>
);
}
<Counter />
tags are rendered at the same position, which means React treats them as the same component.In this example, ticking the checkbox will replace <Counter>
with a <p>
:
import { useState } from "react";
export default function App() {
const [isPaused, setIsPaused] = useState(false);
return (
<div>
{isPaused ? <p>See you later!</p> : <Counter />}
<label>
<input
type="checkbox"
checked={isPaused}
onChange={(e) => {
setIsPaused(e.target.checked);
}}
/>
Take a break
</label>
</div>
);
}
function Counter() {
const [score, setScore] = useState(0);
const [hover, setHover] = useState(false);
let className = "counter";
if (hover) {
className += " hover";
}
return (
<div
className={className}
onPointerEnter={() => setHover(true)}
onPointerLeave={() => setHover(false)}
>
<h1>{score}</h1>
<button onClick={() => setScore(score + 1)}>Add one</button>
</div>
);
}
When you switch between different component types at the same position, the state is reset. Initially, <Counter>
is rendered, but when you replace it with <p>
, React removes <Counter>
from the UI tree and destroys its state.
<Counter>
with state score = 3
<Counter>
is removed, replaced by <p>
Switching components resets the entire subtree state. Increment the counter and then tick the checkbox:
import { useState } from "react";
export default function App() {
const [isFancy, setIsFancy] = useState(false);
return (
<div>
{isFancy ? (
<div>
<Counter isFancy={true} />
</div>
) : (
<section>
<Counter isFancy={false} />
</section>
)}
<label>
<input
type="checkbox"
checked={isFancy}
onChange={(e) => {
setIsFancy(e.target.checked);
}}
/>
Use fancy styling
</label>
</div>
);
}
function Counter({ isFancy }) {
const [score, setScore] = useState(0);
const [hover, setHover] = useState(false);
let className = "counter";
if (hover) {
className += " hover";
}
if (isFancy) {
className += " fancy";
}
return (
<div
className={className}
onPointerEnter={() => setHover(true)}
onPointerLeave={() => setHover(false)}
>
<h1>{score}</h1>
<button onClick={() => setScore(score + 1)}>Add one</button>
</div>
);
}
When you toggle the checkbox, the <Counter>
state resets because you are effectively rendering a different component at the same position (from <div>
to <section>
).
<Counter>
with state score = 3
score = 0
.Nesting component definitions can lead to state resets. For example, if MyTextField
is defined inside MyComponent
, clicking the button will reset its state because a new MyTextField
function is created each time MyComponent
re-renders.
In a scoreboard application where two players keep track of their scores, switching players should reset their scores:
import { useState } from "react";
export default function Scoreboard() {
const [isPlayerA, setIsPlayerA] = useState(true);
return (
<div>
{isPlayerA ? <Counter person="Taylor" /> : <Counter person="Sarah" />}
<button
onClick={() => {
setIsPlayerA(!isPlayerA);
}}
>
Next player!
</button>
</div>
);
}
function Counter({ person }) {
const [score, setScore] = useState(0);
const [hover, setHover] = useState(false);
let className = "counter";
if (hover) {
className += " hover";
}
return (
<div
className={className}
onPointerEnter={() => setHover(true)}
onPointerLeave={() => setHover(false)}
>
<h1>
{person}'s score: {score}
</h1>
<button onClick={() => setScore(score + 1)}>Add one</button>
</div>
);
}
When you change the player, the score is preserved because both counters appear in the same position. To reset their state, you can render them in different positions or give each component an explicit identity with a key
.
Rendering components in different positions helps to keep their state independent:
{
isPlayerA && <Counter person="Taylor" />;
}
{
!isPlayerA && <Counter person="Sarah" />;
}
Using keys allows React to distinguish between components. By giving them different keys, you ensure their states are reset:
{
isPlayerA ? (
<Counter key="Taylor" person="Taylor" />
) : (
<Counter key="Sarah" person="Sarah" />
);
}
In a chat application, you may want to reset the text input state when switching contacts. Adding a key to the <Chat>
component ensures the state resets:
<Chat key={to.id} contact={to} />