Parents & Owners in React: Data Flow

There are two, similar but different component trees in React. Being aware of this difference can help you better structure your React application.

Published · Last updated

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.


function App() {
const [currentUser, setCurrentUser] = useState({ name: "Jules" });
return (
<div>
<Header />
<div>
{currentUser ? (
<Dashboard user={currentUser} />
) : (
<LoginScreen onLogin={() => setCurrentUser({ name: "Michael" })} />
)}
</div>
<Footer />
</div>
);
}
function Header() {
return (
<header style={{ backgroundColor: "lightgray" }}>
<h2>Header</h2>
</header>
);
}
function Footer() {
return (
<footer style={{ backgroundColor: "lightgray" }}>
<h2>Footer</h2>
</footer>
);
}
function Dashboard({ user }) {
return (
<main>
<h2>The Dashboard</h2>
<DashboardNav />
<DashboardContent user={user} />
</main>
);
}
function DashboardNav() {
return (
<nav>
<h3>Dashboard Nav</h3>
</nav>
);
}
function DashboardContent({ user }) {
return (
<div>
<h3>Dashboard Content</h3>
<WelcomeMessage user={user} />
</div>
);
}
function WelcomeMessage({ user }) {
return (
<div>
<p>Welcome {user.name}</p>
</div>
);
}

First, we’ll ignore a few less relevant components.


function App() {
const [currentUser, setCurrentUser] = useState({ name: "Jules" });
return (
<div>
<Header />
<div>
{currentUser ? (
<Dashboard user={currentUser} />
) : (
<LoginScreen onLogin={() => setCurrentUser({ name: "Michael" })} />
)}
</div>
<Footer />
</div>
);
}
function Dashboard({ user }) {
return (
<main>
<h2>The Dashboard</h2>
<DashboardNav />
<DashboardContent user={user} />
</main>
);
}
function DashboardContent({ user }) {
return (
<div>
<h3>Dashboard Content</h3>
<WelcomeMessage user={user} />
</div>
);
}
function WelcomeMessage({ user }) {
return (
<div>
<p>Welcome {user.name}</p>
</div>
);
}

The currentUser state has to go from <App> at the top to <WelcomeMessage> at the bottom.


function App() {
const [currentUser, setCurrentUser] = useState({ name: "Jules" });
return (
<div>
<Header />
<div>
{currentUser ? (
<Dashboard user={currentUser} />
) : (
<LoginScreen onLogin={() => setCurrentUser({ name: "Michael" })} />
)}
</div>
<Footer />
</div>
);
}
function Dashboard({ user }) {
return (
<main>
<h2>The Dashboard</h2>
<DashboardNav />
<DashboardContent user={user} />
</main>
);
}
function DashboardContent({ user }) {
return (
<div>
<h3>Dashboard Content</h3>
<WelcomeMessage user={user} />
</div>
);
}
function WelcomeMessage({ user }) {
return (
<div>
<p>Welcome {user.name}</p>
</div>
);
}

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.


function App() {
const [currentUser, setCurrentUser] = useState({ name: "Jules" });
return (
<div>
<Header />
<div>
{currentUser ? (
<Dashboard user={currentUser} />
) : (
<LoginScreen onLogin={() => setCurrentUser({ name: "Michael" })} />
)}
</div>
<Footer />
</div>
);
}
function Dashboard({ user }) {
return (
<main>
<h2>The Dashboard</h2>
<DashboardNav />
<DashboardContent user={user} />
</main>
);
}
function DashboardContent({ user }) {
return (
<div>
<h3>Dashboard Content</h3>
<WelcomeMessage user={user} />
</div>
);
}
function WelcomeMessage({ user }) {
return (
<div>
<p>Welcome {user.name}</p>
</div>
);
}

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.


function App() {
const [currentUser, setCurrentUser] = useState({ name: "Jules" });
return (
<div>
<Header />
<div>
{currentUser ? (
<Dashboard />
) : (
<LoginScreen onLogin={() => setCurrentUser({ name: "Michael" })} />
)}
</div>
<WelcomeMessage user={currentUser} />
<Footer />
</div>
);
}
function Dashboard() {
return (
<main>
<h2>The Dashboard</h2>
<DashboardNav />
<DashboardContent />
</main>
);
}
function DashboardContent() {
return (
<div>
<h3>Dashboard Content</h3>
<div>Some content</div>
</div>
);
}
function WelcomeMessage({ user }) {
return (
<div>
<p>Welcome {user.name}</p>
</div>
);
}

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.


