useReducer reducer functions without the switch

useReducer is typically used with a Redux-style reducer function. For example, here's a useCounter hook written with useReducer:

function reducer(state, action) {
    switch (action.type) {
        case 'increment':
            return { count: state.count + 1 };
        case 'decrement':
            return { count: state.count - 1 };
        case 'reset':
            return action.payload;
        default:
            throw new Error('Invalid action type');
    }
}

function useCounter(initialState = 0) {
    const [state, dispatch] = React.useReducer(reducer, initialState);
    // ...
}

The action parameter contains all the data needed to compute the next state. By convention, the next state is computed with a switch statement.

This convention is useful for managing complex state logic, but you're not forced to use it. useReducer is just another way of managing state; the reducer function simply accepts the current state (state), accepts some new state (action), and returns the next state.

I've found that useReducer is handy when you have brief or condense logic for computing the next state that depends on the previous state. To demonstrate this, here's a simple example of a useIncrement hook written with useState:

function useIncrement() {
    const [count, setCount] = React.useState(0);

    const increment = () => setCount(count + 1);

    return [count, increment];
}

Instead of defining a separate increment function, you could use useReducer and increment the count in the reducer function:

function useIncrement() {
    const [count, increment] = React.useReducer((prev) => prev + 1, 0);

    return [count, increment];
}

Below are a few examples of more interesting hooks that use useReducer without a Redux-style reducer function.

Example 1: useToggle

This hook is useful for controlling toggle inputs or other UI elements with a boolean state. It simply returns a boolean isEnabled value and a toggle function to toggle the value.

interface UseToggleProps {
    initialState?: boolean;
}

function useToggle({ initialState = false }: UseToggleProps = {}) {
    const [isEnabled, toggle] = React.useReducer((x) => !x, initialState);

    return [isEnabled, toggle];
}

Here's an example:

Enabled: No

Example 2: useSelection

This hook lets you select an item and deselect it by selecting it while it's selected.

interface UseSelectionProps<T> {
    isEqual?: (prev: T | null, next: T) => boolean;
    initialState?: T | null;
}

function useSelection<T>({
    isEqual = (prev, next) => prev === next,
    initialState = null,
}: UseSelectionProps<T> = {}) {
    const [selection, setSelection] = React.useReducer(
        (prev: T | null, next: T) => (isEqual(prev, next) ? null : next),
        initialState
    );

    return [selection, setSelection];
}

Here's an example:

Select your favorite song:

Example 3: useMultiSelection

This hook extends useSelection to allow selecting multiple items. The reducer function is not the most readable and you'd probably want to do this with useState or break out the reducer function, but I've included it here anyway.

interface UseMultiSelectionProps<T> {
    isEqual?: (prev: T, next: T) => boolean;
    initialState?: T[];
}

function useMultiSelection<T>({
    isEqual = (prev, next) => prev === next,
    initialState = [],
}: UseMultiSelectionProps<T> = {}) {
    const [selection, setSelection] = React.useReducer((prev: T[], next: T) => {
        const index = prev.findIndex((x) => isEqual(x, next));
        return index === -1
            ? [...prev, next]
            : prev.filter((_, i) => i !== index);
    }, initialState);

    return [selection, setSelection];
}

Here's an example:

Select your favorite songs:

Bonus - Example 4: useForceUpdate

This hook comes from the official React docs. Calling the forceUpdate dispatch function forces a re-render.

function useForceUpdate() {
    const [ignored, forceUpdate] = React.useReducer((x) => x + 1, 0);

    return [ignored, forceUpdate];
}

Let's connect

Come connect with me on LinkedIn, Twitter, and GitHub!

If you found this post helpful, please consider supporting my work financially:

☕️Buy me a coffee!

Subscribe for the latest news

Sign up for my mailing list to get the latest blog posts and content from me. Unsubscribe anytime.