When, inside your application, you already use Redux state management for centralized access to the data and for some slices of the data, there is no need to store it in the common store. For example, the case with the form, when it is handled in one determined component.
It will be unuseful to store the data in a global state and change the data only from one piece of application that is already logically combined, I mean the form component and its children. In this scenario, useReducer
hook will look pretty in its place.
The useReducer
hook is a way to keep and manage data in the store for the particular component and its children. So, in the basic scenario when there are no needs in the redux, I will only use the hook itself without heavy libraries just for the small feature.
const [type, dispatch] = useReducer(reducer, initializer(editedType));
For each action, it will require writing the action type and the payload. It grabs quite a lot of lines of code in the file just for the action annotation and their invocation.
interface ChangeNameAction {
type: 'CHANGE_NAME';
name: string;
}
To keep all reducer staff in one place, I will create another hook and return back only functions that will dispatch required actions in it to change the state. And hook will return the State and functions to change the state. Then the function to change the name:
const setName = (name: string) => {
dispatch({ type: 'CHANGE_NAME', name });
};
The reducer function then will have a standard switch-case implementation; I wrapped it with produce
function from the Immer lib. It’s to avoid mistakes while coping in full state. But yeah, in the very original reducer where the object of the state is without multiple nested objects, I will only use the spread operator.
const reducer: Reducer<TypeDraft, TypeEditorAction> = produce(
(draft: TypeDraft, action: CustomTypeEditorAction) => {
switch (action.type) {
case 'CHANGE_NAME':
draft.name = action.name;
break;
...
default:
}
}
);
And the new hook will be like this:
export function useTypeEditor(editedType?: EditedType) {
const [type, dispatch] = useReducer(reducer, initializer(editedType));
const setName = (name: string) => {
dispatch({ type: 'CHANGE_NAME', name });
};
return {
type,
setName
};
}
Since the reducer is just a pure function, instead of creating a reducer manually and wrapping it in the Immer produce function, it’s much more convenient to use RTK createReducer or createSlice function for the useReducer
hook.
const reducer = createReducer(initialState, builder =>
builder
.addCase(changeNameAction, (state, { payload }) => {
state.name = payload;
})
);
The actions also create from createAction
function instead of describing it through an interface.
const name = 'typeForm';
const changeNameAction = createAction<string>(`${name}/changeName`);
It will be the same reducer as from the result of produce
function or just pure reducer function but in a more decent way. I don’t require to keep in mind how to properly handle the action. And at the end, for the component, it will be another hook you can simply use.
const TypeEditorForm: React.FC = () => {
const editor = useTypeEditor(type);
...
return (<>...</>)
}
By the way, type
and initializer
is here to properly set the initial state, e.g., if the type was already created, it needs to init the state from it.
Photo by Steve Johnson on Unsplash