Jonathan Davies

Derive State, Don’t Sync It – An Example

Kent C Dodds (I believe), coined the term, ‘Derive State, Don’t Sync It’.

I recently refactored, a sortable table on my RSS Reader app, and wanted to talk through one of the key ideas that informed the refactor: state should be derived, no synced. At a high level if follows well established patterns. Use the back-end for sorting and ordering, not the front-end.

A Screenshot of the table

Here’s a breakdown of the key components required with the more naive approach:

  • Some sorted state on the table to track which column has been sorted, and in which direction
  • A function to update the table sorting (this is passed down to a SortableButton component)
  • A useEffect to call the backend based on the sorted params changing.
// Table State <Table />

function Table() {
  const [sorted, setSorted] = useState({
    column: "",
    order: "",
  });

  useEffect(() => {
    Inertia.get(
      `/feeds`,
      {
        column: sorted.column,
        order: sorted.order,
      },
      {
        only: ["feeds"],
        preserveState: true,
      }
    );
  }, [sorted]);

  const sortResults = (column, order) => {
    setSorted({ column, order });
  };

  return <table>...</table>;
}
// SortableButton in Table

function SortableButton({ activeSort, onSort, order, column, label }) {
  <button
    type="button"
    onClick={() => onSort(column, order === "desc" ? "asc" : "desc")}
  >
    {label}>
    {active && (
      <ChevronUpIcon
        className={`${
          order === "desc" ? "rotate-180" : ""
        } h-4 w-4 transform transition`}
      />
    )}
    )}
  </button>;
}

<SortableButton
  activeSort={sorted.column}
  onSort={sortResults}
  order={sorted.order}
  column="name"
  label="Feed"
/>;

The main thing to think about with this implementation is that we effectively have the table’s sorting state in two places. The server (I’d call that the global state) and the table itself. When we update the table state, we’re firing off a useEffect hook to make sure the global state is in sync.

What we want to do is derive the state of the table from the global state. And we can do that using the URL:

/feeds?&column=name&order=asc

This means we can reduce a tonne of complexity, removing both the local state and the side-effects.

Here’s a refactored SortableButton component:

function SortableButton({ column, children }) {
  const { url } = usePage();
  const urlParams = new URLSearchParams(url);

  const active = urlParams.get("column") === column;
  const order = urlParams.get("order");

  const newOrder = order === "asc" ? "desc" : "asc";

  return (
    <Link
      class="button"
      href={`/manage/feeds?&column=${column}&order=${newOrder}`}
    >
      {children}
      {active && (
        <ChevronUpIcon
          className={`${
            order === "desc" ? "rotate-180" : ""
          } h-4 w-4 transform transition`}
        />
      )}
    </Link>
  );
}

We can access the current URL using InertiaJS’s usePage hook. From this we can derive if the column is active and what the order of that column is if so.

Then instead of having a button, calling a method to sort the table, we can ditch all the special React stuff and use a good old-fashioned Link . (InertiaJS also gives us preserveState and preserveScroll options to make the experience seamless)

We’ve also gone from passing 5 props into this component, to two.

A Lesson from Production

Beyond the code clarity and complexity savings there are further benefits to deriving local state from the URL. Specifically: shareability.

I once worked on building a complicated dashboard with lots of filters and options. We’d save all the settings into a table in the database, so that they could be retrieved later.

Not long after shipping that feature, we got asked a question by a user: “I want to share this dashboard with a colleague”. But because all the state data was handled in local state, we had to build a specific sharing feature to allow users to grant and revoke access, manage editing controls etc. A lot of extra work.

If we’d just saved the options as params in the url, it would be been a copy and paste away to make a shareable dashboard. A good lesson that it’s worth sticking as close as possible to the browser APIs when you can.

Further Reading