function App() {
const [currentUser, setCurrentUser] = useState({ name: "Jules" });
const [personalMessage, setPersonalMessage] = useState();
return (
<div>
<Header />
<div>
{currentUser ? (
<Dashboard user={currentUser} personalMessage={personalMessage} />
) : (
<LoginScreen onLogin={() => setCurrentUser({ name: "Michael" })} />
)}
</div>
<Footer />
</div>
);
}
function Dashboard({ user, personalMessage }) {
return (
<main>
<h2>The Dashboard</h2>
<DashboardNav />
<DashboardContent user={user} personalMessage={personalMessage} />
</main>
);
}
function DashboardContent({ user, personalMessage }) {
return (
<div>
<h3>Dashboard Content</h3>
<WelcomeMessage user={user} personalMessage={personalMessage} />
</div>
);
}
function WelcomeMessage({ user, personalMessage }) {
return (
<div>
<p>Welcome {user.name} {personalMessage}</p>
</div>
);
}

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.


function App() {
const [currentUser, setCurrentUser] = useState({ name: "Jules" });
return (
<div>
<Header />
<div>
{currentUser ? (
<Dashboard user={currentUser} />
) : (
<LoginScreen onLogin={() => setCurrentUser({ name: "Michael" })} />
)}
</div>
<Footer />
</div>
);
}
function Header() {
return (
<header style={{ backgroundColor: "lightgray" }}>
<h2>Header</h2>
</header>
);
}
function Footer() {
return (
<footer style={{ backgroundColor: "lightgray" }}>
<h2>Footer</h2>
</footer>
);
}
function Dashboard({ user }) {
return (
<main>
<h2>The Dashboard</h2>
<DashboardNav />
<DashboardContent user={user} />
</main>
);
}
function DashboardNav() {
return (
<nav>
<h3>Dashboard Nav</h3>
</nav>
);
}
function DashboardContent({ user }) {
return (
<div>
<h3>Dashboard Content</h3>
<WelcomeMessage user={user} />
</div>
);
}
function WelcomeMessage({ user }) {
return (
<div>
<p>Welcome {user.name}</p>
</div>
);
}

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 .

You can think of a component with a children prop as having a ‘slot’ that can be filled in by its parent components . 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 .

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:


function App() {
return (
<Dashboard>
<DashboardContent currentUser={currentUser} />
</Dashboard>
);
}
function Dashboard({ children }) {
return (
<div>
<h2>Dashboard</h2>
{children} {/* DashboardContent will be rendered here */}
</div>
);
}

Recognize that there are two different relations between components:

  1. Parent: the component or in which the other component is nested.
  2. Owner: the component which renders the other component .

I think this distinction is overlooked. It’s important because props come from owners. Sometimes a 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 or 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.


function App() {
const [currentUser, setCurrentUser] = useState({ name: "Jules" });
return (
<div>
<Header />
<div>
{currentUser ? (
<Dashboard user={currentUser} />
) : (
<LoginScreen onLogin={() => setCurrentUser({ name: "Michael" })} />
)}
</div>
<Footer />
</div>
);
}
function Dashboard({ user }) {
return (
<main>
<h2>The Dashboard</h2>
<DashboardNav />
<DashboardContent user={user} />
</main>
);
}
function DashboardContent({ user }) {
return (
<div>
<h3>Dashboard Content</h3>
<WelcomeMessage user={user} />
</div>
);
}
function WelcomeMessage({ user }) {
return (
<div>
<p>Welcome {user.name}</p>
</div>
);
}

If we turn <Dashboard> and <DashboardContent> into slotted components…


function App() {
const [currentUser, setCurrentUser] = useState({ name: "Jules" });
return (
<div>
<Header />
<div>
{currentUser ? (
<Dashboard>
<DashboardNav />
<DashboardContent>
<WelcomeMessage user={currentUser} />
</DashboardContent>
</Dashboard>
) : (
<LoginScreen onLogin={() => setCurrentUser({ name: "Michael" })} />
)}
</div>
<Footer />
</div>
);
}
function Dashboard({ children }) {
return (
<main>
<h2>The Dashboard</h2>
{children}
</main>
);
}
function DashboardContent({ children }) {
return (
<div>
<h3>Dashboard Content</h3>
{children}
</div>
);
}
function WelcomeMessage({ user }) {
return (
<div>
<p>Welcome {user.name}</p>
</div>
);
}

  • <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.


