Skip to content

Musing about useBoolean

I saw a useBoolean utility hook the other day which looked something along the lines of:

const useBoolean = (defaultValue = false) => {
  const [value, setValue] = useState(defaultValue);

  const toggle = useCallback(() => {
    setValue((value) => !value);
  }, []);

  const on = useCallback(() => {
    setValue(true);
  }, []);

  const off = useCallback(() => {
    setValue(false);
  }, []);

  return [
    value,
    {
      toggle,
      on,
      off,
      setValue,
    },
  ];
};

It got me thinking about how simple terse I could make the implementation, so I decided to write things out to see what it might look like.

Since we want to represent a single boolean value with three different actions (setting to true, setting to false, and toggling), I immediately thought about using a reducer to represent the relationship.

const [value, dispatch] = useReducer(...TBD... , defaultValue);

For constructing the reducer, if the state is a boolean value, then it also makes sense that the action be a boolean. That way we can dispatch a boolean for either of the "set to true" and "set to false" scenarios:

function booleanReducer(state: boolean, action: boolean) {
  return action;
}

The third state can then be undefined which we can get if we make action optional, which turns our reducer into:

function booleanReducer(state: boolean, action?: boolean) {
  return action ?? !state;
}

The reducer alone isn't too bad for a one-off component:

const [value, setValue] = useReducer(
  (state: boolean, action?: boolean) => action ?? !state,
  false
);

return (
  <button type="button" onClick={() => setValue()}>
    Toggle
  </button>
);

It'd be nice to have the ergonomics of functions that can be safely passed into even handlers, like on, off, and toggle in the original code.

To add that feature it's important to note that dispatch is a stable function reference, so we could attach a utility function to it to have another stable function reference by using ??= to only set the function the first time the hook is called.

This means we can pair down useBoolean to:

const useBoolean = (defaultValue: boolean = false) => {
  const [value, setValue] = useReducer(
    (data: boolean, action?: boolean) => action ?? !data,
    defaultValue
  ) as [
    boolean,
    { (value?: boolean): void; toggle(): void; on(): void; off(): void }
  ];

  setValue.toggle ??= () => setValue();
  setValue.on ??= () => setValue(true);
  setValue.off ??= () => setValue(false);

  return [value, setValue] as const;
};

But is it really better that the original?

Not particularly.

The explicit types muddy things up, but even so, the reducer isn't a big enough improvement over a useState version:

const useBoolean = (defaultValue: boolean = false) => {
  const [value, setValue] = useState(defaultValue) as [
    boolean,
    Dispatch<SetStateAction<boolean>> & {
      toggle(): void;
      on(): void;
      off(): void;
    }
  ];

  setValue.toggle ??= () => setValue((value) => !value);
  setValue.on ??= () => setValue(true);
  setValue.off ??= () => setValue(false);

  return [value, setValue] as const;
};