Passing props between components is inevitable when writing React applications. But have you ever wonder:
Should the parent component pass the domain entity/object or just the ID of the object into the child component?
And how would this affect the boundaries of smart vs. dumb components?
Recap on Smart vs. Dumb components
There are a lot of great articles covering this pattern and illustrating the advantages. I have also briefly mentioned the concept of smart and dumb components in my previous post, but let’s do a quick recap here.
In the early days, class components have been doing the heavy-lifting works such as data fetching, state mutation, asynchronous tasks with lifecycle methods, etc., while the function components are more simple, render-only components. And thus, the terms “smart components” and “dumb components” are contrived in the React community.
Smart components are usually the parent components that contain business logic and have more knowledge on the application — they know where to fetch data, when to update state and child components, which context/store to subscribe, what actions can be dispatched, etc.
Dumb components, on the other hand, are relatively “dumb” and agnostic to more of the application states and domain knowledge. They just do one thing and do one thing well) — take props and render correctly.
The problem
Imagine we are creating a page showing the user’s name and groups that the user belongs to. We have UserContext
storing the user
object and GroupContext
with all the available groups
. The schema of user
and group
look like:
type User = {
name: string,
groupIds: string[],
}type Group = {
id: string,
name: string,
}
Note that the user
object only has an array with group IDs but not the group objects. It is in the normalized form and we have to use the groupId
to lookup the group
object somewhere in the page or components we are going to build.
That’s the problem we are facing — when should we do the lookup with groupId
and whether we should pass down groupId
to the child components of the group
object?
Ideally the user
object should be in the denormalized form so we can easily iterate all groups via user.groups.map(...)
, but sometimes there are limitations (e.g. subject to API design or performance concerns) that we can’t denormalize every related object and put into the React context.
First trial — passing ID into child components
A very straightforward approach would look like the following:
- A
UserPage
component that retrievesuser
from theUserContext
, render theuser.name
, iterateuser.groupIds
and pass eachgroupId
to the child componentGroup
- A
Group
component that receives a groupid
as props, retrieves all availablegroups
from theGroupContext
, lookup the requiredgroup
byid
and render information.
As you can see, the parent UserPage
component didn’t do anything else except passing whatever it has to the child components Group
. So the Group
component has to i) find the group
and ii) render it. It could be even more complicated if it needs to fetch from API or perform some data transformation before rendering.
The child component has to do more jobs because the parent component does less. It also makes the lower-level component needs to care more about the data source and the data schema. Kinds of obfuscating the boundaries between smart and dumb components.
So how can we do better?
There was a similar discussion on StackOverflow by Dan Abramov from the React team exploring different approaches for this puzzle. Although the post was back in 2014 and the example used was React Flux, the concept is akin:
All components can read their own data
vs.
All data is read once at the top level and passed down to components
Eventually, Dan proposed two principles:
- No component ever receives ID as a prop; all components receive their respective objects.
- If child components need an entity, it’s parent’s responsibility to retrieve it and pass as a prop.
In short: The child components should receive its data (entity) as a prop but not ID. It is the dumb component whose only job is to render the entity without caring for the data source.
Second trial — passing entity into child components
Back to our example, how do we follow this pattern and make sure the Group
component receives the group
entity instead of groupId
as props?
If the parent UserPage
component only has the groupIds
list, we may make use of HOCs as the “middle layer” to retrieve the group
entities for the child component. In this case, the HOC is the smart component while keeping the child component dumb.
Simplified with hooks
Lastly, with the thriving hooks and GraphQL, we may ditch the HOC and React context to further simplify the codes.
I found keeping this principle in mind leads to a better component structure — it makes a clear between the responsibility of parent and child components; embraces “domain object/entity”; and facilitates reusability of the child components.
This may sound like a trivial problem at first, but there is a deeper mental model underlying and it could have a huge impact on how we organize our components as the codebase expands. I hope you found this puzzle interesting and please let me know your thoughts!