Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add state reconciler option #19

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -304,12 +304,13 @@ 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`)*
- **persistDebounce** *(optional)* - how much time should the persistence be debounced by in milliseconds. If provided, persistence will not be throttled, and the `persistThrottle` option will be ignored. The debounce is a simple trailing-edge-only debounce.
- **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
50 changes: 50 additions & 0 deletions src/__tests__/rehydrate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}
});
});
});
});
4 changes: 3 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ const rememberEnhancer = <Ext extends {} = {}, StateExt extends {} = {}>(
prefix = '@@remember-',
serialize = (data, key) => JSON.stringify(data),
unserialize = (data, key) => JSON.parse(data),
stateReconciler,
persistThrottle = 100,
persistDebounce,
persistWholeStore = false,
Expand All @@ -91,7 +92,8 @@ const rememberEnhancer = <Ext extends {} = {}, StateExt extends {} = {}>(
persistThrottle,
persistDebounce,
persistWholeStore,
errorHandler
errorHandler,
stateReconciler,
}
);

Expand Down
5 changes: 3 additions & 2 deletions src/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {};
Expand Down
24 changes: 13 additions & 11 deletions src/rehydrate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<
Expand Down Expand Up @@ -64,7 +64,8 @@ export const rehydrate = async (
driver,
persistWholeStore,
unserialize,
errorHandler
errorHandler,
stateReconciler,
}: RehydrateOptions
) => {
let state = store.getState();
Expand All @@ -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));
}
Expand Down
2 changes: 2 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -12,6 +13,7 @@ export type Options = {
prefix: string,
serialize: SerializeFunction,
unserialize: UnserializeFunction,
stateReconciler?: StateReconcilerFunction,
persistThrottle: number,
persistDebounce?: number,
persistWholeStore: boolean,
Expand Down