diff --git a/docs/docs/store.mdx b/docs/docs/store.mdx index 58026be8..fd1d7281 100644 --- a/docs/docs/store.mdx +++ b/docs/docs/store.mdx @@ -37,13 +37,13 @@ When you want to update the store, you can call the `update()` method passing th ```ts title="session.service.ts" import { SessionStore } from './session.store'; - + export class SessionService { constructor(private sessionStore: SessionStore) {} updateUserName(newName: string) { this.sessionStore.update({ name: newName }); - } + } } ``` @@ -51,7 +51,7 @@ The second `update()` option gives you more control. It receives a `callback` fu ```ts title="session.service.ts" import { SessionStore } from './session.store'; - + export class SessionService { constructor(private sessionStore: SessionStore) {} @@ -59,18 +59,53 @@ export class SessionService { this.sessionStore.update(state => ({ name: newName })); - } + } } ``` +### `overwrite()` + +When you don't just want to update the current store state but replace the state completely, you can call the `overwrite()` method passing the new `state`: + +```ts title="session.service.ts" +import { SessionStore } from './session.store'; + +export class SessionService { + constructor(private sessionStore: SessionStore) {} + + replaceState(newName: string, newToken: string) { + this.sessionStore.overwrite({ name: newName, token: newToken }); + } +} +``` + +As with `update()` the `overwrite()` method also has a second option which gives you more control. It receives a `callback` function, which gets the current state, and returns a new **immutable** state, which will be the new value of the store. For example: + +```ts title="session.service.ts" +import { SessionStore } from './session.store'; + +export class SessionService { + constructor(private sessionStore: SessionStore) {} + + replaceState(newName: string, newToken: string) { + this.sessionStore.overwrite(state => ({ + name: newName, + token: newToken + })); + } +} +``` + +You should use `overwrite()` over `update()` when you want to completely replace the current state at the top level. + ### `setLoading()` Set the `loading` state: ```ts title="session.service.ts" import { SessionStore } from './session.store'; - + export class SessionService { - constructor(private sessionStore: SessionStore, + constructor(private sessionStore: SessionStore, private http: HttpClient) {} async updateUserName(newName: string) { @@ -78,7 +113,7 @@ export class SessionService { await this.http(...).toPromise(); this.sessionStore.update({ name: newName}); this.sessionStore.setLoading(false); - } + } } ``` @@ -87,9 +122,9 @@ Set the `error` state: ```ts title="session.service.ts" import { SessionStore } from './session.store'; - + export class SessionService { - constructor(private sessionStore: SessionStore, + constructor(private sessionStore: SessionStore, private http: HttpClient) {} async updateUserName(newName: string) { @@ -98,7 +133,7 @@ export class SessionService { } catch(error) { this.sessionStore.setError(error); } - } + } } ``` diff --git a/libs/akita/src/__tests__/overwrite.spec.ts b/libs/akita/src/__tests__/overwrite.spec.ts new file mode 100644 index 00000000..488f2e64 --- /dev/null +++ b/libs/akita/src/__tests__/overwrite.spec.ts @@ -0,0 +1,54 @@ +import { Store, StoreConfig } from '@datorama/akita'; + +type ExampleState = ExampleStateA | ExampleStateB; + +interface ExampleStateA { + _tag: 'a'; + uniqueToA: string; +} + +interface ExampleStateB { + _tag: 'b'; + uniqueToB: string; +} + +const initialState: ExampleState = { + _tag: 'a', + uniqueToA: 'This value is unique to a', +}; + +@StoreConfig({ + name: 'example-store', + resettable: true, +}) +class ExampleStore extends Store { + constructor() { + super(initialState); + } +} + +const exampleStore = new ExampleStore(); + +describe('Store Overwrite', () => { + beforeEach(() => { + exampleStore.reset(); + }); + + it('should overwrite the store value replacing the previous state using a state object', () => { + exampleStore.overwrite({ _tag: 'b', uniqueToB: 'This value is unique to b' }); + expect(exampleStore._value()).toBeTruthy(); + expect(exampleStore._value()).toEqual({_tag: 'b', uniqueToB: 'This value is unique to b'}); + }); + + it('should overwrite the store value replacing the previous state using a callback function', () => { + exampleStore.overwrite((_) => ({ _tag: 'b', uniqueToB: 'This value is unique to b' })); + expect(exampleStore._value()).toBeTruthy(); + expect(exampleStore._value()).toEqual({_tag: 'b', uniqueToB: 'This value is unique to b'}); + }); + + it('should update the store value but only replace specified properties', () => { + exampleStore.update({ _tag: 'b', uniqueToB: 'This value is unique to b' }); + expect(exampleStore._value()).toBeTruthy(); + expect(exampleStore._value()).toEqual({_tag: 'b', uniqueToB: 'This value is unique to b', uniqueToA: 'This value is unique to a'}); + }); +}); diff --git a/libs/akita/src/lib/store.ts b/libs/akita/src/lib/store.ts index 2a1aff69..489e377b 100644 --- a/libs/akita/src/lib/store.ts +++ b/libs/akita/src/lib/store.ts @@ -226,7 +226,8 @@ export class Store { /** * - * Update the store's value + * Update the store's value, only replacing the specified properties + * * * @example * @@ -244,18 +245,43 @@ export class Store { update(state: Partial); update(stateOrCallback: Partial | UpdateStateCallback) { isDev() && setAction('Update'); + const hookFn = (curr: Readonly, newS: Readonly) => this.akitaPreUpdate(curr, { ...curr, ...newS } as S); + this._setState(this.prepareNewState(stateOrCallback, this._value(), hookFn)); + } + + /** + * + * Overwrite the store's value, replacing the previous value. + * + * @example + * + * this.store.overwrite(state => { + * return {...} + * }) + */ + overwrite(stateCallback: UpdateStateCallback); + /** + * + * @example + * + * this.store.overwrite({ token: token }) + */ + overwrite(state: S); + overwrite(stateOrCallback: S | UpdateStateCallback): void { + isDev() && setAction('Overwrite'); + const hookFn = (curr: Readonly, newS: Readonly) => this.akitaPreOverwrite(curr, newS as S); + this._setState(this.prepareNewState(stateOrCallback, this._value(), hookFn)); + } + private prepareNewState(stateOrCallback: Partial | UpdateStateCallback, currentState: S, hookFn: (curr: Readonly, newS: Readonly) => S): S { let newState; - const currentState = this._value(); if (isFunction(stateOrCallback)) { newState = isFunction(this._producerFn) ? this._producerFn(currentState, stateOrCallback) : stateOrCallback(currentState); } else { newState = stateOrCallback; } - - const withHook = this.akitaPreUpdate(currentState, { ...currentState, ...newState } as S); - const resolved = isPlainObject(currentState) ? withHook : new (currentState as any).constructor(withHook); - this._setState(resolved); + const withHook = hookFn(currentState, newState); + return isPlainObject(currentState) ? withHook : new (currentState as any).constructor(withHook); } updateStoreConfig(newOptions: UpdatableStoreConfigOptions) { @@ -267,6 +293,11 @@ export class Store { return nextState; } + // @internal + akitaPreOverwrite(_: Readonly, nextState: Readonly): S { + return nextState; + } + ngOnDestroy() { this.destroy(); }