Choose the owner
When sibling components need the same changing data, move that state to their closest common parent. The parent passes values down as props and passes handlers down so children can request changes. This creates one source of truth.
function App() { const [tasks, setTasks] = useState(initialTasks); return <> <TaskForm onAdd={handleAdd} /> <TaskList tasks={tasks} onToggle={handleToggle} /> </>; }
Derive rather than duplicate
Do not store a value that can be calculated cheaply from existing props or state. Calculate const remaining = tasks.filter(task => !task.complete).length during rendering. Duplicated state can fall out of sync.
State follows position
React associates state with a component's type and position in the rendered tree. Changing its key gives it a new identity and resets its state. Use that deliberately, not as a general way to force updates.
Trace one update
When TaskForm calls onAdd, it is calling the function supplied by App. That function updates the tasks owned by App. React renders App again, calculates a new remaining count and gives TaskList the new array. The child never edits the parent's array directly.
Put state as low as possible but high enough for every component that needs it. Moving all state to the top makes components unnecessarily coupled; leaving shared state in one child makes siblings disagree.
Challenge: add a task filter
Add filter state to App with values all, active and complete. Pass the current filter and an onFilterChange handler to a TaskFilters component. Derive the visible tasks during rendering rather than storing a second task array.
- Changing the filter does not modify the tasks.
- The summary still uses all tasks.
- Adding or toggling a task immediately updates the filtered list.
