owned this note
owned this note
Published
Linked with GitHub
# ComponentStore for @ngrx Design Doc
authors: Alex Okrushko (this doc + further implementation), Kevin Elko (original implementation)
created: 2020-04-09
status: [draft | in review | approved | **implemented** | obsolete]
doc url: https://okrushko.dev/component-store-dd
feedback url: https://github.com/ngrx/platform/issues/2489
design doc template version: 1.0.0
basic DEMO: https://stackblitz.com/edit/ngrx-component-store-simple-demo?file=src%2Fcomponent_store.ts
DEMO with observables: https://stackblitz.com/edit/ngrx-component-store-simple-demo-with-observable?file=src%2Fcomponent_store.ts
## Objective
To have an alternative to a "*Service with BehaviorSubject*" for state management solution that:
* Has the data/state tied to a Component life-cycle (for setup and tear down),
* Allows complex components to be easily shareable across pages,
* Allows to have multiple instances of components that require state management,
* Is reactive (push-based).
<!--
A rough overall explanation of the idea as well as related terms and a quick scatch of the need.
-->
### Approvals
NgRx Team - Tim, Brandon, Wes and Mike
### Expected User/Developer Experience
<!--
Describe what will the user be able to do and how will they be able to do it thanks to this new feature. This should be done in a story format, described from the user’s/developer’s perspective, with as many details as possible but without going into the implementation details.
-->
#### "Module state" example
Jennifer is part of a larger team that is working on a Web App that consists of large fairly-independent pieces (aka "***modules***") of the app. While the app shares some global data (e.g. user permissions), a large chunk of data is self-contained within a ***module*** that she is developing.
Jennifer understands that the data within her ***module*** is considered to be "*local state*" and does not belong to *`ngrx/store`* so she reaches for **`ComponentStore`** to have a reactive Service that would erase the data when the User navigates to the other part of the Web App.
#### "Multiple instances of a complex component" example
Jennifer is working on the app that would have multiple `BookShelfComponent` components visible at the same time. `BookShelfComponent` is a complex component that has *child* components, like individual `BookComponent`s.
Each `BookShelfComponent` is independent from another, and doesn't share any state with its siblings, yet each one needs to do independent requests and destroy any data it had when the component is removed.
`ComponentStore` is the solution that Jennifer is reaching for.
#### "Large shareable component" example
Jennifer is working closely with Dan, who is building an independent Web App (separate from Jennifer's). Both teams that Jennifer and Dan are part of, are sharing the same design system.
Dan saw that the complex component that he needs was already built by Jennifer. Since this component was self-sufficient and didn't need any data from the global store, it was using `ComponentStore` for its local state management.
Dan easily imports this component into his component tree.
One of the services used in `ComponentStore` was calling another HTTP endpoints. Dan overrides that service in his `providers`, but other than that Jennifer's component was "plug-and-play".
#### Developer Experience
From the DX perspective, using `ComponentStore` is just like using typical Services within Angular.
```typescript=
@Component({
selector: "book-shelf",
templateUrl: "book_shelf.component.html",
// provides 👇 Component-level Service
providers: [BooksStore]
})
export class BookShelfComponent {
readonly authors$: Observable<Author[]> =
// reactive way to 👇 read all authors
this.booksStore.getAuthors();
readonly authorsCount$: Observable<number> =
// reactive way to 👇 read transformed/derived data
this.booksStore.getAuthorsCount();
// Injects the specialized ComponentStore
// that knows how to work with book data 👇
constructor(private readonly booksStore: BooksStore) {}
addAuthor(author: Author) {
// adds Author to the local state that is kept
// within BooksStore 👇 and doesn't call APIs.
this.booksStore.addAuthor(author);
}
saveAuthor(author: Author) {
// saves Author to the backend. This is a way
// to interact with services that wrap API calls
// and persist or read data 👇
this.booksStore.saveAuthor(author);
}
}
```
### Background
<!--
Stuff one needs to know to understand this doc:
motivating examples, previous versions, and problems, links to related projects/design docs, etc. You should mention related work if applicable. This is background; do not write about ideas to solve problems here.
-->
Local State within Angular components can mean different things to different people.`ComponentStore` builds off the idea that keeping too much state AND business logic within a Component makes it hard to work with (use/refactor/read) and test such a Component.
Following [Single Responsibility Principles (SRP)](https://angular.io/guide/styleguide#single-responsibility) developers try to separate that logic into Services. Further improvement is to make such Services to be **push-based**, which typically means these stateful Services keep state in some type of `Subject` from RxJS (typically `BehaviorSubject`).
Such [solutions work well for local state](https://dev.to/avatsaev/simple-state-management-in-angular-with-only-services-and-rxjs-41p8), however it's hard to create it, and it's quite error prone. These solutions also frequently overlook race conditions: e.g. what if `addTodo(newTodo)` clicked multiple times and it persists this change to the backend?
The idea of having a reactive local state in some sort of library is not new and a number of libraries were created for it, some also try to solve "ngrx/store" replacement as a Global State solution at the same time.
In fact, [Mike Ryan](https://twitter.com/MikeRyanDev) added ["Local State API"](https://github.com/ngrx/platform/issues/858) proposal way back in early 2018.
The most recent example that I saw is by [Michael Hladky's `ngx-rx-state`](https://github.com/BioPhoton/ngx-rx/tree/master/libs/ngx-rx-state) aka `StateService` aka `RxState`.
### Goals/NoGoals
Goals
* Can be used without ngrx/store
* helps component to clear its local state
* is reactive/push-based solution
* is type-safe
* no "magic strings" that assume presence of any properties
* keeps state immutable
* makes it performant
* keeps testing simple
NoGoals:
* Does not try to keep the data in a single storage, as it's meant for local state.
* is not meant to be replacement for Global Store (ngrx/store)
* is not meant to be tied to app-level component (which would effectively be Global Store)
* does not need to be attached to Redux dev tools
## Prototype
The main idea revolves around the terms that were already popularized with Redux/NgRx ecosystems, and so maybe be familiar to the potential user of **`ComponentStore`**.
These are:
* ***reducer/updater*** - takes current state and applies new changes, returning immutable new state of the same shape.
* ***effect*** - side-effects, that typically trigger async events, such as network calls.
* ***selector*** - selects, transforms and composes data from `ComponentStore`'s state into something that `Component` can easily consume.
### APIs
> **Note: The exact API naming is still in discussion, while the functionality is settling in.**
>
❗️APIs for `ComponentStore` allow to **read** the state through `Observable` and to **update** the state either **reactively** (through `Observable`) or **imperatively** (by passing a single value).
API for reading the state imperatively is not added on purpose - to promote reactive approach.
`ComponentStore<T>`:
* **`setState(state => ({...state, value: newValue}))`**
Updates local state within Component (`newValue` is available).
This method is just a shortcut for `updater(state => ({...state, value: newValue})()`
* **`updater`** (alternative names could be `reducer` or `updater`) - updates local state imperatively or reactively through Observable.
State is destroyed/cleaned when `ComponentStore` is destoyed (or a Component that is attached to it).
* **`updater((state, newValue) => ({...state, value: newValue}))(newValue)`**
Update states with a **single value** `newValue` imperatively - frequently is a result of a callback from template (e.g. on button click). Typically `updater` would be in a Sevice together with business logic, and `(newValue)` is called in a Component. e.g.:
```typescript
// in books_store.ts
addValue = this.updater(
(state, newValue) => ({...state, value: newValue}));
// in book-shelf.component.ts
this.booksStore.addValue(newValue);
```
* **`updater((state, newValue) => ({...state, value: newValue}))(newValueObservable$)`**
update state using values from `an Observable`. Typically `updater` would be in would be in a Sevice together with business logic, and `(newValueObservable$)` is called in a Component. Each new Observable that is passed into the function would be updating the state.
```typescript
// in books_store.ts
setId = this.updater(
(state, newId) => ({...state, id: newId}));
// in book-shelf.component.ts
id$ = this.router.paramMap.pipe(
map(params => params.get('productId')));
this.booksStore.setId(id$);
```
* **`selector`** (alertnative names `selector` or `reader`) - selects/gets local state.
Unsubscribes when `ComponentStore` is destoyed (or a Component that is attached to it).
* **`selector(state => state.value)`**
Returns an `() => Observable` that emits new value whenever `state.value` changes.
* **`selector(selector1, selector2, (value1, value2) => value1 + value2)`**
Returns an `() => Observable` that emits new value, that is a result of the function execution (e.g. `value1 + value2` in this example), whenever `selector1` or `selector2` Observables emit a value.
* **`selector(selector1, selector2, observable$ (value1, value2, observableValue) => value1 + value2 + observableValue)`**
Returns an `() => Observable` that emits new value, that is a result of the function execution (e.g. `value1 + value2 + observableValue` in this example), whenever `selector1` or `selector2` Observables or `observable$` emit a value. Demonstrates that selectors can combine values not only from other selectors, but from another Observable sources as well (including Global Store, for example).
* **`effect`** (alertnative names `effect` or `asyncUpdater`) - orchestrates a side-effect, that can handle async operations, such as network calls. Unsubscribes side-effects when `ComponentStore` is destoyed (or a Component that is attached to it) OR when a returned `Subscription` is explicitely unsubscribed.
* **`effect(() => observable$)()`**
Creates a side-effect that would be unsubscribed/destroyed **only** when `ComponentStore` is destroyed.
```typescript
this.effect(
() => this.router.paramMap.pipe(
map(params => params.get('productId')),
tap(id => save(id))
)();
```
> While such API exists, I would recommend the exact equivalent with just adding `takeUntil(this.destroy$)` instead. It's provided by `ComponentStore`.
> ```typescipt
> this.router.paramMap.pipe(
> map(params => params.get('productId')),
> takeUntil(this.destroy),
> ).subscribe(id => save(id));
>```
* **`effect(value$ => value$.pipe(...))(newValue);`**
Creates a side-effect that receives new values imperatively by passing `newValue`. Each `newValue` is passed into `values$` observable.
Unsubscribes when `ComponentStore` is destroyed.
```typescript
// in books_store.ts
getBook = effect(
id$ => id$.pipe(
concatMap(id => this.api.getBook(id).pipe(
tap(book => this.addBook(book)),
catchError(e => this.addError(e)),
)),
));
// in book-shelf.component.ts
this.booksStore.setId(id);
```
* **`effect(value$ => value$.pipe(...))(observable$);`**
Creates a side-effect that receives new values emitted by `observable$`. Each new value is passed into `values$` observable. Every new Observable that is passed into the function would be updating the state.
Unsubscribes when `ComponentStore` is destroyed. User is also able to stop a particular `observable$` emissions when returned Subscription is unsubscribed.
```typescript
// in books_store.ts
getBook = effect(
id$ => id$.pipe(
concatMap(id => this.api.getBook(id).pipe(
tap(book => this.addBook(book)),
catchError(e => this.addError(e)),
)),
));
// in book-shelf.component.ts
id$ = this.router.paramMap.pipe(
map(params => params.get('productId')));
this.booksStore.setId(id$);
// or
const subscription = this.booksStore.setId(id$);
// user may want to terminate sooner
// than BooksStore is destroyed by calling
subscription.unsubscribe();
```
### Minimal Design
<!--
Include only the essential parts of the code. No error handling, no performance consideration.
This section should help to understand the essential implementation in one small piece of code
-->
**`ComponentStore`** fits in a single file and is within a readable number of Lines of Code.
While **`ComponentStore`** be used directly by a **Component**, it is not really meant to be. It includes the generic methods that deal with state/orchestration/race conditions/cleanup and **Business Logic** should live a separate Service.
```typescript=
// Sets the type of the local state 👇
export abstract class ComponentStore<T> implements OnDestroy {
// Takes initial state via constructor
constructor(initialState: T) {
this.state$ = new BehaviorSubject(initialState);
}
// Function that applies state changes. Any state changes
// are done through updater ONLY.
updater<V>(
reducerFn: (state: T, reducerArg: V) => T): (a: V) => void {
...
}
// Factory function that allows to provide data
// for side-effects
effect<V>(
generator: (origin$: Observable<V>) => Observable<unknown>
): (arg: V) => void {
...
}
// Function for selecting data for components
selector<R>(mapFn: (s: T) => R): () => Observable<R> {
...
}
}
```
The **Business Logic** would be in the Services that extend `ComponentStore`, for example `BookStore`, and it would use these methods to control flow/state of the data.
**This is what the Developer might implement:**
```typescript=
export interface BooksState {
books: Book[];
authors: {[name: string]: Author};
}
@Injectable()
export class BooksStore extends ComponentStore<BooksState> {
// HTTP Service for reading/persisting changes to
// the backend 👇
constructor(private readonly api: BooksHttpService) {
// 👇 initial state
super({books: [], authors: {}});
}
// Adds/replaces author in the local state only.
// This is a PUBLIC reducer
readonly addAuthor = this.updater(
(state: BooksState, author: Author) => {
return {
...state,
authors: {
...state.authors,
[author.name]: { ...author, saveState: SaveState.UNSAVED }
}
};
}
);
// These are PRIVATE reducers, and are meant to be
// used ONLY within the BooksStore. See `saveAuthor`.
private readonly savingAuthor = this.updater(savingAuthor);
private readonly saveAuthorSuccess = this.updater(saveAuthorSuccess);
// Effect example
readonly saveAuthor = this.effect<Author>(
// every time `saveAuthor` is called, its argument
// is emitted into author$ Observable.
author$ => author$.pipe(
// calls private reducer to set state to SAVING
tap(author => this.savingAuthor(author)),
// allows to choose flattening strategy and
// calls API.
concatMap(author => this.api.saveAuthor(author)),
// calls private reducer to set state to SAVED
tap((author: Author) => this.saveAuthorSuccess(author)),
)
);
// Example selectors
readonly getAuthors = this.selector(state => {
return Object.values(state.authors);
});
readonly getAuthorsCount = this.selector(
this.getAuthors,
authors => authors.length
);
}
```
### Detailed Design
<!--
Include some important cases. Error handling, and consider how to deal with e.g. performance.
This section should help to understand the tricky implementations in more detail.
Don't take too much attention to typing if it bloats the code too much.
-->
One notable part that is missing compared to the ngrx/store are **actions**. Reducers and effects are called directly from Components. I think the removal of this indirection layer is fine, given that the state is local is not meant to be used throughout the Web App.
The entire implementation of **`ComponentStore`** is fairly small and would likely be contained within a single file in under 200-250 Line of Code.
Let's break down each of the existing parts of the `ComponentStore`:
#### Initial State
`ComponentStore` takes initial state as the only argument that is passed to its constructor, and it passes it to the `BehaviorSubject` that is the reactive heart of the class.
```typescript
// component_store.ts
constructor(initialState: T) {
this.state$ = new BehaviorSubject(initialState);
}
```
It is initialized from the business-logic focused Service, e.g. `BookStore` in our example:
```typescript
// book_store.ts
@Injectable()
export class BooksStore extends ComponentStore<BooksState> {
constructor() {
// 👇 initial state
super({books: [], authors: {}});
}
...
}
```
#### ComponentStore State
Once initialized, the state is available via `getState()` or a top level selector (when just the projector function is used) - this is how `getState()` itself is initialized for convenience.
Similar to `@ngrx/store`, the state is **NOT** exposed for imperative read - this is done to promote reactive thinking.
```typescript
export abstract class ComponentStore<T> implements OnDestroy {
readonly getState: () => Observable<T>;
private readonly state$: BehaviorSubject<T>;
constructor(defaultState: T) {
this.state$ = new BehaviorSubject(defaultState);
this.getState = this.selector(s => s);
}
```
#### OnDestroy life-cycle
One of the key objectives is that the local state is tied to the life-cycle of the components. When Component is destroyed we want the local state to be cleaned up as well and on the subscriptions be unsubscribed.
`ComponentStore` implements `OnDestroy` interface and provides `destroy$` Observable that extending Service can listen to for `takeUntil()` triggers.
```typescript
export abstract class ComponentStore<T> implements OnDestroy {
private readonly state$: BehaviorSubject<T>;
// Should be used only in ngOnDestroy
private readonly destroySubject$ = new ReplaySubject<void>();
// Exposed to any extending Store to be used for the teardowns.
readonly destroy$ = this.destroySubject$.asObservable();
constructor(defaultState: T) {
this.state$ = new BehaviorSubject(defaultState);
...
}
/** Completes all relevant Observable streams. */
ngOnDestroy() {
this.state$.complete();
this.destroySubject$.next();
}
}
```
All selectors and effects are listening for `destroy$` as well, and would unsubscribe with `takeUntil(destroy$)` when it's fired.
The state itself is completed.
#### updater
The state of the `ComponentStore` can be changed only through `updater`. There are two types of reducers:
* Private reducers - meant to be used only within the specific Store, e.g. within Effects.
* Public reducers - meant to be used by Components and also could be used by Effects.
Reducer takes new values imperitavely as well as reactively. (see APIs).
```typescript
/**
* Creates a reducer.
*
* @param reducerFn A static reducer function that takes 2 parameters (the
* current state and an argument object) and returns a new instance of the
* state.
* @return A function that accepts one argument which is forwarded as the
* second argument to `reducerFn`. Everytime this function is called
* subscribers will be notified of the state change.
*/
updater(reducerFn: (state: T) => T): () => void;
updater<V>(
reducerFn: (state: T, value: V) => T
): (observableOrValue: V | Observable<V>) => Subscription;
updater<V>(
reducerFn: (state: T, value?: V) => T
): (observableOrValue?: V | Observable<V>) => Subscription {
return (observableOrValue?: V | Observable<V>): Subscription => {
const observable$: Observable<V> = isObservable(observableOrValue)
? observableOrValue
: of(observableOrValue);
return observable$
.pipe(
distinctUntilChanged(),
takeUntil(this.destroy$)
)
.subscribe(value =>
this.state$.next(reducerFn(this.state$.value, value))
);
};
}
```
#### effect
Unlike `@ngrx/store`, where Actions trigger Reducers and/or Effects, in `ComponentStore` effects are triggered directly from Components, and if the state needs to be changed as part of the side-effect - these reducers are invoked within effects themselves.
Effect takes new values imperitavely as well as reactively. (see APIs).
```typescript
/**
* This effect is subscribed to for the life of the @Component.
* @param generator A function that takes an origin Observable input and
* returns an Observable. The Observable that is returned will be
* subscribed to for the life of the component.
* @return A function that, when called, will trigger the origin Observable.
*/
effect<V, R = unknown>(
generator: (origin$: Observable<V>) => Observable<R>
): EffectReturnFn<V> {
const origin$ = new Subject<V>();
// 👇 Wrapped with retryable function, similar to ngrx/effect
generator(origin$)
// tied to the lifecycle 👇 of ComponentStore
.pipe(takeUntil(this.destroy$))
.subscribe();
return (observableOrValue?: V | Observable<V>): Subscription => {
const observable$ = isObservable(observableOrValue)
? observableOrValue
: of(observableOrValue);
return observable$.pipe(takeUntil(this.destroy$)).subscribe(value => {
// any new 👇 value is pushed into a stream
origin$.next(value);
});
};
}
```
#### selector
Selectors are a way to read the data from ComponentStore.
Top level selector can access the state.
Selectors can also be combined with other Observable-providing sources (as well as other selectors), and their values are projected into the last argument of selector - a `projector` function.
```typescript
selector<R>(projector: (s: T) => R): () => Observable<R>;
selector<R, S1>(
s1: () => Observable<S1>,
projector: (s1: S1) => R
): () => Observable<R>;
selector<R, S1, S2>(
s1: () => Observable<S1>,
s2: () => Observable<S2>,
projector: (s1: S1, s2: S2) => R
): () => Observable<R>;
selector<R, S1, S2, S3>(
s1: () => Observable<S1>,
s2: () => Observable<S2>,
s3: () => Observable<S3>,
projector: (s1: S1, s2: S2, s3: S3) => R
): () => Observable<R>;
selector<R, S1, S2, S3, S4>(
s1: () => Observable<S1>,
s2: () => Observable<S2>,
s3: () => Observable<S3>,
s4: () => Observable<S4>,
projector: (s1: S1, s2: S2, s3: S3, s4: S4) => R
): () => Observable<R>;
selector<R>(...args: any[]): () => Observable<R> {
let observable$: Observable<R>;
const projector: (...args: any[]) => R = args.pop();
if (!args.length) {
// If there's only one argument, it's just a map function.
observable$ = this.state$.pipe(map(projector));
} else {
// If there are multiple arguments, we're chaining selectors, so we need
// to take the combineLatest of them before calling the map function.
observable$ = combineLatest(args.map(a => a())).pipe(
map((args: any[]) => projector(...args))
);
}
const distinctSharedObservable$ = observable$.pipe(
distinctUntilChanged(),
shareReplay({
refCount: true,
bufferSize: 1,
}),
takeUntil(this.destroy$),
);
return () => distinctSharedObservable$;
}
```
Selectors are making sure the same values are not emitted with `distictUntilChanged()` operator.
They are also making sure that the execution context is reused with `shareReplay()` operator. That means if the same selector is used in multiple places they will be sharing the results instead of each of them triggering a new chain of Observables.
#### Combining with data from Store or other Observable sources
Unlike selectors in `@ngrx/store`, here they take Observables (or actually functions that return Observables), that means that selectors can combine data not only from `ComponentStore` but from any other Observable producing source as well.
So, it is possible to combine local state with other Observables, for example with `store.select(selectFromGlobalState)`:
```typescript
readonly getLastAuthorWithContent = this.selector(
// 👇 local state selector
this.getAuthors,
// 👇 another local state selector
this.getAuthorsCount,
// 👇 selector from Global Store (@ngrx/store)
() => this.store.select(storeContent),
(authors, authorsCount, content) => ({
...authors[authorsCount - 1],
content
})
);
```
#### Children Components accessing ComponentStore
Children Components can easily access the ComponentStore that is provided at the parent level (or higher), by simply injecting it. It works because of the Angular Injector hierarchy.
> Note it shouldn't put it at the `providers` as well, as that would create a new instance of the service.
```typescript=
@Component({
selector: "book",
templateUrl: "book.component.html",
// child 👇 component should NOT provide the service
providers: []
})
export class BookComponent {
readonly authors$: Observable<Author[]> =
// reactive way to 👇 read all authors
this.booksStore.getAuthors();
// Injects the specialized ComponentStore
// that knows how to work with book data 👇
constructor(private readonly booksStore: BooksStore) {}
}
```
In the example above, `BooksStore` is tied to the `BookComponent`'s parent component (`BookShelfComponent`).
![](https://i.imgur.com/S20U5M6.png)
#### Multiple levels of ComponentStores
Should the child component need its own local state `ComponentStore`, it's easy to create and provide at child component level.
Child's `SingleBookStore` would be tied to its life-cycle.
```typescript
@Component({
selector: "book",
templateUrl: "book.ng.html",
// child's own 👇 component store
providers: [SingleBookStore]
})
export class BookComponent {
// Has access to both parent's BooksStore
// and its own SingleBookStore 👇
constructor(
private readonly booksStore: BooksStore,
private readonly singleBookStore: SingleBookStore) {}
}
```
![](https://i.imgur.com/QXKQKoA.png)
#### Ability to pass the initial state from Component
// TODO: describe how
### Caveats
<!--
You may need to describe what you did not do or why simpler approaches don't work. Mention other things to watch out for (if any).
Security Considerations
How you’ll be secure. Considerations should include sanitization, escaping, existing security protocols and web standards, permission, user privacy, etc.
None
-->
#### String selectors
String selectors do not make a lot of sense, as it's quite easy to provide a mapping/projector function. On top of that, string selectors do not work well with advanced code optimizers that do "property renaming".
### Performance Considerations
<!--
Try to describe under which conditions the suggested solution is performant and which factors play the key role when starting to get bad performance.
Describe a specific situation in which we can run into performance problems
If possible provide a POC or a theoretical explanation of a possible solution.
-->
Selectors have:
1. `distinctUntilChanged()` referential comparison, which prevents same values to be passed for selector consumers.
2. `shareReplay({refCount: true, bufferSize: 1})` on top of that, so if the same selector is used in multiple places - they all share the same execution context and not trigger the same operations multiple times.
Currently, selectors are not memoized.
### Open Questions
<!--
List here all open questions, things that need more research or other further investigation.
The goal is not to answer them but ask the right questions or point out the area of related research.
-->
1. Which package would be best for this library? Should we add it to `@ngrx/component` or create new `@ngrx/component-store`.
Initially I was more inclined to add to `@ngrx/component`, but I think it might cause some confusion, since it's not related to any of `ngrxPush`, `ngrxLet` or any of the coalesing stuff.
Since this library is completely self-contained, I suggest to push it to the new `@ngrx/component-store` library. It would also help with the Docs.
1. Let's decided on the ***naming*** of the methods. There are multiple ways we can go about it:
* Have a naming that's close to what NgRx/Redux currently has (`reducer`/`selector`/`effect`).
Pros:
* It would be familar to people who already use NgRx.
* When combining with Global store - it might be more intuitive.
Cons:
* Might cause confusion that this library and `@ngrx/store` are dependent on each other (they are completely independent).
* Naming that's exactly the same as what NgRx has (`createReducer`/`createSelector`/`createEffect`).
Pros:
* Very familar to NgRx users.
Cons:
* Might cause confusion, wrong imports and etc.
* à la XState naming (`updater`/`reader`/`asyncUpdater`)
Pros:
* Clear distinction that it's a separate library
* Might be easer for new users
Cons:
* A bit more akward integration if used with Global Store
* `asyncUpdater` does not describe well what it's doing or what the intentions are.
* Hybrid naming (`updater`/`selector`/`effect` or `updater`/`reader`/`effect`)
Pros:
* Mostly removes `reducer` that some new users find scary or intimidating.
* Clearer names that might be even more intuitive to both new users and those who are familiar with NgRx.
* Clear separation from `@ngrx/store`.
Cons:
* Cannot find any?
* **Any other???**
1. Selectors do we want to have them to be `() => Observable` or just `Observable`?
Currently they are `() => Observable` for two reasons:
* A bit easier to mock for tests
* If used in templates and mistyped they would throw an error instead of being `undefined`.
1. Do we want to have selectors memoized? (lower priority)
If we do, that would significantly increase the size of `ComponentStore`.
1. Should we add to `getState` imperatively? It won't be coming from selectors (they are Observable) and might promote non-reactive bad practices.
1. Should we add Lazy init? e.g. hold off selector emissions until explicit first `setState()` call? Would reqiure a bit of rewiring and `ReplaySubject` instead of `BehaviorSubject` as the state container.
1. ~~Should we add a reactive way to push data into `ComponentStore`? (similar to `connect` from `RxState`)
It might be a good feature.~~
**Implemenented**
## Developer Impact
### Public API Surface Impact
<!--
Are you adding new public APIs? If so, what's the change? (method signature changes, new classes/interfaces, new npm packages, config file properties or new config files altogether)
Are the new APIs consistent with the existing APIs? Do they follow naming conventions and use correct terminology?
-->
I suggest putting this abstract class under the `@ngrx/component` library, as it targets `Component` improvements.
If it's not used, it would be tree-shaken out of the code.
### Breaking Changes
<!--
Include all breaking changes
-->
New API
### Rollout Plan
<!--
Are the implementation delays about to negatively affect or delay the release of other features or increase the size.
-->
Suggest to rollout as alpha, and maybe include in v10.
### Maintenance Plan
<!--
Explain how this library will be maintained going forward in releases after the initial release.
This includes not only releases of the subject of this document but also respects its dependencies.
-->
Maintained by NgRx team.
## Alternatives considered
<!--
Include alternate design ideas you tried out, but didn't continue with them.
List their disadvantages or at least why you did not invest more time in researching them.
<!--
Include alternate design ideas you tried out, but didn't continue with them.
List their disadvantages or at least why you did not invest more time in researching them.
**Name1**
A rough description of the case
List different sub path:
- A)
-B)
Drawbacks:
- Filesize
- Performance
- Workflow
- Tooling
- Documentation
- Maintainance
- Breaking Changes
**Name2**
...
-->
#### Solution that is kept as part of the global store.
Drawbacks:
* Cannot be used as a standalone and would require @ngrx/store to be included as well.
* Would be significantly larger.
* Would require additional magic and complexity when actions are dispatched and in effects, to distinguish for which particular instance the action was fired.
* Would keep an extra indirection layer (actions) for something that should be fine without it.
Benefits:
* Would be visible in Redux DevTools.
* Part of the Global Store.
#### RxState [linked here](https://github.com/BioPhoton/ngx-rx/tree/master/libs/state)
RxState's `setState` acts like a `updater`, as it operates on the state itself, setting the values. Imperatively setting values is also possible in RxState by wrapping it with a function.
~~RxState has another way to set values - via `.connect`ing Observables to the state. It could be useful when we want to set the state based on Observable emissions and a similar method could be added to the `ComponentStore` API.~~
(Update: such API was added)
`hold` is a way RxState is working with side-effects. It takes an Observable as an input and can trigger side-effects.
`effect`, however, can also take values imperatively and pipe it to the Subject that's within the effect. That helps when values are pushed to the effect from Component's callbacks (e.g. when the User clicks a button to save an input).
`select` takes a string or an RxJS operator. I'm yet to figure out how operator helps more than a simple function that `selector` takes. `map` operator is used within a selector itself.
Overall, I find RxState's APIs are not as convenient as `ComponentStore`'s.