diff --git a/apps/docs/docs/state/actions/actions.mdx b/apps/docs/docs/state/actions/actions.mdx index d8c9d105d3..457519d497 100644 --- a/apps/docs/docs/state/actions/actions.mdx +++ b/apps/docs/docs/state/actions/actions.mdx @@ -16,7 +16,7 @@ hide_title: true - ✅ Fully Typed - ✅ No-Boilerplate -- ✅ Configurable transformations to have lines in the template +- ✅ Configurable transformations to have less lines in the template - ✅ Minimal memory footprint through a Proxy object and lazy initialization ## Demos: diff --git a/apps/docs/docs/state/selections/selections.mdx b/apps/docs/docs/state/selections/selections.mdx index 3dafe2fa11..87a072fbf9 100644 --- a/apps/docs/docs/state/selections/selections.mdx +++ b/apps/docs/docs/state/selections/selections.mdx @@ -14,11 +14,10 @@ hide_title: true ## Key features -- ✅ - -## Demos: - -- ⚡ GitHub +- ✅ Fully Typed +- ✅ No-Boilerplate +- ✅ Performant selections +- ✅ Minimal memory footprint through a Proxy object and lazy initialization ## Install @@ -28,16 +27,252 @@ npm install --save @rx-angular/state yarn add @rx-angular/state ``` -## Documentation +# Motivation + +![Selections (1)](https://user-images.githubusercontent.com/10064416/152422745-b3d8e094-d0f0-4810-b1b2-5f81fae25938.png) + +When managing state you want to maintain a core unit of data. +This data is then later on distributed to multiple places in your component template (local) or whole app (global). + +We can forward this state to their consumers directly or compute specific derivations (selections) for the core unit. + +As an example we could think of the following shape: + +**A list and a list title** + +```typescript +interface GlobalModel { + title: string; + list: Array<{ id: number; date: Date }>; +} +``` + +This data is consumed in different screens: + +**A list of all items sorted by id** + +```typescript +interface SelectionScreen1 { + title: string; + sortDirection: 'asc' | 'desc' | 'none'; + sortedList: Array<{ id: number }>; +} +``` + +**A list of items filtered by date** + +```typescript +interface SelectionScreen2 { + title: string; + startingDate: Date; + filteredList: { id: number }; +} +``` + +The 2 rendered lists are a derivation, a modified version of the core set of items. +One time they are displayed in a sorted order, the other time only filtered subset of the items. + +> **Hint:** +> Derivations are always redundant information of our core data and therefore should not get stored, +> but cached in the derivation logic. + +![Selections (2)](https://user-images.githubusercontent.com/10064416/152422803-bfd07ab2-0a6f-4521-836e-b71677e11923.png) + +As this process contains a lot of gotchas and possible pitfalls in terms of memory usage and performance this small helper library was created. + +# Benefits + +![Selections (3)](https://user-images.githubusercontent.com/10064416/152422856-a483a06c-84e0-4067-9eaa-f3bb54a0156d.png) + +- Sophisticated set of helpers for any selection problem +- Enables lazy rendering +- Computes only distinct values +- Shares computed result with multiple subscriber +- Select distinct sub-sets +- Select from static values +- Fully tested +- Strongly typed + +# Concepts + +## Selection setup - Template vs Class + +As Observables are cold their resulting stream will only get activated by a subscription. +This leads to a situations called: "the late subscriber problem" or "the early subscriber problem". (LINK) + +![Selections (5)](https://user-images.githubusercontent.com/10064416/152422955-cb89d198-1a69-450b-be84-29dd6c8c4fdb.png) + +In most cases it's best to go with solving problems on the early subscriber side and be sure we never loose values that should render on the screen. + +![Selections (4)](https://user-images.githubusercontent.com/10064416/152422883-0b5f6006-7929-4520-b0b2-79eb61e4eb08.png) + +# Usage + +## select + +`select` is the stand-alone version of the `RxState#select` top level method. It helps to create default selection's from a changing state source. + +```typescript +// emissions: +// 0. - no emission ever happened +// 1. {a: 1} - incomplete state leads to `?` pollution in the template +// 2. {a: 1, b: 'a'} - render relevant emission +// 2. {a: 1, b: 'a'} - same instance emission +// 3. {a: 1, b: 'a', c: true} - render irrelevant change +// 4. {a: 1, b: 'b', c: true} - render relevant emission +const model$: Observable>; +``` + +**Problem** + +```html + +
B: {{vm?.b}}
+B: {{(model$ | push)?.b}} +``` + +### single property short hand + +```typescript +const vm$ = model$.pipe(select('b')); +``` + +```html + +
B: {{ b }}
+B: {{(vm$ | push)}} +``` + +### single operators + +```typescript +const vm$: Observable<> = model$.pipe(select(map(({ b }) => b === 'a'))); +``` + +```html + +
B: {{vm.b}}
+B: {{(vm$ | push).b}} +``` + +## selectSlice + +## distinctUntilSomeChanges + +## stateful + +# Advanced derivation architecture + +**The problem** + +We have the following state sources to manage: + +- the list of products received form global state - `Product[]` +- the title of the list including it's number of children computed in the component class - `string` +- the sort direction triggered over a UI element click - `boolean` + +A setup of the components class based on `RxState` could look like this: + +```typescript +@Component({ + selector: 'app-problem', + template: ` + +

{{vm.title}} - {{vm.sortDirection}}

+ +
+ `, + providers: [RxState], +}) +export class ProblemComponent { + + viewModel$: Observable; // ??? + + constructor(private globalState: GlobalState, private state: RxState) { + this.state.connect('title', this.globalState.title$); + this.state.connect('products', this.globalState.products$); + } + + toggleSort() { + this.state.set('sort', ({sort}) => !sort)) + } +} + +``` + +In a components template we want to render the the UI for the above explained view model `SelectionScreen1`. + +```typescript +interface SelectionScreen1 { + title: string; + sortDirection: 'asc' | 'desc' | 'none'; + sortedList: Array<{ id: number }>; +} +``` + +A common implementations looks like this: + +```typescript +export class ProblemComponent { + private sortedList$ = this.state.select( + selectSlice(['sortDirection', 'list']), + map(() => { + // sort `list` by `sortDirection` to `sortedList` here + return sortedList; + }) + ); -- [Selections](https://rx-angular.io/docs/state/selections) + viewModel$ = this.state.select( + selectSlice(['title', 'sortedList', 'sortDirection']) + ); -## Motivation + // ❌ BAD: model and view model mix up 👇 + constructor( + private globalState: GlobalState, + private state: RxState> + ) { + // ... -TBD + // ❌ BAD: store derived state 👇 + this.state.connect('sortedList', this.sortedList$); + } + + // ... +} +``` + +![Selections (6)](https://user-images.githubusercontent.com/10064416/152422999-db8260f0-69e1-4d99-b6ac-b2b1d043b4b7.png) + +By removing the sorted list form the state and moving it into the selection we can clean up the state's typing and have a nice separation of which data is owned by the component (model) and which data is owned by the template (view model) + +```typescript +export class ProblemComponent { + private sortedSlice$ = this.state.select( + ['sortDirection', 'list'], + ({ list, sortDirection }) => { + // sort `list` by `sortDirection` to `sortedList` here + return { sortDirection, sortedList }; + } + ); + + // ✔ GOOD: Derive view model from model 👇 + vm$ = // what to promote here as smosh isn't part of RxA? + + constructor(private globalState: GlobalState, private state: RxState) { + // ... + } + + // ... +} +``` -- [`Interfaces`](api/interfaces/) -- [`distinctUntilSomeChanged`](../api/rxjs-operators/distinct-until-some-changed.md) -- [`select`](../api/rxjs-operators/select.md) -- [`selectSlice`](../api/rxjs-operators/select-slice.md) -- [`stateful`](../api/rxjs-operators/stateful.md) +![Selections (7)](https://user-images.githubusercontent.com/10064416/152423026-d23326c2-97d5-4bd0-9015-f498c3fc0e55.png)