diff --git a/404.html b/404.html index f97a4968..9e41a81b 100644 --- a/404.html +++ b/404.html @@ -4,13 +4,13 @@ Page Not Found | Elf | A Reactive Store with Magical Powers - +
Skip to main content

Page Not Found

We could not find what you were looking for.

Please contact the owner of the site that linked you to the original URL and let them know their link is broken.

- + \ No newline at end of file diff --git a/assets/js/9efc358d.fd63206d.js b/assets/js/9efc358d.eea461ce.js similarity index 71% rename from assets/js/9efc358d.fd63206d.js rename to assets/js/9efc358d.eea461ce.js index 7071e0e7..75513216 100644 --- a/assets/js/9efc358d.fd63206d.js +++ b/assets/js/9efc358d.eea461ce.js @@ -1 +1 @@ -"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[975],{3905:(e,t,n)=>{n.d(t,{Zo:()=>l,kt:()=>u});var o=n(7294);function a(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function s(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);t&&(o=o.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,o)}return n}function r(e){for(var t=1;t=0||(a[n]=e[n]);return a}(e,t);if(Object.getOwnPropertySymbols){var s=Object.getOwnPropertySymbols(e);for(o=0;o=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(a[n]=e[n])}return a}var c=o.createContext({}),f=function(e){var t=o.useContext(c),n=t;return e&&(n="function"==typeof e?e(t):r(r({},t),e)),n},l=function(e){var t=f(e.components);return o.createElement(c.Provider,{value:t},e.children)},p={inlineCode:"code",wrapper:function(e){var t=e.children;return o.createElement(o.Fragment,{},t)}},d=o.forwardRef((function(e,t){var n=e.components,a=e.mdxType,s=e.originalType,c=e.parentName,l=i(e,["components","mdxType","originalType","parentName"]),d=f(n),u=a,m=d["".concat(c,".").concat(u)]||d[u]||p[u]||s;return n?o.createElement(m,r(r({ref:t},l),{},{components:n})):o.createElement(m,r({ref:t},l))}));function u(e,t){var n=arguments,a=t&&t.mdxType;if("string"==typeof e||a){var s=n.length,r=new Array(s);r[0]=d;var i={};for(var c in t)hasOwnProperty.call(t,c)&&(i[c]=t[c]);i.originalType=e,i.mdxType="string"==typeof e?e:a,r[1]=i;for(var f=2;f{n.r(t),n.d(t,{assets:()=>c,contentTitle:()=>r,default:()=>p,frontMatter:()=>s,metadata:()=>i,toc:()=>f});var o=n(7462),a=(n(7294),n(3905));const s={},r="Managing Side Effects",i={unversionedId:"side-effects",id:"side-effects",title:"Managing Side Effects",description:"Elf is a state management solution, and it doesn't force you to manage side effects in a certain way. But the same team also created companion packages that can be used with Elf to handle side effects.",source:"@site/docs/side-effects.mdx",sourceDirName:".",slug:"/side-effects",permalink:"/elf/docs/side-effects",draft:!1,editUrl:"https://github.com/ngneat/elf/docusaurus/edit/main/website/docs/side-effects.mdx",tags:[],version:"current",frontMatter:{},sidebar:"docs",previous:{title:"CLI",permalink:"/elf/docs/cli"},next:{title:"Production Mode",permalink:"/elf/docs/miscellaneous/production"}},c={},f=[{value:"Using Services",id:"using-services",level:2},{value:"Using Effects",id:"using-effects",level:2},{value:"Using Effect Functions",id:"using-effect-functions",level:2}],l={toc:f};function p(e){let{components:t,...n}=e;return(0,a.kt)("wrapper",(0,o.Z)({},l,n,{components:t,mdxType:"MDXLayout"}),(0,a.kt)("h1",{id:"managing-side-effects"},"Managing Side Effects"),(0,a.kt)("p",null,"Elf is a state management solution, and it doesn't force you to manage side effects in a certain way. But the same team also created companion packages that can be used with Elf to handle side effects."),(0,a.kt)("p",null,"A side-effect is anything that happens outside of the normal flow of the store\u2014interacting with the API asynchronously, setting intervals and timeouts, updating the local storage, etc."),(0,a.kt)("p",null,"It's entirely up to the developer to model and implement those tasks and update the store."),(0,a.kt)("p",null,"Let's examine three ways to handle side effects in our application:"),(0,a.kt)("h2",{id:"using-services"},"Using Services"),(0,a.kt)("p",null,"In most cases, services are the most straightforward solution:"),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-ts",metastring:'title="todos.service.ts"',title:'"todos.service.ts"'},"import { setTodos, addTodo } from './todos.repository';\nimport { tap } from 'rxjs/operators';\n\nexport function fetchTodos() {\n return http.get('todos').pipe(\n tap(setTodos)\n )\n}\n\nexport function addTodo(todo: Todo) {\n return http.post('todos', todo).pipe(\n tap(addTodo)\n )\n}\n")),(0,a.kt)("p",null,"And ",(0,a.kt)("inlineCode",{parentName:"p"},"subscribe")," in the component. Below is an example using a React component:"),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-tsx"},"import { useObservable } from '@ngneat/react-rxjs';\nimport { todos$ } from './todos.repository';\nimport { fetchTodos } from './todos.service';\n\nfunction Todos() {\n const [todos] = useObservable(todos$);\n \n useEffect(() => {\n fetchTodos().subscribe();\n }, [])\n \n return
{todos}
\n}\n")),(0,a.kt)("p",null,"Check out the ",(0,a.kt)("a",{parentName:"p",href:"https://github.com/ngneat/react-rxjs"},(0,a.kt)("inlineCode",{parentName:"a"},"@ngneat/react-rxjs"))," library for more information."),(0,a.kt)("h2",{id:"using-effects"},"Using Effects"),(0,a.kt)("p",null,"We can register ",(0,a.kt)("inlineCode",{parentName:"p"},"effects")," that'll execute when we dispatch actions using ",(0,a.kt)("a",{parentName:"p",href:"https://github.com/ngneat/effects"},(0,a.kt)("inlineCode",{parentName:"a"},"@ngneat/effects")),"."),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-ts",metastring:'title="todos.effects.ts"',title:'"todos.effects.ts"'},"\nimport { createEffect, ofType } from '@ngneat/effects';\n\nconst loadTodos = createAction('[Todos] Load');\n\nexport const loadTodos$ = createEffect(actions => \n actions.pipe(\n ofType(loadTodos),\n switchMap((todo) => todosApi.loadTodos()),\n tap(setTodos)\n )\n);\n")),(0,a.kt)("p",null,"Below is an example using a React component:"),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-tsx"},"import { useEffects } from '@ngneat/effects-hook';\nimport { dispatch } from '@ngneat/effects';\nimport { useObservable } from '@ngneat/react-rxjs';\nimport { useEffect } from 'react';\n\nexport function TodosPage() {\n const [todos] = useObservable(todos$);\n\n useEffects([loadTodos$]);\n\n useEffect(() => dispatch(loadTodos()), []);\n\n return {todos}\n}\n")),(0,a.kt)("p",null,"In the ",(0,a.kt)("a",{parentName:"p",href:"https://github.com/ngneat/effects"},"official")," documentation, you can find more information and an Angular example."),(0,a.kt)("h2",{id:"using-effect-functions"},"Using Effect Functions"),(0,a.kt)("p",null,"You may prefer effect functions if you're not a big fan of ",(0,a.kt)("inlineCode",{parentName:"p"},"actions"),"."),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-ts",metastring:'title="todos.effects.ts"',title:'"todos.effects.ts"'},"import { createEffectFn } from '@ngneat/effects';\n\nexport const searchTodoEffect = createEffectFn((searchTerm$: Observable) => {\n return searchTerm$.pipe(\n debounceTime(300),\n switchMap((searchTerm) => fetchTodos({ searchTerm })),\n tap(setTodos)\n );\n});\n")),(0,a.kt)("p",null,"Below is an example using a React component:"),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-tsx"},"import { useEffectFn } from '@ngneat/effects-hooks';\n\nfunction SearchComponent() {\n const searchTodo = useEffectFn(searchTodoEffect);\n\n return searchTodo(value) }/>\n}\n")),(0,a.kt)("p",null,"In the ",(0,a.kt)("a",{parentName:"p",href:"https://github.com/ngneat/effects#effect-functions"},"official")," documentation, you can find more information and an Angular example. It's possible to use effects and effect functions simultaneously if you like."))}p.isMDXComponent=!0}}]); \ No newline at end of file +"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[975],{3905:(e,t,n)=>{n.d(t,{Zo:()=>l,kt:()=>u});var o=n(7294);function a(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function s(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);t&&(o=o.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,o)}return n}function r(e){for(var t=1;t=0||(a[n]=e[n]);return a}(e,t);if(Object.getOwnPropertySymbols){var s=Object.getOwnPropertySymbols(e);for(o=0;o=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(a[n]=e[n])}return a}var c=o.createContext({}),f=function(e){var t=o.useContext(c),n=t;return e&&(n="function"==typeof e?e(t):r(r({},t),e)),n},l=function(e){var t=f(e.components);return o.createElement(c.Provider,{value:t},e.children)},p={inlineCode:"code",wrapper:function(e){var t=e.children;return o.createElement(o.Fragment,{},t)}},d=o.forwardRef((function(e,t){var n=e.components,a=e.mdxType,s=e.originalType,c=e.parentName,l=i(e,["components","mdxType","originalType","parentName"]),d=f(n),u=a,m=d["".concat(c,".").concat(u)]||d[u]||p[u]||s;return n?o.createElement(m,r(r({ref:t},l),{},{components:n})):o.createElement(m,r({ref:t},l))}));function u(e,t){var n=arguments,a=t&&t.mdxType;if("string"==typeof e||a){var s=n.length,r=new Array(s);r[0]=d;var i={};for(var c in t)hasOwnProperty.call(t,c)&&(i[c]=t[c]);i.originalType=e,i.mdxType="string"==typeof e?e:a,r[1]=i;for(var f=2;f{n.r(t),n.d(t,{assets:()=>c,contentTitle:()=>r,default:()=>p,frontMatter:()=>s,metadata:()=>i,toc:()=>f});var o=n(7462),a=(n(7294),n(3905));const s={},r="Managing Side Effects",i={unversionedId:"side-effects",id:"side-effects",title:"Managing Side Effects",description:"Elf is a state management solution, and it doesn't force you to manage side effects in a certain way. But the same team also created companion packages that can be used with Elf to handle side effects.",source:"@site/docs/side-effects.mdx",sourceDirName:".",slug:"/side-effects",permalink:"/elf/docs/side-effects",draft:!1,editUrl:"https://github.com/ngneat/elf/docusaurus/edit/main/website/docs/side-effects.mdx",tags:[],version:"current",frontMatter:{},sidebar:"docs",previous:{title:"CLI",permalink:"/elf/docs/cli"},next:{title:"Production Mode",permalink:"/elf/docs/miscellaneous/production"}},c={},f=[{value:"Using Services",id:"using-services",level:2},{value:"Using Effects",id:"using-effects",level:2},{value:"Using Effect Functions",id:"using-effect-functions",level:2}],l={toc:f};function p(e){let{components:t,...n}=e;return(0,a.kt)("wrapper",(0,o.Z)({},l,n,{components:t,mdxType:"MDXLayout"}),(0,a.kt)("h1",{id:"managing-side-effects"},"Managing Side Effects"),(0,a.kt)("p",null,"Elf is a state management solution, and it doesn't force you to manage side effects in a certain way. But the same team also created companion packages that can be used with Elf to handle side effects."),(0,a.kt)("p",null,"A side-effect is anything that happens outside of the normal flow of the store\u2014interacting with the API asynchronously, setting intervals and timeouts, updating the local storage, etc."),(0,a.kt)("p",null,"It's entirely up to the developer to model and implement those tasks and update the store."),(0,a.kt)("p",null,"Let's examine three ways to handle side effects in our application:"),(0,a.kt)("h2",{id:"using-services"},"Using Services"),(0,a.kt)("p",null,"In most cases, services are the most straightforward solution:"),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-ts",metastring:'title="todos.service.ts"',title:'"todos.service.ts"'},"import { setTodos, addTodo } from './todos.repository';\nimport { tap } from 'rxjs/operators';\n\nexport function fetchTodos() {\n return http.get('todos').pipe(\n tap(setTodos)\n )\n}\n\nexport function addTodo(todo: Todo) {\n return http.post('todos', todo).pipe(\n tap(addTodo)\n )\n}\n")),(0,a.kt)("p",null,"And ",(0,a.kt)("inlineCode",{parentName:"p"},"subscribe")," in the component. Below is an example using a React component:"),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-tsx"},"import { useObservable } from '@ngneat/react-rxjs';\nimport { todos$ } from './todos.repository';\nimport { fetchTodos } from './todos.service';\n\nfunction Todos() {\n const [todos] = useObservable(todos$);\n \n useEffect(() => {\n fetchTodos().subscribe();\n }, [])\n \n return
{todos}
\n}\n")),(0,a.kt)("p",null,"Check out the ",(0,a.kt)("a",{parentName:"p",href:"https://github.com/ngneat/react-rxjs"},(0,a.kt)("inlineCode",{parentName:"a"},"@ngneat/react-rxjs"))," library for more information."),(0,a.kt)("h2",{id:"using-effects"},"Using Effects"),(0,a.kt)("p",null,"We can register ",(0,a.kt)("inlineCode",{parentName:"p"},"effects")," that'll execute when we dispatch actions using ",(0,a.kt)("a",{parentName:"p",href:"https://github.com/ngneat/effects"},(0,a.kt)("inlineCode",{parentName:"a"},"@ngneat/effects")),"."),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-ts",metastring:'title="todos.effects.ts"',title:'"todos.effects.ts"'},"\nimport { createAction, createEffect, ofType } from '@ngneat/effects';\n\nconst loadTodos = createAction('[Todos] Load');\n\nexport const loadTodos$ = createEffect(actions => \n actions.pipe(\n ofType(loadTodos),\n switchMap((todo) => todosApi.loadTodos()),\n tap(setTodos)\n )\n);\n")),(0,a.kt)("p",null,"Below is an example using a React component:"),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-tsx"},"import { useEffects } from '@ngneat/effects-hook';\nimport { dispatch } from '@ngneat/effects';\nimport { useObservable } from '@ngneat/react-rxjs';\nimport { useEffect } from 'react';\n\nexport function TodosPage() {\n const [todos] = useObservable(todos$);\n\n useEffects([loadTodos$]);\n\n useEffect(() => dispatch(loadTodos()), []);\n\n return {todos}\n}\n")),(0,a.kt)("p",null,"In the ",(0,a.kt)("a",{parentName:"p",href:"https://github.com/ngneat/effects"},"official")," documentation, you can find more information and an Angular example."),(0,a.kt)("h2",{id:"using-effect-functions"},"Using Effect Functions"),(0,a.kt)("p",null,"You may prefer effect functions if you're not a big fan of ",(0,a.kt)("inlineCode",{parentName:"p"},"actions"),"."),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-ts",metastring:'title="todos.effects.ts"',title:'"todos.effects.ts"'},"import { createEffectFn } from '@ngneat/effects';\n\nexport const searchTodoEffect = createEffectFn((searchTerm$: Observable) => {\n return searchTerm$.pipe(\n debounceTime(300),\n switchMap((searchTerm) => fetchTodos({ searchTerm })),\n tap(setTodos)\n );\n});\n")),(0,a.kt)("p",null,"Below is an example using a React component:"),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-tsx"},"import { useEffectFn } from '@ngneat/effects-hooks';\n\nfunction SearchComponent() {\n const searchTodo = useEffectFn(searchTodoEffect);\n\n return searchTodo(value) }/>\n}\n")),(0,a.kt)("p",null,"In the ",(0,a.kt)("a",{parentName:"p",href:"https://github.com/ngneat/effects#effect-functions"},"official")," documentation, you can find more information and an Angular example. It's possible to use effects and effect functions simultaneously if you like."))}p.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/runtime~main.4ccdca0a.js b/assets/js/runtime~main.2255268e.js similarity index 61% rename from assets/js/runtime~main.4ccdca0a.js rename to assets/js/runtime~main.2255268e.js index 1aa378dd..54f7cf5b 100644 --- a/assets/js/runtime~main.4ccdca0a.js +++ b/assets/js/runtime~main.2255268e.js @@ -1 +1 @@ -(()=>{"use strict";var e,a,t,r,f,c={},o={};function b(e){var a=o[e];if(void 0!==a)return a.exports;var t=o[e]={id:e,loaded:!1,exports:{}};return c[e].call(t.exports,t,t.exports,b),t.loaded=!0,t.exports}b.m=c,b.c=o,e=[],b.O=(a,t,r,f)=>{if(!t){var c=1/0;for(i=0;i=f)&&Object.keys(b.O).every((e=>b.O[e](t[d])))?t.splice(d--,1):(o=!1,f0&&e[i-1][2]>f;i--)e[i]=e[i-1];e[i]=[t,r,f]},b.n=e=>{var a=e&&e.__esModule?()=>e.default:()=>e;return b.d(a,{a:a}),a},t=Object.getPrototypeOf?e=>Object.getPrototypeOf(e):e=>e.__proto__,b.t=function(e,r){if(1&r&&(e=this(e)),8&r)return e;if("object"==typeof e&&e){if(4&r&&e.__esModule)return e;if(16&r&&"function"==typeof e.then)return e}var f=Object.create(null);b.r(f);var c={};a=a||[null,t({}),t([]),t(t)];for(var o=2&r&&e;"object"==typeof o&&!~a.indexOf(o);o=t(o))Object.getOwnPropertyNames(o).forEach((a=>c[a]=()=>e[a]));return c.default=()=>e,b.d(f,c),f},b.d=(e,a)=>{for(var t in a)b.o(a,t)&&!b.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:a[t]})},b.f={},b.e=e=>Promise.all(Object.keys(b.f).reduce(((a,t)=>(b.f[t](e,a),a)),[])),b.u=e=>"assets/js/"+({37:"4aa3214b",41:"ba61d949",49:"f9f03544",53:"935f2afb",77:"eb008bab",98:"15084a13",114:"5452bbaa",122:"cce0c757",190:"fb18d329",215:"ccd6bc29",237:"1df93b7f",258:"c69ffb60",357:"07a0157e",367:"311d683c",369:"279ddf62",372:"3a408875",420:"c7f9a275",443:"65244ba9",478:"2594a8a0",481:"3dc74342",514:"1be78505",534:"a53e715a",591:"4a3fc6b4",627:"512bc059",694:"2007eed0",697:"213b363c",733:"b9c28fea",793:"367cf555",801:"4330e889",806:"5c541335",812:"d3549650",918:"17896441",920:"1a4e3797",930:"fa4d91bf",937:"ea313555",949:"6ab7bb82",971:"69645345",975:"9efc358d"}[e]||e)+"."+{37:"7bea9113",41:"d248b340",49:"e747c60c",53:"fc71d975",77:"6d4993af",98:"632be5e6",114:"cfd17454",122:"025931c0",190:"1769131c",215:"0001942e",237:"f252199e",258:"edd2794e",357:"8f5e8a63",367:"fdcafbdf",369:"63941b4a",372:"5e6b99d0",420:"a4a48b98",443:"574d3f28",478:"899a01e1",481:"190bb3c2",514:"83cb0979",534:"8785deed",591:"708ec268",627:"79adb9e2",694:"fb41da70",697:"c9af0aef",724:"fba00600",733:"4311a6f3",793:"1aede32b",801:"a07ade30",806:"2962ce5c",812:"85dea532",894:"662af41b",918:"ceddfb68",920:"2c2e3375",930:"80218416",937:"901700b0",945:"e8a8f394",949:"cc9c89d3",971:"1264d52f",972:"af7814cb",975:"fd63206d",984:"ce9b7035"}[e]+".js",b.miniCssF=e=>{},b.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),b.o=(e,a)=>Object.prototype.hasOwnProperty.call(e,a),r={},f="docs:",b.l=(e,a,t,c)=>{if(r[e])r[e].push(a);else{var o,d;if(void 0!==t)for(var n=document.getElementsByTagName("script"),i=0;i{o.onerror=o.onload=null,clearTimeout(s);var f=r[e];if(delete r[e],o.parentNode&&o.parentNode.removeChild(o),f&&f.forEach((e=>e(t))),a)return a(t)},s=setTimeout(l.bind(null,void 0,{type:"timeout",target:o}),12e4);o.onerror=l.bind(null,o.onerror),o.onload=l.bind(null,o.onload),d&&document.head.appendChild(o)}},b.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},b.p="/elf/",b.gca=function(e){return e={17896441:"918",69645345:"971","4aa3214b":"37",ba61d949:"41",f9f03544:"49","935f2afb":"53",eb008bab:"77","15084a13":"98","5452bbaa":"114",cce0c757:"122",fb18d329:"190",ccd6bc29:"215","1df93b7f":"237",c69ffb60:"258","07a0157e":"357","311d683c":"367","279ddf62":"369","3a408875":"372",c7f9a275:"420","65244ba9":"443","2594a8a0":"478","3dc74342":"481","1be78505":"514",a53e715a:"534","4a3fc6b4":"591","512bc059":"627","2007eed0":"694","213b363c":"697",b9c28fea:"733","367cf555":"793","4330e889":"801","5c541335":"806",d3549650:"812","1a4e3797":"920",fa4d91bf:"930",ea313555:"937","6ab7bb82":"949","9efc358d":"975"}[e]||e,b.p+b.u(e)},(()=>{var e={303:0,532:0};b.f.j=(a,t)=>{var r=b.o(e,a)?e[a]:void 0;if(0!==r)if(r)t.push(r[2]);else if(/^(303|532)$/.test(a))e[a]=0;else{var f=new Promise(((t,f)=>r=e[a]=[t,f]));t.push(r[2]=f);var c=b.p+b.u(a),o=new Error;b.l(c,(t=>{if(b.o(e,a)&&(0!==(r=e[a])&&(e[a]=void 0),r)){var f=t&&("load"===t.type?"missing":t.type),c=t&&t.target&&t.target.src;o.message="Loading chunk "+a+" failed.\n("+f+": "+c+")",o.name="ChunkLoadError",o.type=f,o.request=c,r[1](o)}}),"chunk-"+a,a)}},b.O.j=a=>0===e[a];var a=(a,t)=>{var r,f,c=t[0],o=t[1],d=t[2],n=0;if(c.some((a=>0!==e[a]))){for(r in o)b.o(o,r)&&(b.m[r]=o[r]);if(d)var i=d(b)}for(a&&a(t);n{"use strict";var e,a,t,r,f,c={},o={};function d(e){var a=o[e];if(void 0!==a)return a.exports;var t=o[e]={id:e,loaded:!1,exports:{}};return c[e].call(t.exports,t,t.exports,d),t.loaded=!0,t.exports}d.m=c,d.c=o,e=[],d.O=(a,t,r,f)=>{if(!t){var c=1/0;for(i=0;i=f)&&Object.keys(d.O).every((e=>d.O[e](t[b])))?t.splice(b--,1):(o=!1,f0&&e[i-1][2]>f;i--)e[i]=e[i-1];e[i]=[t,r,f]},d.n=e=>{var a=e&&e.__esModule?()=>e.default:()=>e;return d.d(a,{a:a}),a},t=Object.getPrototypeOf?e=>Object.getPrototypeOf(e):e=>e.__proto__,d.t=function(e,r){if(1&r&&(e=this(e)),8&r)return e;if("object"==typeof e&&e){if(4&r&&e.__esModule)return e;if(16&r&&"function"==typeof e.then)return e}var f=Object.create(null);d.r(f);var c={};a=a||[null,t({}),t([]),t(t)];for(var o=2&r&&e;"object"==typeof o&&!~a.indexOf(o);o=t(o))Object.getOwnPropertyNames(o).forEach((a=>c[a]=()=>e[a]));return c.default=()=>e,d.d(f,c),f},d.d=(e,a)=>{for(var t in a)d.o(a,t)&&!d.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:a[t]})},d.f={},d.e=e=>Promise.all(Object.keys(d.f).reduce(((a,t)=>(d.f[t](e,a),a)),[])),d.u=e=>"assets/js/"+({37:"4aa3214b",41:"ba61d949",49:"f9f03544",53:"935f2afb",77:"eb008bab",98:"15084a13",114:"5452bbaa",122:"cce0c757",190:"fb18d329",215:"ccd6bc29",237:"1df93b7f",258:"c69ffb60",357:"07a0157e",367:"311d683c",369:"279ddf62",372:"3a408875",420:"c7f9a275",443:"65244ba9",478:"2594a8a0",481:"3dc74342",514:"1be78505",534:"a53e715a",591:"4a3fc6b4",627:"512bc059",694:"2007eed0",697:"213b363c",733:"b9c28fea",793:"367cf555",801:"4330e889",806:"5c541335",812:"d3549650",918:"17896441",920:"1a4e3797",930:"fa4d91bf",937:"ea313555",949:"6ab7bb82",971:"69645345",975:"9efc358d"}[e]||e)+"."+{37:"7bea9113",41:"d248b340",49:"e747c60c",53:"fc71d975",77:"6d4993af",98:"632be5e6",114:"cfd17454",122:"025931c0",190:"1769131c",215:"0001942e",237:"f252199e",258:"edd2794e",357:"8f5e8a63",367:"fdcafbdf",369:"63941b4a",372:"5e6b99d0",420:"a4a48b98",443:"574d3f28",478:"899a01e1",481:"190bb3c2",514:"83cb0979",534:"8785deed",591:"708ec268",627:"79adb9e2",694:"fb41da70",697:"c9af0aef",724:"fba00600",733:"4311a6f3",793:"1aede32b",801:"a07ade30",806:"2962ce5c",812:"85dea532",894:"662af41b",918:"ceddfb68",920:"2c2e3375",930:"80218416",937:"901700b0",945:"e8a8f394",949:"cc9c89d3",971:"1264d52f",972:"af7814cb",975:"eea461ce",984:"ce9b7035"}[e]+".js",d.miniCssF=e=>{},d.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),d.o=(e,a)=>Object.prototype.hasOwnProperty.call(e,a),r={},f="docs:",d.l=(e,a,t,c)=>{if(r[e])r[e].push(a);else{var o,b;if(void 0!==t)for(var n=document.getElementsByTagName("script"),i=0;i{o.onerror=o.onload=null,clearTimeout(s);var f=r[e];if(delete r[e],o.parentNode&&o.parentNode.removeChild(o),f&&f.forEach((e=>e(t))),a)return a(t)},s=setTimeout(l.bind(null,void 0,{type:"timeout",target:o}),12e4);o.onerror=l.bind(null,o.onerror),o.onload=l.bind(null,o.onload),b&&document.head.appendChild(o)}},d.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},d.p="/elf/",d.gca=function(e){return e={17896441:"918",69645345:"971","4aa3214b":"37",ba61d949:"41",f9f03544:"49","935f2afb":"53",eb008bab:"77","15084a13":"98","5452bbaa":"114",cce0c757:"122",fb18d329:"190",ccd6bc29:"215","1df93b7f":"237",c69ffb60:"258","07a0157e":"357","311d683c":"367","279ddf62":"369","3a408875":"372",c7f9a275:"420","65244ba9":"443","2594a8a0":"478","3dc74342":"481","1be78505":"514",a53e715a:"534","4a3fc6b4":"591","512bc059":"627","2007eed0":"694","213b363c":"697",b9c28fea:"733","367cf555":"793","4330e889":"801","5c541335":"806",d3549650:"812","1a4e3797":"920",fa4d91bf:"930",ea313555:"937","6ab7bb82":"949","9efc358d":"975"}[e]||e,d.p+d.u(e)},(()=>{var e={303:0,532:0};d.f.j=(a,t)=>{var r=d.o(e,a)?e[a]:void 0;if(0!==r)if(r)t.push(r[2]);else if(/^(303|532)$/.test(a))e[a]=0;else{var f=new Promise(((t,f)=>r=e[a]=[t,f]));t.push(r[2]=f);var c=d.p+d.u(a),o=new Error;d.l(c,(t=>{if(d.o(e,a)&&(0!==(r=e[a])&&(e[a]=void 0),r)){var f=t&&("load"===t.type?"missing":t.type),c=t&&t.target&&t.target.src;o.message="Loading chunk "+a+" failed.\n("+f+": "+c+")",o.name="ChunkLoadError",o.type=f,o.request=c,r[1](o)}}),"chunk-"+a,a)}},d.O.j=a=>0===e[a];var a=(a,t)=>{var r,f,c=t[0],o=t[1],b=t[2],n=0;if(c.some((a=>0!==e[a]))){for(r in o)d.o(o,r)&&(d.m[r]=o[r]);if(b)var i=b(d)}for(a&&a(t);n CLI | Elf | A Reactive Store with Magical Powers - +

