Chalarangelo/30-seconds-of-code

View on GitHub
content/snippets/react/s/advanced-react-state-hooks.md

Summary

Maintainability
Test Coverage
---
title: Advanced React state hooks
type: story
language: react
tags: [hooks,state,effect,callback]
cover: engine
excerpt: Is `useState()` too limited for you? Perhaps `useReducer()` doesn't quite cut it either? Let's explore some advanced state management hooks.
listed: true
dateModified: 2024-07-03
---

React's toolbox is intentionally quite limited, providing you some very versatile building blocks to create your own abstractions. But, if you find `useState()` too limited for your needs, and `useReducer()` doesn't quite cut it either, how do you go about creating more advanced state management hooks? Let's deep dive into some advanced state management hooks.

## `useToggler` hook

Starting off with a simple one, the `useToggler` hook provides a **boolean state variable** that can be toggled between its two states. Instead of managing the state manually, you can simply call the `toggleValue` function to toggle the state.

The implementation is rather simple, as well. You use the `useState()` hook to create the `value` state variable and its setter. Then, you create a function that toggles the value of the state variable and memoize it, using the `useCallback()` hook. Finally, you return the `value` state variable and the memoized function.

```jsx
const useToggler = initialState => {
  const [value, setValue] = React.useState(initialState);
  const toggleValue = React.useCallback(() => setValue(prev => !prev), []);

  return [value, toggleValue];
};

const Switch = () => {
  const [val, toggleVal] = useToggler(false);
  return <button onClick={toggleVal}>{val ? 'ON' : 'OFF'}</button>;
};

ReactDOM.createRoot(document.getElementById('root')).render(
  <Switch />
);
```

## `useMap` hook

The `Map` object is a very versatile **data structure** in JavaScript, but it's not directly supported by React's state management hooks. The `useMap` hook creates a stateful `Map` object and a set of functions to manipulate it.

Using the `useState()` hook and the `Map()` constructor, you create a new `Map` from the `initialValue`. Then, you use the `useMemo()` hook to create a set of non-mutating actions that manipulate the state variable, using the state setter to create a new `Map` every time. Finally, you return the `map` state variable and the created `actions`.

```jsx
const useMap = initialValue => {
  const [map, setMap] = React.useState(new Map(initialValue));

  const actions = React.useMemo(
    () => ({
      set: (key, value) =>
        setMap(prevMap => {
          const nextMap = new Map(prevMap);
          nextMap.set(key, value);
          return nextMap;
        }),
      remove: (key, value) =>
        setMap(prevMap => {
          const nextMap = new Map(prevMap);
          nextMap.delete(key, value);
          return nextMap;
        }),
      clear: () => setMap(new Map()),
    }),
    [setMap]
  );

  return [map, actions];
};

const MyApp = () => {
  const [map, { set, remove, clear }] = useMap([['apples', 10]]);

  return (
    <div>
      <button onClick={() => set(Date.now(), new Date().toJSON())}>Add</button>
      <button onClick={() => clear()}>Reset</button>
      <button onClick={() => remove('apples')} disabled={!map.has('apples')}>
        Remove apples
      </button>
      <pre>
        {JSON.stringify(
          [...map.entries()].reduce(
            (acc, [key, value]) => ({ ...acc, [key]: value }),
            {}
          ),
          null,
          2
        )}
      </pre>
    </div>
  );
};

ReactDOM.createRoot(document.getElementById('root')).render(
  <MyApp />
);
```

## `useSet` hook

Similar to `useMap`, the `useSet` hook creates a stateful `Set` object and a set of functions to manipulate it. The implementation is very similar to the previous hook, but instead of using a `Map`, you use a `Set`.

```jsx
const useSet = initialValue => {
  const [set, setSet] = React.useState(new Set(initialValue));

  const actions = React.useMemo(
    () => ({
      add: item => setSet(prevSet => new Set([...prevSet, item])),
      remove: item =>
        setSet(prevSet => new Set([...prevSet].filter(i => i !== item))),
      clear: () => setSet(new Set()),
    }),
    [setSet]
  );

  return [set, actions];
};

const MyApp = () => {
  const [set, { add, remove, clear }] = useSet(new Set(['apples']));

  return (
    <div>
      <button onClick={() => add(String(Date.now()))}>Add</button>
      <button onClick={() => clear()}>Reset</button>
      <button onClick={() => remove('apples')} disabled={!set.has('apples')}>
        Remove apples
      </button>
      <pre>{JSON.stringify([...set], null, 2)}</pre>
    </div>
  );
};

ReactDOM.createRoot(document.getElementById('root')).render(
  <MyApp />
);
```

