From e50f8987fddd9de2e9c71d78d43c0678e989c70e Mon Sep 17 00:00:00 2001 From: Louie Colgan Date: Sat, 1 Feb 2025 15:21:08 +0000 Subject: [PATCH] feat: add state reconciler option --- README.md | 3 +- src/__tests__/rehydrate.test.ts | 50 +++++++++++++++++++++++++++++++++ src/index.ts | 4 ++- src/init.ts | 5 ++-- src/rehydrate.ts | 24 ++++++++-------- src/types.ts | 2 ++ 6 files changed, 73 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index bdd5643..b1f0686 100644 --- a/README.md +++ b/README.md @@ -304,7 +304,7 @@ API reference 1. **driver** *(required)* - storage driver instance, that implements the `setItem(key, value)` and `getItem(key)` functions; 2. **rememberedKeys** *(required)* - an array of persistable keys - if an empty array is provided nothing will get persisted; 3. **options** *(optional)* - plain object of extra options: - - **prefix**: storage key prefix *(default: `'@@remember-'`)*; + - **prefix** - storage key prefix *(default: `'@@remember-'`)*; - **serialize** - a plain function that takes unserialized store state and its key (`serialize(state, stateKey)`) and returns serialized state to be persisted *(default: `JSON.stringify`)*; - **unserialize** - a plain function that takes serialized persisted state and its key (`serialize(state, stateKey)`) and returns unserialized to be set in the store *(default: `JSON.parse`)*; - **persistThrottle** - how much time should the persistence be throttled in milliseconds *(default: `100`)* @@ -312,4 +312,5 @@ API reference - **persistWholeStore** - a boolean which specifies if the whole store should be persisted at once. Generally only use this if you're using your own storage driver which has gigabytes of storage limits. Don't use this when using window.localStorage, window.sessionStorage or AsyncStorage as their limits are quite small. When using this option, key won't be passed to `serialize` nor `unserialize` functions - *(default: `false`)*; - **errorHandler** - an error handler hook function which is gets a first argument of type `PersistError` or `RehydrateError` - these include a full error stack trace pointing to the source of the error. If this option isn't specified the default behaviour is to log the error using console.warn() - *(default: `console.warn`)*; - **initActionType** (optional) - a string which allows you to postpone the initialization of `Redux Remember` until an action with this type is dispatched to the store. This is used in special cases whenever you want to do something before state gets rehydrated and persisted automatically (e.g. preload your state from SSR). **NOTE: With this option enabled Redux Remember will be completely disabled until `dispatch({ type: YOUR_INIT_ACTION_TYPE_STRING })` is called**; + - **stateReconciler** (optional) - a plain function that takes the current state and the driver loaded state (`stateReconciler(currentState, loadedState)`) and returns a new state. This can be used to deeply merge the two states before it's dispatched through the `REMEMBER_REHYDRATED` action. - Returns - an enhancer to be used with Redux diff --git a/src/__tests__/rehydrate.test.ts b/src/__tests__/rehydrate.test.ts index bd8e872..e94b5bb 100644 --- a/src/__tests__/rehydrate.test.ts +++ b/src/__tests__/rehydrate.test.ts @@ -339,5 +339,55 @@ describe('rehydrate.ts', () => { } }); }); + + it('merges with existing state using the stateReconciler()', async () => { + mockState = { + 1: { + 1: 'prev-state-sub-1', + 2: 'prev-state-sub-2' + }, + 2: 'prev-state-2' + }; + + await exec({ + stateReconciler: (c, l) => { + const state = { ...c }; + + Object.keys(l).forEach((key) => { + if (typeof state[key] === 'object' && typeof l[key] === 'object') { + Object.keys(l[key]).forEach((innerKey) => { + state[key][innerKey] = l[key][innerKey]; + }); + } else { + state[key] = l[key]; + } + }); + + return state; + }, + driver: { + setItem: () => { throw new Error('not implemented'); }, + getItem: jest.fn() + .mockReturnValueOnce('number-3') + .mockReturnValueOnce('prev-state-2') + .mockReturnValueOnce({ + 1: 'loaded-state-sub-1' + }) + }, + unserialize: (o: any) => o, + }); + + expect(mockStore.dispatch).toHaveBeenNthCalledWith(1, { + type: REMEMBER_REHYDRATED, + payload: { + 1: { + 1: 'loaded-state-sub-1', + 2: 'prev-state-sub-2' + }, + 2: 'prev-state-2', + 3: 'number-3' + } + }); + }); }); }); diff --git a/src/index.ts b/src/index.ts index ab57d41..3b81352 100644 --- a/src/index.ts +++ b/src/index.ts @@ -67,6 +67,7 @@ const rememberEnhancer = ( prefix = '@@remember-', serialize = (data, key) => JSON.stringify(data), unserialize = (data, key) => JSON.parse(data), + stateReconciler, persistThrottle = 100, persistDebounce, persistWholeStore = false, @@ -91,7 +92,8 @@ const rememberEnhancer = ( persistThrottle, persistDebounce, persistWholeStore, - errorHandler + errorHandler, + stateReconciler, } ); diff --git a/src/init.ts b/src/init.ts index 2575e80..0957f79 100644 --- a/src/init.ts +++ b/src/init.ts @@ -17,13 +17,14 @@ const init = async ( persistThrottle, persistDebounce, persistWholeStore, - errorHandler + errorHandler, + stateReconciler, }: ExtendedOptions ) => { await rehydrate( store, rememberedKeys, - { prefix, driver, unserialize, persistWholeStore, errorHandler } + { prefix, driver, unserialize, persistWholeStore, errorHandler, stateReconciler } ); let oldState = {}; diff --git a/src/rehydrate.ts b/src/rehydrate.ts index 5f96b17..e1d92e4 100644 --- a/src/rehydrate.ts +++ b/src/rehydrate.ts @@ -6,7 +6,7 @@ import { RehydrateError } from './errors'; type RehydrateOptions = Pick< ExtendedOptions, - 'driver' | 'prefix' | 'unserialize' | 'persistWholeStore' | 'errorHandler' + 'driver' | 'prefix' | 'unserialize' | 'persistWholeStore' | 'errorHandler' | 'stateReconciler' > type LoadAllOptions = Pick< @@ -64,7 +64,8 @@ export const rehydrate = async ( driver, persistWholeStore, unserialize, - errorHandler + errorHandler, + stateReconciler, }: RehydrateOptions ) => { let state = store.getState(); @@ -74,15 +75,16 @@ export const rehydrate = async ( ? loadAll : loadAllKeyed; - state = { - ...state, - ...await load({ - rememberedKeys, - driver, - prefix, - unserialize - }) - }; + const loadedState = await load({ + rememberedKeys, + driver, + prefix, + unserialize + }); + + state = typeof stateReconciler === 'function' + ? stateReconciler(state, loadedState) + : ({ ...state, ...loadedState }); } catch (err) { errorHandler(new RehydrateError(err)); } diff --git a/src/types.ts b/src/types.ts index a3278cd..a6c8f5d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -2,6 +2,7 @@ import { PersistError, RehydrateError } from './errors'; export type SerializeFunction = (data: any, key: string) => any; export type UnserializeFunction = (data: any, key: string) => any; +export type StateReconcilerFunction = (currentState: any, loadedState: any) => any; export type Driver = { getItem: (key: string) => any; @@ -12,6 +13,7 @@ export type Options = { prefix: string, serialize: SerializeFunction, unserialize: UnserializeFunction, + stateReconciler?: StateReconcilerFunction, persistThrottle: number, persistDebounce?: number, persistWholeStore: boolean,