Parents & Owners in React: Context Providers

Understanding how parent and owner components affect context updates can help you write more performant context providers

Published

I'm available for freelance works, preferably involving data-rich technical applications. I specialize in making complex information accessible through thoughtfully designed interfaces. If you're looking for help with (geospatial) data visualization, interactive technical tools, or frontend development for specialized domains, let's discuss how I can contribute to your project.
Reach out to me at: jules.blom@bedrock.engineer

I recently read Gabriel Pichot’s post Why you need a custom context provider after finding it in This Week In React.

In his post, he argues that context providers with state should be split into separate components that encapsulates the state management logic and wrap a component tree. He’s not the first to argue for custom provider components and it’s fairly common practice from what I’ve seen. Nevertheless, it’s a good post with sound advice. It’s got neat interactive visuals too.

While Gabriel’s explanation is solid, I believe we can make it even clearer by using the parent vs owner conceptual framework. The term “component tree” is ambiguous, we’re actually dealing with two distinct but related component trees:

  1. The parent tree: Which components are nested inside of another
  2. The owner tree: which component renders another component

This distinction is crucial for updates, it helps us understand exactly why components re-render.

First, let us revisit some fundamentals of rendering in React:

When a component re-renders, by default all of the components it renders (owns) are called too . This process is recursive, updates cascade down the owner tree.

A component can re-render for three reasons:

  1. Self-update: React calls the component that owns the state whose updater function (f.e. setCount) was called.
  2. Context-update: React calls the component that consumes a context value that was changed .
  3. Owner-update: React calls the component because its owner component re-renders.


function Toolbar() {
return <CounterButton />;
}
function App() {
const [count, setCount] = useState(0);
return (
<CounterContext.Provider value={count}>
<CounterDisplay />
<UnrelatedComponent />
<Toolbar />
</CounterContext.Provider>
);
}

<CounterDisplay> and <CounterButton> are components that consume the CounterContext.

Trigger count state updatecountAppApp All components within this square can consume this contextcountContext valueApp/CounterContext.ProviderCounterContext.Provider App/CounterContext.Provider/CounterDisplayCounterDisplay App/CounterContext.Provider/UnrelatedComponentUnrelatedComponent App/CounterContext.Provider/ToolbarToolbar App/CounterContext.Provider/Toolbar/CounterButtonCounterButton
Parent Tree
Trigger count state updatecountAppApp countContext valueApp/CounterContext.ProviderCounterContext.Provider App/CounterDisplayCounterDisplay App/UnrelatedComponentUnrelatedComponent App/ToolbarToolbar App/Toolbar/CounterButtonCounterButton
Owner Tree

In a naked context provider:

  • <App> owns the count state that is passed into the context.
  • <App> is both the parent and owner of <CounterContext.Provider>.
  • <CounterContext.Provider> is the parent of child components <CounterDisplay>, <UnrelatedComponent>, and <Toolbar>

When count state is updated, all components owned by <App> are re-rendered. That’s not ideal, because not all child components depend on the count state. We’re doing unnecessary work.

With a Custom Context Provider, <Context.Provider> and the count state are separated into a new component, <CounterProvider>, that contains a slot for child components (children).


function CounterProvider({ children }) {
const [count, setCount] = useState(0);
return (
<CounterContext.Provider value={count}>
{children}
</CounterContext.Provider>
);
}
function App() {
return (
<CounterProvider>
<CounterDisplay />
<UnrelatedComponent />
<Toolbar />
</CounterProvider>
)
}

AppApp Trigger count state updatecountApp/CounterProviderCounterProvider All components within this square can consume this contextcountContext valueApp/CounterProvider/CounterContext.ProviderCounterContext.Provider App/CounterProvider/CounterContext.Provider/CounterDisplayCounterDisplay App/CounterProvider/CounterContext.Provider/UnrelatedComponentUnrelatedComponent App/CounterProvider/CounterContext.Provider/ToolbarToolbar App/CounterProvider/CounterContext.Provider/Toolbar/CounterButtonCounterButton
Parent Tree
AppApp Trigger count state updatecountApp/CounterProviderCounterProvider countContext valueApp/CounterProvider/CounterContext.ProviderCounterContext.Provider App/CounterDisplayCounterDisplay App/UnrelatedComponentUnrelatedComponent App/ToolbarToolbar App/Toolbar/CounterButtonCounterButton
Owner Tree
  • Now <CounterProvider> owns the count state that is passed into the context.
  • <App> still owns the components <CounterDisplay>, <UnrelatedComponent>, and <Toolbar> but it places them in <CounterProvider> as its children.
  • Meaning <CounterProvider> is still the parent of these components

When the context value count updates, only the components that consume the context re-render, <App> itself does not.

Since <App> doesn’t re-render, the React elements it provides as children remain the objects. React recognizes this and skips re-rendering these elements . This way we avoid unnecessary updates to components that don’t depend on the changed state.

The fundamental pattern here is using slotted components (with children) to separate parts that change from parts that don’t change. By restructuring the owner tree without changing the parent tree, we create more targeted “units of update”.

This approach isn’t unique to context, it’s the same pattern Dan Abramov describes in Before You Memo and that I highlighted in my previous post in this Parents & Owners series. Context providers are just one high-impact example of this optimization strategy.

Finally, the point I want to hammer home is: understanding the distinction between parent and owner components in React gives you a powerful mental model for writing better React apps. This distinction impacts:

  • Cleaner data flow: Props follow the owner tree, making data flow easier to understand.
  • More performant apps: Good component structure makes your updates more targeted.
    • More efficient context updates is one of the things it can help with.
    • This approach makes your apps more performant without the need for explicit memoization.
  • Server components: which I still haven’t really looked at! Someday. But I know it’s important for them too.

Thanks to Gabriel Pichot for his post.

  1. This process is recursive: if the updated component returns some other component, React will render that component next, and if that component also returns something, it will render that component next, and so on. The process will continue until there are no more nested components and React knows exactly what should be displayed on screen.
    React Docs: Render and Commit - Step 2: React renders your components
  2. React automatically re-renders all the children that use a particular context starting from the provider that receives a different value. The previous and the next values are compared with the Object.is comparison. Skipping re-renders with memo does not prevent the children receiving fresh context values.
    React Docs: useContext - Caveats
  3. There’s also a lesser-known technique as well: if a React component returns the exact same element reference in its render output as it did the last time, React will skip re-rendering that particular child. There’s at least a couple ways to implement this technique: If you include props.children in your output, that element is the same if this component does a state update If you wrap some elements with useMemo(), those will stay the same until the dependencies change.
    Mark's Dev Blog: Blogged Answers A (Mostly) Complete Guide to React Rendering Behavior - Component Render Optimization Techniques
  4. This is a little known React optimization pattern. React re-renders this [slotted] component deeply when [the state updater] is called, and since children can’t have changed, it automatically bails out of trying to update the children. No need for [...] memo, etc.
    Sebastian Markbåge tweet