If you are a Medium member, please read the article here
Obviously, this topic is not new. When using Angular with NgRx, the need for some state persistence will arise sooner or later. Usually sooner 😅. We want our application to have the same state if the user leaves temporarily, or refreshes the site. The most convenient method to go about it is syncing state changes to localStorage, and syncing the saved state back from localStorage as initialState, a.k.a. rehydrating💧 the state. Of course, there are npm libraries achieving similar things, but implementing it is also as easy as one function. Still, if you prefer having util functions tested for you here’s the npm package of the code below.
The function
export function createRehydrateReducer<S, A extends Action = Action>(
key: string,
initialState: S,
...ons: ReducerTypes<S, ActionCreator[]>[]
): ActionReducer<S, A> {
const item = localStorage.getItem(key);
const newInitialState =
(item && JSON.parse(item)) ?? initialState;
const newOns: ReducerTypes<S, ActionCreator[]>[] = [];
ons.forEach((oldOn: ReducerTypes<S, ActionCreator[]>) => {
const newReducer: ActionReducer<S, A> = (
state: S | undefined,
action: ActionType<ActionCreator[][number]>
) => {
const newState = oldOn.reducer(state, action);
localStorage.setItem(key, JSON.stringify(newState));
return newState;
};
newOns.push({ ...oldOn, reducer: newReducer });
});
return createReducer(newInitialState, ...newOns);
}

This might seem a tad weird at first, but this function simply wraps the NgRx createReducer to intercept the initial state and all On reducers. Some confusion might come from NgRx’s naming, but On objects also contain a reducer property that holds our callback (“listener”) function from on(…). Aside from that, it is quite straightforward: for every On object we create a new reducer that first saves the new state to localStorage and then returns the new state as would the original reducer. The real trick is in the first step:
const newInitialState = (localStorage.getItem(key) && JSON.parse(item)) ?? initialState;
While other techniques rely on meta reducers and the first dispatched action, since this function itself is the reducer creator, it is called on app initialization. Hence the usage is as simple as calling createRehydrateReducer instead of createReducer 🎉
// instead of this
// export const someReducer = createReducer(
// initialState,
// on(
// SomeAction.someSuccess,
// (state: SomeState) => {
// return {
// ...state,
// busy: false
// };
// }
// )
// );
export const someReducer = createRehydrateReducer(
SOME_STORAGE_KEY,
initialState,
on(
SomeAction.someSuccess,
(state: SomeState) => {
return {
...state,
busy: false
};
}
)
);
In my opinion, there is no lighter, easier solution. I definitely use this for my applications. This can be further sophisticated by options like partial persistence, or some invalidation logic that clears the values after some time, but for the basic purpose of having our states always at the ready, one simple function is plenty enough.
Komentar