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.