CLI

Elf comes with a CLI that enables a fast and easy setup of your store. It offers the following commands:

Install

$ npx @ngneat/elf-cli install

Using the above command, you can choose which packages to install. Your package manager will be detected and used for installation.

Repo

$ npx @ngneat/elf-cli repo
$ npx @ngneat/elf-cli repo --dry-run

Using the above command, you can create a repository file. All the boilerplate will be created for you based on which features you select.

Config

You can set the configuration by providing the package.json file:

package.json
{
"elf": {
"cli": {
"repoTemplate": "class",
"inlineStoreInClass": true,
"idKey": "_id",
"repoLibrary": "state",
"plugins": []
}
}
}

repoTemplate

By default, the repository file generates exported functions. If you prefer to use a class, for instance, when working with Angular, you can set this option to class.

inlineStoreInClass

By default, a store is created outside of the class. If you prefer creating the store inside a class you can set this option to true or withoutConstructor. It might be helpful when you create a component store or you want to set the initial value to the store given via Angular DI (Works only with repoTemplate set as class).

idKey

The default idKey for the package @ngneat/elf-entities is id. By setting this option, you can change it globally.

repoLibrary

The repository file is created by default at the root path you specify (i.e., flat). If you set this option, you can specify the directory you want.

plugins

Specify which plugins you want to use.

