DEV Community

D Keshav L
D Keshav L

Posted on

Refactoring a Prop-Heavy Table Into Compound Components (React + Context)

My Table Had 8 PROPS. NOW IT HAS 1

Here's a component I'm not proud of, from a few weeks ago:

<DataTable
  data={users}
  search={search}
  setSearch={setSearch}
  filter={filter}
  setFilter={setFilter}
  columns={columns}
  page={page}
  pageSize={10}
/>
Enter fullscreen mode Exit fullscreen mode

Eight props. And every parent that rendered this table had to declare search/setSearch and filter/setFilter state itself, just to hand it back down. The table wasn't managing its own state — the parent was doing that work and the table was just consuming the result.

THE PROPS ARE GONE.THE STATE IS SHARED THROUGH CONTEXT.

The refactor

I split it into compound components sharing state through Context:

<DataTable.Root data={users}>
  <DataTable.Search />
  <DataTable.Filter />
</DataTable.Root>
Enter fullscreen mode Exit fullscreen mode

One prop on the outer component. Here's roughly what's underneath:

const TableContext = createContext(null);

function Root({ data, children }) {
  const [search, setSearch] = useState('');
  const [filter, setFilter] = useState('all');

  const filtered = data.filter((row) => {
    const matchesSearch = row.name.toLowerCase().includes(search.toLowerCase());
    const matchesFilter = filter === 'all' || row.status === filter;
    return matchesSearch && matchesFilter;
  });

  return (
    <TableContext.Provider value={{ search, setSearch, filter, setFilter, filtered }}>
      {children}
    </TableContext.Provider>
  );
}

function useTable() {
  const ctx = useContext(TableContext);
  if (!ctx) throw new Error('DataTable.* must be rendered inside DataTable.Root');
  return ctx;
}

Root.Search = function Search() {
  const { search, setSearch } = useTable();
  return <input value={search} onChange={(e) => setSearch(e.target.value)} placeholder="Search…" />;
};
Enter fullscreen mode Exit fullscreen mode

Search never sees a search or setSearch prop. It reaches into context directly. Same for Filter. The parent doesn't need to know any of this exists — it just passes data once and gets a working table.

What actually changed, mechanically

Nothing about React itself changed. This is createContext + useContext wrapped in a hook, plus a naming convention (DataTable.Search as a static property on the Root export) that makes the API read like a mini design system instead of a pile of loose exports.

Trade-offs I ran into

Implicit dependency. DataTable.Search throws if it's rendered outside DataTable.Root. That's the right failure mode — loud and immediate instead of silently broken — but it means the components can't be used standalone anymore. If you need a search input that works with or without the table context, this isn't the right shape for it.

Harder to trace state. With props, you can follow state by reading the component tree top to bottom. With context, you have to know to look for a Provider somewhere above. For a junior dev unfamiliar with the pattern, that's a real onboarding cost, not just a style preference.

Not free performance-wise. Every consumer of TableContext re-renders when any value in the context changes. For a component this small it doesn't matter. For a bigger tree, you'd want to split the context or memoize consumers — something to watch if you scale this pattern up.

When I'd use this vs. plain props

Compound components make sense when multiple related pieces need to share state and you want their JSX arrangement to stay flexible — search, filter, and a table body are a reasonable case. Plain props are still the better default when a component does one job and its shape isn't going to change. Don't add a Root + Context layer just because it looks more "advanced" — that's ceremony without a payoff.

Compound Components vs Props

Try it

Live: https://compound-table.vercel.app/
Source: https://github.com/dkeshavl/compound-table (React 18, Tailwind, Vite)
Support : https://buymeacoffee.com/dkeshavl

What would you have done differently — split the context further, add a reducer instead of multiple useState calls, something else?

Support

Top comments (0)