## `useDefault` hook

Similar to the previous hook, we might also need a hook that provides a **default value** if the state is `null` or `undefined`. The `useDefault` hook does exactly that. It creates a stateful value with a default fallback if it's `null` or `undefined`, and a function to update it.

The approach is very similar to the previous hook. You use the `useState()` hook to create the stateful value. Then, you check if the value is either `null` or `undefined`. If it is, you return the `defaultState`, otherwise you return the actual `value` state, alongside the `setValue` function.

```jsx
const useDefault = (defaultState, initialState) => {
  const [value, setValue] = React.useState(initialState);
  const isValueEmpty = value === undefined || value === null;

  return [isValueEmpty ? defaultState : value, setValue];
};

const UserCard = () => {
  const [user, setUser] = useDefault({ name: 'Adam' }, { name: 'John' });

  return (
    <>
      <div>User: {user.name}</div>
      <input onChange={e => setUser({ name: e.target.value })} />
      <button onClick={() => setUser(null)}>Clear</button>
    </>
  );
};

ReactDOM.createRoot(document.getElementById('root')).render(
  <UserCard />
);
```

## `useGetSet` hook

Instead of returning a single state variable and its setter, you might want to return **a getter and a setter** function. This is the job of the `useGetSet` hook. It creates a stateful value, returning a getter and a setter function.

In order to implement this hook, you use the `useRef()` hook to create a ref that holds the stateful value, initializing it with `initialState`. Then, you use the `useReducer()` hook that creates a new object every time it's updated and return its dispatch.

Finally, you use the `useMemo()` hook to memoize a pair of functions. The first one will return the current value of the `state` ref and the second one will update it and force a re-render.

```jsx
const useGetSet = initialState => {
  const state = React.useRef(initialState);
  const [, update] = React.useReducer(() => ({}));

  return React.useMemo(
    () => [
      () => state.current,
      newState => {
        state.current = newState;
        update();
      },
    ],
    []
  );
};

const Counter = () => {
  const [getCount, setCount] = useGetSet(0);
  const onClick = () => {
    setTimeout(() => {
      setCount(getCount() + 1);
    }, 1_000);
  };

  return <button onClick={onClick}>Count: {getCount()}</button>;
};

ReactDOM.createRoot(document.getElementById('root')).render(
  <Counter />
);
```

## `useMergeState` hook

Similar to the `useReducer()` hook, the `useMergeState` hook allows you to update the state by **merging the new state** provided with the existing one. It creates a stateful value and a function to update it by merging the new state provided.

All you need to do to implement it is use the `useState()` hook to create a state variable, initializing it to `initialState`. Then, create a function that will update the state variable by merging the new state provided with the existing one. If the new state is a **function**, call it with the previous state as the argument and use the result.

Omit the argument to initialize the state variable with an empty object (`{}`).

```jsx
const useMergeState = (initialState = {}) => {
  const [value, setValue] = React.useState(initialState);

  const mergeState = newState => {
    if (typeof newState === 'function') newState = newState(value);
    setValue({ ...value, ...newState });
  };

  return [value, mergeState];
};

const MyApp = () => {
  const [data, setData] = useMergeState({ name: 'John', age: 20 });

  return (
    <>
      <input
        value={data.name}
        onChange={e => setData({ name: e.target.value })}
      />
      <button onClick={() => setData(({ age }) => ({ age: age - 1 }))}>
        -
      </button>
      {data.age}
      <button onClick={() => setData(({ age }) => ({ age: age + 1 }))}>
        +
      </button>
    </>
  );
};

ReactDOM.createRoot(document.getElementById('root')).render(
  <MyApp />
);
```

## `usePrevious` hook

The `usePrevious` hook is a very useful hook that stores the **previous state** or props. It's a custom hook that takes a `value` and returns the previous value. It uses the `useRef()` hook to create a ref for the `value` and the `useEffect()` hook to remember the latest `value`.

