useReducer keeps state transitions in a reducer and exposes dispatch for requesting them. It is a useful built-in option when state has several related operations: the transition logic lives in one place instead of being spread across component handlers. For application-wide data, context can expose the resulting state and actions without immediately adding a state-management library. section 6.1.5 section 8.9
A reorderable playlist
Model the list as State<T> = T[] and actions as a discriminated union: moveUp, moveDown, moveToTop, moveToBottom, and moveToPosition carry indices rather than song values. A generic reorder<T>(state, action) returns a State<T>; its moveUp case returns the original state for an invalid index and a newly reordered array otherwise. A custom hook calls useReducer(reorder, initial) and exposes the list plus named movement operations. The playlist component uses those operations in button handlers, while the same hook can reorder lists of other element types. section 6.1.5
Listing 6.2 The useReorderable hook
import { useReducer } from "react";
type State<T> = T[];
type Action =
| { type: "moveUp"; index: number }
| { type: "moveDown"; index: number }
| { type: "moveToTop"; index: number }
| { type: "moveToBottom"; index: number }
| { type: "moveToPosition"; index: number; targetIndex: number };
function reorder<T>(state: State<T>, action: Action): State<T> {
const newState: T[] = [...state];
switch (action.type) {
case "moveUp": {
if (action.index <= 0 || action.index >= newState.length) {
return state;
}
// Swap previous element with index
[newState[action.index - 1], newState[action.index]] = [
newState[action.index],
newState[action.index - 1],
];
return newState;
}
case "moveDown": {
if (action.index < 0 || action.index >= newState.length - 1) {
return state;
}
// Swap next element with index
[newState[action.index], newState[action.index + 1]] = [
newState[action.index + 1],
newState[action.index],
];
return newState;
}
case "moveToTop": {
if (action.index <= 0 || action.index >= newState.length) {
return state;
}
// Move index to top
const itemToTop = newState.splice(action.index, 1)[0];
newState.unshift(itemToTop);
return newState;
}
case "moveToBottom": {
if (action.index < 0 || action.index >= newState.length) {
return state;
}
// Move index to bottom
const itemToBottom = newState.splice(action.index, 1)[0];
newState.push(itemToBottom);
return newState;
}
default:
return state;
}
}
export function useReorderable<T>(initial: State<T>) {
const [state, dispatch] = useReducer<
Reducer<State<T>, Action>
>(reorder, initial);
return {
list: state,
moveUp: (index: number) => #1
dispatch({ type: "moveUp", index }), #1
moveDown: (index: number) => #1
dispatch({ type: "moveDown", index }), #1
moveToTop: (index: number) => #1
dispatch({ type: "moveToTop", index }), #1
moveToBottom: (index: number) => #1
dispatch({ type: "moveToBottom", index }), #1
};
}
The full reducer and custom hook show how typed actions become reusable list operations, including index checks and array updates. section 6.1.5
TypeScript generally follows the reducer’s state and action types. For generic reducers, older React typings can lose the element type at useReducer; explicitly supplying Reducer<State<T>, Action> restores it. React 19 fixes this inference issue, so that hint is no longer needed. Keep the version-specific workaround only where the project requires it. section 6.1.5
For larger shared state, pair useReducer with context and consider useContextSelector to limit re-renders. useReduction can reduce reducer boilerplate, while Immer eases immutable updates. RTK, Zustand, and XState are alternatives, not automatic upgrades: choose according to the state and workflow rather than adding a library by default. section 8.4 section 8.9


