From 71703a320a9203b23c33b0bde432c9966cc42fb9 Mon Sep 17 00:00:00 2001 From: Michael Hladky Date: Wed, 11 Jan 2023 15:23:28 +0100 Subject: [PATCH 1/5] docs: add selection docs --- apps/docs/docs/state/actions/actions.mdx | 2 +- .../docs/docs/state/selections/selections.mdx | 289 +++++++++++++++++- 2 files changed, 276 insertions(+), 15 deletions(-) diff --git a/apps/docs/docs/state/actions/actions.mdx b/apps/docs/docs/state/actions/actions.mdx index a8e2343592..5e93f4f47d 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..f8d6c79532 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,278 @@ 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 composition - lazy vs eager + +## Selection composition - functional vs reactive + +## 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. + +```typescritp +// 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 emisssion +// 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 + +```typescritp +const vm$ = model$.pipe(select('b')); +``` + +```html + +
B: {{vm.b}}
+B: {{(model$ | push).b}} +``` + +### single operators + +```typescritp +const vm$: Observable<> = model$.pipe(select(map(({b}) => b === 'a'))); +``` + +```html + +
B: {{vm.b}}
+B: {{(model$ | push).b}} +``` + +## selectSlice + +## smosh + +## distinctUntilSomeChanges + +# 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 computen in the component class - `string` +- the sort direction triggered over a UI element click - `boolean` + +A setup of the compoents class based on `RxState` could look like this: + +```typescript +@Component({ + selector: 'app-problem', + template: ` + +

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

+
    +
  • {{item}}
  • +
+
+ `, + 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 +// template removed for brevity +export class ProblemComponent { + private sortedList$ = this.state.select( + selectSlice(['sortDirection', 'list']), + map(() => { + // sort `list` by `sortDirection` to `sortedList` here + return sortedList; + }) + ); + + viewModel$ = this.state.select( + selectSlice(['title', 'sortedList', 'sortDirection']) + ); + + // ❌ BAD: modle viewmodel mix up 👇 + constructor( + private globalState: GlobalState, + private state: RxState> + ) { + // ... + + // ❌ 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) -- [Selections](https://rx-angular.io/docs/state/selections) +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) -## Motivation +```typescript +// template removed for brevity +export class ProblemComponent { + private sortedSlice$ = this.state.select( + selectSlice(['sortDirection', 'list']), + map(({ list, sortDirection }) => { + // sort `list` by `sortDirection` to `sortedList` here + return { sortDirection, sortedList }; + }) + ); -TBD + // ✔ GOOD: Derive view model from model 👇 + viewModel$ = smosh({ title: this.state.select('title') }, this.sortedSlice$); + + // target API + viewModel$ = smosh( + { + prop1: 'prop1', // string + prop2: prop1$, // Observable + }, + slice1$, // Observable<{prop3: 3}> + slice2$ // Observable<{prop4: 'four'}>, + // durationSelector$ (optional) + ); + + // ✔ GOOD: Derive view model from model 👇 + viewModel$ = smosh( + { + title: this.state.select('title'), + }, + [this.sortedSlice$] + ); + + 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) From 5a61861a835831700833e4c32057fb608b3c0ad6 Mon Sep 17 00:00:00 2001 From: Edouard Bozon Date: Mon, 30 Jan 2023 10:26:09 +0100 Subject: [PATCH 2/5] docs: improve selection docs --- .../docs/docs/state/selections/selections.mdx | 54 ++++++------------- 1 file changed, 16 insertions(+), 38 deletions(-) diff --git a/apps/docs/docs/state/selections/selections.mdx b/apps/docs/docs/state/selections/selections.mdx index f8d6c79532..9f2c2807e7 100644 --- a/apps/docs/docs/state/selections/selections.mdx +++ b/apps/docs/docs/state/selections/selections.mdx @@ -116,15 +116,15 @@ In most cases it's best to go with solving problems on the early subscriber side `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. -```typescritp +```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 emisssion +// 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>; +const model$: Observable>; ``` **Problem** @@ -140,7 +140,7 @@ B: {{(model$ | push)?.b}} ### single property short hand -```typescritp +```typescript const vm$ = model$.pipe(select('b')); ``` @@ -154,8 +154,8 @@ B: {{(model$ | push).b}} ### single operators -```typescritp -const vm$: Observable<> = model$.pipe(select(map(({b}) => b === 'a'))); +```typescript +const vm$: Observable<> = model$.pipe(select(map(({ b }) => b === 'a'))); ``` ```html @@ -168,10 +168,10 @@ B: {{(model$ | push).b}} ## selectSlice -## smosh - ## distinctUntilSomeChanges +## stateful + # Advanced derivation architecture **The problem** @@ -179,10 +179,10 @@ B: {{(model$ | push).b}} 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 computen in the component class - `string` +- 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 compoents class based on `RxState` could look like this: +A setup of the components class based on `RxState` could look like this: ```typescript @Component({ @@ -226,7 +226,6 @@ interface SelectionScreen1 { A common implementations looks like this: ```typescript -// template removed for brevity export class ProblemComponent { private sortedList$ = this.state.select( selectSlice(['sortDirection', 'list']), @@ -240,7 +239,7 @@ export class ProblemComponent { selectSlice(['title', 'sortedList', 'sortDirection']) ); - // ❌ BAD: modle viewmodel mix up 👇 + // ❌ BAD: model and view model mix up 👇 constructor( private globalState: GlobalState, private state: RxState> @@ -257,41 +256,20 @@ export class ProblemComponent { ![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) +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 -// template removed for brevity export class ProblemComponent { private sortedSlice$ = this.state.select( - selectSlice(['sortDirection', 'list']), - map(({ list, sortDirection }) => { + ['sortDirection', 'list'], + ({ list, sortDirection }) => { // sort `list` by `sortDirection` to `sortedList` here return { sortDirection, sortedList }; - }) + } ); // ✔ GOOD: Derive view model from model 👇 - viewModel$ = smosh({ title: this.state.select('title') }, this.sortedSlice$); - - // target API - viewModel$ = smosh( - { - prop1: 'prop1', // string - prop2: prop1$, // Observable - }, - slice1$, // Observable<{prop3: 3}> - slice2$ // Observable<{prop4: 'four'}>, - // durationSelector$ (optional) - ); - - // ✔ GOOD: Derive view model from model 👇 - viewModel$ = smosh( - { - title: this.state.select('title'), - }, - [this.sortedSlice$] - ); + vm$ = // what to promote here as smosh isn't part of RxA? constructor(private globalState: GlobalState, private state: RxState) { // ... From d45fd92935abe0e5ecc82c1be787d0609bbee6c1 Mon Sep 17 00:00:00 2001 From: Michael Hladky <10064416+BioPhoton@users.noreply.github.com> Date: Fri, 9 Jun 2023 01:37:44 -0400 Subject: [PATCH 3/5] Update apps/docs/docs/state/selections/selections.mdx Co-authored-by: Julian Jandl --- apps/docs/docs/state/selections/selections.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/docs/docs/state/selections/selections.mdx b/apps/docs/docs/state/selections/selections.mdx index 9f2c2807e7..13faf065ab 100644 --- a/apps/docs/docs/state/selections/selections.mdx +++ b/apps/docs/docs/state/selections/selections.mdx @@ -162,8 +162,8 @@ const vm$: Observable<> = model$.pipe(select(map(({ b }) => b === 'a'))); -
B: {{vm.b}}
-B: {{(model$ | push).b}} +
B: {{vm.b}}
+B: {{(vm$ | push).b}} ``` ## selectSlice From 119c1140eb8f6802821d84068cca87323600a16f Mon Sep 17 00:00:00 2001 From: Michael Hladky <10064416+BioPhoton@users.noreply.github.com> Date: Fri, 9 Jun 2023 01:37:51 -0400 Subject: [PATCH 4/5] Update apps/docs/docs/state/selections/selections.mdx Co-authored-by: Julian Jandl --- apps/docs/docs/state/selections/selections.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/docs/docs/state/selections/selections.mdx b/apps/docs/docs/state/selections/selections.mdx index 13faf065ab..e09dc5d8ab 100644 --- a/apps/docs/docs/state/selections/selections.mdx +++ b/apps/docs/docs/state/selections/selections.mdx @@ -148,8 +148,8 @@ const vm$ = model$.pipe(select('b')); -
B: {{vm.b}}
-B: {{(model$ | push).b}} +
B: {{ b }}
+B: {{(vm$ | push)}} ``` ### single operators From 92df63a3689240d924935625847b4abc8a15ef96 Mon Sep 17 00:00:00 2001 From: Michael Hladky <10064416+BioPhoton@users.noreply.github.com> Date: Fri, 9 Jun 2023 01:47:39 -0400 Subject: [PATCH 5/5] Update apps/docs/docs/state/selections/selections.mdx --- apps/docs/docs/state/selections/selections.mdx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/apps/docs/docs/state/selections/selections.mdx b/apps/docs/docs/state/selections/selections.mdx index e09dc5d8ab..87a072fbf9 100644 --- a/apps/docs/docs/state/selections/selections.mdx +++ b/apps/docs/docs/state/selections/selections.mdx @@ -95,10 +95,6 @@ As this process contains a lot of gotchas and possible pitfalls in terms of memo # Concepts -## Selection composition - lazy vs eager - -## Selection composition - functional vs reactive - ## Selection setup - Template vs Class As Observables are cold their resulting stream will only get activated by a subscription.