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(persist-state): 🔥 Add callback to change state before save #495

Merged
merged 1 commit into from
Oct 26, 2023
Merged
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
34 changes: 33 additions & 1 deletion docs/docs/features/persist-state.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ As the second parameter you should pass a `Options` object, which can be used to

- `storage`: an Object with `setItem`, `getItem` and `removeItem` method for storing the state (required).
- `source`: a method that receives the store and return what to save from it (by default - the entire store).
- `preStoreInit`: a method that run upon initializing the store with a saved value, used for any required modifications before the value is set.
- `preStoreInit`: a method that runs upon initializing the store with a saved value, used for any required modifications before the value is set.
- `preStorageUpdate`: a method that runs before saving the store, used for any required modifications before the value is set.
- `key`: the name under which the store state is saved (by default - the store name plus a `@store` suffix).
- `runGuard` - returns whether the actual implementation should be run. The default is `() => typeof window !== 'undefined'`

Expand Down Expand Up @@ -113,3 +114,34 @@ persistState(todoStore, {
source: () => todoStore.pipe(debounceTime(1000)),
});
```

## preStorageUpdate

The `preStorageUpdate` option is a function that is called before the state is saved to the storage. It receives two parameters: `storeName` and `state`. The `storeName` is a string representing the name of the store, and `state` is the current state of the store.

This function is useful when you want to modify the state before it is saved to the storage. For example, you might want to remove some sensitive data or transform the state in some way.

The function should return the modified state which will be saved to the storage. If you don't return anything from this function, the original state will be saved.

Here is an example of how to use `preStorageUpdate`:

```ts
import { persistState, localStorageStrategy } from '@ngneat/elf-persist-state';

const preStorageUpdate = (storeName, state) => {
const newState = { ...state };
if (storeName === 'todos') {
delete newState.sensitiveData;
}
return newState;
}


persistState(todoStore, {
key: 'todos',
storage: localStorageStrategy,
preStorageUpdate,
});
```

In this example, the `preStorageUpdate` function removes the `sensitiveData` property from the state before it is saved to storage.
1 change: 1 addition & 0 deletions packages/mocks/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export interface Todo {
id: number;
title: string;
completed: boolean;
sensitiveData?: string;
}

const { state, config } = createState(withEntities<Todo>());
Expand Down
32 changes: 32 additions & 0 deletions packages/persist-state/src/lib/persist-state.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,4 +127,36 @@ describe('persist state', () => {
new TypeError(`Cannot read properties of undefined (reading '_storage')`)
);
});

it('should call preStorageUpdate and remove sensitive data before saving to storage', () => {
const storage: StateStorage = {
getItem: jest.fn().mockImplementation(() => of(null)),
setItem: jest.fn().mockImplementation(() => of(true)),
removeItem: jest.fn().mockImplementation(() => of(true)),
};

const preStorageUpdate = jest
.fn()
.mockImplementation((storeName, state) => {
const newState = { ...state };
if (storeName === 'todos') {
delete newState.sensitiveData;
}
return newState;
});

const store = createEntitiesStore();
persistState(store, { storage, preStorageUpdate });
expect(preStorageUpdate).not.toHaveBeenCalled();

const todo = createTodo(1);
todo.sensitiveData = 'secret';
store.update(addEntities(todo));
expect(preStorageUpdate).toHaveBeenCalledTimes(1);
expect(preStorageUpdate).toHaveBeenCalledWith('todos', store.getValue());

const savedState = store.getValue();
expect(savedState).not.toHaveProperty('sensitiveData');
expect(storage.setItem).toHaveBeenCalledWith('todos@store', savedState);
});
});
11 changes: 10 additions & 1 deletion packages/persist-state/src/lib/persist-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ interface Options<S extends Store> {
storage: StateStorage;
source?: (store: S) => Observable<Partial<StoreValue<S>>>;
preStoreInit?: (value: StoreValue<S>) => Partial<StoreValue<S>>;
preStorageUpdate?: (
storeName: string,
state: Partial<StoreValue<S>>
) => Partial<StoreValue<S>>;
key?: string;
runGuard?(): boolean;
}
Expand Down Expand Up @@ -54,7 +58,12 @@ export function persistState<S extends Store>(store: S, options: Options<S>) {
const saveToStorageSubscription = merged.source!(store)
.pipe(
skip(1),
switchMap((value) => storage.setItem(merged.key!, value))
switchMap((value) => {
const updatedValue = merged.preStorageUpdate
? merged.preStorageUpdate(store.name, value)
: value;
return storage.setItem(merged.key!, updatedValue);
})
)
.subscribe();

Expand Down
Loading