We are now going to implement the toggle component using useReducer
instead of useState
.
We start from the Context
type and initial value, the same as useState
:
import { useReducer } from "react";
type Context = boolean;
const initialContext = false;
export default function UseReducer() {
return (<></>);
}
With useReducer
instead of directly calling setContext
, we define events responsible for changing the context:
import { useReducer } from "react";
type Event = { type: "toggle" };
type Context = boolean;
const initialContext = false;
export default function UseReducer() {
return (<></>);
}
Each event has a
type
property, which is used to identify the event.In this example we have a single
"toggle"
event.
Outside of the component, we then define the reducer
function responsible for updating the context.
reducer
accepts the current context and an event, and returns the new context:
import { useReducer } from "react";
type Event = { type: "toggle" };
type Context = boolean;
const initialContext = false;
const reducer = (context: Context, event: Event): Context => {
// 👇 On "toggle" event, invert the context `boolean`
if (event.type === "toggle") {
return !context;
}
return context;
};
export default function UseReducer() {
return (<></>);
}
We connect the component to the reducer
function by calling useReducer
, passing the reducer
function as the initial context:
import { useReducer } from "react";
type Event = { type: "toggle" };
type Context = boolean;
const initialContext = false;
const reducer = (context: Context, event: Event): Context => {
if (event.type === "toggle") {
return !context;
}
return context;
};
export default function UseReducer() {
// 👇 Connect the component to the reducer function with the initial context
const [context, dispatch] = useReducer(reducer, initialContext);
return (<></>);
}
The UI code contains the same <input>
as before with useState
. Inside onChange
instead of directly calling setContext
we are now triggering the "toggle" event using send
:
import { useReducer } from "react";
type Event = { type: "toggle" };
type Context = boolean;
const initialContext = false;
const reducer = (context: Context, event: Event): Context => {
if (event.type === "toggle") {
return !context;
}
return context;
};
export default function UseReducer() {
const [context, dispatch] = useReducer(reducer, initialContext);
return (
<input
type="checkbox"
checked={context}
onChange={() => dispatch({ type: "toggle" })}
/>
);
}
That's all we need to do to create a toggle component with useReducer
.
useReducer
allows to separate the state logic from the component, which brings the following benefits:
- The logic becomes reusable between components
- We can test the logic independently of the component
- We can understand the logic in isolation without looking at the component
- All the logic is defined in the same place, which makes it easier to refactor, maintain, extend, and debug
The cost of this approach is that the code is now more verbose and understanding the full component requires to read both reducer
and UI code.
The benefits of this refactoring are more apparent when the state logic is more complex.
Bare with me on this. For now, we are focusing more on understanding the differences between
useState
anduseReducer
with the objective of comparing them with XState.