State in Components
State in React Components
State is data that changes over time in your React application. When state changes, React re-renders the component to reflect the new data.
Understanding State
State is:
- Private to a component
- Mutable (can be changed)
- Triggers re-renders when updated
- Preserved between re-renders
import React, { useState } from 'react';
function Counter() {
// Declare state variable
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
State in Functional Components (useState)
Basic Usage
function Example() {
// Declare a state variable
const [value, setValue] = useState(initialValue);
// value: current state value
// setValue: function to update state
// initialValue: initial state value
}
Multiple State Variables
function UserProfile() {
const [name, setName] = useState('');
const [age, setAge] = useState(0);
const [email, setEmail] = useState('');
return (
<form>
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Name"
/>
<input
type="number"
value={age}
onChange={(e) => setAge(parseInt(e.target.value))}
placeholder="Age"
/>
<input
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
/>
</form>
);
}
Object State
function UserForm() {
const [user, setUser] = useState({
name: '',
email: '',
age: 0
});
const updateUser = (field, value) => {
setUser(prevUser => ({
...prevUser,
[field]: value
}));
};
return (
<form>
<input
value={user.name}
onChange={(e) => updateUser('name', e.target.value)}
placeholder="Name"
/>
<input
value={user.email}
onChange={(e) => updateUser('email', e.target.value)}
placeholder="Email"
/>
</form>
);
}
Array State
function TodoList() {
const [todos, setTodos] = useState([]);
const [inputValue, setInputValue] = useState('');
const addTodo = () => {
if (inputValue.trim()) {
setTodos([...todos, {
id: Date.now(),
text: inputValue,
completed: false
}]);
setInputValue('');
}
};
const toggleTodo = (id) => {
setTodos(todos.map(todo =>
todo.id === id
? { ...todo, completed: !todo.completed }
: todo
));
};
const deleteTodo = (id) => {
setTodos(todos.filter(todo => todo.id !== id));
};
return (
<div>
<input
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && addTodo()}
/>
<button onClick={addTodo}>Add Todo</button>
<ul>
{todos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
<span className={todo.completed ? 'completed' : ''}>
{todo.text}
</span>
<button onClick={() => deleteTodo(todo.id)}>Delete</button>
</li>
))}
</ul>
</div>
);
}
State in Class Components
Basic State
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
increment = () => {
this.setState({ count: this.state.count + 1 });
}
render() {
return (
<div>
<p>Count: {this.state.count}</p>
<button onClick={this.increment}>Increment</button>
</div>
);
}
}
Updating State Correctly
class Example extends React.Component {
state = {
count: 0,
name: 'React'
};
// ❌ Wrong - Don't mutate state directly
wrongUpdate = () => {
this.state.count = 5; // Never do this!
}
// ✅ Correct - Use setState
correctUpdate = () => {
this.setState({ count: 5 });
}
// ✅ Update based on previous state
incrementCount = () => {
this.setState(prevState => ({
count: prevState.count + 1
}));
}
// ✅ Update multiple properties
updateMultiple = () => {
this.setState({
count: 10,
name: 'Updated React'
});
}
}
Functional Updates
When new state depends on previous state, use functional updates:
function Counter() {
const [count, setCount] = useState(0);
// ❌ Might not work as expected with multiple rapid clicks
const incrementWrong = () => {
setCount(count + 1);
setCount(count + 1);
// Still increments by 1, not 2!
};
// ✅ Correct way using functional update
const incrementCorrect = () => {
setCount(prevCount => prevCount + 1);
setCount(prevCount => prevCount + 1);
// Increments by 2 as expected
};
return (
<div>
<p>Count: {count}</p>
<button onClick={incrementCorrect}>+2</button>
</div>
);
}
Lazy Initial State
For expensive computations, use lazy initialization:
function ExpensiveComponent() {
// ❌ Runs on every render
const [data, setData] = useState(expensiveComputation());
// ✅ Runs only once
const [data, setData] = useState(() => expensiveComputation());
return <div>{/* Use data */}</div>;
}
// Example with localStorage
function Settings() {
const [settings, setSettings] = useState(() => {
const saved = localStorage.getItem('settings');
return saved ? JSON.parse(saved) : defaultSettings;
});
const updateSettings = (newSettings) => {
setSettings(newSettings);
localStorage.setItem('settings', JSON.stringify(newSettings));
};
return <div>{/* Settings UI */}</div>;
}
State Management Patterns
Lifting State Up
function TemperatureInput({ scale, temperature, onTemperatureChange }) {
return (
<fieldset>
<legend>Enter temperature in {scale}:</legend>
<input
value={temperature}
onChange={e => onTemperatureChange(e.target.value)}
/>
</fieldset>
);
}
function Calculator() {
const [temperature, setTemperature] = useState('');
const [scale, setScale] = useState('celsius');
const handleCelsiusChange = (temp) => {
setScale('celsius');
setTemperature(temp);
};
const handleFahrenheitChange = (temp) => {
setScale('fahrenheit');
setTemperature(temp);
};
const celsius = scale === 'fahrenheit'
? tryConvert(temperature, toCelsius)
: temperature;
const fahrenheit = scale === 'celsius'
? tryConvert(temperature, toFahrenheit)
: temperature;
return (
<div>
<TemperatureInput
scale="celsius"
temperature={celsius}
onTemperatureChange={handleCelsiusChange}
/>
<TemperatureInput
scale="fahrenheit"
temperature={fahrenheit}
onTemperatureChange={handleFahrenheitChange}
/>
</div>
);
}
Derived State
function SearchableList({ items }) {
const [searchTerm, setSearchTerm] = useState('');
// Derived state - computed from other state
const filteredItems = items.filter(item =>
item.name.toLowerCase().includes(searchTerm.toLowerCase())
);
return (
<div>
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search..."
/>
<ul>
{filteredItems.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
);
}
State vs Props
function ParentComponent() {
const [parentState, setParentState] = useState('Hello');
return (
<ChildComponent
propValue={parentState} // Passed as prop
onUpdate={setParentState}
/>
);
}
function ChildComponent({ propValue, onUpdate }) {
const [localState, setLocalState] = useState('World'); // Component's own state
return (
<div>
<p>Prop from parent: {propValue}</p>
<p>Local state: {localState}</p>
<button onClick={() => onUpdate('Updated!')}>
Update Parent State
</button>
<button onClick={() => setLocalState('Changed!')}>
Update Local State
</button>
</div>
);
}
Common State Patterns
Toggle State
function Toggle() {
const [isOn, setIsOn] = useState(false);
return (
<button onClick={() => setIsOn(!isOn)}>
{isOn ? 'ON' : 'OFF'}
</button>
);
}
Form State
function ContactForm() {
const [formData, setFormData] = useState({
name: '',
email: '',
message: ''
});
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
};
const handleSubmit = (e) => {
e.preventDefault();
console.log('Form submitted:', formData);
// Reset form
setFormData({
name: '',
email: '',
message: ''
});
};
return (
<form onSubmit={handleSubmit}>
<input
name="name"
value={formData.name}
onChange={handleChange}
placeholder="Name"
/>
<input
name="email"
value={formData.email}
onChange={handleChange}
placeholder="Email"
/>
<textarea
name="message"
value={formData.message}
onChange={handleChange}
placeholder="Message"
/>
<button type="submit">Submit</button>
</form>
);
}
Loading State
function DataFetcher() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const fetchData = async () => {
setLoading(true);
setError(null);
try {
const response = await fetch('/api/data');
const result = await response.json();
setData(result);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
if (!data) return <button onClick={fetchData}>Load Data</button>;
return <div>{/* Display data */}</div>;
}
Best Practices
- Keep state as local as possible
- Don't duplicate props in state
- Use functional updates for state that depends on previous state
- Group related state variables
- Consider using useReducer for complex state logic
- Avoid deeply nested state objects
- Use lazy initialization for expensive computations
Common Mistakes
Mutating State
// ❌ Never mutate state directly
const [items, setItems] = useState([1, 2, 3]);
// Wrong
items.push(4);
setItems(items);
// ✅ Create a new array
setItems([...items, 4]);
Stale Closures
function Timer() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
// ❌ This will always log 0
console.log(count);
// ✅ Use functional update
setCount(c => c + 1);
}, 1000);
return () => clearInterval(id);
}, []); // Empty dependency array
return <div>Count: {count}</div>;
}
State is fundamental to React applications. Master state management and you'll be able to build dynamic, interactive user interfaces!