React patterns: Combining hooks with child functions for composable data components
Combining React hooks with the "function as child" pattern is a powerful way to achieve composable UI that relies on data.
Photo by Galen Crout on Unsplash
I'd like to share a quick example of a React pattern I find myself reaching for a lot recently. Well really it's a combination of two patterns:
A React hook which gets some data or application state
A provider component which accepts a child function and passes the data to it
I find this pattern allows you to make use of your application state while keeping your UI components reusable and composable.
Here's an example in code:
// src/data/myInboxUnreadCount.tsx
export const useMyInboxUnreadCount = () => {
return useSelector(state => state.inbox.unreadCounts.myInbox);
}
export const MyInboxUnreadCountProvider = (props: {
children: (arg: { unreadCount: number }) => JSX.Element;
}) => {
const unreadCount = useMyInboxUnreadCount();
return children({ unreadCount });
};
When to use this ?
This pattern is especially useful when you want to render data dynamically, for example inside a collapsible dropdown.
I recently had to build a sidebar for an messaging inbox which looked something like this...
This component requires three pieces of data:
The "My inbox" unread count
The list of teams
The unread count for each team
So lets create 3 hooks with an associated provider component for each:
// src/data/myInboxUnreadCount.tsx
export const useMyInboxUnreadCount = () => {
return useSelector(state => state.inbox.unreadCounts.myInbox);
}
export const MyInboxUnreadCountProvider = (props: {
children: (arg: { unreadCount: number }) => JSX.Element;
}) => {
const unreadCount = useMyInboxUnreadCount();
return children({ unreadCount });
};
// src/data/teams.tsx
export const useTeams = () => {
return useSelector(state => state.inbox.teams);
}
export const TeamsProvider = (props: {
children: (arg: { teams: Team[] }) => JSX.Element;
}) => {
const teams = useTeams();
return children({ teams });
};
// src/data/teamUnreadCount.tsx
export const useTeamUnreadCount = (teamId: string) => {
return useSelector(state => state.inbox.unreadCounts.teams[teamId]);
}
export const TeamUnreadCountProvider = (props: {
children: (arg: { unreadCount: number }) => JSX.Element;
}) => {
const unreadCount = useTeamUnreadCount();
return children({ unreadCount });
};
We can then compose these all together in the <InboxSidebar />
component:
export const InboxSidebar = () => (
<SidebarContainer>
<MyInboxUnreadCountProvider>
{({ unreadCount }) => (
<SidebarLink name="My inbox" unreadCount={unreadCount} />
)}
</MyInboxUnreadCountProvider>
<Collabsible heading="Teams">
<TeamsProvider>
{({ teams }) => teams.map(team => (
<TeamUnreadCountProvider key={team.id} teamId={team.id}>
{({ unreadCount }) => (
<SidebarLink name={team.name} unreadCount={unreadCount} />
)}
</TeamUnreadCountProvider>
))}
</TeamsProvider>
</Collabsible>
</SidebarContainer>
);
Advantages
๐ Composition - It lets you build the UI using composition. Look how nice and clear our sidebar component render is.
๐โโ๏ธโโก๏ธ Performance - It only calls the data hooks when they're needed. In this case the
useTeams()
anduseTeamUnreadCount()
hooks are only called when you open the teams section.โป๏ธ Reusability - You can reuse these providers anywhere you want to use the same data.
Thanks for reading ๐คฉ Let me know in the comments if you found this useful or if you think it's a terrible idea.