function App() {
const [currentUser, setCurrentUser] = useState({ name: "Jules" });
return (
<div>
<Header />
<div>
{currentUser ? (
<Dashboard>
<DashboardNav />
<DashboardContent>
<WelcomeMessage user={currentUser} />
</DashboardContent>
</Dashboard>
) : (
<LoginScreen onLogin={() => setCurrentUser({ name: "Michael" })} />
)}
</div>
<Footer />
</div>
);
}
function Dashboard({ children }) {
return (
<main>
<h2>The Dashboard</h2>
{children}
</main>
);
}
function DashboardContent({ children }) {
return (
<div>
<h3>Dashboard Content</h3>
{children}
</div>
);
}
function WelcomeMessage({ user }) {
return (
<div>
<p>Welcome {user.name}</p>
</div>
);
}

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.


function App() {
const [currentUser, setCurrentUser] = useState({ name: "Jules" });
return (
<div>
<Header />
<div>
{currentUser ? (
<Dashboard>
<DashboardNav />
<DashboardContent>
<WelcomeMessage user={currentUser} />
</DashboardContent>
</Dashboard>
) : (
<LoginScreen onLogin={() => setCurrentUser({ name: "Michael" })} />
)}
</div>
<Footer />
</div>
);
}
function Dashboard({ children }) {
return (
<main>
<h2>The Dashboard</h2>
{children}
</main>
);
}
function DashboardContent({ children }) {
return (
<div>
<h3>Dashboard Content</h3>
{children}
</div>
);
}
function WelcomeMessage({ user }) {
return (
<div>
<p>Welcome {user.name}</p>
</div>
);
}

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.


function App() {
const [currentUser, setCurrentUser] = useState({ name: "Jules" });
return (
<div>
<Header />
<div>
{currentUser ? (
<Dashboard user={currentUser} />
) : (
<LoginScreen onLogin={() => setCurrentUser({ name: "Michael" })} />
)}
</div>
<Footer />
</div>
);
}
function Dashboard({ user }) {
return (
<main>
<h2>The Dashboard</h2>
<DashboardNav />
<DashboardContent user={user} />
</main>
);
}
function DashboardContent({ user }) {
return (
<div>
<h3>Dashboard Content</h3>
<WelcomeMessage user={user} />
</div>
);
}
function WelcomeMessage({ user }) {
return (
<div>
<p>Welcome {user.name}</p>
</div>
);
}

We must recognize two — related but different — component hierarchies.

  1. The parent tree: which components are nested inside of another. This is what you see in the React DevTools “⚛️ Components” tab.
  2. 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.

Ill-composed Parent TreeApp (3)Appdiv (4)App/divHeader (5)App/div/Headerdiv (8)App/div/divFooter (23)App/div/Footerheader (6)App/div/Header/headerDashboard (9)App/div/div/Dashboardfooter (24)App/div/Footer/footerh2 (7)App/div/Header/header/h2main (10)App/div/div/Dashboard/mainh2 (25)App/div/Footer/footer/h2h2 (11)App/div/div/Dashboard/main/h2DashboardNav (12)App/div/div/Dashboard/main/DashboardNavDashboardContent (15)App/div/div/Dashboard/main/DashboardContentnav (13)App/div/div/Dashboard/main/DashboardNav/navdiv (16)App/div/div/Dashboard/main/DashboardContent/divh3 (14)App/div/div/Dashboard/main/DashboardNav/nav/h3h3 (17)App/div/div/Dashboard/main/DashboardContent/div/h3WelcomeMessage (18)App/div/div/Dashboard/main/DashboardContent/div/WelcomeMessagediv (19)"App/div/div/Dashboard/main/DashboardContent/div/WelcomeMessage/div"p (20)App/div/div/Dashboard/main/DashboardContent/div/WelcomeMessage/div/p

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?

When you wrap multiple children in a slotted component using children, React passes the children as an 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>. Structurally, the tree is the same, consider it an implementation artifact.

Here’s the owner tree:

Ill-composed Owner TreeApp (3)div (4)Header (5)div (8)Footer (23)Dashboard (9)header (6)h2 (7)footer (24)h2 (25)main (10)h2 (11)DashboardNav (12)DashboardContent (15)nav (13)h3 (14)div (16)h3 (17)WelcomeMessage (18)div (19)p (20)

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 wrote a follow up post on this: Parents & Owners in React: Rendering Performance.

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 parent tree (see tutorial). Who knew!

Screenshot of React Devtools showing the owner tree for the ill-composed example appScreenshot of React Devtools showing the owner tree for the well-composed example app

The React DevTools showing the owner tree for both versions of the example 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.

  1. props.children is available on every component. It contains the content between the opening and closing tags of a component.
    Legacy React Docs: Glossary of React Terms – props.children
  2. When you nest content inside a JSX tag, the parent component will receive that content in a prop called children. […] 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
    React Docs: Passing Props to a Component - Passing JSX as children
  3. 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 children 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.
    Legacy React Docs: Composition vs Inheritance - Containment
  4. 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 — Exploring Owners