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.

ยท

3 min read

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

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:

  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.

ย