@ngneat/elf-cli-ng

Install the package, and add the following code:

package.json
{
"elf": {
"cli": {
"repoTemplate": "class",
"plugins": ["@ngneat/elf-cli-ng"]
}
}
}

The plugin will add the Injectable decorator to the repository class.

fuzzypath

A fuzzy file/directory search and selection prompt. It can be configured as follows:

elf.config.js
module.exports = {
cli: {
fuzzypath: {
rootPath: // defaults to process.cwd()
excludePath(path) {
// defaults to path.includes('node_modules')
}
excludeFilter(path) {
// defaults to path.includes('.');
}
}
}
}
- + \ No newline at end of file diff --git a/docs/dev-tools/index.html b/docs/dev-tools/index.html index 97d7e03d..b0a488ec 100644 --- a/docs/dev-tools/index.html +++ b/docs/dev-tools/index.html @@ -4,13 +4,13 @@ DevTools | Elf | A Reactive Store with Magical Powers - +

DevTools

Elf provides built-in integration with the Redux DevTools Chrome extension.

Usage

Install the Redux extension from the supported App stores ( Chrome, Firefox ).

And call the devtools() method:

import { devTools } from '@ngneat/elf-devtools';

devTools();

Options

The plugin supports the following options passed as the second function parameter:

maxAge - Maximum amount of actions to be stored in the history tree.

preAction - A method that's called before each action.

actionsDispatcher - Observable of actions. For example actions created by @ngeat/effects

logTrace: Outputs a console.trace() for each action in the console.

postTimelineUpdate - A function that'll be invoked after the devtools timeline updates. For example, you can run a change detection when working with Angular:

import { ApplicationRef } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

import { AppModule } from './app/app.module';
import { devTools } from '@ngneat/elf-devtools';

platformBrowserDynamic()
.bootstrapModule(AppModule).then((moduleRef) => {
devTools({
postTimelineUpdate: () => moduleRef.injector.get(ApplicationRef).tick()
});
})

Display actions from @ngeat/effects

Angular

import { EffectsNgModule, Actions } from '@ngneat/effects-ng';
import { SampleEffects } from 'sample/sample.effect.ts';
import { devTools } from '@ngneat/elf-devtools';

export function initElfDevTools(actions: Actions) {
return () => {
devTools({
name: 'Sample Application',
actionsDispatcher: actions
})
};
}

@NgModule({
imports: [
// other modules
EffectsNgModule.forRoot([SampleEffects]),
],
providers: [
{
provide: APP_INITIALIZER,
multi: true,
useFactory: initElfDevTools,
deps: [Actions]
}
]
})
export class AppModule {
}
- + \ No newline at end of file diff --git a/docs/facade/index.html b/docs/facade/index.html index c08c0609..e584fe08 100644 --- a/docs/facade/index.html +++ b/docs/facade/index.html @@ -4,13 +4,13 @@ The Facade Pattern | Elf | A Reactive Store with Magical Powers - +

The Facade Pattern

A Facade is a simple public interface that hides more complex usage. Facades encapsulate all interactions in one place, including queries, updates, and side effects, allowing components to only ever interact with the Facade.

For example, we can create a products.facade.ts:

products.facade.ts

import { createEffectFn } from '@ngneat/effects';
import { createStore } from '@ngneat/elf';
import {
withEntities,
selectAllEntities,
setEntities,
} from '@ngneat/elf-entities';
import {
createRequestDataSource,
withRequestsStatus,
} from '@ngneat/elf-requests';
import { mergeMap, Observable, tap } from 'rxjs';
import { http } from '../http';

export interface Product {
id: number;
name: string;
price: number;
image: string;
category: 'vegetables' | 'fruits' | 'nuts';
}

const store = createStore(
{ name: 'products' },
withEntities<Product>(),
withRequestsStatus()
);

const { setSuccess, trackRequestStatus, data$ } =
createRequestDataSource({
data$: () => store.pipe(selectAllEntities()),
requestKey: 'products',
dataKey: 'products',
store,
});

export const productsDataSource = data$();

function setProducts(products: Product[]) {
store.update(setEntities(products), setSuccess());
}

export const getProductsEffect = createEffectFn(($: Observable<void>) => {
return $.pipe(
trackRequestStatus(),
mergeMap(() =>
http<Product[]>('assets/products.json', {
selector: (res) => res.json(),
})
),
tap(setProducts)
);
});

Creating a Facade with the CLI

Coming soon.

- + \ No newline at end of file diff --git a/docs/faq/index.html b/docs/faq/index.html index 36ca96f7..7bc8f884 100644 --- a/docs/faq/index.html +++ b/docs/faq/index.html @@ -4,13 +4,13 @@ FAQ | Elf | A Reactive Store with Magical Powers - + - + \ No newline at end of file diff --git a/docs/features/entities-management/active-ids/index.html b/docs/features/entities-management/active-ids/index.html index 8c72f064..e44ed58e 100644 --- a/docs/features/entities-management/active-ids/index.html +++ b/docs/features/entities-management/active-ids/index.html @@ -4,14 +4,14 @@ Active ID(s) | Elf | A Reactive Store with Magical Powers - +

Active ID(s)

This feature requires the withEntities to be used in the Store. It lets you hold one or more IDs indicating the entities that are currently active. It is often useful for monitoring which entities the user is interacting with.

info

This feature requires @ngneat/elf-entities

Active Id

To use this feature, provide the withActiveId props factory function in the createStore call:

import { createStore } from '@ngneat/elf';
import { withEntities, withActiveId } from '@ngneat/elf-entities';

interface Todo {
id: number;
label: string;
}

const todosStore = createStore(
{ name: 'todos' },
withEntities<Todo>(),
withActiveId()
);

This will allow you to use the following ready-made mutations and queries:

Queries

selectActiveEntity

Select the active entity:

import { selectActiveEntity } from '@ngneat/elf-entities';

const active$ = todosStore.pipe(selectActiveEntity());

selectActiveId

Select the active id:

import { selectActiveId } from '@ngneat/elf-entities';

const activeId$ = todosStore.pipe(selectActiveId());

getActiveEntity

Get active entity:

import { getActiveEntity } from '@ngneat/elf-entities';

const active = todosStore.query(getActiveEntity());

getActiveId

Get the active id:

import { getActiveId } from '@ngneat/elf-entities';

const activeId = todosStore.query(getActiveId);

Mutations

setActiveId

Set the active id:

import { setActiveId } from '@ngneat/elf-entities';

todosStore.update(setActiveId(id));

resetActiveId

Reset the active id:

import { resetActiveId } from '@ngneat/elf-entities';

todosStore.update(resetActiveId());

Active Ids

To use this feature, provide the withActiveIds props factory function in the createStore call:

import { createStore } from '@ngneat/elf';
import { withEntities, withActiveIds } from '@ngneat/elf-entities';

interface Todo {
id: number;
label: string;
}

const todosStore = createStore(
{ name: 'todos' },
withEntities<Todo>(),
withActiveIds()
);

This will allow you to use the following ready-made mutations and queries:

Queries

selectActiveEntities

Select the active entities:

import { selectActiveEntities } from '@ngneat/elf-entities';

const actives$ = todosStore.pipe(selectActiveEntities());

selectActiveIds

Select the active ids:

import { selectActiveIds } from '@ngneat/elf-entities';

const activeIds$ = todosStore.pipe(selectActiveIds());

getActiveEntities

Get active entities:

import { getActiveEntities } from '@ngneat/elf-entities';

const actives = todosStore.query(getActiveEntities());

getActiveIds

Get active ids:

import { getActiveIds } from '@ngneat/elf-entities';

const activeIds = todosStore.query(getActiveIds);

Mutations

setActiveIds

Set the active ids:

import { setActiveIds } from '@ngneat/elf-entities';

todosStore.update(setActiveIds([id, id]));

addActiveIds

Add active ids:

import { addActiveIds } from '@ngneat/elf-entities';

todosStore.update(addActiveIds([id, id]));

toggleActiveIds

Toggle active ids:

import { toggleActiveIds } from '@ngneat/elf-entities';

todosStore.update(toggleActiveIds([id, id]));

removeActiveIds

Remove active ids:

import { removeActiveIds } from '@ngneat/elf-entities';

todosStore.update(removeActiveIds([id, id]));

resetActiveIds

Reset the active ids:

import { resetActiveIds } from '@ngneat/elf-entities';

todosStore.update(resetActiveIds());
- + \ No newline at end of file diff --git a/docs/features/entities-management/entities-props-factory/index.html b/docs/features/entities-management/entities-props-factory/index.html index 7be64f6c..05f302c6 100644 --- a/docs/features/entities-management/entities-props-factory/index.html +++ b/docs/features/entities-management/entities-props-factory/index.html @@ -4,13 +4,13 @@ Entities Props Factory | Elf | A Reactive Store with Magical Powers - +

Entities Props Factory

There are two built-in entities props included in Elf - withEntities and UIEntities. In addition to that, we can create our own entities props for our stores.

Let's say we have a products page with a shopping cart. As well as managing a store for products, we must also maintain a shopping cart. We can create a new Store for our cart or a cart entity props in the same products store.

First, let's create the products store:

products.repository.ts
import { createStore } from '@ngneat/elf';
import { withEntities } from '@ngneat/elf-entities';

interface Product {
id: number;
title: string;
price: number;
}

export const productsStore = createStore(
{ name: 'products' },
withEntities<Product>()
);

Now we can add a cart entities props to the same store:

products.repository.ts
import { createStore } from '@ngneat/elf';
import { withEntities, entitiesPropsFactory } from '@ngneat/elf-entities';

const { cartEntitiesRef, withCartEntities } = entitiesPropsFactory('cart');

interface Product {
id: number;
title: string;
price: number;
}

interface CartItem {
id: Product['id'];
quantity: number;
}

export const productsStore = createStore(
{ name: 'products' },
withEntities<Product>(),
withCartEntities<CartItem>()
);

The entitiesPropsFactory function takes the name of the feature and returns entitiesRef and entitiesProps we can use in our store.

In the above example, our final state shape will be:

{
entities: Record<number, Product>;
ids: number[];
cartEntities: Record<number, CartItem>;
cartIds: number[];
}

We can pass the cartEntitiesRef to each one of the built-in queries and mutations:

products.repository.ts
import { upsertEntitiesById } from '@ngneat/elf-entities';

export function updateCart(id: Product['id']) {
productsStore.update(
upsertEntitiesById(id, {
updater: (e) => ({ ...e, quantity: e.quantity + 1 }),
creator: (id) => ({ id, quantity: 1 }),
ref: cartEntitiesRef,
})
);
}

One more use case for custom entities props is when we work with a normalized state. For example, we might have a movies page, with actors and genres:

movies.repository.ts
interface Actor {
id: string;
name: string;
}

interface Genre {
id: string;
name: string;
}

interface Movie {
id: string;
title: string;
genres: Array<Genre['id']>;
actors: Array<Actor['id']>;
}

const { actorsEntitiesRef, withActorsEntities } =
entitiesPropsFactory('actors');

const { genresEntitiesRef, withGenresEntities } =
entitiesPropsFactory('genres');

const store = createStore(
{ name: 'movies' },
withEntities<Movie>(),
withGenresEntities<Genre>(),
withActorsEntities<Actor>()
);

store.update(
addEntities({ id: '1', name: 'Nicolas cage' }, { ref: actorsEntitiesRef }),
addEntities({ id: '1', name: 'Action' }, { ref: genresEntitiesRef }),
addEntities({
id: '1',
title: 'Gone in 60 Seconds',
genres: ['1'],
actors: ['1'],
})
);
- + \ No newline at end of file diff --git a/docs/features/entities-management/entities/index.html b/docs/features/entities-management/entities/index.html index ce032a2e..ef639e11 100644 --- a/docs/features/entities-management/entities/index.html +++ b/docs/features/entities-management/entities/index.html @@ -4,7 +4,7 @@ Entities | Elf | A Reactive Store with Magical Powers - + @@ -14,7 +14,7 @@ everything you need to manage it.

