When using useReducer
we want all the state to be stored in a single object.
Therefore, we need to extend Context
with the loading
state:
import { useReducer } from "react";
import { initialContext, type Context } from "./shared";
type ReducerContext = Context & {
loading: boolean;
};
const reducer = // TODO
export default function UseReducer() {
const [context, dispatch] = useReducer(reducer, {
...initialContext,
loading: false,
});
return (<></>);
}
An alternative would be to store the
loading
state in a separateuseState
hook. However, in this example the requirement is using onlyuseReducer
.
Updating username
requires to add a new event. The reducer
function will update the state based on the event:
import { useReducer } from "react";
import { initialContext, type Context } from "./shared";
type Event = { type: "update-username"; value: string };
type ReducerContext = Context & {
loading: boolean;
};
export const reducer = (
context: ReducerContext,
event: Event
): ReducerContext => {
if (event.type === "update-username") {
return { ...context, username: event.value };
}
return context;
};
export default function UseReducer() {
const [context, dispatch] = useReducer(reducer, {
...initialContext,
loading: false,
});
return (
<form>
<input
type="text"
value={context.username}
onChange={(e) =>
dispatch({ type: "update-username", value: e.target.value })
}
/>
<button disabled={context.loading}>
Confirm
</button>
</form>
);
}
This is the code required to handle the form state and context using useReducer
.
Async request with useReducer
useReducer
allows implementing synchronous logic outside the component.
However, the reducer
function cannot work with async requests (Promise
). Therefore, we are required to implement the async request inside the component, same as we did with useState
:
export default function UseReducer() {
const [context, dispatch] = useReducer(reducer, {
...initialContext,
loading: false,
});
// 👇 Async logic implemented inside the component
const onSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
// TODO
};
return (
<form onSubmit={onSubmit}>
<input
type="text"
value={context.username}
onChange={(e) =>
dispatch({ type: "update-username", value: e.target.value })
}
/>
<button disabled={context.loading}>
Confirm
</button>
</form>
);
}
We also need to add a new event to update the loading
state:
import { useReducer } from "react";
import { initialContext, postRequest, type Context } from "./shared";
type Event =
| { type: "update-username"; value: string }
| { type: "update-loading"; value: boolean };
type ReducerContext = Context & {
loading: boolean;
};
const reducer = (context: ReducerContext, event: Event): ReducerContext => {
if (event.type === "update-username") {
return { ...context, username: event.value };
} else if (event.type === "update-loading") {
return { ...context, loading: event.value };
}
return context;
};
export default function UseReducer() {
const [context, dispatch] = useReducer(reducer, {
...initialContext,
loading: false,
});
const onSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
// TODO
};
return (
<form onSubmit={onSubmit}>
<input
type="text"
value={context.username}
onChange={(e) =>
dispatch({ type: "update-username", value: e.target.value })
}
/>
<button disabled={context.loading}>
Confirm
</button>
</form>
);
}
With this we can implement the async request the same as useState
:
import { useReducer } from "react";
import { initialContext, postRequest, type Context } from "./shared";
type Event =
| { type: "update-username"; value: string }
| { type: "update-loading"; value: boolean };
type ReducerContext = Context & {
loading: boolean;
};
const reducer = (context: ReducerContext, event: Event): ReducerContext => {
if (event.type === "update-username") {
return { ...context, username: event.value };
} else if (event.type === "update-loading") {
return { ...context, loading: event.value };
}
return context;
};
export default function UseReducer() {
const [context, dispatch] = useReducer(reducer, {
...initialContext,
loading: false,
});
const onSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (!context.loading) {
dispatch({ type: "update-loading", value: true });
await postRequest(context);
dispatch({ type: "update-loading", value: false });
}
};
return (
<form onSubmit={onSubmit}>
<input
type="text"
value={context.username}
onChange={(e) =>
dispatch({ type: "update-username", value: e.target.value })
}
/>
<button disabled={context.loading}>
Confirm
</button>
</form>
);
}
That's all. Whereas useReducer
allows separating sync logic from the component, for async logic we are required to fallback to writing functions inside the component.
This breaks most of the practical benefits of useReducer
.
The logic is now implemented in multiple places inside and outside the component. This makes it harder to understand the code, since we need to jump to multiple places (or even multiple files) to understand what's going on.
This causes useReducer
to be more complex than useState
without adding many benefits.