Article summary
Managing stateful content is a challenging aspect of web development. This content usually takes the form of modals, form workflows, or anything a user can click through in multiple orders. The smoothest way to implement these workflows is with React’s useReducer hook. Reducers track a state and allow specific actions to mutate that state.
First Step
The first step is defining the valid states for the page content as a type. React’s useReducer takes two parameters: the state and the action for the reducer. Having the types defined elsewhere will give us better type completion when writing the hook. For this example, I will use a series of modals related to deleting a message.
type ContentState =
| { state: "SHOW_CONFIRMATION"; content: React.ReactNode }
| { state: "CONTENT_HIDDEN"; content: React.ReactNode | null }
| { state: "SHOW_ERROR"; content: React.ReactNode };
Next
The next piece defines actions the webpage will call to mutate the state outlined previously. The actions will modify the state and then React will know to redraw the page with the new content. One modification is changing what content is being displayed along with the fact that content is visible.
type ContentAction =
| {
type: "SHOW_CONFIRMATION";
concentrationFactorId: string;
recipeName: string;
}
| { type: "HIDE_CONTENT" }
| {
type: "SHOW_ERROR";
error: string;
};
Now that the state and the actions are defined, the reducer needs to be implemented. The useReducer hook takes two parameters. The first is a function that will be used whenever an action is called and the second is the initial state.
const useDynamicContentReducer = () => {
const initialState: ContentState = {
content: null,
state: "CONTENT_HIDDEN",
};
const [shownContent, dispatchModalAction] = useReducer(
(_state: ContentState, action: ContentAction): ContentState => {
const type = action.type;
switch (type) {
case "HIDE_CONTENT":
return {
state: "CONTENT_HIDDEN",
content: null,
};
case "SHOW_CONFIRMATION":
return {
state: "SHOW_CONFIRMATION",
content: (
),};
case "SHOW_ERROR":
return {
state: "SHOW_ERROR",
content: (
Delete Failed
),
};
}
assertUnreachable(type);
},
initialState,
);
return [shownContent, dispatchModalAction] as const;
};
The above reducer acts like a hub for the state we defined. The first argument to the useReducer is a function. The function will be called every time the action is called by the component using this hook. Based on the structure of the action, the switch statement will mutate the state in a specific way and return the new state. That new state will be used by the next action and by the component.
Finally
To use the new hook, call it in any component like the example below. The actions are being called from buttons on the page so the actions are called by user interactions.
const CustomComponent = () => {
const [shownContent, dispatchModalAction] = useDynamicContentReducer();
return (
{shownContent.content}
);
};
The best part about this solution to stateful content is its scalability. One reducer managing page state is less prone to state conflicts than multiple different useState or useEffect hooks. I highly encourage swapping to reducers when states flow into another or require additional validation before changing from one state to another.