Difficulty: Beginner
State is data that a component manages internally and can change over time. While props come from a parent and are read-only, state is owned by the component itself and can be updated. When state changes, React automatically re-renders the component to reflect the new data. This is what makes React interfaces interactive - user actions trigger state changes, which trigger re-renders, which update the UI.
The `useState` hook is the primary way to add state to a functional component. It takes an initial value as its argument and returns an array with exactly two elements: the current state value and a function to update it. By convention, these are destructured as `[value, setValue]`. For example, `const [count, setCount] = useState(0)` creates a state variable `count` starting at 0 and a function `setCount` to update it.
State updates in React are asynchronous and batched. When you call `setCount(5)`, the component does not re-render immediately. React batches multiple state updates together and processes them in a single re-render for performance. This means if you call `setCount(count + 1)` twice in a row, you will not get count + 2 because both calls see the same value of count. To handle this, use the functional update form: `setCount(prev => prev + 1)`, which always receives the latest state value.
The initial value you pass to useState is only used on the first render. On subsequent re-renders, React remembers the current state and ignores the initial value. If computing the initial value is expensive (e.g., reading from localStorage), you can pass a function: `useState(() => JSON.parse(localStorage.getItem('data')))`. This lazy initialization function only runs once on mount.
Understanding the difference between props and state is crucial. Props are passed in from outside and the component cannot change them. State is internal and the component controls it. A value should be state if it changes over time and those changes should cause the UI to update. If a value is computed from props or other state, it should be a derived value (a regular variable), not additional state. Keeping your state minimal avoids bugs and unnecessary complexity.
// Simulating useState behavior
function createState(initialValue) {
let state = initialValue;
const getState = () => state;
const setState = (newValue) => {
if (typeof newValue === 'function') {
state = newValue(state);
} else {
state = newValue;
}
};
return [getState, setState];
}
const [getCount, setCount] = createState(0);
console.log('Initial: ' + getCount());
setCount(5);
console.log('After setCount(5): ' + getCount());
setCount(prev => prev + 1);
console.log('After increment: ' + getCount());
useState returns a value and a setter. The setter can take a direct value or a function that receives the previous state and returns the new state.
// Props: passed from parent, read-only
// State: managed internally, changeable
function simulateComponent(props) {
// State is local - component owns it
let state = { clicks: 0 };
function handleClick() {
state = { clicks: state.clicks + 1 };
}
// Initial render
console.log(props.label + ' - Clicks: ' + state.clicks);
// Simulate user clicks
handleClick();
handleClick();
handleClick();
// After state changes
console.log(props.label + ' - Clicks: ' + state.clicks);
// Props are still unchanged
console.log('Prop label unchanged: ' + props.label);
}
simulateComponent({ label: 'Submit Button' });
Props come from outside and don't change. State is internal and changes in response to user interaction, causing re-renders.
// Problem: multiple direct updates use stale state
function directUpdates() {
let count = 0;
// These all see count as 0
count = count + 1; // 0 + 1 = 1
count = 0 + 1; // stale! still reads original 0
count = 0 + 1; // stale! still reads original 0
return count;
}
// Solution: functional updates always get latest state
function functionalUpdates() {
let count = 0;
const update = (fn) => { count = fn(count); };
update(prev => prev + 1); // 0 -> 1
update(prev => prev + 1); // 1 -> 2
update(prev => prev + 1); // 2 -> 3
return count;
}
console.log('Direct (stale): ' + directUpdates());
console.log('Functional (correct): ' + functionalUpdates());
When you need to update state based on the previous value, always use the functional form (prev => prev + 1) to avoid stale state bugs.
useState, state updates, re-rendering, state vs props, initial state