Parents & Owners in React: Data Flow
React composes. React components render other components. This way, we assemble components into a hierarchy — a tree — that forms the UI.
Let’s do a quick thought experiment: What if you gave the same UI design to 100 React devs to turn it into an app? It would have to look and behave the same.
Would everyone break down the same visual elements in to components? How many components would they need? How granular or monolithic would their components be? Would there be ‘natural fault lines’ that everyone follows? And how are those components put together?
It would make an interesting experiment. No doubt. There are many ways to compose components and get the same UI. I’m sure the code would be different for all 100. Everyone has their own habits, preferences, and experience level. There is no one right way to do it.
That’s why deciding how to compose components is one of hardest things in React! But some of those apps would be better structured than others. What makes a React app well-composed? I’d say:
- An easy-to-follow data flow
- Well-encapsulated components
- Good rendering performance
Consider this small app.
Each component is in control of what content it returns. This works fine.
_68function App() {_68 const [currentUser, setCurrentUser] = useState({ name: "Jules" });_68_68 return (_68 <div>_68 <Header />_68 <div>_68 {currentUser ? (_68 <Dashboard user={currentUser} />_68 ) : (_68 <LoginScreen onLogin={() => setCurrentUser({ name: "Michael" })} />_68 )}_68 </div>_68 <Footer />_68 </div>_68 );_68}_68_68function Header() {_68 return (_68 <header style={{ backgroundColor: "lightgray" }}>_68 <h2>Header</h2>_68 </header>_68 );_68}_68_68function Footer() {_68 return (_68 <footer style={{ backgroundColor: "lightgray" }}>_68 <h2>Footer</h2>_68 </footer>_68 );_68}_68_68function Dashboard({ user }) {_68 return (_68 <main>_68 <h2>The Dashboard</h2>_68 <DashboardNav />_68 <DashboardContent user={user} />_68 </main>_68 );_68}_68_68function DashboardNav() {_68 return (_68 <nav>_68 <h3>Dashboard Nav</h3>_68 </nav>_68 );_68}_68_68function DashboardContent({ user }) {_68 return (_68 <div>_68 <h3>Dashboard Content</h3>_68 <WelcomeMessage user={user} />_68 </div>_68 );_68}_68_68function WelcomeMessage({ user }) {_68 return (_68 <div>_68 <p>Welcome {user.name}</p>_68 </div>_68 );_68}
First, we’ll ignore a few less relevant components.
_44function App() {_44 const [currentUser, setCurrentUser] = useState({ name: "Jules" });_44_44 return (_44 <div>_44 <Header />_44 <div>_44 {currentUser ? (_44 <Dashboard user={currentUser} />_44 ) : (_44 <LoginScreen onLogin={() => setCurrentUser({ name: "Michael" })} />_44 )}_44 </div>_44 <Footer />_44 </div>_44 );_44}_44_44function Dashboard({ user }) {_44 return (_44 <main>_44 <h2>The Dashboard</h2>_44 <DashboardNav />_44 <DashboardContent user={user} />_44 </main>_44 );_44}_44_44function DashboardContent({ user }) {_44 return (_44 <div>_44 <h3>Dashboard Content</h3>_44 <WelcomeMessage user={user} />_44 </div>_44 );_44}_44_44function WelcomeMessage({ user }) {_44 return (_44 <div>_44 <p>Welcome {user.name}</p>_44 </div>_44 );_44}
The currentUser
state has to go from <App>
at the top to <WelcomeMessage>
at the bottom.
_44function App() {_44 const [currentUser, setCurrentUser] = useState({ name: "Jules" });_44_44 return (_44 <div>_44 <Header />_44 <div>_44 {currentUser ? (_44 <Dashboard user={currentUser} />_44 ) : (_44 <LoginScreen onLogin={() => setCurrentUser({ name: "Michael" })} />_44 )}_44 </div>_44 <Footer />_44 </div>_44 );_44}_44_44function Dashboard({ user }) {_44 return (_44 <main>_44 <h2>The Dashboard</h2>_44 <DashboardNav />_44 <DashboardContent user={user} />_44 </main>_44 );_44}_44_44function DashboardContent({ user }) {_44 return (_44 <div>_44 <h3>Dashboard Content</h3>_44 <WelcomeMessage user={user} />_44 </div>_44 );_44}_44_44function WelcomeMessage({ user }) {_44 return (_44 <div>_44 <p>Welcome {user.name}</p>_44 </div>_44 );_44}
Therefore each intermediate component (<Dashboard>
& <DashboardContent>
) has to pass user
forward, while having no use for it itself. That makes user
a false dependency for these components.
_44function App() {_44 const [currentUser, setCurrentUser] = useState({ name: "Jules" });_44_44 return (_44 <div>_44 <Header />_44 <div>_44 {currentUser ? (_44 <Dashboard user={currentUser} />_44 ) : (_44 <LoginScreen onLogin={() => setCurrentUser({ name: "Michael" })} />_44 )}_44 </div>_44 <Footer />_44 </div>_44 );_44}_44_44function Dashboard({ user }) {_44 return (_44 <main>_44 <h2>The Dashboard</h2>_44 <DashboardNav />_44 <DashboardContent user={user} />_44 </main>_44 );_44}_44_44function DashboardContent({ user }) {_44 return (_44 <div>_44 <h3>Dashboard Content</h3>_44 <WelcomeMessage user={user} />_44 </div>_44 );_44}_44_44function WelcomeMessage({ user }) {_44 return (_44 <div>_44 <p>Welcome {user.name}</p>_44 </div>_44 );_44}
This is known as prop drilling.
Prop drilling can make your app hard to maintain. Over time, as product requirements change, so will your app. It should be prepared for that.
For example, the designer wants to turn <WelcomeMessage>
in to a floating element in <App/>
. Now you have to remove the user
prop from all of its parent components.
_45function App() {_45 const [currentUser, setCurrentUser] = useState({ name: "Jules" });_45_45 return (_45 <div>_45 <Header />_45 <div>_45 {currentUser ? (_45 <Dashboard />_45 ) : (_45 <LoginScreen onLogin={() => setCurrentUser({ name: "Michael" })} />_45 )}_45 </div>_45 <WelcomeMessage user={currentUser} />_45 <Footer />_45 </div>_45 );_45}_45_45function Dashboard() {_45 return (_45 <main>_45 <h2>The Dashboard</h2>_45 <DashboardNav />_45 <DashboardContent />_45 </main>_45 );_45}_45_45function DashboardContent() {_45 return (_45 <div>_45 <h3>Dashboard Content</h3>_45 <div>Some content</div>_45 </div>_45 );_45}_45_45function WelcomeMessage({ user }) {_45 return (_45 <div>_45 <p>Welcome {user.name}</p>_45 </div>_45 );_45}
Whenever <WelcomeMessage>
needs more props from the top, you have to add them at all the intermediate levels too.
These sorts of things make change tedious and can become time-consuming in large applications.
_45function App() {_45 const [currentUser, setCurrentUser] = useState({ name: "Jules" });_45 const [personalMessage, setPersonalMessage] = useState();_45_45 return (_45 <div>_45 <Header />_45 <div>_45 {currentUser ? (_45 <Dashboard user={currentUser} personalMessage={personalMessage} />_45 ) : (_45 <LoginScreen onLogin={() => setCurrentUser({ name: "Michael" })} />_45 )}_45 </div>_45 <Footer />_45 </div>_45 );_45}_45_45function Dashboard({ user, personalMessage }) {_45 return (_45 <main>_45 <h2>The Dashboard</h2>_45 <DashboardNav />_45 <DashboardContent user={user} personalMessage={personalMessage} />_45 </main>_45 );_45}_45_45function DashboardContent({ user, personalMessage }) {_45 return (_45 <div>_45 <h3>Dashboard Content</h3>_45 <WelcomeMessage user={user} personalMessage={personalMessage} />_45 </div>_45 );_45}_45_45function WelcomeMessage({ user, personalMessage }) {_45 return (_45 <div>_45 <p>Welcome {user.name} {personalMessage}</p>_45 </div>_45 );_45}
Consider this small app.
Each component is in control of what content it returns. This works fine.
First, we’ll ignore a few less relevant components.
The currentUser
state has to go from <App>
at the top to <WelcomeMessage>
at the bottom.
Therefore each intermediate component (<Dashboard>
& <DashboardContent>
) has to pass user
forward, while having no use for it itself. That makes user
a false dependency for these components.
This is known as prop drilling.
Prop drilling can make your app hard to maintain. Over time, as product requirements change, so will your app. It should be prepared for that.
For example, the designer wants to turn <WelcomeMessage>
in to a floating element in <App/>
. Now you have to remove the user
prop from all of its parent components.
Whenever <WelcomeMessage>
needs more props from the top, you have to add them at all the intermediate levels too.
These sorts of things make change tedious and can become time-consuming in large applications.
_68function App() {_68 const [currentUser, setCurrentUser] = useState({ name: "Jules" });_68_68 return (_68 <div>_68 <Header />_68 <div>_68 {currentUser ? (_68 <Dashboard user={currentUser} />_68 ) : (_68 <LoginScreen onLogin={() => setCurrentUser({ name: "Michael" })} />_68 )}_68 </div>_68 <Footer />_68 </div>_68 );_68}_68_68function Header() {_68 return (_68 <header style={{ backgroundColor: "lightgray" }}>_68 <h2>Header</h2>_68 </header>_68 );_68}_68_68function Footer() {_68 return (_68 <footer style={{ backgroundColor: "lightgray" }}>_68 <h2>Footer</h2>_68 </footer>_68 );_68}_68_68function Dashboard({ user }) {_68 return (_68 <main>_68 <h2>The Dashboard</h2>_68 <DashboardNav />_68 <DashboardContent user={user} />_68 </main>_68 );_68}_68_68function DashboardNav() {_68 return (_68 <nav>_68 <h3>Dashboard Nav</h3>_68 </nav>_68 );_68}_68_68function DashboardContent({ user }) {_68 return (_68 <div>_68 <h3>Dashboard Content</h3>_68 <WelcomeMessage user={user} />_68 </div>_68 );_68}_68_68function WelcomeMessage({ user }) {_68 return (_68 <div>_68 <p>Welcome {user.name}</p>_68 </div>_68 );_68}
Let us deviate for a second to go over how content of components can be defined. Components can be fully in control of their own content, like in the example above — or components can leave it up to a parent to place content inside of them.
A component can wrap content by nesting it between its tags, just like HTML.
The wrapped content is passed down as the special children
prop 1.
You can think of a component with a children prop as having a ‘slot’ that can be filled in by its parent components 2.
I like to think of children
as the default slot provided by React.
If you need multiple slots, you can pass JSX as props too 3.
Strangely enough, there is no widely-recognized name for such components. I’ve heard ‘layout’, ‘wrapper’, ‘container’, and ‘slotted’ components. I’ll go with slotted components in this post.
Let’s turn <Dashboard>
into a slotted component:
_16function App() {_16 return (_16 <Dashboard>_16 <DashboardContent currentUser={currentUser} />_16 </Dashboard>_16 );_16}_16_16function Dashboard({ children }) {_16 return (_16 <div>_16 <h2>Dashboard</h2>_16 {children} {/* DashboardContent will be rendered here */}_16 </div>_16 );_16}
Recognize that there are two different relations between components:
- Parent: the component or in which the other component is nested.
- Owner: the component which renders the other component 4.
I think this distinction is overlooked. It’s important because props come from owners. Sometimes an component’s parent is also its owner, but usually they're different. A parent can only pass props when it is an owner as well.
In this example:
<App>
is the parent and owner of<Dashboard/>
<Dashboard>
is the parent of<DashboardNav>
and<DashboardContent>
but not the owner.<App>
places<DashboardContent>
inside<Dashboard>
so it is the owner.
Source-to-surface measure for props
A useful measure for prop drilling is the source-to-surface: the number of components some piece of data needs to pass through to surface in the UI.
To find it, answer the following: Where is a piece of data defined and where does it surface in the UI?
- Where is it defined?
- Which component holds the state
- Where is the 'static prop' passed (e.g. to configure a component)
- Where is it used?
- Where is it visible? Where is it used in render?
- It can be ‘invisible’: In what event handler/effect is it used?
Then count the number of component it has to pass through. A lower source-to-surface (meaning a shorter path through component tree) is generally better.
How are slotted components useful? Let’s look at our app again.
_44function App() {_44 const [currentUser, setCurrentUser] = useState({ name: "Jules" });_44_44 return (_44 <div>_44 <Header />_44 <div>_44 {currentUser ? (_44 <Dashboard user={currentUser} />_44 ) : (_44 <LoginScreen onLogin={() => setCurrentUser({ name: "Michael" })} />_44 )}_44 </div>_44 <Footer />_44 </div>_44 );_44}_44_44function Dashboard({ user }) {_44 return (_44 <main>_44 <h2>The Dashboard</h2>_44 <DashboardNav />_44 <DashboardContent user={user} />_44 </main>_44 );_44}_44_44function DashboardContent({ user }) {_44 return (_44 <div>_44 <h3>Dashboard Content</h3>_44 <WelcomeMessage user={user} />_44 </div>_44 );_44}_44_44function WelcomeMessage({ user }) {_44 return (_44 <div>_44 <p>Welcome {user.name}</p>_44 </div>_44 );_44}
If we turn <Dashboard>
and <DashboardContent>
into slotted components…
_48function App() {_48 const [currentUser, setCurrentUser] = useState({ name: "Jules" });_48_48 return (_48 <div>_48 <Header />_48 <div>_48 {currentUser ? (_48 <Dashboard>_48 <DashboardNav />_48 <DashboardContent>_48 <WelcomeMessage user={currentUser} />_48 </DashboardContent>_48 </Dashboard>_48 ) : (_48 <LoginScreen onLogin={() => setCurrentUser({ name: "Michael" })} />_48 )}_48 </div>_48 <Footer />_48 </div>_48 );_48}_48_48function Dashboard({ children }) {_48 return (_48 <main>_48 <h2>The Dashboard</h2>_48 {children}_48 </main>_48 );_48}_48_48function DashboardContent({ children }) {_48 return (_48 <div>_48 <h3>Dashboard Content</h3>_48 {children}_48 </div>_48 );_48}_48_48function WelcomeMessage({ user }) {_48 return (_48 <div>_48 <p>Welcome {user.name}</p>_48 </div>_48 );_48}
<App>
is still the parent and owner of<Dashboard>
- Now
<Dashboard>
is the parent of<DashboardContent>
but not the owner - Likewise,
<DashboardContent>
is the parent of<WelcomeMessage>
but not the owner <App>
now owns<DashboardContent>
and<WelcomeMessage>
Because <WelcomeMessage>
is lifted to <App>
, currentUser
can be passed to it directly. The source-to-surface has gone from 3 to 1.
_48function App() {_48 const [currentUser, setCurrentUser] = useState({ name: "Jules" });_48_48 return (_48 <div>_48 <Header />_48 <div>_48 {currentUser ? (_48 <Dashboard>_48 <DashboardNav />_48 <DashboardContent>_48 <WelcomeMessage user={currentUser} />_48 </DashboardContent>_48 </Dashboard>_48 ) : (_48 <LoginScreen onLogin={() => setCurrentUser({ name: "Michael" })} />_48 )}_48 </div>_48 <Footer />_48 </div>_48 );_48}_48_48function Dashboard({ children }) {_48 return (_48 <main>_48 <h2>The Dashboard</h2>_48 {children}_48 </main>_48 );_48}_48_48function DashboardContent({ children }) {_48 return (_48 <div>_48 <h3>Dashboard Content</h3>_48 {children}_48 </div>_48 );_48}_48_48function WelcomeMessage({ user }) {_48 return (_48 <div>_48 <p>Welcome {user.name}</p>_48 </div>_48 );_48}
Additionally, <Dashboard>
and <DashboardContent>
are no longer passed irrelevant data.
They’re disentangled from their content and therefore better encapsulated.
This makes our — admittedly very small — data flow is easier to follow! And we didn’t even have to reach for React Context.
_50_50function App() {_50 const [currentUser, setCurrentUser] = useState({ name: "Jules" });_50_50 return (_50 <div>_50 <Header />_50 <div>_50 {currentUser ? (_50 <Dashboard>_50 <DashboardNav />_50 <DashboardContent>_50 <WelcomeMessage user={currentUser} />_50 </DashboardContent>_50 </Dashboard>_50 ) : (_50 <LoginScreen onLogin={() => setCurrentUser({ name: "Michael" })} />_50 )}_50 </div>_50 <Footer />_50 </div>_50 );_50 _50}_50_50function Dashboard({ children }) {_50 return (_50 <main>_50 <h2>The Dashboard</h2>_50 {children}_50 </main>_50 );_50}_50_50function DashboardContent({ children }) {_50 return (_50 <div>_50 <h3>Dashboard Content</h3>_50 {children}_50 </div>_50 );_50}_50_50function WelcomeMessage({ user }) {_50 return (_50 <div>_50 <p>Welcome {user.name}</p>_50 </div>_50 );_50}
How are slotted components useful? Let’s look at our app again.
If we turn <Dashboard>
and <DashboardContent>
into slotted components…
<App>
is still the parent and owner of<Dashboard>
- Now
<Dashboard>
is the parent of<DashboardContent>
but not the owner - Likewise,
<DashboardContent>
is the parent of<WelcomeMessage>
but not the owner <App>
now owns<DashboardContent>
and<WelcomeMessage>
Because <WelcomeMessage>
is lifted to <App>
, currentUser
can be passed to it directly. The source-to-surface has gone from 3 to 1.
Additionally, <Dashboard>
and <DashboardContent>
are no longer passed irrelevant data.
They’re disentangled from their content and therefore better encapsulated.
This makes our — admittedly very small — data flow is easier to follow! And we didn’t even have to reach for React Context.
_44function App() {_44 const [currentUser, setCurrentUser] = useState({ name: "Jules" });_44_44 return (_44 <div>_44 <Header />_44 <div>_44 {currentUser ? (_44 <Dashboard user={currentUser} />_44 ) : (_44 <LoginScreen onLogin={() => setCurrentUser({ name: "Michael" })} />_44 )}_44 </div>_44 <Footer />_44 </div>_44 );_44}_44_44function Dashboard({ user }) {_44 return (_44 <main>_44 <h2>The Dashboard</h2>_44 <DashboardNav />_44 <DashboardContent user={user} />_44 </main>_44 );_44}_44_44function DashboardContent({ user }) {_44 return (_44 <div>_44 <h3>Dashboard Content</h3>_44 <WelcomeMessage user={user} />_44 </div>_44 );_44}_44_44function WelcomeMessage({ user }) {_44 return (_44 <div>_44 <p>Welcome {user.name}</p>_44 </div>_44 );_44}
We must recognize two — related but different — component hierarchies.
- The parent tree: which components are nested inside of another. This is what you see in the React DevTools “⚛️ Components” tab.
- The owner tree: which component renders another components. This is the hierarchy we see in code.
The parent tree is what is created by rendering. The owner tree is how it is created. Having a clear idea of this distinction is key to composing well.
Everything is clearer as a diagram, so here’s one for the parent tree.
The dashed arcs between components indicate ownership. The moving dot represents the user
prop trickling down.
Take a moment to relate the diagram to the code then hit the toggle above and compare.
Notice that the parent tree is exactly* the same as before! And by extension, the resulting DOM tree and UI too.
* What's the [] in the diagram of the updated version?
Structurally, the tree is the same.
When you wrap multiple children in a slotted component using children
, React passes the children in array to the parent.
If you would pass this JSX as a named prop, you would have to wrap it in a <Fragment>
.
With children
, React (kind of) does this for you. The []
is like an implicit <Fragment>
.
Consider it an implementation artifact.
Here’s the owner tree:
Notice how it has changed. It has become flatter by using slotted components to lift up <DashboardContent>
and <WelcomeMessage>
.
Different owner trees can generate identical parent trees. Data flow — props — follows the owner tree! Flattening the owner hierarchy by lifting components can make the data flow shorter, resulting in better encapsulated components.
Lifting components can help rendering performance too. Props follows the owner tree, so updates do so too. We can use this to seperate components such that updates are avoided in parts that don’t need to be updated.
This is really a subject of its own so I won’t go into it here. I will (probably) do a follow-up post on this, including diagrams of course.
I’ve been scribbling these trees using pen and paper whenever my components were becoming a tangle. While writing this post, I learned that you can view the owner hierarchy in the React DevTools by double clicking on a component in the component tree (see tutorial). Who knew!


