Key Principle
Four component patterns address the same root tension -- separating data logic from rendering -- but at different structural levels:
- Provider Pattern: Decouples data producers from consumers across the component tree using
createContext/Provider/useContext. Intermediate components never touch data they do not use, so the dependency graph matches actual usage rather than tree structure. - Compound Pattern: Uses Context privately within a component family so that sibling sub-components (
FlyOut.Toggle,FlyOut.List) coordinate through shared state without prop drilling. The parent owns state; children autonomously read context and decide their own rendering. - HOC Pattern: Wraps components to inject cross-cutting behavior (loading states, data fetching, hover tracking). Each HOC addresses exactly one concern without binding to the identity of what it wraps.
- Container/Presentational: Separates "what data" from "how to render" at the component boundary, making presentational components pure functions of props -- deterministic, reusable, testable.
Why This Matters
Prop drilling forces every intermediate component to accept and forward data it never uses. The real cost is coupling: renaming or restructuring a prop requires changes across the entire chain, scaling with tree depth. These patterns each break that coupling in structurally different ways, and choosing the wrong one creates unnecessary indirection or fragility.
Good Examples
Provider Pattern -- custom hook with boundary validation:
A useThemeContext hook encapsulates useContext(ThemeContext) and throws a descriptive error when called outside its Provider, catching misconfiguration at dev time rather than producing silent undefined behavior. Extracting a ThemeProvider component isolates state ownership so the provider can be reused across different component trees without duplicating logic. (Chapter 6: Provider Pattern)
Compound Pattern -- Context API variant:
A FlyOut component owns open/toggle state and broadcasts through createContext. Children like FlyOut.Toggle and FlyOut.List consume context at any nesting depth. Static property attachment (FlyOut.List) enforces a namespace boundary, signaling that sub-components are meaningless outside their parent's context. (Chunk 039)
HOC Pattern -- withLoader:
withLoader(Element, url) centralizes fetch + loading logic. The double-agnosticism (component-agnostic + data-source-agnostic) is what makes it composable. The wrapped component's export swaps to the HOC-wrapped version, so the consumption site is unaware of the HOC layer. (Chapter 5: HOC Pattern)
Counterpoints
HOC composition breaks traceability. Stacking HOCs creates ordering dependencies -- the sequence of composition silently determines which props reach the inner component. Prop naming collisions are worse than typical bugs because no error is thrown; the overwrite is invisible until runtime behavior diverges. This is the structural reason Apollo migrated from graphql() HOC to useMutation/useQuery Hooks. (Chapter 5: HOC Pattern)
Compound Pattern (cloneElement variant) limits depth. React.Children.map + cloneElement iterates only direct children, so wrapping any child in an intermediate element silently breaks state sharing. The Context API variant avoids this. (Compound Component -- Cons)
Hooks changed the structural expression, not the principle. The Container/Presentational pattern is not obsolete; Hooks replaced the container component layer but not the separation of concerns it enforced. Confusing the mechanism with the principle leads teams to abandon separation entirely when adopting Hooks. (Chapter 4: Container/Presentational Pattern)
Key Quotes
- "We didn't have to pass down any data to components that didn't care about the current value of the theme." (Chapter 6: Provider Pattern)
- "We can create a HOC that wraps this component to provide its values. This way, we can separate the context logic from the rendering components, which improves the reusability of the provider." (Chapter 6: Provider Pattern)
- "Generally speaking, React Hooks don't replace the HOC pattern." (Chapter 5: HOC Pattern)
- "In many cases, the Container/Presentational pattern can be replaced with React Hooks. The introduction of Hooks made it easy for developers to add statefulness without needing a container component to provide that state." (Chapter 4: Container/Presentational Pattern)
- "The compound pattern is great when you're building a component library. You'll often see this pattern when using UI libraries like Semantic UI." (Chunk 039)
- "Composing multiple HOCs can make it difficult to understand how the data is passed to your components. The order of the HOCs can matter in some cases, which can easily lead to bugs when refactoring the code." (Chapter 5: HOC Pattern)
- "Only direct children of the parent component will have access to the open and toggle props, meaning we can't wrap any of these components in another component." (Compound Component -- Cons)
Rules of Thumb
- Provider for global, infrequently-changing state (theme, locale, auth). Context re-renders all subscribers, so high-frequency updates need external state managers.
- Compound for multi-part UI components (dropdowns, accordions, tabs) where sub-components must coordinate but consumers should compose freely in JSX.
- HOC for uniform cross-cutting concerns applied identically across many components. Choose Hooks instead when behavior needs per-component customization or is localized to few components.
- Always use the Context API variant for compound components over
cloneElement-- it works at any depth and avoids prop collision. - Extract Provider into a dedicated component rather than embedding context logic in App. Separate the concern of context management from rendering.
- Custom context hooks over raw useContext -- they add boundary validation and decouple consumers from the Context object identity.
Related References
- Provider Pattern relates to Redux/Zustand, which solve prop-drilling at different scales of complexity and performance.
- HOCs use Hooks internally -- they are a composition-layer pattern, not a replacement for Hooks.
- Compound Pattern uses Context like Provider Pattern, but privately within a component family rather than application-wide.
- The Apollo Client migration from
graphql()HOC touseQuery/useMutationHooks is a concrete case study of the HOC-to-Hooks transition.