A React hook to handle state with dependencies
Tags: react, javascript, webdev
Published: 2019-02-02 23:54:07

---
title: A React hook to handle state with dependencies
published: true
description: A description of a custom React hook that can simplify dealing with state that must be initialised and potentially invalidated based on one or more dependencies.
tags: #react, #javascript, #webdev
---

In order to experiment with React's new hooks API, I've been building an app called [Fretfull](https://jonrimmer.github.io/fretfull), which lets you explore different ways to play chords on guitar. The app's [source code](https://github.com/jonrimmer/fretfull) uses hooks throughout for context and state.

While building the app, I created something I call a "dependent state" custom hook. It's useful when you have a piece of state with a range of valid values that are calculated based on one or more dependencies. If a dependency changes, the state's current value may no longer be valid, and will need to be checked and possibly reset.

To make that more concrete, consider the following situation:

 1. An app receives a list of product categories from the server.
 2. The app displays the list of categories in the UI.
 3. The user selects a category.
 4. The app receives an updated list of categories from the server.

At this point, the selected category may or may not be valid, depending on whether it still exists in the list of updated categories. Therefore, the app needs to be smart about how it applies the update. If the category no longer exists, keeping it selected will result an inconsistent and invalid application state. However, automatically resetting it will result in a poor user experience if the category *is* still valid. The code will need to check the updated list and reset the selection *only* if the selection is not found.

Let's consider how we might implement this scenario using React hooks.

```tsx
function Categories({ apiData }: { apiData: CategoriesApiResult }) {
  const categories = useMemo(() => {
    return apiData.data.map(cat => cat.name);
  }, [apiData]);

  const [category, setCategory] = useState(categories[0]);

  return <OptionList
    options={categories}
    selected={category}
    onSelect={e => setCategory(e.value)}
  />;
}
```

Here the Categories component creates the list of category options by mapping over the data from an API call received as a prop. We memoize the calculation so it is only executed when the API data changes. We also store the selected category as a piece of state, defaulting it to the first category in the list.

However, this code has a bug: If `categories` changes, the value of `category` may no longer be valid. We need to check that it's still valid, and optionally reset it. We can do this as follows:

```ts
let [category, setCategory] = useState(null);

const categories = useMemo(() => {
  const result = apiData.data.map(cat => cat.name);

  if (!result.includes(category) {
    setCategory(category = result[0]);
  }
}, [apiData]);
```

Now we avoid the bug, but at the expense of muddying up our render logic. We have to make `category` reassignable, define it before `categories`, and include a side effect in our `categories` memoization function that resets `category`.

We can make this approach cleaner and more reusable by implementing a custom hook, that we'll call `useDependentState`:

```ts
function useDependentState<S>(
  factory: (prevState?: S) => S,
  inputs: ReadonlyArray<any>,
): [S, Dispatch<SetStateAction<S>>] {
  let [state, setState] = useState<S>(factory());

  useMemo(() => {
    const newState = factory(state);

    if (newState !== state) {  
      setState(state = newState);
    }
  }, inputs);

  return [state, setState];
}
```

This hook captures the essence of the above logic in a generic form. It defines a piece of state and run a memoized function that runs only when the dependencies change. This memoized function delegates to a factory function that we must provide, and which is responsible for either generating the initial value or changing the current value if it's no longer valid. Let's see how we could use it in the previous example:

```ts
const categories = useMemo(() => {
  return apiData.data.map(cat => cat.name);
}, [apiData]);

const [category, setCategory] = useDependentState(prevState => {
  return (prevState && categories.includes(prevState)) ?
    prevState
    :
    categories[0];
}, [categories]);
```

Our custom hook means we can keep `category` as a const, keep the original definition order, and the only logic we have to implement is the check as to whether the `prevState` value is still valid.

### Conclusion

Hopefully this custom hook may prove useful to anyone facing a similar issue with state whose validity depends on some dependencies.

The only downside I see to this custom hook is that it has to call `setState` to update the state value when it changes, which will result in a second render. But I can't see any way to avoid this. I have [submitted a React feature suggestion](https://github.com/facebook/react/issues/14738) with the idea of enhancing the regular `setState` hook with the ability to provide dependencies that cause it to be re-initialised in a similar manner to this custom hook. If implemented, this would eliminate the need for the additional render, as the `prevState` value would not have "leaked", because the check logic would occur within the `useState` call.