React State Explained — Managing Component Data
In this tutorial, you will learn about React State Explained. We cover key concepts, practical examples, and best practices to help you master this topic.
React state is data that changes over time within a component, managed with the useState hook, and preserved between re-renders while triggering UI updates when it changes.
What You'll Learn
- What state is and how it differs from props
- How to use useState hook
- How to update state correctly
- How to lift state up to share between components
- When to use local state vs shared state
Why It Matters
State makes your React apps interactive. Without state, components are static. With state, they respond to user input, fetch data, and update in real time. Understanding state is essential for building dynamic applications.
Real-World Use
Durga Antivirus Pro's scan status component uses state to track scan progress percentage, current file being scanned, threats found, and scan phase (idle, scanning, complete, error).
flowchart LR
A[User Action] --> B[State Update]
B --> C[Re-render]
C --> D[New UI]
D --> E[User Sees Change]
style A fill:#3b82f6,color:#fff
What is State?
State is data that changes over time in a component:
import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<button onClick={() => setCount(count - 1)}>Decrement</button>
<button onClick={() => setCount(0)}>Reset</button>
</div>
);
}
Expected output: A counter that increments, decrements, and resets. Each click updates the displayed count.
useState(initialValue) returns an array with two elements: the current state value and a setter function. When the setter is called with a new value, React re-renders the component with the updated state.
State Updates Are Async
State updates do not happen immediately:
function AsyncCounter() {
const [count, setCount] = useState(0);
const handleIncorrectClick = () => {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
// Only increments by 1! All three use the same stale value.
};
const handleCorrectClick = () => {
setCount(c => c + 1);
setCount(c => c + 1);
setCount(c => c + 1);
// Increments by 3! Each call gets the latest value.
};
return (
<div>
<p>Count: {count}</p>
<button onClick={handleIncorrectClick}>+3 (Wrong)</button>
<button onClick={handleCorrectClick}>+3 (Correct)</button>
</div>
);
}
Expected output: The first button only increments by 1. The second button correctly increments by 3.
When the new state depends on the previous state, use the function form of the setter: setCount(prev => prev + 1). This guarantees you always work with the latest value, even in batched updates.
State with Objects
State can hold objects, arrays, and complex data:
function UserForm() {
const [user, setUser] = useState({
firstName: "",
lastName: "",
email: "",
preferences: {
newsletter: false,
theme: "light",
}
});
const updateField = (field, value) => {
setUser(prev => ({
...prev,
[field]: value
}));
};
const updateNestedField = (field, value) => {
setUser(prev => ({
...prev,
preferences: {
...prev.preferences,
[field]: value
}
}));
};
return (
<div>
<input value={user.firstName} onChange={e => updateField("firstName", e.target.value)} />
<input value={user.lastName} onChange={e => updateField("lastName", e.target.value)} />
<input value={user.email} onChange={e => updateField("email", e.target.value)} />
<label>
<input type="checkbox" checked={user.preferences.newsletter}
onChange={e => updateNestedField("newsletter", e.target.checked)} />
Newsletter
</label>
</div>
);
}
Expected output: A form that tracks user data in state. Each field update creates a new state object with the spread operator.
State in React should be treated as immutable. Never mutate state directly: user.firstName = "John". Always create a new object or array with the changes applied.
State with Arrays
Manage array state immutably:
function ShoppingList() {
const [items, setItems] = useState([
{ id: 1, name: "Apples", purchased: false },
{ id: 2, name: "Bread", purchased: false },
]);
const addItem = (name) => {
setItems(prev => [...prev, { id: Date.now(), name, purchased: false }]);
};
const toggleItem = (id) => {
setItems(prev => prev.map(item =>
item.id === id ? { ...item, purchased: !item.purchased } : item
));
};
const removeItem = (id) => {
setItems(prev => prev.filter(item => item.id !== id));
};
const clearPurchased = () => {
setItems(prev => prev.filter(item => !item.purchased));
};
return (
<div>
<button onClick={() => addItem("Milk")}>Add Milk</button>
<ul>
{items.map(item => (
<li key={item.id} style={{ textDecoration: item.purchased ? "line-through" : "none" }}>
<input type="checkbox" checked={item.purchased} onChange={() => toggleItem(item.id)} />
{item.name}
<button onClick={() => removeItem(item.id)}>Remove</button>
</li>
))}
</ul>
<button onClick={clearPurchased}>Clear Purchased</button>
</div>
);
}
Expected output: A shopping list with add, toggle, remove, and clear functionality. All operations create new arrays.
For arrays: use spread to add, map to update, filter to remove. Never use push, pop, splice, or direct index assignment.
Lifting State Up
Share state between sibling components by lifting it to their common parent:
function TemperatureInput({ scale, temperature, onTemperatureChange }) {
return (
<fieldset>
<legend>Enter temperature in {scale === "c" ? "Celsius" : "Fahrenheit"}:</legend>
<input
value={temperature}
onChange={e => onTemperatureChange(e.target.value, scale)}
/>
</fieldset>
);
}
function BoilingVerdict({ celsius }) {
if (celsius >= 100) return <p>The water would boil.</p>;
return <p>The water would not boil.</p>;
}
function Calculator() {
const [temperature, setTemperature] = useState("");
const [scale, setScale] = useState("c");
const handleChange = (value, inputScale) => {
setTemperature(value);
setScale(inputScale);
};
const toCelsius = (fahrenheit) => (fahrenheit - 32) * 5 / 9;
const toFahrenheit = (celsius) => (celsius * 9 / 5) + 32;
const celsius = scale === "c" ? temperature : temperature ? toCelsius(parseFloat(temperature)).toString() : "";
const fahrenheit = scale === "f" ? temperature : temperature ? toFahrenheit(parseFloat(temperature)).toString() : "";
return (
<div>
<TemperatureInput scale="c" temperature={celsius} onTemperatureChange={handleChange} />
<TemperatureInput scale="f" temperature={fahrenheit} onTemperatureChange={handleChange} />
<BoilingVerdict celsius={parseFloat(celsius)} />
</div>
);
}
Expected output: Two temperature inputs that stay in sync. Changing Celsius updates Fahrenheit and vice versa.
Lifting state up means moving state from a child to a shared parent, then passing it down as props. The parent becomes the single source of truth.
Local vs Shared State
Decide between component-local and shared state:
// Local state: Only this component needs it
function AccordionSection({ title, children }) {
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<button onClick={() => setIsOpen(!isOpen)}>{title} {isOpen ? "-" : "+"}</button>
{isOpen && <div>{children}</div>}
</div>
);
}
// Shared state: Multiple components need it
// Lifted to App level
function App() {
const [selectedUserId, setSelectedUserId] = useState(null);
const [notifications, setNotifications] = useState([]);
return (
<div>
<Header notificationCount={notifications.length} />
<Sidebar onSelectUser={setSelectedUserId} />
<MainContent userId={selectedUserId} notifications={notifications} />
</div>
);
}
Expected output: Accordion toggles are local. Selected user and notifications are shared between header, sidebar, and main content.
Rule: If a piece of data is used by only one component, keep it local. If multiple components need the same data, lift it up or use context.
Common Mistakes
Directly mutating state —
state.value = "new"does not trigger re-render. Always use the setter function with a new value.Using the same state for derived values — If a value can be computed from existing state or props, do not store it in state. Compute it during render.
Calling setState synchronously in a loop — Each
setStatein a loop triggers a re-render. Batch updates with the functional form or collect changes into a single update.Storing redundant state — If state A always changes with state B, store only one and derive the other.
Forgetting that useState is async — Reading state immediately after setting it gives the old value. Use effects or the functional updater.
Practice Questions
What does useState return? An array with the current state value and a setter function to update it.
How do you update state based on previous state? Use the functional form:
setCount(prev => prev + 1).How do you update nested state immutably? Spread each level:
setObj(prev => ({ ...prev, nested: { ...prev.nested, key: newValue } })).What is lifting state up? Moving shared state from multiple components to their nearest common ancestor, then passing it down as props.
Why should you avoid redundant state? It creates synchronization bugs. Always derive values from existing state or props when possible.
Challenge
Build a SpreadsheetCell with editing state (when clicked, becomes an input). Track which cell is being edited in the parent. When the user clicks another cell, save the current cell and open the new one. Use lifted state to manage this.
FAQ
Mini Project
Build a FormWizardComponent with multi-step form state. Track current step (1-3), form data as an object, and validation errors per step. Each step has its own fields: Step 1 (name, email), Step 2 (address, phone), Step 3 (review and submit). Use lifted state for the wizard and local state for each step's form validation. Show a progress bar based on current step.
What's Next
Continue with lifecycle, effects, and refs:
React Lifecycle, React Effects, React Refs
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro