The Two Trees in React
React encourages thinking about your UI as a tree. But there are two different trees that matter for understanding re-renders, and confusing them leads to performance problems and wasted effort.
React encourages understanding your UI as a tree. But when reasoning about re-renders, there are two different trees: the component tree and the ownership tree.
Confusing them is one of the most common React performance misconceptions.
Let me show you
Here’s a slightly adapted example from the React docs:
export default function App() { return ( <> <FancyText /> <InspirationGenerator> <Copyright /> </InspirationGenerator> </> )}
function InspirationGenerator({ children }) { let [quote, setQuote] = useState(generateQuote()) return ( <div> <FancyText text={quote} /> <button onClick={() => setQuote(generateQuote())}>New Quote</button> {children} </div> )}Look at the component tree (what the JSX shows you):
App├── FancyText└── InspirationGenerator ├── FancyText └── CopyrightHere’s my question: which components re-render when a new quote is generated?
You might think: InspirationGenerator re-renders, so its children FancyText and Copyright re-render too. That’s React’s rule, right? “When a component renders, all of its children render.”
But Copyright doesn’t re-render.
The component tree is not the ownership tree
InspirationGenerator doesn’t own Copyright. It just receives it.
The “owner” is simply the component that executes the JSX (calls React.createElement). In this case, App created <Copyright />, so App owns it.
Since App isn’t running, React.createElement isn’t called again for Copyright. Copyright doesn’t re-render because its owner (App) didn’t re-render. By the time InspirationGenerator sees it, Copyright is already a rendered element.
The ownership tree looks like this:
App├── FancyText├── InspirationGenerator│ └── FancyText└── CopyrightCopyright is a sibling of InspirationGenerator, not a child. When InspirationGenerator’s state changes, Copyright is unaffected.
There’s nothing special about children. It’s a prop with syntactic sugar. These two are equivalent:
<InspirationGenerator> <Copyright /></InspirationGenerator>
<InspirationGenerator children={<Copyright />} />Any prop that accepts a React element works the same way.
React Context is better than you think
React Context has a bad reputation for performance. “Context causes everything to re-render.” That’s only true if you structure your components poorly.
Here’s a good example.
export default function App() { return ( <div> <p>App</p> <Provider slot={<DoesNotUseContext />}> <UsesContext /> <DoesNotUseContext /> </Provider> </div> )}
let CountContext = createContext(0)
function Provider({ children, slot }) { let [count, setCount] = useState(0) return ( <CountContext value={count}> <div> <p>Provider</p> {children} <div> <button onClick={() => setCount(c => c + 1)}>Increment Count</button> </div> {slot} </div> </CountContext> )}When count changes, Provider re-renders. But UsesContext and DoesNotUseContext were passed as props. They’re owned by App, not Provider. They don’t re-render just because Provider did.
UsesContext re-renders because it consumes the context. DoesNotUseContext is untouched.
You can verify this yourself:
Full Code
import { useState, useEffect, createContext, useRef, useContext } from "react"
export default function App() { let ref = useFlashOnRender() return ( <div className="bg-background space-y-2 p-4" ref={ref}> <p className="font-semibold">App</p> <Provider slot={<DoesNotUseContext />}> <UsesContext /> <DoesNotUseContext /> </Provider> </div> )}
let CountContext = createContext(0)
function Provider({ children, slot,}: { children: React.ReactNode slot: React.ReactNode}) { let [count, setCount] = useState(0) let ref = useFlashOnRender() return ( <CountContext value={count}> <div ref={ref} className="bg-background border-border space-y-2 rounded-lg border p-4" > <p className="font-semibold">Provider</p> {children} <button onClick={() => setCount(c => c + 1)} className="bg-accent text-background hover:bg-accent/90 block rounded px-4 py-2 text-sm" > Increment Count </button> {slot} </div> </CountContext> )}
function UsesContext() { let count = useContext(CountContext) let ref = useFlashOnRender()
return ( <div ref={ref} className="border-border bg-background rounded-lg border p-4" > <p className="font-semibold">UsesContext</p> <div className="text-muted text-sm">Count: {count}</div> </div> )}
function DoesNotUseContext() { let ref = useFlashOnRender()
return ( <div ref={ref} className="border-border bg-background rounded-lg border p-4" > <div className="font-semibold">DoesNotUseContext</div> <div className="text-muted text-sm">Static content</div> </div> )}
function useFlashOnRender() { let renderCount = useRef(0) let elementRef = useRef<HTMLDivElement>(null)
useEffect(() => { renderCount.current++ if (renderCount.current > 1 && elementRef.current) { elementRef.current.animate( [{ backgroundColor: "yellow" }, { backgroundColor: "transparent" }], { duration: 300, easing: "ease-out" }, ) } })
return elementRef}The takeaway
When reasoning about re-renders, don’t look at where components appear in your JSX. Look at which component is calling them.
Passing React elements through children (or props) moves ownership up the tree. It lets you keep your UI deeply nested, while keeping your ownership tree flat and shallow, reducing unnecessary re-renders without any extra work.
This isn’t a performance trick. It’s how React is designed to work.