New React Component Pattern? Compound Components w/ a Hook
Tags: react, javascript, webdev, typescript
Published: 2021-02-10 22:55:17

## TL;DR
- React Components == UI and React Hooks == Behavior
- Often, UI is coupled to behavior. That's okay.
  - `isOpen`, and `closeModal` (Behavior), feel pretty coupled to a `Modal` component (UI).
- Sometimes the parent component needs access to that "behavior data".
  - So should the parent own the "behavior data" even though it is coupled to the child component?
  - Ex: The parent creating a `Modal` needs to know if a modal has closed so the parent can cancel an async request. So does the parent have to own the `isOpen` state and recreate the modal boilerplate every usage?
- **The big thesis**: Expanding the [Compound Components](https://kentcdodds.com/blog/compound-components-with-react-hooks) pattern to also return hooks could be an elegant solution.

Here is the final solution if want to jump straight into the code*.

[https://codesandbox.io/s/compount-components-with-a-hook-txolo](https://codesandbox.io/s/compount-components-with-a-hook-txolo?file=/SortingDemo.js)

**I am using a Material UI table here because this stemmed from a work project. However, the concepts should apply with or without a component library.*

> In this article I am building a `Table` component, but in [a previous article](https://dev.to/droopytersen/new-react-hooks-pattern-return-a-component-31bh), I ran into similar problems building a `Modal`. There, I experimented with returning a Component definition from a custom React hook.  Here, I flip it and return the hook on the Component.

## Coupled UI & Behavior 
The fundamental problem is that you have UI and behavior that are tightly coupled. You need the "behavior data" inside the component to render, but you also need access to the "behavior data" outside/above the component.

For example you want a custom `Table` component that can:
- Be used very simply just to encapsulate some brand styling.
- Optionally, be configured to sort items, and display the column headers in a way that indicates which column is being sorted.

![Table Component](https://res.cloudinary.com/droopytersen/image/upload/v1612994906/blog/table.png)

If the `Table` itself were to own the sorting behavior, the `Table` would need to be explicitly given the full set of `items`. But wait, how would you control what the table looks like then?

*If the `Table` component were to own the sorting behavior, you'd have to pass it all your items*
```jsx
<Table items={myData} enableSort >
  {/* What do you map over to display table rows? */}
  {/* It's not 'myData' because that isn't sorted. */}
</Table>
```

You could try something like a `renderRow` prop, or use the "render as children" pattern. 

*Neither option feels right*
```jsx
// OPTION A: renderRow prop - This will to turn into prop sprawl 
// as we identify more render scenarios (or be very un-flexible)
<Table
  items={myData}
  enableSort
  renderRow={(item) => <tr><td>{item.name}</td/>...</tr>}
/>

// OPTION B: Render as children - this syntax just feels gross
<Table items={myData} enableSort>
  {({ sortedItems} ) => (
    {sortedItems.map((item) => (
      <tr>
        <td>{item.name}</td/>
        ...
      </tr>
    )}
  )}
</Table>
```
Besides the fact that it already smells, we'd still have to figure out how to render the Table Header. 
- How would the `Table` know which columns to use? 
- We could expose a `renderHeader` prop and let developers show whatever they want. But then we'd be forcing developers to handle the sorting UI (showing the correct Sort Icon) on their own too.
- That feels like it defeats the purpose of the `Table` component!

We've already hit a wall and we've only discussed sorting. What if we also want to support paging? What about a textbox to filter table rows?
- We don't want to force developers to implement those behaviors themselves.
- But we also can't bake it into the component because we need to give them control over what it looks like. 
- Lastly, we want to provide "happy path" UI defaults to make the component really simple to use.

## Compound Components w/ Hooks
My idea is to take the [Compound Components Pattern](https://kentcdodds.com/blog/compound-components-with-react-hooks) and combine it with custom React Hook composition. 

*Take a look at this usage example, then scroll below to see a breakdown of the notable elements.*
```jsx
import React from "react";
import Table from "./table/table";
import users from "./data";


export default function SortingDemo() {
  // This is the interesting bit, the Component definition has
  // a custom hook attached to it.
  const { showingItems, sorting } = Table.useTable(users, {
    sortKey: "firstName",
    sortDir: "desc"
  });

  // The parent has access to behavior data
  console.log("You are sorting by: ", sorting.sortKey);

  return (
    <Table>
      {/* 
          Here, we take advantage the fact that the hook
          returns the behavior data, 'sorting', in the same
          shape needed for the Table.Header props.
      */}
      <Table.Header {...sorting}>
        <Table.Column id="firstName">First Name</Table.Column>
        <Table.Column id="lastName">Last Name</Table.Column>
        <Table.Column id="department">Department</Table.Column>
        <Table.Column id="jobTitle">Title</Table.Column>
      </Table.Header>

      <Table.Body>
        {/* Show the first 10 sorted items */}
        {showingItems.slice(0, 10).map((item) => (
          <Table.Row key={item.id}>
            <Table.Cell>{item.firstName}</Table.Cell>
            <Table.Cell>{item.lastName}</Table.Cell>
            <Table.Cell>{item.department}</Table.Cell>
            <Table.Cell>{item.jobTitle}</Table.Cell>
          </Table.Row>
        ))}
      </Table.Body>
    </Table>
  );
}
```

Things to note:
1. In addition to compound components like `Table.Column` and `Table.Cell`, **the `Table` component also has a `useTable` hook attached to it**.
2. The `useTable` hook returns a `sorting` object that:
  - Provides the parent component access to the sorting behavior like the current `sortKey`.
  - The `sorting` object is structured to overlap the prop signature of the `Table.Header` component so that it's really easy to use the built-in sorting UI if desired.
    - `<Table.Header {...sorting}>` is all it takes to opt into the sorting UI.

The beauty of this pattern is it doesn't complicate the simple scenarios. We can use the `Table` for UI things without having to worry about any of the hook/behavior code.

*A simple table w/ zero behavior*
```jsx
import React from "react";
import Table from "./table/table";
import users from "./data";

export default function SimpleDemo() {
  return (
    <Table>
      <Table.Header>
        <Table.Column>First Name</Table.Column>
        <Table.Column>Last Name</Table.Column>
        <Table.Column>Department</Table.Column>
        <Table.Column>Title</Table.Column>
      </Table.Header>

      <Table.Body>
        {users.slice(0, 5).map((item) => (
          <Table.Row key={item.id}>
            <Table.Cell width="120px">{item.firstName}</Table.Cell>
            <Table.Cell width="130px">{item.lastName}</Table.Cell>
            <Table.Cell width="170px">{item.department}</Table.Cell>
            <Table.Cell width="250px">{item.jobTitle}</Table.Cell>
          </Table.Row>
        ))}
      </Table.Body>
    </Table>
  );
}
```

This pattern can also scale to add more and more behavior without over complicating the usage.  

*We could add more behavior to our `useTable` hook*
```js
const { showingItems, sorting, paging, filtering, stats } = Table.useTable(
  users,
  {
    sortKey: "firstName",
    sortDir: "desc",
    filterKeys: ["firstName", "lastName", "department", "jobTitle"],
    pageSize: 10
  }
);
```
Because the behavior data comes from a hook we have it readily available to do whatever our application needs from a logic perspective, but we can also easily (and optionally) render it using the coupling between the built-in `Table` compound components and the useTable hook.

```jsx

// Render the built-in paging controls
<Table.Paging {...paging} onChange={paging.goTo} />

// Render the built-in search box
<Table.Search
  value={filtering.filterText}
  onChange={filtering.setFilterText}
/>

// Render custom "stats" 
<div>
  Showing {stats.start} - {stats.end} of {stats.totalItems}
</div>
```
## Isn't tight coupling bad?

You may have read *"The sorting object is structured to overlap the prop signature of the `Table.Header`"* and involuntarily shuddered at the tight coupling. 

However, **because hooks are so easy to compose, we can build the "core behaviors" totally decoupled, then compose them (in the `useTable` hook) in a way that couples them to the (Table) UI**.

If you look at the implementation of `useTable`, you'll see it is mostly the composition of individual, decoupled behavior hooks, `useFilteredItems`, `usePaging`, and `useSorting`.

*useTable.js is really just responsible for pulling in decoupled behavior hooks, and tweaking things to line up perfectly with the `Table` components.*
```js
import { useFilteredItemsByText } from "../hooks/useFilteredItems";
import { usePagedItems } from "../hooks/usePaging";
import { useSortedItems } from "../hooks/useSorting";

export function useTable(
  allItems,
  { filterKeys = [], sortKey, sortDir, pageSize }
) {
  pageSize = pageSize || allItems.length;
  const { filteredItems, ...filtering } = useFilteredItemsByText(
    allItems,
    filterKeys
  );
  const { sortedItems, ...sorting } = useSortedItems(filteredItems, {
    sortKey,
    sortDir
  });

  const [showingItems, paging] = usePagedItems(sortedItems, pageSize);

  const stats = {
    totalItems: allItems.length,
    start: (paging.currentPage - 1) * pageSize + 1,
    end: Math.min(paging.currentPage * pageSize, allItems.length)
  };

  return {
    showingItems,
    filtering,
    sorting,
    paging,
    stats
  };
}
```
In the end there is nothing really earth shattering here. We've already been building hooks like this, and we've already been building components like this. I'm just suggesting (for certain situations) to embrace the coupling and package them up together.

Thanks for making it this far. Let me know what you think in the comments. I haven't really seen anyone doing something like this yet so I am nervous I'm missing a tradeoff.

Here is the final codesandbox
{% codesandbox txolo %}