First, you need to install the package by using the CLI command elf-cli install and selecting the entities package, or via npm:

npm i @ngneat/elf-entities

To use this feature, provide the withEntities props factory function in the createStore call:

import { createStore } from '@ngneat/elf';
import { withEntities } from '@ngneat/elf-entities';

interface Todo {
id: number;
label: string;
}

const todosStore = createStore({ name: 'todos' }, withEntities<Todo>());

This will allow you to use the following ready-made mutations and queries:

Queries

selectAllEntities

Select the entire store's entity collection:

import { selectAllEntities } from '@ngneat/elf-entities';

const todos$ = todosStore.pipe(selectAllEntities());

selectAllEntitiesApply

Select the entire store's entity collection, and apply a filter/map:

import { selectAllEntitiesApply } from '@ngneat/elf-entities';

const titles$ = todosStore.pipe(
selectAllEntitiesApply({
mapEntity: (e) => e.title,
filterEntity: (e) => e.completed,
})
);

In the above example, it'll first apply the filter and then the map function.

getAllEntitiesApply

Get the entire store's entity collection, and apply a filter/map:

import { getAllEntitiesApply } from '@ngneat/elf-entities';

const titles = todosStore.query(
getAllEntitiesApply({
mapEntity: (e) => e.title,
filterEntity: (e) => e.completed,
})
);

selectEntities

Select the entire store's entity collection as object:

import { selectEntities } from '@ngneat/elf-entities';

const todos$ = todosStore.pipe(selectEntities());

selectEntity

Select an entity or a slice of an entity:

import { selectEntity } from '@ngneat/elf-entities';

const todo$ = todosStore.pipe(selectEntity(id));
const title$ = todosStore.pipe(selectEntity(id, { pluck: 'title' }));
const title$ = todosStore.pipe(selectEntity(id, { pluck: (e) => e.title }));

selectEntityByPredicate

Select an entity from the store by predicate:

import { selectEntityByPredicate } from '@ngneat/elf-entities';

const todo$ = todosStore.pipe(
selectEntityByPredicate(({ completed }) => !completed)
);
const title$ = todosStore.pipe(
selectEntityByPredicate(({ completed }) => !completed, {
pluck: 'title',
idKey: '_id',
})
);
const title$ = todosStore.pipe(
selectEntityByPredicate(({ completed }) => !completed, {
pluck: (e) => e.title,
idKey: '_id',
})
);

selectMany

Select multiple entities from the store:

import { selectMany } from '@ngneat/elf-entities';

const todos$ = todosStore.pipe(selectMany([id, id]));
const titles$ = todosStore.pipe(selectMany(id, { pluck: 'title' }));
const titles$ = todosStore.pipe(selectMany(id, { pluck: (e) => e.title }));

selectManyByPredicate

Select multiple entities from the store by predicate:

import { selectManyByPredicate } from '@ngneat/elf-entities';

const todos$ = todosStore.pipe(
selectManyByPredicate((entity) => entity.completed === false)
);
const titles$ = todosStore.pipe(
selectManyByPredicate((entity) => entity.completed === false, {
pluck: 'title',
})
);
const titles$ = todosStore.pipe(
selectManyByPredicate((entity) => entity.completed === false, {
pluck: (e) => e.title,
})
);

selectFirst

Select the first entity from the store:

import { selectFirst } from '@ngneat/elf-entities';

const first$ = todosStore.pipe(selectFirst());

selectLast

Select the last entity from the store:

import { selectLast } from '@ngneat/elf-entities';

const last$ = todosStore.pipe(selectLast());

selectEntitiesCount

Select the store's entity collection size:

import { selectEntitiesCount } from '@ngneat/elf-entities';

const count$ = todosStore.pipe(selectEntitiesCount());

selectEntitiesCountByPredicate

Select the store's entity collection size:

import { selectEntitiesCountByPredicate } from '@ngneat/elf-entities';

const count$ = todosStore.pipe(
selectEntitiesCountByPredicate((entity) => entity.completed)
);

getAllEntities

Get the entity collection:

import { getAllEntities } from '@ngneat/elf-entities';

const todos = todosStore.query(getAllEntities());

getEntitiesIds

Get the entities ids:

import { getEntitiesIds } from '@ngneat/elf-entities';

const todosIds = todosStore.query(getEntitiesIds());

getEntity

Get an entity by id:

import { getEntity } from '@ngneat/elf-entities';

const todo = todosStore.query(getEntity(id));

getEntityByPredicate

Get first entity from the store by predicate:

import { getEntityByPredicate } from '@ngneat/elf-entities';

const todo = todosStore.query(getEntityByPredicate(({ title }) => title === 'Elf'));

getManyByPredicate

Get multiple entities from the store by predicate:

import { getManyByPredicate } from '@ngneat/elf-entities';