```jsx
const usePrevious = value => {
  const ref = React.useRef();
  React.useEffect(() => {
    ref.current = value;
  });
  return ref.current;
};

const Counter = () => {
  const [value, setValue] = React.useState(0);
  const lastValue = usePrevious(value);

  return (
    <div>
      <p>
        Current: {value} - Previous: {lastValue}
      </p>
      <button onClick={() => setValue(value + 1)}>Increment</button>
    </div>
  );
};

ReactDOM.createRoot(document.getElementById('root')).render(
  <Counter />
);
```

## `useDebounce` hook

Similar to the `usePrevious` hook, the `useDebounce` hook is a custom hook that **debounces the given value**. It takes a `value` and a `delay` and returns the debounced value. It uses the `useState()` hook to store the debounced value and the `useEffect()` hook to update the debounced value every time the `value` is updated.

Using `setTimeout()`, it creates a timeout that delays invoking the setter of the previous state variable by `delay` milliseconds. Then, it uses `clearTimeout()` to clean up when dismounting the component. This is particularly useful when dealing with user input.

```jsx
const useDebounce = (value, delay) => {
  const [debouncedValue, setDebouncedValue] = React.useState(value);

  React.useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => {
      clearTimeout(handler);
    };
  }, [value]);

  return debouncedValue;
};

const Counter = () => {
  const [value, setValue] = React.useState(0);
  const lastValue = useDebounce(value, 500);

  return (
    <div>
      <p>
        Current: {value} - Debounced: {lastValue}
      </p>
      <button onClick={() => setValue(value + 1)}>Increment</button>
    </div>
  );
};

ReactDOM.createRoot(document.getElementById('root')).render(
  <Counter />
);
```

## `useDelayedState` hook

Instead of creating a stateful value immediately, you might want to **delay its creation** until some condition is met. This is where the `useDelayedState` hook comes in. It creates a stateful value that is only updated if the `condition` is met.

Implementing this hook requires the use of the `useState()` and `useEffect()` hooks. You create a stateful value containing the actual `state` and a boolean, `loaded`. Then, you update the stateful value if the `condition` or `loaded` changes. Finally, you create a function, `updateState`, that only updates the `state` value if `loaded` is truthy.

```jsx
const useDelayedState = (initialState, condition) => {
  const [{ state, loaded }, setState] = React.useState({
    state: null,
    loaded: false,
  });

  React.useEffect(() => {
    if (!loaded && condition) setState({ state: initialState, loaded: true });
  }, [condition, loaded]);

  const updateState = newState => {
    if (!loaded) return;
    setState({ state: newState, loaded });
  };

  return [state, updateState];
};

const App = () => {
  const [branches, setBranches] = React.useState([]);
  const [selectedBranch, setSelectedBranch] = useDelayedState(
    branches[0],
    branches.length
  );

  React.useEffect(() => {
    const handle = setTimeout(() => {
      setBranches(['master', 'staging', 'test', 'dev']);
    }, 2000);
    return () => {
      handle && clearTimeout(handle);
    };
  }, []);

  return (
    <div>
      <p>Selected branch: {selectedBranch}</p>
      <select onChange={e => setSelectedBranch(e.target.value)}>
        {branches.map(branch => (
          <option key={branch} value={branch}>
            {branch}
          </option>
        ))}
      </select>
    </div>
  );
};

ReactDOM.createRoot(document.getElementById('root')).render(
  <App />
);
```

## `useForm` hook

Last but not least, the `useForm` hook can be used to create a stateful value from the **fields in a form**. It uses the `useState()` hook to create a state variable for the values in the form and a function that will be called with an appropriate event by a form field to update the state variable accordingly.

```jsx
const useForm = initialValues => {
  const [values, setValues] = React.useState(initialValues);

  return [
    values,
    e => {
      setValues({
        ...values,
        [e.target.name]: e.target.value
      });
    }
  ];
};

const Form = () => {
  const initialState = { email: '', password: '' };
  const [values, setValues] = useForm(initialState);

  const handleSubmit = e => {
    e.preventDefault();
    console.log(values);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input type="email" name="email" onChange={setValues} />
      <input type="password" name="password" onChange={setValues} />
      <button type="submit">Submit</button>
    </form>
  );
};

ReactDOM.createRoot(document.getElementById('root')).render(
  <Form />
);
```