Skip to content

Home

Advanced React state hooks

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.

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.

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.

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.

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.

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 ({}).

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.

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.

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.

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.

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 />
);

More like this

Start typing a keyphrase to see matching snippets.