React Reducer Patterns

One-Way Flag

const [isEnabled, enable] = useReducer(() => true, false);

isEnabled; // false
enable();
isEnabled; // true
enable();
isEnabled; // true

Source

Toggle Flag

const [on, toggle] = useReducer(flag => !flag, false);

on; // false
toggle();
on; // true
toggle();
on; // false
toggle();
on; // true

Source

type Menu = null | "file" | "edit" | "view"; // null means closed

const [openMenu, tap] = useReducer(
  (current: Menu, action: Menu) => {
    if (action === current) {
      return null; // Close if matches
    }

    return action; // Use passed value
  },
  null
);

tap("file");
openMenu; // "file"
tap(null);
openMenu; // null
tap("file");
openMenu; // "file"
tap("file");
openMenu; // null
tap("file");
openMenu; // "file"
tap("edit");
openMenu; // "edit"
tap("view");
openMenu; // "view"
tap("edit");
openMenu; // "edit"
tap("edit");
openMenu; // null
tap(null);
openMenu; // null

You can of course condense it to a ternary if you want:

// You can of course shorten it into a ternary if you really want:
const [openMenu, tap] = useReducer(
  (current: Menu, action: Menu) => (action === current) ? null : action,
  null
);

Jitter Generator

Inspired by AWS: Exponential Backoff And Jitter

const baseMs = 5;
const capMs = 2000;
const [{attempt, delayMs}, dispatch] = useReducer((state, random) => {
  return {
    attempt: state.attempt + 1,
    delayMs: random * Math.min(capMs, baseMs * Math.pow(2, attempt))
  };
}, { attempt: 0, delay: baseMs });
const nextAttempt = useCallback(() => dispatch(Math.random()), [dispatch]);

Logical Clock

const [t, tick] = useReducer(n => n + 1, 0);

t; // 0
tick();
t; // 1
tick();
t; // 2
tick();
t; // 3

Lamport Timestamp

const [{ t, toSend }, dispatch] = useReducer((state, command) => {
  if (command.type === "send") {
    const t = state.t + 1;
    return {
      t,
      toSend: {
        t,
        message: command.message
      }
    };
  } else if (command.type === "receive") {
    const t = Math.max(state.t, command.t) + 1;
    return {
      t,
      toSend: null
    };
  } else {
    return {
      t: state.t,
      toSend: null
    };
  }
}, { t: 0, toSend: null });
useEffect(() => {
  if (toSend !== null) {
    send(toSend.message, toSend.t); // Second argument can be used for idempotency
  }
}, [toSend]);

t; // 0
toSend; // null
dispatch({ type: "send", message: "hello" });
t; // 1
toSend; // { t: 1, message: "hello" }
dispatch({ type: "receive", t: 3, message: "howdy" });
t; // 4
toSend; // null