React patterns: Combining hooks with child functions for composable data components

Dan BahramiDan Bahrami
3 min read

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:

  1. A React hook which gets some data or application state

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

A wireframe mockup of a messaging inbox sidebar. The sidebar has a "My inbox" button and displays 4 unread messages. Underneath is a collapsed dropdown with the heading "Teams". On the right you can see what happens when you open the teams dropdown - a list of teams displays, each with a team name and its own unread count.

This component requires three pieces of data:

  1. The "My inbox" unread count

  2. The list of teams

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

  1. 💅 Composition - It lets you build the UI using composition. Look how nice and clear our sidebar component render is.

  2. 🏃‍♂️‍➡️ Performance - It only calls the data hooks when they're needed. In this case the useTeams() and useTeamUnreadCount() hooks are only called when you open the teams section.

  3. ♻️ 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.

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