Alex Okrushko
    • Create new note
    • Create a note from template
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Write
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Engagement control Commenting, Suggest edit, Emoji Reply
      • Invitee
    • Publish Note

      Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

      Your note will be visible on your profile and discoverable by anyone.
      Your note is now live.
      This note is visible on your profile and discoverable online.
      Everyone on the web can find and read all notes of this public team.
      See published notes
      Unpublish note
      Please check the box to agree to the Community Guidelines.
      View profile
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Engagement control
    • Transfer ownership
    • Delete this note
    • Save as template
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Sharing URL Create Help
Create Create new note Create a note from template
Menu
Options
Versions and GitHub Sync Engagement control Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Write
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Engagement control Commenting, Suggest edit, Emoji Reply
Invitee
Publish Note

Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

Your note will be visible on your profile and discoverable by anyone.
Your note is now live.
This note is visible on your profile and discoverable online.
Everyone on the web can find and read all notes of this public team.
See published notes
Unpublish note
Please check the box to agree to the Community Guidelines.
View profile
Engagement control
Commenting
Permission
Disabled Forbidden Owners Signed-in users Everyone
Enable
Permission
  • Forbidden
  • Owners
  • Signed-in users
  • Everyone
Suggest edit
Permission
Disabled Forbidden Owners Signed-in users Everyone
Enable
Permission
  • Forbidden
  • Owners
  • Signed-in users
Emoji Reply
Enable
Import from Dropbox Google Drive Gist Clipboard
   owned this note    owned this note      
Published Linked with GitHub
Subscribed
  • Any changes
    Be notified of any changes
  • Mention me
    Be notified of mention me
  • Unsubscribe
Subscribe
# 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.

Import from clipboard

Paste your markdown or webpage here...

Advanced permission required

Your current role can only read. Ask the system administrator to acquire write and comment permission.

This team is disabled

Sorry, this team is disabled. You can't edit this note.

This note is locked

Sorry, only owner can edit this note.

Reach the limit

Sorry, you've reached the max length this note can be.
Please reduce the content or divide it to more notes, thank you!

Import from Gist

Import from Snippet

or

Export to Snippet

Are you sure?

Do you really want to delete this note?
All users will lose their connection.

Create a note from template

Create a note from template

Oops...
This template has been removed or transferred.
Upgrade
All
  • All
  • Team
No template.

Create a template

Upgrade

Delete template

Do you really want to delete this template?
Turn this template into a regular note and keep its content, versions, and comments.

This page need refresh

You have an incompatible client version.
Refresh to update.
New version available!
See releases notes here
Refresh to enjoy new features.
Your user state has changed.
Refresh to load new user state.

Sign in

Forgot password

or

By clicking below, you agree to our terms of service.

Sign in via Facebook Sign in via Twitter Sign in via GitHub Sign in via Dropbox Sign in with Wallet
Wallet ( )
Connect another wallet

New to HackMD? Sign up

Help

  • English
  • 中文
  • Français
  • Deutsch
  • 日本語
  • Español
  • Català
  • Ελληνικά
  • Português
  • italiano
  • Türkçe
  • Русский
  • Nederlands
  • hrvatski jezik
  • język polski
  • Українська
  • हिन्दी
  • svenska
  • Esperanto
  • dansk

Documents

Help & Tutorial

How to use Book mode

Slide Example

API Docs

Edit in VSCode

Install browser extension

Contacts

Feedback

Discord

Send us email

Resources

Releases

Pricing

Blog

Policy

Terms

Privacy

Cheatsheet

Syntax Example Reference
# Header Header 基本排版
- Unordered List
  • Unordered List
1. Ordered List
  1. Ordered List
- [ ] Todo List
  • Todo List
> Blockquote
Blockquote
**Bold font** Bold font
*Italics font* Italics font
~~Strikethrough~~ Strikethrough
19^th^ 19th
H~2~O H2O
++Inserted text++ Inserted text
==Marked text== Marked text
[link text](https:// "title") Link
![image alt](https:// "title") Image
`Code` Code 在筆記中貼入程式碼
```javascript
var i = 0;
```
var i = 0;
:smile: :smile: Emoji list
{%youtube youtube_id %} Externals
$L^aT_eX$ LaTeX
:::info
This is a alert area.
:::

This is a alert area.

Versions and GitHub Sync
Get Full History Access

  • Edit version name
  • Delete

revision author avatar     named on  

More Less

Note content is identical to the latest version.
Compare
    Choose a version
    No search result
    Version not found
Sign in to link this note to GitHub
Learn more
This note is not linked with GitHub
 

Feedback

Submission failed, please try again

Thanks for your support.

On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

Please give us some advice and help us improve HackMD.

 

Thanks for your feedback

Remove version name

Do you want to remove this version name and description?

Transfer ownership

Transfer to
    Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

      Link with GitHub

      Please authorize HackMD on GitHub
      • Please sign in to GitHub and install the HackMD app on your GitHub repo.
      • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
      Learn more  Sign in to GitHub

      Push the note to GitHub Push to GitHub Pull a file from GitHub

        Authorize again
       

      Choose which file to push to

      Select repo
      Refresh Authorize more repos
      Select branch
      Select file
      Select branch
      Choose version(s) to push
      • Save a new version and push
      • Choose from existing versions
      Include title and tags
      Available push count

      Pull from GitHub

       
      File from GitHub
      File from HackMD

      GitHub Link Settings

      File linked

      Linked by
      File path
      Last synced branch
      Available push count

      Danger Zone

      Unlink
      You will no longer receive notification when GitHub file changes after unlink.

      Syncing

      Push failed

      Push successfully