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(store): add set method to akita store #663

Open
wants to merge 3 commits 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
55 changes: 45 additions & 10 deletions docs/docs/store.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -37,48 +37,83 @@ 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 });
}
}
}
```

The second `update()` option 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) {}

updateUserName(newName: string) {
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';
NetanelBasal marked this conversation as resolved.
Show resolved Hide resolved

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) {
this.sessionStore.setLoading(true);
await this.http(...).toPromise();
this.sessionStore.update({ name: newName});
this.sessionStore.setLoading(false);
}
}
}
```

Expand All @@ -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) {
Expand All @@ -98,7 +133,7 @@ export class SessionService {
} catch(error) {
this.sessionStore.setError(error);
}
}
}
}
```

Expand Down
54 changes: 54 additions & 0 deletions libs/akita/src/__tests__/overwrite.spec.ts
Original file line number Diff line number Diff line change
@@ -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<ExampleState> {
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'});
});
});
43 changes: 37 additions & 6 deletions libs/akita/src/lib/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,8 @@ export class Store<S = any> {

/**
*
* Update the store's value
* Update the store's value, only replacing the specified properties
*
*
* @example
*
Expand All @@ -244,18 +245,43 @@ export class Store<S = any> {
update(state: Partial<S>);
update(stateOrCallback: Partial<S> | UpdateStateCallback<S>) {
isDev() && setAction('Update');
const hookFn = (curr: Readonly<S>, newS: Readonly<S>) => 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<S>);
/**
*
* @example
*
* this.store.overwrite({ token: token })
*/
overwrite(state: S);
overwrite(stateOrCallback: S | UpdateStateCallback<S>): void {
isDev() && setAction('Overwrite');
const hookFn = (curr: Readonly<S>, newS: Readonly<S>) => this.akitaPreOverwrite(curr, newS as S);
this._setState(this.prepareNewState(stateOrCallback, this._value(), hookFn));
}

private prepareNewState<S>(stateOrCallback: Partial<S> | UpdateStateCallback<S>, currentState: S, hookFn: (curr: Readonly<S>, newS: Readonly<S>) => 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) {
Expand All @@ -267,6 +293,11 @@ export class Store<S = any> {
return nextState;
}

// @internal
akitaPreOverwrite(_: Readonly<S>, nextState: Readonly<S>): S {
return nextState;
}

ngOnDestroy() {
this.destroy();
}
Expand Down