React patterns: Combining hooks with child functions for composable data components
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.
Subscribe to my newsletter
Read articles from Dan Bahrami directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Dan Bahrami
Dan Bahrami
I've been a frontend dev for 10+ years. I'm sharing things I learn here before the AI's destroy us all. Join me to learn about TypeScript, React, tooling, design patterns and all things frontend.