The React DevTools showing the owner tree for both versions of the app.
React is fundamentally about composition.
Composition has always been relevant for maintainable, performant components.
It will become even more relevant with React Server Components, where children
can be received from the server.
I must confess I haven’t looked at RSC that much so I’ll leave it for another time.
- Understanding the owner vs parent component distinction is important for composing well.
- I feel this distinction is under-discussed.
- They create similar but different hierarchies. They’re related, which makes it easy to mix them up.
- The owner tree is the shape of the data flow.
- If the data flow is a mess, look at the owner trees. This can reveal when it’s a good idea to lift a component up.
- Slotted component can skip a level in the owner tree while creating the same parent tree.
- Which is useful when ‘prop drilling’.
To close on a nuanced note: passing props down a few levels is not necessarily bad. And giving more control to parents by lifting components isn’t always the right solution.
This inversion of control [lifting components] can make your code cleaner in many cases by reducing the amount of props you need to pass through your application and giving more control to the root components.
Such inversion, however, isn’t the right choice in every case; moving more complexity higher in the tree makes those higher-level components more complicated and forces the lower-level components to be more flexible than you may want.
There’s a right time and place for every pattern, recognizing when and where is what it’s all about. I hope this post and these diagrams helped with that.
The example comes from Micheal Jackson's video Using Composition in React to Avoid "Prop Drilling". Credits to him for coming up with it and explaining it well.
Credits to Marcos for proofreading and providing valuable feedback.
- One simple trick to optimize React re-renders
- Before you memo — Overreacted
- React components composition: how to get it right - developerway.com
- One React mistake that's slowing you down
-
↩
Legacy React Docs: Glossary of React Terms – props.childrenprops.children
is available on every component. It contains the content between the opening and closing tags of a component. -
↩When you nest content inside a JSX tag, the parent component will receive that content in a prop called
React Docs: Passing Props to a Component - Passing JSX as childrenchildren
. […] You can think of a component with a children prop as having a “hole” that can be “filled in” by its parent components with arbitrary JSX -
↩Some components don’t know their children ahead of time. This is especially common for components like Sidebar or Dialog that represent generic “boxes”. We recommend that such components use the special
Legacy React Docs: Composition vs Inheritance - Containmentchildren
prop to pass children elements directly into their output: [...] While this is less common, sometimes you might need multiple “holes” in a component. In such cases you may come up with your own convention instead of using children: React elements like<Contacts />
and<Chat />
are just objects, so you can pass them as props like any other data. This approach may remind you of “slots” in other libraries but there are no limitations on what you can pass as props in React. -
↩In React, an element's owner refers to the thing that rendered it. Sometimes an element's parent is also its owner, but usually they're different. This distinction is important because props come from owners.
React DevTools Tutorial