useReducer: mix context and state

We adopt the same strategy as useState also with useReducer, by adding explicit states to the context:

type State = "Editing" | "Loading" | "Error" | "Complete";

type ReducerContext = Context & {
  state: State;
};

We then need to add some new events to transition between the states:

type Event =
  | { type: "update-username"; value: string }
  | { type: "request-start" }
  | { type: "request-complete" }
  | { type: "request-fail" };

We add a new context update inside the reducer function for each event:

const reducer = (context: ReducerContext, event: Event): ReducerContext => {
  if (event.type === "update-username") {
    return { ...context, username: event.value };
  } else if (event.type === "request-start") {
    return { ...context, state: "Loading" };
  } else if (event.type === "request-complete") {
    return { ...context, state: "Complete" };
  } else if (event.type === "request-fail") {
    return { ...context, state: "Error" };
  }
  return context;
};

The rest of the logic looks the same as useState. The difference is that useReducer doesn't update the state directly, but instead sends events to reducer:

export default function UseReducer() {
  const [context, dispatch] = useReducer(reducer, {
    ...initialContext,
    state: "Editing",
  });

  const onSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    if (context.state !== "Loading" && context.state !== "Complete") {
      dispatch({ type: "request-start" });
      try {
        await postRequest(context);
        dispatch({ type: "request-complete" });
      } catch (_) {
        dispatch({ type: "request-fail" });
      }
    }
  };

  /// ...

The UI is the same as before, but the state is extracted from context:

export default function UseReducer() {
  const [context, dispatch] = useReducer(reducer, {
    ...initialContext,
    state: "Editing",
  });

  const onSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    if (context.state !== "Loading" && context.state !== "Complete") {
      dispatch({ type: "request-start" });
      try {
        await postRequest(context);
        dispatch({ type: "request-complete" });
      } catch (_) {
        dispatch({ type: "request-fail" });
      }
    }
  };

  if (context.state === "Complete") {
    return <p>Done</p>;
  }

  return (
    <form onSubmit={onSubmit}>
      <input
        type="text"
        value={context.username}
        onChange={(e) =>
          dispatch({ type: "update-username", value: e.target.value })
        }
      />
      <button type="submit" disabled={context.state === "Loading"}>
        Confirm
      </button>
      {context.state === "Error" && <p>Error occurred</p>}
    </form>
  );
}

Again, also with useReducer the only thing we did is making the states explicit. We also needed a new event for each state transition.