const todo = todosStore.query(getManyByPredicate(({ active }) => active));
const todo = todosStore.query(getManyByPredicate(({ active }) => active), { pluck: 'title });
const todo = todosStore.query(getManyByPredicate(({ active }) => active), { ref: UIEntitiesRef, pluck: 'title });

hasEntity

Returns whether an entity exists:

import { hasEntity } from '@ngneat/elf-entities';

if (todosStore.query(hasEntity(id))) {
}

getEntitiesCount

Get the store's entity collection size:

import { getEntitiesCount } from '@ngneat/elf-entities';

const count = todosStore.query(getEntitiesCount());

getEntitiesCountByPredicate

Get the store's entity collection size:

import { getEntitiesCountByPredicate } from '@ngneat/elf-entities';

const count = todosStore.query(
getEntitiesCountByPredicate((entity) => entity.completed)
);

Mutations

setEntities

Replace current collection with the provided collection:

import { setEntities } from '@ngneat/elf-entities';

todosStore.update(setEntities([todo, todo]));

setEntitiesMap

Replace current collection with the provided map:

import { setEntitiesMap } from '@ngneat/elf-entities';

const todos = {
1: {
id: 1,
task: 'Buy milk',
},
2: {
id: 2,
task: 'Fix car',
},
};
todosStore.update(setEntitiesMap(todos));

addEntities

Add an entity or entities to the store:

import { addEntities } from '@ngneat/elf-entities';

todosStore.update(addEntities(todo));

todosStore.update(addEntities([todo, todo]));

todosStore.update(addEntities([todo, todo], { prepend: true }));

addEntitiesFifo

Add an entity or entities to the store using fifo strategy:

import { addEntitiesFifo } from '@ngneat/elf-entities';

todosStore.update(addEntitiesFifo([entity, entity]), { limit: 3 });

updateEntities

Update an entity or entities in the store:

import { updateEntities } from '@ngneat/elf-entities';

todosStore.update(updateEntities(id, { name }));

todosStore.update(updateEntities(id, (entity) => ({ ...entity, name })));

todosStore.update(updateEntities([id, id, id], { open: true }));

updateEntitiesByPredicate

Update an entity or entities in the store:

import { updateEntitiesByPredicate } from '@ngneat/elf-entities';

todosStore.update(
updateEntitiesByPredicate(({ count }) => count === 0, { open: false })
);

todosStore.update(
updateEntitiesByPredicate(({ count }) => count === 0),
(entity) => ({ ...entity, open: false })
);

updateAllEntities

Update all entities in the store:

import { updateAllEntities } from '@ngneat/elf-entities';

todosStore.update(updateAllEntities({ name: 'elf' }));

todosStore.update(
updateAllEntities((entity) => ({ ...entity, count: entity.count + 1 }))
);

upsertEntities

Add or update entities.

To identify entities in the store, every entity must have an id property. Any partial entities will be merged with the existing ones:

import { upsertEntitiesBy } from '@ngneat/elf-entities';

todosStore.update(upsertEntities({ id: '1', happy: true }));

todosStore.update(
upsertEntities([
{ id: '1', happy: true },
{ id: '2', name: 'elf 2', happy: false },
])
);

upsertEntitiesById

Insert or update an entity. When the id isn't found, it creates a new entity; otherwise, it performs an update:

import { upsertEntitiesById } from '@ngneat/elf-entities';

const creator = (id) => createTodo(id);

todosStore.update(
upsertEntitiesById(1, {
updater: { name: 'elf' },
creator,
})
);

todosStore.update(
upsertEntitiesById([1, 2], {
updater: (entity) => ({ ...entity, count: entity.count + 1 }),
creator,
})
);

To perform a merge between a new entity and an updater result, use the mergeUpdaterWithCreator option:

todosStore.update(
upsertEntitiesById([1, 2], {
updater: (entity) => ({ ...entity, name }),
creator,
mergeUpdaterWithCreator: true,
})
);

The above example will first create the entity using the creator method, then pass the result to the updater method, and merge both.

updateEntitiesIds

Update id of an entity or entities in the store:

import { updateEntitiesIds } from '@ngneat/elf-entities';

todosStore.update(updateEntitiesIds(oldId, newId));

todosStore.update(updateEntitiesIds([oldId1, oldId2], [newId1, newId2]));

The most common use case for this is "optimistic updates":

function addTodo(todo: Todo) {
const tempId = generateRandomId();
todosStore.update(addEntities({ ...todo, id: tempId }));

addTodoToServer(todo).then(
(response) => {
todosStore.update(
updateEntitiesIds(tempId, response.id),
updateEntities(response.id, response)
);
},
(error) => {
todosStore.update(deleteEntities(tempId));
// handle error
}
);
}

deleteEntities

Delete an entity or entities from the store:

import { deleteEntities } from '@ngneat/elf-entities';

todosStore.update(deleteEntities(id));
todosStore.update(deleteEntities([id, id]));

deleteEntitiesByPredicate

Delete an entity or entities from the store:

import { deleteEntitiesByPredicate } from '@ngneat/elf-entities';

todosStore.update(deleteEntitiesByPredicate(({ completed }) => completed));

deleteAllEntities

Delete all entities from the store:

import { deleteAllEntities } from '@ngneat/elf-entities';

todosStore.update(deleteAllEntities());

moveEntity

Moves an entity within the store:

import { moveEntity } from '@ngneat/elf-entities';

todosStore.update(moveEntity({ fromIndex: 0, toIndex: 1 }));

idKey

By default, Elf takes the id key from the entity id field. To change it, you can pass the idKey option to the withEntities function:

import { createStore } from '@ngneat/elf';
import { addEntities } from '@ngneat/elf-entities';

interface Todo {
_id: number;
label: string;
}

const todosStore = createStore(
{ name: 'todos' },
withEntities<Todo, '_id'>({ idKey: '_id' })
);

initialValue

In case that you need to start the entities state with a value, you can specify it in the initialValue configuration:

import { createStore } from '@ngneat/elf';

const store = createStore(
{ name },
withEntities<Widget>({ initialValue: [{ id: 1, name: '' }] })
);
- + \ No newline at end of file diff --git a/docs/features/entities-management/ui-entities/index.html b/docs/features/entities-management/ui-entities/index.html index 9ea8c2df..36193a09 100644 --- a/docs/features/entities-management/ui-entities/index.html +++ b/docs/features/entities-management/ui-entities/index.html @@ -4,7 +4,7 @@ UI Entities | Elf | A Reactive Store with Magical Powers - + @@ -12,7 +12,7 @@

UI Entities

This feature allows the store to hold UI-specific entity data, for instance, whether the user has opened the card representing an entity. When used in conjunction with withEntities this can be used to store additional UI data separately from the entities themselves.

import { createStore } from '@ngneat/elf';
import { withEntities, withUIEntities } from '@ngneat/elf-entities';

interface TodoUI {
id: number;
open: boolean;
}
interface Todo {
id: number;
name: string;
}

const todosStore = createStore(
{ name: 'todos' },
withEntities<Todo>(),
withUIEntities<TodoUI>()
);

The usage is similar to that of entities - you can use the same selectors and mutations, with the addition of passing the UIEntitiesRef ref in the method's options parameter, e.g.:

import { addEntities, UIEntitiesRef, selectEntity } from '@ngneat/elf-entities';

todosStore.update(
addEntities({ id: 1, name: 'foo' }),
addEntities({ id: 1, open: true }, { ref: UIEntitiesRef })
);

uiEntity$ = todosStore.pipe(selectEntity(1, { ref: UIEntitiesRef }));

We can use the unionEntities() or unionEntitiesAsMap() operator to get a combination of the entities and their corresponding UIEntities:

import {
unionEntities,
selectAllEntities,
selectEntities,
UIEntitiesRef,
} from '@ngneat/elf-entities';

todos$ = todosStore
.combine({
entities: todosStore.pipe(selectAllEntities()),
UIEntities: todosStore.pipe(selectEntities({ ref: UIEntitiesRef })),
})
.pipe(unionEntities());

You can also pass a different idKey: unionEntities('_id').

- + \ No newline at end of file diff --git a/docs/features/history/entities-history/index.html b/docs/features/history/entities-history/index.html index 23fbf2d2..fad6ff7a 100644 --- a/docs/features/history/entities-history/index.html +++ b/docs/features/history/entities-history/index.html @@ -4,13 +4,13 @@ Entities State History | Elf | A Reactive Store with Magical Powers - +

Entities State History

The entitiesStateHistory function provides a convenient way for undo and redo functionality for specific entity, saving you the trouble of maintaining a history of the entity yourself.

First, you need to install the package by using the CLI command elf-cli install and selecting the stat-history package, or via npm:

npm i @ngneat/elf-state-history

Then, call the entitiesStateHistory method when you want to start monitoring.

import { createStore } from '@ngneat/elf';
import { entitiesStateHistory } from '@ngneat/elf-state-history';

const { withCartEntities, cartEntitiesRef } = entitiesPropsFactory('cart');

const todosStore = createStore(
{ name },
withEntities<Todo>(),
withUIEntities<TodoUI>(),
withCartEntities<CartItem>()
);

export const todosStateHistory = entitiesStateHistory(todosStore);
export const todosUIStateHistory = entitiesStateHistory(todosStore, {
entitiesRef: UIEntitiesRef,
});
export const cartsStateHistory = entitiesStateHistory(todosStore, {
entitiesRef: cartEntitiesRef,
});

As the second parameter you can pass a EntitiesStateHistoryOptions object:

export const todosUIStateHistory = entitiesStateHistory(todosStore, {
maxAge: 15,

// Ref to entities plugin
entitiesRef: UIEntitiesRef,

// Define which entities should be monitoring. By default, all entities are monitored.
entityIds: [1, 5, 15]

// entitiesStateHistory always checks entity changes by top level refs. You can pass comparatorFn to perform extra checks, e.g. deep equal checks.
comparatorFn: deepEqual,
});

API

undo

Undo the last change:

// Performs `undo` for each entity.
todosStateHistory.undo();

todosStateHistory.undo(id);

todosStateHistory.undo([id, id]);

redo

redo the last change:

// Performs `redo` for each entity.
todosStateHistory.redo();

todosStateHistory.redo(id);

todosStateHistory.redo([id, id]);

jumpToPast

Jump to the provided index in the past (assuming index is valid):

// Performs `jumpToPast` for each entity.
todosStateHistory.jumpToPast(number);

todosStateHistory.jumpToPast(number, id);

todosStateHistory.jumpToPast(number, [id, id]);

jumpToFuture

Jump to the provided index in the future (assuming index is valid):

// Performs `jumpToFuture` for each entity.
todosStateHistory.jumpToFuture(number);

todosStateHistory.jumpToFuture(number, id);

todosStateHistory.jumpToFuture(number, [id, id]);

clearPast

Clear the past history:

// Clear past for each entity.
todosStateHistory.clearPast();

todosStateHistory.clearPast(id);

todosStateHistory.clearPast([id, id]);

clearFuture

Clear the future history:

// Clear future for each entity.
todosStateHistory.clearFuture();

todosStateHistory.clearFuture(id);

todosStateHistory.clearFuture([id, id]);

clear

Clear the history:

// Clear history for each entity.
todosStateHistory.clear();
todosStateHistory.clear([], customUpdateFn);

todosStateHistory.clear(id);
todosStateHistory.clear(id, customUpdateFn);

todosStateHistory.clear([id, id]);
todosStateHistory.clear([id, id], customUpdateFn);

pause

Stop monitoring the entity changes:

// Pause monitoring of changes for each entity.
todosStateHistory.pause();

todosStateHistory.pause(id);

todosStateHistory.pause([id, id]);

resume

Continue monitoring the entity changes:

// Resume monitoring of changes for each entity.
todosStateHistory.resume();

todosStateHistory.resume(id);

todosStateHistory.resume([id, id]);

getEntitiesPast

Get an object with past of each entity:

todosStateHistory.getEntitiesPast();

// Add an empty array if entity's past is absent.
todosStateHistory.getEntitiesPast({ showIfEmpty: true });

hasPast

A boolean flag that returns whether the entity history is not empty:

todosStateHistory.hasPast(id);

hasFuture

A boolean flag that returns whether entity is not in the latest step in the history:

todosStateHistory.hasFuture(id);

getEntitiesFuture

Get an object with past of each entity:

todosStateHistory.getEntitiesFuture();

// Add an empty array if entity's future is absent.
todosStateHistory.getEntitiesFuture({ showIfEmpty: true });
- + \ No newline at end of file diff --git a/docs/features/history/index.html b/docs/features/history/index.html index d7be15cf..40f4bfe8 100644 --- a/docs/features/history/index.html +++ b/docs/features/history/index.html @@ -4,13 +4,13 @@ State History | Elf | A Reactive Store with Magical Powers - +

State History

The stateHistory function provides a convenient way for undo and redo functionality, saving you the trouble of maintaining a history in the app yourself.

First, you need to install the package by using the CLI command elf-cli install and selecting the stat-history package, or via npm:

npm i @ngneat/elf-state-history

Then, call the stateHistory method when you want to start monitoring.

import { createStore } from '@ngneat/elf';
import { stateHistory } from '@ngneat/elf-state-history';

const propsStore = createStore({ name }, withProps<Props>());

export const propsStateHistory = stateHistory(propsStore);

As the second parameter you can pass a StateHistoryOptions object, which can be used to define the store's maximum age and state comparator function.

API

undo

Undo the last change:

propsStateHistory.undo();

redo

redo the last change:

propsStateHistory.redo();

jumpToPast

Jump to the provided index in the past (assuming index is valid):

propsStateHistory.jumpToPast(number);

jumpToFuture

Jump to the provided index in the future (assuming index is valid):

propsStateHistory.jumpToFuture(number);

clear

Clear the history:

propsStateHistory.clear();

propsStateHistory.clear(customUpdateFn);

pause

Stop monitoring the state changes:

propsStateHistory.pause();

resume

Continue monitoring the state changes:

propsStateHistory.resume();

getPast

Get the whole past history:

propsStateHistory.getPast();

hasPast

A boolean flag that returns whether the history is not empty:

propsStateHistory.hasPast;

hasPast$

An observable that returns whether the history is not empty:

propsStateHistory.hasPast$;

getFuture

Get the whole future history:

propsStateHistory.getFuture();

hasFuture

A boolean flag that returns whether you're not in the latest step in the history:

propsStateHistory.hasFuture;

hasFuture$

An observable that returns whether you're not in the latest step in the history:

propsStateHistory.hasFuture$;

resetFutureOnNewState

A boolean flag in the StateHistoryOptions that controls whether the future redo states should be cleared when a new state is added after the user has undone one or more state changes.

If resetFutureOnNewState is set to true, the future states will be cleared when a new state is added. If it's set to false (which is the defalt value), the future states will be preserved.

Here is how you can set resetFutureOnNewState when calling the stateHistory method:

const propsStateHistory = stateHistory(propsStore, {
resetFutureOnNewState: false,
});

In this example, the future states will not be cleared when a new state is added, allowing the user to still redo previously undone state changes even after a new state has been added.

- + \ No newline at end of file diff --git a/docs/features/pagination/index.html b/docs/features/pagination/index.html index add185b9..fbb6c6df 100644 --- a/docs/features/pagination/index.html +++ b/docs/features/pagination/index.html @@ -4,7 +4,7 @@ Pagination | Elf | A Reactive Store with Magical Powers - + @@ -13,7 +13,7 @@ Instead, server-side pagination is used, where the server sends just a single page of data at a time.

Usually, we also want to cache pages that already have been fetched, in order to spare the need for an additional request.

info

This feature requires @ngneat/elf-entities

Using this feature, you can manage pagination by using the store. First, you need to install the package by using the CLI command elf-cli install and selecting the pagination package, or via npm:

npm i @ngneat/elf-pagination

To use this feature, provide the withPagination props factory function in the createStore call:

import { createStore } from '@ngneat/elf';
import { withEntities } from '@ngneat/elf-entities';
import { withPagination } from '@ngneat/elf-pagination';

interface Todo {
id: number;
label: string;
}

const todosStore = createStore(
{ name: 'todos' },
withEntities<Todo>(),
withPagination()
);

Call updatePaginationData() with a configuration object that determines the various pagination settings, and call setPage() whenever you want to define the ids that belong to a certain page number.

Note: pagination can start at index 0 or 1.

todos.repository
import { PaginationData } from '@ngneat/elf-pagination';

export function addTodos(response: PaginationData & { data: Todo[] }) {
const { data, ...paginationData } = response;

todosStore.update(
addEntities(todos),
updatePaginationData(paginationData),
setPage(
1,
data.map((c) => c.id)
)
);
}

In your server calls, you can use the skipWhilePageExists operator, which enables you to skip server calls if the page exists in the store:

import { skipWhilePageExists } from '@ngneat/elf-pagination';

export function getTodosPage(page: number) {
return http.get(todosUrl).pipe(
tap((todos) => addTodos(todos)),
skipWhilePageExists(page)
);
}

Additional queries and mutations available are:

Queries

selectCurrentPageEntities

Select the current page entities:

import { selectCurrentPageEntities } from '@ngneat/elf-pagination';

todos$ = store.pipe(selectCurrentPageEntities());

selectCurrentPage

Select the current page number (by default it's page 1):

import { selectCurrentPage } from '@ngneat/elf-pagination';

currentPage$ = store.pipe(selectCurrentPage());

selectHasPage

Select whether the page exists:

import { selectHasPage } from '@ngneat/elf-pagination';

hasPage$ = store.pipe(selectHasPage(1));

hasPage

Get whether the page exists:

import { hasPage } from '@ngneat/elf-pagination';

hasPage = store.query(hasPage(1));

selectPaginationData

Select the pagination data:

import { selectPaginationData } from '@ngneat/elf-pagination';

data$ = store.pipe(selectPaginationData());

getPaginationData

Get pagination data:

import { getPaginationData } from '@ngneat/elf-pagination';

data = store.query(getPaginationData());

Mutations

setCurrentPage

Set the current page:

import { setCurrentPage } from '@ngneat/elf-pagination';

store.update(setCurrentPage(2));

setPage

Set the ids belongs to a page:

import { setPage } from '@ngneat/elf-pagination';

store.update(setPage(2, [id, id, id]));

updatePaginationData

Set the current page:

import { updatePaginationData } from '@ngneat/elf-pagination';

store.update(
updatePaginationData({
currentPage: 1,
perPage: 10,
total: 96,
lastPage: 10,
})
);

deletePage

Delete a page:

import { deletePage } from '@ngneat/elf-pagination';

store.update(deletePage(2));

deleteAllPages

Delete all pages:

import { deleteAllPages } from '@ngneat/elf-pagination';

store.update(deleteAllPages());
- + \ No newline at end of file diff --git a/docs/features/persist-state/index.html b/docs/features/persist-state/index.html index b14a28de..74348984 100644 --- a/docs/features/persist-state/index.html +++ b/docs/features/persist-state/index.html @@ -4,13 +4,13 @@ Persist State | Elf | A Reactive Store with Magical Powers - +

Persist State

The persistState() function gives you the ability to persist some of the app’s state, by saving it to localStorage/sessionStorage or anything that implements the StorageEngine API, and restore it after a refresh.

First, you need to install the package by using the CLI command elf-cli install and selecting the persist-state package, or via npm:

npm i @ngneat/elf-persist-state

To use it you should call the persistState() function, passing the store and the options:

import { createStore, withProps, select } from '@ngneat/elf';
import {
persistState,
localStorageStrategy,
sessionStorageStrategy,
} from '@ngneat/elf-persist-state';

interface AuthProps {
user: { id: string } | null;
}

const authStore = createStore({ name }, withProps<AuthProps>({ user: null }));

export const persist = persistState(authStore, {
key: 'auth',
storage: localStorageStrategy,
});

export const user$ = authStore.pipe(select((state) => state.user));

As the second parameter you should pass a Options object, which can be used to define the following:

  • storage: an Object with setItem, getItem and removeItem method for storing the state (required).
  • source: a method that receives the store and return what to save from it (by default - the entire store).
  • preStoreInit: a method that runs upon initializing the store with a saved value, used for any required modifications before the value is set.
  • preStorageUpdate: a method that runs before saving the store, used for any required modifications before the value is set.
  • key: the name under which the store state is saved (by default - the store name plus a @store suffix).
  • runGuard - returns whether the actual implementation should be run. The default is () => typeof window !== 'undefined'

Elf also exposes the `initialized$` observable. This observable emits after Elf initialized the stores based on the storage's value. For example:
import { persistState, localStorageStrategy } from '@ngneat/elf-persist-state';

const instance = persistState(todoStore, {
key: 'todos',
storage: localStorageStrategy,
});

instance.initialized$.subscribe(console.log);

Async Support

This gives you the option to save a store’s value to a persistent storage, such as indexDB, websql, or any other asynchronous API. Here’s an example that leverages localForage:

import * as localForage from 'localforage';

localForage.config({
driver: localForage.INDEXEDDB,
name: 'myApp',
version: 1.0,
storeName: 'auth',
});

export const persist = persistState(authStore, {
key: 'auth',
storage: localForage,
});

Excluding keys from the state

The excludeKeys() operator can be used to exclude keys from the state:

import {
excludeKeys,
persistState,
localStorageStrategy,
} from '@ngneat/elf-persist-state';

persistState(todoStore, {
key: 'todos',
storage: localStorageStrategy,
source: () => todoStore.pipe(excludeKeys(['ids', 'entities'])),
});

Performance Optimization

By default, the plugin will update the storage upon each store's change. Some applications perform multiple updates in a second, and update the storage on each change can be costly.

For such cases, it's recommended to use the debounceTime operator. For example:

import { persistState, localStorageStrategy } from '@ngneat/elf-persist-state';
import { debounceTime } from 'rxjs/operators';
persistState(todoStore, {
key: 'todos',
storage: localStorageStrategy,
source: () => todoStore.pipe(debounceTime(1000)),
});

preStorageUpdate

The preStorageUpdate option is a function that is called before the state is saved to the storage. It receives two parameters: storeName and state. The storeName is a string representing the name of the store, and state is the current state of the store.

This function is useful when you want to modify the state before it is saved to the storage. For example, you might want to remove some sensitive data or transform the state in some way.

The function should return the modified state which will be saved to the storage. If you don't return anything from this function, the original state will be saved.

Here is an example of how to use preStorageUpdate:

import { persistState, localStorageStrategy } from '@ngneat/elf-persist-state';

const preStorageUpdate = (storeName, state) => {
const newState = { ...state };
if (storeName === 'todos') {
delete newState.sensitiveData;
}
return newState;
}


persistState(todoStore, {
key: 'todos',
storage: localStorageStrategy,
preStorageUpdate,
});

In this example, the preStorageUpdate function removes the sensitiveData property from the state before it is saved to storage.

- + \ No newline at end of file diff --git a/docs/features/requests-result/index.html b/docs/features/requests-result/index.html index 996f5eae..7d265673 100644 --- a/docs/features/requests-result/index.html +++ b/docs/features/requests-result/index.html @@ -4,14 +4,14 @@ Requests Result | Elf | A Reactive Store with Magical Powers - +

Requests Result

Elf provides a convenient way to track the status of async requests and combine it with your store selectors. First, you need to install the package by using the CLI command elf-cli install and selecting the requests package, or via npm:

npm i @ngneat/elf-requests

Now, simply add to your source request the trackRequestResult operator, and give it a unique key:

todos.service.ts
import { trackRequestResult } from '@ngneat/elf-requests';
import { setTodos } from './todos.repository';

export function fetchTodos() {
return http.get(todosUrl).pipe(
tap(setTodos),
trackRequestResult(['todos'])
);
}

Now, we can use the joinRequestResult operator with our store selectors:

todos.repository
import { createStore } from '@ngneat/elf';
import { withEntities } from '@ngneat/elf-entities';
import { joinRequestResult } from '@ngneat/elf-requests';

interface Todo {
id: number;
label: string;
}

const todosStore = createStore({ name: 'todos' }, withEntities<Todo>());

export const entities$ = store.pipe(
selectAllEntities(),
joinRequestResult(['todos'])
);

The entities$ selector will now track the todos request and will provide the following information:

entities$.subscribe(
({ isLoading, isError, isSuccess, data, error, status }) => {
console.log(
isLoading,
isError,
isSuccess,
status,
successfulRequestsCount,
data, // typed as Todo[]
error
);
}
);

We can also initialize the selector as idle by using joinRequestResult(['todos'], { initialStatus: 'idle' })

Here is an example of a dynamic selector:

export const selectTodo = (id: Todo['id]) => store.pipe(
selectEntity(id),
joinRequestResult(['todos', id])
);

Additional Options

  • staleTime - When we should refetch
todos.service.ts
import { trackRequestResult } from '@ngneat/elf-requests';
import { setTodos } from './todos.repository';

export function fetchTodos() {
return http.get(todosUrl).pipe(
tap(setTodos),
trackRequestResult(['todos'], { staleTime: 10_000 })
);
}
  • skipCache - Ignore everything and perform the request
todos.service.ts
import { trackRequestResult } from '@ngneat/elf-requests';
import { setTodos } from './todos.repository';

export function fetchTodos() {
return http.get(todosUrl).pipe(
tap(setTodos),
trackRequestResult(['todos'], { skipCache: true })
);
}
  • preventConcurrentRequest - Don't perform the request if there is a pending request, defaults to true
todos.service.ts
import { trackRequestResult } from '@ngneat/elf-requests';
import { setTodos } from './todos.repository';

export function fetchTodos() {
return http.get(todosUrl).pipe(
tap(setTodos),
trackRequestResult(['todos'], { preventConcurrentRequest: false })
);
}
  • cacheResponseData - Cache the response data and emit it as responseData for skipped requests, defaults to false
todos.service.ts
import { trackRequestResult } from '@ngneat/elf-requests';
import { setTodos } from './todos.repository';

export function fetchTodos() {
return http.get(todosUrl).pipe(
tap(setTodos),
trackRequestResult(['todos'], { cacheResponseData: true })
);
}

fetchTodos().subscribe((todos) => {
// This will be called with the cached result, or once the request is completed
});
  • additionalKeys - Cache the request result for additional keys, e.g. based on properties from the response data
todos.service.ts
import { trackRequestResult } from '@ngneat/elf-requests';
import { setTodos } from './todos.repository';

export function fetchTodos() {
return http.get(todosUrl).pipe(
tap(setTodos),
trackRequestResult(['todos'], { additionalKeys: todos => todos.map(todo => (['todos', todo.id])) })
);
}

Operators

import { filterSuccess, filterError } from '@ngneat/elf-requests';

entities$.pipe(filterSuccess()).subscribe((result) => {
// This will be called only upon success
});

entities$.pipe(filterError()).subscribe((result) => {
// This will be called only upon error
});

entities$
.pipe(
mapResultData((data) => {
// This will be called only when data is defined (not `null` or `undefined`)
return data.filter((todo) => todo.id === 1);
})
)
.subscribe((result) => {});

API

  • getRequestResult - getRequestResult(key): Observable<RequestResult>
  • deleteRequestResult - deleteRequestResult(key): void
  • resetStaleTime - resetStaleTime(key): void
  • clearRequestsResult - clearRequestsResult(): void
- + \ No newline at end of file diff --git a/docs/features/requests/requests-cache/index.html b/docs/features/requests/requests-cache/index.html index 9cecfaa6..d7361de8 100644 --- a/docs/features/requests/requests-cache/index.html +++ b/docs/features/requests/requests-cache/index.html @@ -4,7 +4,7 @@ Cache | Elf | A Reactive Store with Magical Powers - + @@ -15,7 +15,7 @@ request is cached. The default return value is EMPTY observable.

todos.service.ts
import { skipWhileTodosCached, setTodos } from './todos.repository';

export function fetchTodos() {
return http.get(todosUrl).pipe(
tap((todos) => setTodos(todos)),
skipWhileTodosCached('todos', { returnSource: of([]) })
);
}

You can monitor and change the request cache status for your APIs using the following queries and mutations:

Queries

selectRequestCache

Select the cache status of the provided request key:

import { selectRequestCache } from '@ngneat/elf-requests';

todosCacheStatus$ = store.pipe(selectRequestCache('todos'));

getRequestCache

Get the cache status of the provided request key:

import { getRequestCache } from '@ngneat/elf-requests';

todosCacheStatus = store.query(getRequestCache('todos'));

selectIsRequestCached

Select whether the cache status of the provided request key isn't none:

import { selectIsRequestCached } from '@ngneat/elf-requests';

const isCached$ = store.pipe(selectIsRequestCached('todos'));
const isPartialCached$ = store.pipe(
selectIsRequestCached('todos', { value: 'partial' })
);

Get whether the cache status of the provided request key isn't none:

import { isRequestCached } from '@ngneat/elf-requests';

const isCached = store.query(isRequestCached('todos'));
const isPartialCached = store.query(
isRequestCached('todos', { value: 'partial' })
);

Mutations

updateRequestCache

import { updateRequestCache } from '@ngneat/elf-requests';

store.update(updateRequestCache('todos'));
store.update(updateRequestCache('todos', { value: 'partial' }));
store.update(updateRequestCache('todos', { value: 'none' }));
store.update(updateRequestCache('todos', { ttl: 1000 }));

If you pass ttl (time to live) when updating a cache record, that represents the time (in milliseconds) that key will have the value that was set (afterward, it reverts to 'none').

updateRequestsCache

import { updateRequestsCache } from '@ngneat/elf-requests';

store.update(
updateRequestsCache({
keyOne: {
value: 'partial',
},
})
);

store.update(updateRequestsCache(['keyOne', 'keyTwo'], { value: 'partial' }));

store.update(
updateRequestsCache(['keyOne', 'keyTwo'], { value: 'partial', ttl: 1000 })
);

If you pass ttl (time to live) when updating a cache record, that represents the time (in milliseconds) that key will have the value that was set (afterward, it reverts to 'none'). This parameter can be used to set individual ttl values for each key when updating multiple keys at once. If a ttl is not passed for a key, the value for that key does not expire.

clearRequestsCache

import { clearRequestsCache } from '@ngneat/elf-requests';

store.update(clearRequestsCache());
- + \ No newline at end of file diff --git a/docs/features/requests/requests-data-source/index.html b/docs/features/requests/requests-data-source/index.html index 115bb2a4..a552d74b 100644 --- a/docs/features/requests/requests-data-source/index.html +++ b/docs/features/requests/requests-data-source/index.html @@ -4,13 +4,13 @@ Data Source | Elf | A Reactive Store with Magical Powers - +

Data Source

With the createRequestDataSource function, we can easily select the state of an async request from our store:

import { createStore } from '@ngneat/elf';
import {
withRequestsStatus,
withRequestsCache,
createRequestDataSource,
} from '@ngneat/elf-requests';
import { selectAllEntities, withEntities } from '@ngneat/elf-entities';

const store = createStore(
{ name: 'todos' },
withEntities<Todo>(),
withRequestsStatus(),
withRequestsCache()
);

export const todosDataSource = createRequestDataSource({
data$: () => store.pipe(selectAllEntities()),
requestKey: 'todos',
dataKey: 'todos',
store,
});

The todosDataSource will return a function named data$ that returns an observable with the following shape:

todosDataSource.data$().subscribe(({ todos, loading, error }) => {});

And the following operators and functions that operates on the provided requestKey:

store.update(
setTodos(todos),
todosDataSource.setSuccess();
todosDataSource.setCached();
)

todosDataSource.trackRequestStatus();
todosDataSource.skipWhileCached();

Dynamic DataSource

We can use the createRequestDataSource with a dynamic key:

import { createStore } from '@ngneat/elf';
import {
withRequestsStatus,
withRequestsCache,
createRequestDataSource,
} from '@ngneat/elf-requests';
import { selectEntity, withEntities } from '@ngneat/elf-entities';

const store = createStore(
{ name: 'todos' },
withEntities<Todo>(),
withRequestsStatus(),
withRequestsCache()
);

export const todoDataSource = createRequestDataSource({
data$: (key: number) => store.pipe(selectEntity(key)),
dataKey: 'todo',
store,
});

Note that you should not pass a requestKey in this case. With this change, you will get the following API:

todoDataSource.data$({ key: 1 }).subscribe(({ todo, loading, error }) => {})

store.update(
addTodo(todo),
todoDataSource.setSuccess({ key: 1 });
todoDataSource.setCached({ key: 1 });
)

todoDataSource.trackRequestStatus({ key: 1 });
todoDataSource.skipWhileCached({ key: 1 });
- + \ No newline at end of file diff --git a/docs/features/requests/requests-status/index.html b/docs/features/requests/requests-status/index.html index 4ecd8d28..9dbc73c5 100644 --- a/docs/features/requests/requests-status/index.html +++ b/docs/features/requests/requests-status/index.html @@ -4,14 +4,14 @@ Status | Elf | A Reactive Store with Magical Powers - +

Status

Using this feature, you can manage the status of API calls in your store. First, you need to install the package by using the CLI command elf-cli install and selecting the requests package, or via npm:

npm i @ngneat/elf-requests

To use this feature, provide the withRequestsStatus props factory function in the createStore call:

todos.repository
import { createStore } from '@ngneat/elf';
import { withEntities } from '@ngneat/elf-entities';
import {
withRequestsStatus,
createRequestsStatusOperator,
} from '@ngneat/elf-requests';

interface Todo {
id: number;
label: string;
}

const todosStore = createStore(
{ name: 'todos' },
withEntities<Todo>(),
// You can pass the keys type
withRequestsStatus<`todos` | `todo-${string}`>()
);

Now we can use the createRequestsStatusOperator function that takes a store and returns a custom operator. That operator takes the request key and sets its initial status to pending. It also updates it to error when it fails.

todos.repository
import {
withRequestsStatus,
createRequestsStatusOperator,
} from '@ngneat/elf-requests';

// ...
const todosStore = createStore({ name: 'todos', withEntities<Todo>(); });
export const trackTodosRequestsStatus =
createRequestsStatusOperator(todosStore);

And use it with our async source:

todos.service.ts
import { setTodos, trackTodosRequestsStatus } from './todos.repository';

export function fetchTodos() {
return http.get(todosUrl).pipe(
tap(setTodos),
trackTodosRequestsStatus('todos')
);
}

Upon successful completion, the success status must be manually set as follows:

todos.repository.ts
import { updateRequestStatus } from '@ngneat/elf-requests';
import { setTodos } from './todos.repository';

export function setTodos(todos: Todo[]) {
store.update(
addEntities(todos),
updateRequestStatus('todos', 'success')
);
}

You need to set it manually to avoid a redundant update and have the option to define what a "successful" response is.

The default status of any request is idle. You can use the initializeAsPending function to initialize a request as pending:

import { createStore } from '@ngneat/elf';
import { withEntities } from '@ngneat/elf-entities';
import { withRequestsStatus, initializeAsPending } from '@ngneat/elf-requests';

const todosStore = createStore(
{ name: 'todos' },
withEntities<Todo>(),
withRequestsStatus(
initializeAsPending('todos')
)
);

You can monitor and change the request status for your APIs using the following queries and mutations:

Queries

selectRequestStatus

Select the status of the provided request key:

import { selectRequestStatus } from '@ngneat/elf-requests';

todosStatus$ = store.pipe(selectRequestStatus('todos'));

// This will return success when either the `todos` key or the `todo-1` key is succeeded
todoStatus$ = store.pipe(selectRequestStatus('todo-1', { groupKey: 'todos' }));

getRequestStatus

Get the status of the provided request key:

import { getRequestStatus } from '@ngneat/elf-requests';

todosStatus = store.query(getRequestStatus('todos'));

selectIsRequestPending

Select whether the status of the provided request key is pending:

import { selectIsRequestPending } from '@ngneat/elf-requests';

pending$ = store.pipe(selectIsRequestPending('todos'));

Mutations

updateRequestStatus

import { updateRequestStatus } from '@ngneat/elf-requests';

store.update(updateRequestStatus('todos', 'idle'));
store.update(updateRequestStatus('todos', 'pending'));
store.update(updateRequestStatus('todos', 'success'));
store.update(updateRequestStatus('todos', 'error', error));

updateRequestsStatus

import { updateRequestsStatus } from '@ngneat/elf-requests';

store.update(
updateRequestsStatus({
keyOne: {
value: 'success',
},
})
);

store.update(updateRequestsStatus(['keyOne', 'keyTwo'], 'success'));
store.update(updateRequestStatus(['keyOne', 'keyTwo'], 'error', error));

clearRequestsStatus

import { clearRequestsStatus } from '@ngneat/elf-requests';

store.update(clearRequestsStatus());
- + \ No newline at end of file diff --git a/docs/immer/index.html b/docs/immer/index.html index c8812252..37d5e6ec 100644 --- a/docs/immer/index.html +++ b/docs/immer/index.html @@ -4,13 +4,13 @@ Using Immer | Elf | A Reactive Store with Magical Powers - +

Using Immer

When working with immutable objects, we often get to what’s called a “spread hell” situation. If you prefer working with immutable objects in a mutable fashion, you can use immer with Elf.

Create a mutation function:

store/mutations.ts
import { produce } from 'immer';

export function write<S>(updater: (state: S) => void): (state: S) => S {
return function (state) {
return produce(state, (draft) => {
updater(draft as S);
});
};
}

Now you can use it in the store's update function:

todos.respository.ts
import { withProps, createStore } from '@ngneat/elf';
import { withEntities, selectAllEntities, updateEntities } from '@ngneat/elf-entities';

interface Todo {
id: number;
title: string;
completed: boolean;
}

export interface TodosProps {
filter: 'ALL' | 'ACTIVE' | 'COMPLETED';
}

const store = createStore(
{ name: 'todos' },
withEntities<Todo>(),
withProps<TodosProps>({ filter: 'ALL' })
);

export const todos$ = store.pipe(selectAllEntities());

export function updateFilter(filter: TodosProps['filter']) {
store.update(
write((state) => {
state.filter = filter;
})
);
}

export function updateCompleted(id: Todo['id']) {
store.update(
updateEntities(
id,
write<Todo>((entity) => (entity.completed = !entity.completed))
)
);
}
- + \ No newline at end of file diff --git a/docs/installation/index.html b/docs/installation/index.html index e18dfcec..37675751 100644 --- a/docs/installation/index.html +++ b/docs/installation/index.html @@ -4,13 +4,13 @@ Installation | Elf | A Reactive Store with Magical Powers - + - + \ No newline at end of file diff --git a/docs/miscellaneous/batching/index.html b/docs/miscellaneous/batching/index.html index d13f438b..206e003e 100644 --- a/docs/miscellaneous/batching/index.html +++ b/docs/miscellaneous/batching/index.html @@ -4,14 +4,14 @@ Batching | Elf | A Reactive Store with Magical Powers - +

Batching

When using the store's update function, you can pass multiple mutation functions:

store.update(
setProp('count', 1),
addEntities([todo, todo]),
deleteEntities(1)
);

In this case, subscribers will only receive one emission instead of three.

emitOnce

There are cases where you have multiple update functions of the same store that you want to batch together. To do so you can use the emitOnce function:

todos.repository.ts
export function updateCount() {
store.update(
setProp('count', 1),
);
}

export function updateUser() {
store.update(
setProp('user', null),
);
}
import { emitOnce } from '@elf/store';
import { updateCount, updateUser } from './todos.repository';

emitOnce(() => {
updateCount();
updateUser();
});

In this case, subscribers will only receive one emission instead of two.

Also, you might face the need to use functions that use emitOnce inside another emitOnce:

table.repository.ts
export function restoreFilters() {
emitOnce(() => {
store.update(
setProp('filters', null),
);
resetPagination();
});
}

export function resetSort() {
emitOnce(() => {
store.update(
setProp('sort', null),
);
resetPagination();
});
}

export function resetPagination() {
store.update(
setProp('pagination', null),
);
}
import { emitOnce } from '@elf/store';
import { restoreFilters, resetSort } from './table.repository';

emitOnce(() => {
restoreFilters();
resetSort();
});

In this case, subscribers will only receive one emission instead of two.

emitOnceAsync

In some cases, you might need to use emitOnce with async functions or observables. To do so, you can use emitOnceAsync:

todos.repository.ts
export async function updateCount() {
const newCount = await fetchCount(); // Fetch count from API
store.update(setProp('count', newCount));
}

export async function updateUser() {
const newUser = await fetchUser(); // Fetch user from API
store.update(setProp('user', newUser));
}

export function clearCount() {
store.update(setProp('user', null));
}

export function clearUser() {
store.update(setProp('user', null));
}
import { emitOnceAsync } from '@elf/store';
import { updateCount, updateUser } from './todos.repository';

await emitOnceAsync(async () => {
await updateCount();
await updateUser();
});

In this case, subscribers will also only receive one emission instead of two.

You can also use emitOnce and emitOnceAsync inside another emitOnceAsync:

import { emitOnce, emitOnceAsync } from '@elf/store';
import { updateCount, updateUser } from './todos.repository';

async function updateCountAndUser() {
await emitOnceAsync(async () => {
await updateCount();
await updateUser();
});
}

await emitOnceAsync(async () => {
emitOnce(() => {
clearCount();
clearUser();
});
await updateCountAndUser();
});

You can also provide an observable to emitOnceAsync, in this case, the store will only update when the observable emits its first value.

Using emitOnceAsync inside emitOnce will not work as expected because emitOnce will not wait for the async function to finish.

Use emitOnceAsync with caution, the store will not update until the async function finishes or the observable emits its first value. If your async function or observable takes too long to finish, the app might appear unresponsive.

- + \ No newline at end of file diff --git a/docs/miscellaneous/hooks/index.html b/docs/miscellaneous/hooks/index.html index 68e21930..7086c91c 100644 --- a/docs/miscellaneous/hooks/index.html +++ b/docs/miscellaneous/hooks/index.html @@ -4,13 +4,13 @@ Hooks | Elf | A Reactive Store with Magical Powers - +

Hooks

Elf allows customizing some of the behavior via elfHooks

registerPreStoreUpdate

Provide a function to modify the store value after reducers has run.

The callback function takes three arguments:

  • currentState - state before reducers has run
  • nextState - state after reducers has run
  • storeName - the name of the store

One scenario when this functionality could be useful is when a developer wants to prevent bugs associated with mutability by freezing the store value during the development:

import { elfHooks, deepFreeze } from '@ngneat/elf';

if (!environment.production) {
elfHooks.registerPreStoreUpdate((currentState, nextState, storeName) => {
return deepFreeze(nextState);
});
}

registerPreStateInit

Provide a function to modify initial state of the store.

The callback function takes two arguments:

  • initialState - the initial state of the store
  • storeName - the name of the store

It could be useful is when a developer wants to prevents bugs associated with mutability by freezing initial state value during the development:

import { elfHooks, deepFreeze } from '@ngneat/elf';

if (!environment.production) {
elfHooks.registerPreStateInit((initialState, storeName) => {
return deepFreeze(initialState);
});
}
- + \ No newline at end of file diff --git a/docs/miscellaneous/operators/index.html b/docs/miscellaneous/operators/index.html index 480d92bb..721b4392 100644 --- a/docs/miscellaneous/operators/index.html +++ b/docs/miscellaneous/operators/index.html @@ -4,13 +4,13 @@ Operators | Elf | A Reactive Store with Magical Powers - +

Operators

Elf provides the following operators:

filterNil

Filters undefined or null values:

import { filterNil } from '@ngneat/elf';

foo$.pipe(filterNil());

Get the the first item from an array:

import { head } from '@ngneat/elf';

skills$.pipe(head());

distinctUntilArrayItemChanged

A distinctUntilChanged implementation for arrays:

import { distinctUntilArrayItemChanged } from '@ngneat/elf';

skills$.pipe(distinctUntilArrayItemChanged());
- + \ No newline at end of file diff --git a/docs/miscellaneous/production/index.html b/docs/miscellaneous/production/index.html index f6b71d3a..ae982d18 100644 --- a/docs/miscellaneous/production/index.html +++ b/docs/miscellaneous/production/index.html @@ -4,13 +4,13 @@ Production Mode | Elf | A Reactive Store with Magical Powers - + - + \ No newline at end of file diff --git a/docs/miscellaneous/props-factory/index.html b/docs/miscellaneous/props-factory/index.html index b3dab6ef..1d2f9cac 100644 --- a/docs/miscellaneous/props-factory/index.html +++ b/docs/miscellaneous/props-factory/index.html @@ -4,13 +4,13 @@ Props Factory | Elf | A Reactive Store with Magical Powers - +

Props Factory

Using propsFactory is ideal when there are a number of stores that need the same properties. For example, let's say we want to have a version property for each of our stores:

store-props.ts
import { propsFactory } from '@ngneat/elf';

export const {
withVersion,
updateVersion,
selectVersion,
resetVersion,
getVersion,
setVersion,
setVersionInitialValue,
} = propsFactory('version', {
initialValue: 1,
});

The propsFactory function takes the name of a property and the initial value and returns everything we need to add, query, and mutate that property. The type of a property is inferred based on the initialValue.

todos.repository.ts
import { withVersion, updateVersion, selectVersion, setVersionInitialValue } from '@app/store-props.ts';

setVersionInitialValue(1.1);

const store = createStore({ name: 'todos' }, withVersion());

store.update(updateVersion(2));
store.pipe(selectVersion());
store.query(getVersion);

If you need to use a complex type you can use initialValue: {} as MyInterface.

Props Array Factory

propsArrayFactory is similar to propsFactory but for properties of type array:

store-props.ts
import { propsArrayFactory } from '@ngneat/elf';

export const {
withSkills,
addSkills,
removeSkills,
toggleSkills,
updateSkills,
getSkills,
resetSkills,
selectSkills,
setSkills,
setSkillsInitialValue
inSkills,
} = propsArrayFactory('skills', {
initialValue: [] as string[],
});

propsArrayFactory is designed to handle primitive arrays. For managing collections of objects, it's recommended to use entitiesPropsFactory.

tip

In addition, it's useful for managing a collection of primitives in one store. Consider the case of a books store, and a userCollectionIds that contains book ids.

- + \ No newline at end of file diff --git a/docs/miscellaneous/registry/index.html b/docs/miscellaneous/registry/index.html index 7e2fc892..dfec9554 100644 --- a/docs/miscellaneous/registry/index.html +++ b/docs/miscellaneous/registry/index.html @@ -4,13 +4,13 @@ Registry | Elf | A Reactive Store with Magical Powers - +

Registry

Elf keeps your stores in a registry and exposes the following functions:

getRegistry

Get the the registry:

import { getRegistry } from '@ngneat/elf';

const stores = getRegistry();

getStore

Get a reference to a store by name:

import { getStore } from '@ngneat/elf';

const todosStore = getStore('name');

getStoresSnapshot

Get a snapshot of stores values:

import { getStoresSnapshot } from '@ngneat/elf';

const storesValues = getStoresSnapshot();

registry$

An observable that emits when a store is added or removed:

import { registry$ } from '@ngneat/elf';

registry$.subscribe(event => {
// event = { type: 'add' | 'remove'; store: Store }
})
- + \ No newline at end of file diff --git a/docs/recipes/index.html b/docs/recipes/index.html index ee8e64b3..2d7a72c4 100644 --- a/docs/recipes/index.html +++ b/docs/recipes/index.html @@ -4,13 +4,13 @@ Recipes | Elf | A Reactive Store with Magical Powers - +

Recipes

Selectors Optimization

tip

Beware of premature optimizations

Imagine we have a todos store and we subscribe to the following selectors twice, at two different places simultaneously:

todos.repository.ts
export const todos$ = store.pipe(selectAllEntities());

// One component
useObservable(todos$) // React
todos$ | async // Angular

// Second component
useObservable(todos$) // React
todos$ | async // Angular

Due to the nature of observables, the selectAllEntities() operator will map over the entities twice, once for each subscription. We can use the shareReplay operator to optimize it:

todos.repository.ts
import { shareReplay } from 'rxjs/operators';

export const todos$ = store.pipe(selectAllEntities(), shareReplay({ refCount: true }))

With this change, the selectAllEntities operator will now share the result with every subscriber.

Reset Stores

Resetting your stores can be useful when you want to clean the store's data upon user logout. We can combine the registry and store.reset() to create a resetStores function:

import { getRegistry } from '@ngneat/elf';

export function resetStores() {
getRegistry().forEach(store => store.reset())
}
- + \ No newline at end of file diff --git a/docs/repository/index.html b/docs/repository/index.html index 05382e7f..c186a767 100644 --- a/docs/repository/index.html +++ b/docs/repository/index.html @@ -4,13 +4,13 @@ The Repository Pattern | Elf | A Reactive Store with Magical Powers - +

The Repository Pattern

One way to use Elf is following the Repository Design Pattern. Implementing the Repository pattern is relatively simple. It's a file that encapsulates the store queries and mutations:

auth.repository.ts
import { createStore, withProps, select } from '@ngneat/elf';

interface AuthProps {
user: { id: string } | null;
}

const authStore = createStore(
{ name: 'auth' },
withProps<AuthProps>({ user: null })
);

export const user$ = authStore.pipe(select((state) => state.user));

export function updateUser(user: AuthProps['user']) {
authStore.update((state) => ({
...state,
user,
}));
}

The Repository pattern provides 2 main benefits:

  1. Using the pattern, you can replace your data store without changing your business code.
  2. It encourages you to implement all store operations in one place, making your code more reusable and easy to find.

You can also use the object-oriented programming (OOP) approach:

auth.repository.ts
import { createStore, withProps, select } from '@ngneat/elf';

interface AuthProps {
user: { id: string } | null;
}

const authStore = createStore(
{ name: 'auth' },
withProps<AuthProps>({ user: null })
);

export class AuthRepository {
user$ = authStore.pipe(select((state) => state.user));

updateUser(user: AuthProps['user']) {
authStore.update((state) => ({
...state,
user,
}));
}
}

Creating a Repository with the CLI

Elf comes with a CLI that'll generate a repository with all the features you need. Check out the docs for more information.

- + \ No newline at end of file diff --git a/docs/side-effects/index.html b/docs/side-effects/index.html index f50bd13d..66db9148 100644 --- a/docs/side-effects/index.html +++ b/docs/side-effects/index.html @@ -4,13 +4,13 @@ Managing Side Effects | Elf | A Reactive Store with Magical Powers - +
-

Managing Side Effects

Elf is a state management solution, and it doesn't force you to manage side effects in a certain way. But the same team also created companion packages that can be used with Elf to handle side effects.

A side-effect is anything that happens outside of the normal flow of the store—interacting with the API asynchronously, setting intervals and timeouts, updating the local storage, etc.

It's entirely up to the developer to model and implement those tasks and update the store.

Let's examine three ways to handle side effects in our application:

Using Services

In most cases, services are the most straightforward solution:

todos.service.ts
import { setTodos, addTodo } from './todos.repository';
import { tap } from 'rxjs/operators';

export function fetchTodos() {
return http.get('todos').pipe(
tap(setTodos)
)
}

export function addTodo(todo: Todo) {
return http.post('todos', todo).pipe(
tap(addTodo)
)
}

And subscribe in the component. Below is an example using a React component:

import { useObservable } from '@ngneat/react-rxjs';
import { todos$ } from './todos.repository';
import { fetchTodos } from './todos.service';

function Todos() {
const [todos] = useObservable(todos$);

useEffect(() => {
fetchTodos().subscribe();
}, [])

return <div>{todos}</div>
}

Check out the @ngneat/react-rxjs library for more information.

Using Effects

We can register effects that'll execute when we dispatch actions using @ngneat/effects.

todos.effects.ts

import { createEffect, ofType } from '@ngneat/effects';

const loadTodos = createAction('[Todos] Load');

export const loadTodos$ = createEffect(actions =>
actions.pipe(
ofType(loadTodos),
switchMap((todo) => todosApi.loadTodos()),
tap(setTodos)
)
);

Below is an example using a React component:

import { useEffects } from '@ngneat/effects-hook';
import { dispatch } from '@ngneat/effects';
import { useObservable } from '@ngneat/react-rxjs';
import { useEffect } from 'react';

export function TodosPage() {
const [todos] = useObservable(todos$);

useEffects([loadTodos$]);

useEffect(() => dispatch(loadTodos()), []);

return {todos}
}

In the official documentation, you can find more information and an Angular example.

Using Effect Functions

You may prefer effect functions if you're not a big fan of actions.

todos.effects.ts
import { createEffectFn } from '@ngneat/effects';

export const searchTodoEffect = createEffectFn((searchTerm$: Observable<string>) => {
return searchTerm$.pipe(
debounceTime(300),
switchMap((searchTerm) => fetchTodos({ searchTerm })),
tap(setTodos)
);
});

Below is an example using a React component:

import { useEffectFn } from '@ngneat/effects-hooks';

function SearchComponent() {
const searchTodo = useEffectFn(searchTodoEffect);

return <input onChange = {({ target: { value } }) => searchTodo(value) }/>
}

In the official documentation, you can find more information and an Angular example. It's possible to use effects and effect functions simultaneously if you like.

- +

Managing Side Effects

Elf is a state management solution, and it doesn't force you to manage side effects in a certain way. But the same team also created companion packages that can be used with Elf to handle side effects.

A side-effect is anything that happens outside of the normal flow of the store—interacting with the API asynchronously, setting intervals and timeouts, updating the local storage, etc.

It's entirely up to the developer to model and implement those tasks and update the store.

Let's examine three ways to handle side effects in our application:

Using Services

In most cases, services are the most straightforward solution:

todos.service.ts
import { setTodos, addTodo } from './todos.repository';
import { tap } from 'rxjs/operators';

export function fetchTodos() {
return http.get('todos').pipe(
tap(setTodos)
)
}

export function addTodo(todo: Todo) {
return http.post('todos', todo).pipe(
tap(addTodo)
)
}

And subscribe in the component. Below is an example using a React component:

import { useObservable } from '@ngneat/react-rxjs';
import { todos$ } from './todos.repository';
import { fetchTodos } from './todos.service';

function Todos() {
const [todos] = useObservable(todos$);

useEffect(() => {
fetchTodos().subscribe();
}, [])

return <div>{todos}</div>
}

Check out the @ngneat/react-rxjs library for more information.

Using Effects

We can register effects that'll execute when we dispatch actions using @ngneat/effects.

todos.effects.ts

import { createAction, createEffect, ofType } from '@ngneat/effects';

const loadTodos = createAction('[Todos] Load');

export const loadTodos$ = createEffect(actions =>
actions.pipe(
ofType(loadTodos),
switchMap((todo) => todosApi.loadTodos()),
tap(setTodos)
)
);

Below is an example using a React component:

import { useEffects } from '@ngneat/effects-hook';
import { dispatch } from '@ngneat/effects';
import { useObservable } from '@ngneat/react-rxjs';
import { useEffect } from 'react';

export function TodosPage() {
const [todos] = useObservable(todos$);

useEffects([loadTodos$]);

useEffect(() => dispatch(loadTodos()), []);

return {todos}
}

In the official documentation, you can find more information and an Angular example.

Using Effect Functions

You may prefer effect functions if you're not a big fan of actions.

todos.effects.ts
import { createEffectFn } from '@ngneat/effects';

export const searchTodoEffect = createEffectFn((searchTerm$: Observable<string>) => {
return searchTerm$.pipe(
debounceTime(300),
switchMap((searchTerm) => fetchTodos({ searchTerm })),
tap(setTodos)
);
});

Below is an example using a React component:

import { useEffectFn } from '@ngneat/effects-hooks';

function SearchComponent() {
const searchTodo = useEffectFn(searchTodoEffect);

return <input onChange = {({ target: { value } }) => searchTodo(value) }/>
}

In the official documentation, you can find more information and an Angular example. It's possible to use effects and effect functions simultaneously if you like.

+ \ No newline at end of file diff --git a/docs/store/index.html b/docs/store/index.html index 84419e63..1d4ad0cd 100644 --- a/docs/store/index.html +++ b/docs/store/index.html @@ -4,7 +4,7 @@ The Store | Elf | A Reactive Store with Magical Powers - + @@ -12,7 +12,7 @@

The Store

The createStore function is used to initialize a store with a state. The first argument is a configuration object where a name of the store is specified. After the first argument any number of features can be specified which describe the nature of the store.

An example of a simple store:

import { createStore, withProps } from '@ngneat/elf';

interface AuthProps {
user: { id: string } | null;
}

const authStore = createStore(
{ name: 'auth' },
withProps<AuthProps>({ user: null })
);

An example of a store that contains multiple state features:

const store = createStore(
{ name: 'todo' },
withEntities<Todo>(),
withUIEntities<UIEntity>(),
withProps<{ foo: string }>({ foo: '' })
);

The features can be either one or more of the available features in Elf, or additional features you can create or add from other sources.

Querying the Store

A store is a BehaviorSubject. Therefore, we can subscribe to it to get its initial value and its subsequent values:

authStore.subscribe((state) => {
console.log(state);
});

The select operator

Select a slice from the store:

import { select } from '@ngneat/elf';

const user$ = authStore.pipe(select((state) => state.user));

The select() operator returns an observable that calls distinctUntilChanged() internally, meaning it will only fire when the state changes, i.e., when there is a new reference to the selected state.

We can also query its value once without the need to subscribe:

const state = authstore.getValue();

Updating the Store

To update the store, we can use the update method which receives a callback function, which gets the current state, and returns a new immutable state, which will be the new value of the store:

authStore.update((state) => ({
...state,
user: { id: 'foo' },
}));

Updating a root property

import { setProp, setProps } from '@ngneat/elf';

store.update(setProp('foo', 'bar'));
store.update(setProp('count', (count) => count + 1));

store.update(
setProps({
count: 1,
child: 2,
})
);

store.update(
setProps((state) => ({
count: 1,
nested: {
...state.nested,
bar: 'baz',
},
}))
);
- + \ No newline at end of file diff --git a/docs/third-party/sync-state/index.html b/docs/third-party/sync-state/index.html index 12502dde..fe34a878 100644 --- a/docs/third-party/sync-state/index.html +++ b/docs/third-party/sync-state/index.html @@ -4,14 +4,14 @@ Sync State | Elf | A Reactive Store with Magical Powers - +

Sync State

npm GitHub GitHub Repo stars

Syncs elf store state across tabs

The syncState() function gives you the ability to synchronize an elf store state across multiple tabs, windows or iframes using the Broadcast Channel API.

First, you need to install the package via npm:

npm i elf-sync-state

To use it you should call the syncState() function passing the store:

import { createStore, withProps } from '@ngneat/elf';
import { syncState } from 'elf-sync-state';

interface AuthProps {
user: { id: string } | null;
token: string | null;
}

const authStore = createStore(
{ name: 'auth' },
withProps<AuthProps>({ user: null, token: null })
);

syncState(authStore);

As the second parameter you can pass an optional Options object, which can be used to define the following:

  • channel: the name of the channel (by default - the store name plus a @store suffix).
  • source: a function that receives the store and return what to sync from it. The default is (store) => store.
  • preUpdate: a function to map the event message and get the data. The default is (event) => event.data.
  • runGuard: a function that returns whether the actual implementation should be run. The default is () => typeof window !== 'undefined' && typeof window.BroadcastChannel !== 'undefined'.
import { syncState } from 'elf-sync-state';
import { authStore } from './auth.store';

syncState(authStore, { channel: 'auth-channel' });

The sync state also returns the BroadcastChannel object created or undefined if the runGuard function returns false.

import { syncState } from 'elf-sync-state';
import { authStore } from './auth.store';

const channel: BroadcastChannel | undefined = syncState(authStore);

Sync a subset of the state

The includeKeys() operator can be used to sync a subset of the state:

import { includeKeys, syncState } from 'elf-sync-state';
import { authStore } from './auth.store';

syncState(authStore, {
source: (store) => store.pipe(includeKeys(['user'])),
});

Pre Update interceptor

The preUpdate option can be used to intercept the MessageEvent and modify the data to be synchronized taking into account other properties of the event.

import { includeKeys, syncState } from 'elf-sync-state';
import { authStore } from './auth.store';

syncState(authStore, {
preUpdate: (event) => {
console.log(event);
return event.origin === '' ? undefined : event.data;
},
});

Integration with Elf

The use of this library has been tested together with other Elf libraries, such as elf-entities, elf-persist-state or elf-state-history. I have also tried to be consistent with their programming style and documentation to help with integration.

Here you can see an example of using all of these in an Angular application. Just open the result in two different tabs to see the library in action.

⚠️ There may be a desync due to hot reload

- + \ No newline at end of file diff --git a/docs/troubleshooting/stale-emission/index.html b/docs/troubleshooting/stale-emission/index.html index 36a0e236..fb966936 100644 --- a/docs/troubleshooting/stale-emission/index.html +++ b/docs/troubleshooting/stale-emission/index.html @@ -4,13 +4,13 @@ Stale emission | Elf | A Reactive Store with Magical Powers - +

Stale emission

If you have two properties(for example filter and counter) in the store and queries for them and on subscription emission of the filter$ you're updating counter property then you will get stale emission. Let's see the code example:

store.ts
import { createStore, withProps, select } from '@ngneat/elf';

interface Props {
filter: string | null;
counter: number;
}

export const store = createStore(
{ name: 'todo' },
withProps<Props>({ filter: null, counter: 0 })
);

export const filter$ = store.pipe(select(({ filters }) => filters));
export const counter$ = store.pipe(select(({ counter }) => counter));
component.ts
import { filter$, counter$, store } from './store.ts';

// FIRST SUBSCRIBER
filter$.subscribe(() => {
store.update((state) => ({
...state,
counter: state.counter + 1,
}));
});

// SECOND SUBSCRIBER
counter$.subscribe((counter) => {
console.log(counter);
});

// Update the filter
store.update((state) => ({
...state,
filter: 'test',
}));

Why would we see 1 2 1 in logs? Once FIRST SUBSCRIBER receives first emission on subscribing it updates counter to 1. After that, SECOND SUBSCRIBER receives first emission on subscribing and logs 1. When we update the filter it first passes to FIRST SUBSCRIBER which updates the counter property. The SECOND SUBSCRIBER receives this emission and logs the value 2. But the SECOND SUBSCRIBER will still receive the value 1, since the emission of the filter update is still in the pipeline with a staled state.

There are two ways to get around this issue:

  1. Change the subscriptions order - SECONED SUBSCRIBER and then FIRST SUBSCRIBER:
component.ts
import { filter$, counter$, store } from './store.ts';

counter$.subscribe((counter) => {
console.log(counter);
});

filter$.subscribe(() => {
store.update((state) => ({
...state,
counter: state.counter + 1,
}));
});
  1. Delay the FIRST SUBSCRIBER update using one of RxJS operators. (e.g auditTime(0)):
component.ts
import { filter$, counter$, store } from './store.ts';
import { auditTime } from 'rxjs';

filter$.pipe(auditTime(0)).subscribe(() => {
store.update((state) => ({
...state,
counter: state.counter + 1,
}));
});

counter$.subscribe((counter) => {
console.log(counter);
});
- + \ No newline at end of file diff --git a/index.html b/index.html index 902c2403..26438c0e 100644 --- a/index.html +++ b/index.html @@ -4,13 +4,13 @@ Elf | A Reactive Store with Magical Powers - +

elf

A Reactive Store with Magical Powers

Modular by design

Modular by design

Build multiple stores and let your bundler code split them automatically

Tree Shakeable & Fully Typed

Tree Shakeable & Fully Typed

Anything you don't use won't become part of your bundle

CLI

CLI

A fast and efficient way to build up your stores

First Class Entities Support

First Class Entities Support

Simply and easily manage entities in your store

Requests Status & Cache

Requests Status & Cache

Monitor server requests and cache them, preventing redundant API calls

Persist State

Persist State

Persist the store's state, offering a seamless user experience

State History

State History

Save the store's state history, for easy undo/redo functionality

Pagination

Pagination

Get built-in support for managing pagination in the app

Devtools

Devtools

Elf hooks into Redux devtools to give you an enhanced development experience

- + \ No newline at end of file diff --git a/search/index.html b/search/index.html index 3eb7ef58..9f1384f7 100644 --- a/search/index.html +++ b/search/index.html @@ -4,13 +4,13 @@ Search the documentation | Elf | A Reactive Store with Magical Powers - +

Search the documentation

- + \ No newline at end of file