Context, State Ownership, and Project Organization
Where should a piece of state live? Context solves prop drilling for genuinely shared state — but reaching for it as a default answer creates its own maintenance problems.
What you'll learn
- Explain the state-ownership question: which component should own a given piece of state
- Identify when Context is the right tool versus when it's overreach
- Organize a small React project's files by responsibility rather than by file type
Prerequisites
Explanation
Every piece of state has an implicit question attached: which component should own it? The answer is usually "the closest common ancestor of every component that needs to read or change it" — no higher, no lower. State that's needed by three sibling components should live in their shared parent, not scattered across each sibling redundantly, and not hoisted all the way to the app's root "just in case" something far away might need it eventually.
Prop drilling — passing a prop down through several layers of components that don't themselves use it, purely to reach a distant descendant that does — is a real symptom worth naming, but it's not automatically a problem requiring a fix. Two or three layers of drilling for a value that's genuinely scoped to that part of the tree is often perfectly reasonable and easy to trace. Context solves drilling for state that's truly global to a meaningful subtree — the current theme, the signed-in user, a feature flag — letting deeply nested components read a value without every intermediate layer having to know about or forward it.
The overreach is real, though: putting everything in Context to avoid ever thinking about prop drilling creates a different problem — any component consuming that context re-renders whenever any value in it changes, even values that component doesn't use, and the actual data flow becomes harder to trace than an explicit prop chain would have been. The state-ownership question ("who actually needs this, and how far does it really need to travel?") should come first; Context is the answer for state that's genuinely wide in scope, not a default reach for anything inconvenient to pass down two levels.
Project organization follows the same "group by what actually changes together" logic as component decomposition. Grouping files by type (components/, hooks/, utils/) scales poorly as a project grows — working on one feature means jumping between five unrelated folders. Grouping by feature (features/courses/ containing that feature's components, hooks, and utils together) keeps related code physically close, which is what actually gets edited together when a feature changes.
Example
Modeling a Context-like subscription mechanism -- any 'consumer' can read the current value without it being threaded through every intermediate layer as a prop.
function createContext(defaultValue) {
let currentValue = defaultValue;
const subscribers = [];
return {
read() { return currentValue; },
provide(value) {
currentValue = value;
subscribers.forEach((fn) => fn(value));
},
subscribe(fn) { subscribers.push(fn); },
};
}
const ThemeContext = createContext("light");
ThemeContext.subscribe((theme) => console.log("A deeply nested component sees:", theme));
ThemeContext.provide("dark"); // "A deeply nested component sees: dark"Try it yourself
Add a second subscriber (a different deeply-nested component) and confirm both receive the same update.
Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.
Guided exercise
Guided exercise
Three sibling components (SearchBar, FilterPanel, ResultsList) all need the current search query. Set correctOwner to the name of the component that should own this state (their shared parent).
Checks: correctly identifies the shared parent as the state owner
Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.
Stuck? Get a hint.
Independent exercise
Independent exercise
Write a function shouldUseContext(numberOfLevelsDrilled, isGenuinelyGlobalConcern) that returns true (Context is likely justified) only if BOTH the value is drilled through more than 3 levels AND it's a genuinely global-to-a-subtree concern (like theme or current user) -- otherwise return false (plain props are probably fine).
Checks: recommends Context for deep, genuinely global state · does not recommend Context for shallow drilling · does not recommend Context for a narrowly-scoped value regardless of depth
Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.
Stuck? Get a hint.
Common mistakes
- Hoisting state to the app's root 'just in case' something distant might need it eventually, instead of keeping it at the closest common ancestor that actually needs it today.
- Reaching for Context as a default solution to any prop drilling, even for values that are only genuinely relevant to a small, localized part of the tree.
- Organizing files by type (all components together, all hooks together) in a way that scatters a single feature's related code across many unrelated folders.
Knowledge check
Takeaway
State should live at the closest common ancestor that actually needs it, Context is for genuinely wide-scoped state rather than a default fix for any prop drilling, and organizing files by feature keeps related code physically close as a project grows.
Summary
This lesson covered the state-ownership question, when Context is the right tool versus overreach, and why feature-based project organization scales better than organizing files by type.
References
Your notes
Notes save automatically.
Finished this lesson?
Mark it complete to track your progress and schedule a future review.
AI tutor
The optional AI tutor isn't enabled in this deployment. All lessons, exercises, quizzes, and search work fully without it.