What Is State in React? Complete Beginner's Guide
State is one of the most fundamental concepts in React. Without understanding state, you can't build interactive applications. Yet many beginners struggle with what state actually is, why it matters, and how it differs from other React concepts like props.
This guide will give you a complete understanding of React state, starting from the absolute basics and building to practical patterns you'll use every day.
Table of Contents
- What Is State?
- State vs Props
- Why We Need State
- The useState Hook
- How React Tracks State
- Single vs Multiple State Values
- State Updates and Re-renders
- Common Mistakes
- Best Practices
- Real-World Examples
- FAQ
What Is State? {#what-is-state}
State is data that changes over time and affects what your component displays.
When the user interacts with your app — typing into a form, clicking a button, toggling a switch — that's state changing. React's job is to notice when state changes and update the UI to reflect those changes.
State vs Regular Variables
Consider the difference:
// ❌ WRONG: Regular variable doesn't trigger UI updates
function Counter() {
let count = 0;
const increment = () => {
count = count + 1;
console.log(count); // Logs 1, 2, 3...
// But UI doesn't update!
};
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>+</button>
</div>
);
}
// ✅ CORRECT: State triggers UI updates
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
const increment = () => {
setCount(count + 1); // UI updates automatically
};
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>+</button>
</div>
);
}
Key difference: When you change a regular variable, React has no idea it changed. When you change state, React knows and updates the UI.
State vs Props {#state-vs-props}
Understanding the difference between state and props is crucial.
| Aspect | State | Props |
|---|---|---|
| Source | Defined inside component | Passed from parent |
| Ownership | Component owns its state | Parent owns the prop |
| Can Change? | Yes, via setState | No, read-only |
| Purpose | Internal data | Pass data down |
| Triggers Re-render | Yes | Yes |
Practical Example
// Parent component
function App() {
const [userName, setUserName] = useState('Alice'); // State in App
return (
<div>
<UserProfile name={userName} /> {/* Pass as prop */}
<button onClick={() => setUserName('Bob')}>Change Name</button>
</div>
);
}
// Child component
interface UserProfileProps {
name: string; // Received as prop
}
function UserProfile({ name }: UserProfileProps) {
// This component CANNOT change the name prop directly
// It can only receive and display it
return <h1>Welcome, {name}!</h1>;
}
Mental Model
Think of it this way:
- State = Data the component manages internally (like your personal diary)
- Props = Data passed to the component (like a message from a friend)
A component can change its own state, but it cannot change props passed to it. Props are read-only.
Why We Need State {#why-state}
Without state, React would be completely static. You couldn't build interactive applications.
Example: Why State Matters
// BEFORE: No state = no interactivity
function EmailInput() {
return <input type="email" placeholder="Enter email" />;
}
// User types in the input, but we can't access what they typed!
// AFTER: With state = full interactivity
import { useState } from 'react';
function EmailInput() {
const [email, setEmail] = useState('');
const [error, setError] = useState('');
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setEmail(value);
// Validate email
if (value.includes('@')) {
setError(''); // Valid
} else {
setError('Invalid email'); // Invalid
}
};
return (
<div>
<input
type="email"
value={email}
onChange={handleChange}
placeholder="Enter email"
/>
{error && <p className="error">{error}</p>}
</div>
);
}
Now the component:
- ✅ Tracks what the user typed
- ✅ Validates the input
- ✅ Shows/hides error messages
- ✅ Provides real-time feedback
The useState Hook {#usestate-hook}
useState is the hook that lets you add state to functional components. It's the most commonly used React hook.
Basic Syntax
import { useState } from 'react';
const [state, setState] = useState(initialValue);
Breaking It Down
import { useState } from 'react';
function Counter() {
// useState returns an array with two elements:
// [currentValue, functionToUpdateValue]
const [count, setCount] = useState(0);
// │ │ │
// │ │ └─ Initial value
// │ └─ Function to update state
// └─ Current state value
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment from {count} to {count + 1}
</button>
</div>
);
}
What useState Returns
// useState returns an array, but we destructure it
const stateArray = useState(0);
const count = stateArray[0]; // Current value (0)
const setCount = stateArray[1]; // Update function
// Same thing, just more concise:
const [count, setCount] = useState(0);
Different Initial Values
// Numbers
const [age, setAge] = useState(25);
// Strings
const [name, setName] = useState('John');
// Booleans
const [isVisible, setIsVisible] = useState(false);
// Objects
const [user, setUser] = useState({ name: 'John', age: 30 });
// Arrays
const [todos, setTodos] = useState<Todo[]>([]);
// Null
const [data, setData] = useState<Data | null>(null);
TypeScript with useState
// ✅ GOOD: Explicit type
const [count, setCount] = useState<number>(0);
// ✅ ALSO GOOD: Type inference (React figures it out)
const [count, setCount] = useState(0); // Inferred as number
// ✅ GOOD: For complex types
interface User {
id: string;
name: string;
email: string;
}
const [user, setUser] = useState<User | null>(null);
How React Tracks State {#how-react-tracks}
Understanding how React tracks state helps you avoid bugs and write better code.
React's Internal Process
When you call useState:
- Registration — React registers the state value with the component
- Storage — React stores this value internally, associated with this component instance
- Update Detection — When you call the setter, React knows state changed
- Re-render — React re-runs the component function with the new state value
- UI Update — React updates only the parts of the UI that need updating
Visual Example
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1); // Tell React: "Update state!"
};
return (
<div>
<p>Count: {count}</p>
<button onClick={handleClick}>Click me</button>
</div>
);
}
// User clicks button:
// 1. handleClick() runs → setCount(1) called
// 2. React detects state changed: 0 → 1
// 3. React re-runs entire Counter component function
// 4. Now count = 1 inside the function
// 5. JSX is re-evaluated with count = 1
// 6. React updates the DOM: "Count: 1"
// 7. User sees the new number on screen
State is Local
Each component instance has its own state:
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>+</button>
</div>
);
}
// In App component
function App() {
return (
<>
<Counter /> {/* Has its own 'count' state */}
<Counter /> {/* Has its own separate 'count' state */}
<Counter /> {/* Has its own separate 'count' state */}
</>
);
}
// Each Counter has independent state!
// Clicking one button doesn't affect the others
Single vs Multiple State Values {#single-vs-multiple}
You can have one state value or many. The choice depends on how the data is related.
Multiple useState Calls
// ✅ GOOD: Separate states for independent values
function LoginForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [rememberMe, setRememberMe] = useState(false);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
console.log({ email, password, rememberMe });
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
/>
<label>
<input
type="checkbox"
checked={rememberMe}
onChange={(e) => setRememberMe(e.target.checked)}
/>
Remember me
</label>
<button type="submit">Login</button>
</form>
);
}
Single State Object
Sometimes it makes sense to group related state together:
// ✅ ALSO GOOD: When values are closely related
interface FormState {
email: string;
password: string;
rememberMe: boolean;
}
function LoginForm() {
const [form, setForm] = useState<FormState>({
email: '',
password: '',
rememberMe: false,
});
const handleChange = (field: keyof FormState, value: any) => {
// Important: Create new object, don't mutate
setForm((prev) => ({
...prev,
[field]: value,
}));
};
return (
<form>
<input
value={form.email}
onChange={(e) => handleChange('email', e.target.value)}
/>
{/* ... rest of form */}
</form>
);
}
Which to Use?
- Multiple useState — When values are independent (different form fields)
- Single object — When values are related and updated together (user profile data)
State Updates and Re-renders {#updates-rerenders}
Understanding how state updates trigger re-renders is crucial.
Synchronous State Updates
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1);
console.log(count); // ❌ Still logs OLD value!
};
return (
<button onClick={handleClick}>Count: {count}</button>
);
}
// When you click:
// 1. handleClick() runs
// 2. setCount(1) is called
// 3. console.log(count) → logs 0 (still old value!)
// 4. handleClick() finishes
// 5. React updates state
// 6. Component re-runs
// 7. Now count = 1 inside component
Important: State updates don't happen immediately. They're batched by React.
Update Functions
For dependent updates, use the update function pattern:
// ❌ WRONG: Unreliable if multiple updates happen
const increment = () => {
setCount(count + 1);
setCount(count + 1); // Might not work as expected!
};
// ✅ CORRECT: Use update function for reliable updates
const increment = () => {
setCount((prev) => prev + 1);
setCount((prev) => prev + 1); // Always works correctly
};
Re-render Behavior
function Counter() {
const [count, setCount] = useState(0);
const [name, setName] = useState('John');
console.log('Component rendered'); // Logs every time component re-renders
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>+</button>
<button onClick={() => setName('Bob')}>Change Name</button>
</div>
);
}
// Clicking "+" button:
// - Triggers setCount
// - React re-runs entire component
// - "Component rendered" logs
// Clicking "Change Name" button:
// - Triggers setName
// - React re-runs entire component
// - "Component rendered" logs
Common Mistakes {#mistakes}
Mistake 1: Mutating State Directly
// ❌ WRONG: Mutating state directly
const [user, setUser] = useState({ name: 'John', age: 30 });
const updateUser = () => {
user.name = 'Bob'; // ❌ Mutation!
setUser(user); // React might not detect change
};
// ✅ CORRECT: Create new object
const updateUser = () => {
setUser({ ...user, name: 'Bob' }); // New object
};
Mistake 2: Not Using Update Function
// ❌ RISKY: State depends on previous value
const increment = () => {
setCount(count + 1); // count might be stale
};
// ✅ SAFE: Always get latest value
const increment = () => {
setCount((prev) => prev + 1); // Always fresh
};
Mistake 3: Setting State in Render
// ❌ INFINITE LOOP: Never do this!
function Component() {
const [count, setCount] = useState(0);
setCount(count + 1); // ❌ Sets state during render
// This causes:
// 1. Component renders
// 2. setCount called
// 3. Component re-renders
// 4. setCount called again
// ... infinite loop!
return <div>{count}</div>;
}
// ✅ CORRECT: Set state in event handlers
function Component() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
);
}
Mistake 4: Forgetting to Import useState
// ❌ WRONG: Missing import
function Counter() {
const [count, setCount] = useState(0); // ❌ useState is not defined
}
// ✅ CORRECT: Import first
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0); // ✅ Works
}
Best Practices {#best-practices}
1. Keep State as Simple as Possible
// ❌ AVOID: Storing derived data
const [user, setUser] = useState({
firstName: 'John',
lastName: 'Doe',
fullName: 'John Doe', // Derived, can get out of sync!
});
// ✅ GOOD: Compute derived values
const [user, setUser] = useState({
firstName: 'John',
lastName: 'Doe',
});
const fullName = `${user.firstName} ${user.lastName}`; // Computed when needed
2. Name Your State Meaningfully
// ❌ UNCLEAR: Poor naming
const [x, setX] = useState(false);
const [data, setData] = useState([]);
// ✅ CLEAR: Meaningful names
const [isLoading, setIsLoading] = useState(false);
const [todos, setTodos] = useState<Todo[]>([]);
3. Use Objects for Related Values
// ❌ SCATTERED: Related values separate
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [email, setEmail] = useState('');
// ✅ ORGANIZED: Related values grouped
const [formData, setFormData] = useState({
firstName: '',
lastName: '',
email: '',
});
4. Initialize With Correct Type
// ❌ PROBLEMATIC: React infers wrong type
const [data, setData] = useState(null); // Type: null (not what we want)
const stringValue = data.substring(0, 1); // ❌ Type error!
// ✅ CORRECT: Explicit type annotation
const [data, setData] = useState<string | null>(null);
const stringValue = data?.substring(0, 1); // ✅ Type-safe
Real-World Examples {#examples}
Example 1: Toggle Component
function Toggle() {
const [isOn, setIsOn] = useState(false);
return (
<div>
<p>Light is {isOn ? 'ON' : 'OFF'}</p>
<button onClick={() => setIsOn(!isOn)}>
Toggle
</button>
</div>
);
}
Example 2: Counter
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<button onClick={() => setCount(count - 1)}>Decrement</button>
<button onClick={() => setCount(0)}>Reset</button>
</div>
);
}
Example 3: Form Input
function NameInput() {
const [name, setName] = useState('');
const [submitted, setSubmitted] = useState(false);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
setSubmitted(true);
};
if (submitted) {
return (
<div>
<p>Hello, {name}!</p>
<button onClick={() => setSubmitted(false)}>Edit</button>
</div>
);
}
return (
<form onSubmit={handleSubmit}>
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Enter your name"
/>
<button type="submit">Submit</button>
</form>
);
}
Example 4: Todo List
interface Todo {
id: string;
text: string;
completed: boolean;
}
function TodoList() {
const [todos, setTodos] = useState<Todo[]>([]);
const [input, setInput] = useState('');
const addTodo = () => {
if (input.trim() === '') return;
setTodos([
...todos,
{
id: Date.now().toString(),
text: input,
completed: false,
},
]);
setInput('');
};
const toggleTodo = (id: string) => {
setTodos(
todos.map((todo) =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
};
const removeTodo = (id: string) => {
setTodos(todos.filter((todo) => todo.id !== id));
};
return (
<div>
<div>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Add a todo"
onKeyPress={(e) => e.key === 'Enter' && addTodo()}
/>
<button onClick={addTodo}>Add</button>
</div>
<ul>
{todos.map((todo) => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
<span
style={{
textDecoration: todo.completed ? 'line-through' : 'none',
}}
>
{todo.text}
</span>
<button onClick={() => removeTodo(todo.id)}>Remove</button>
</li>
))}
</ul>
</div>
);
}
FAQ {#faq}
Q: Can I use state without useState?
A: No, in functional components you need useState. (Class components had state before hooks, but they're not recommended anymore.)
Q: Should state always start as empty?
A: No, state can have any initial value. Use whatever makes sense: useState(0), useState(''), useState(null), useState({}), etc.
Q: Does changing state immediately update the component?
A: No, React batches state updates. The component re-runs after the current event handler finishes.
Q: Can a component share its state with another component?
A: Not directly. You'd need to "lift state up" to a parent component and pass it down as props.
Q: What's the difference between setCount(5) and setCount(prev => prev + 5)?
A: The first sets count to 5. The second increments by 5. The second is safer when the update depends on the previous value.
Q: Can I update multiple pieces of state in one action?
A: Yes, you can call multiple setState functions in an event handler. React batches them together.
Q: Is there a maximum number of useState calls?
A: No, but it's generally better to split components if they have many independent state values.
Q: Can I delete state?
A: No, state persists as long as the component is mounted. You can set it to null, empty string, empty array, etc.
Master the Foundation: State is the foundation of interactive React applications. Once you fully understand it, every other React concept becomes easier. Practice building small components with state, and you'll develop solid intuition.
Next Steps: Learn about lifting state up to share state between components, explore more hooks like useEffect, and discover state management patterns for complex applications.
Google AdSense Placeholder
CONTENT SLOT