diff --git a/404.html b/404.html index ecd6975..2d80e87 100644 --- a/404.html +++ b/404.html @@ -4,13 +4,13 @@ Page Not Found | React-RxJS - +
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/b0a8b060.9aff46db.js b/assets/js/b0a8b060.9aff46db.js new file mode 100644 index 0000000..1f33f4a --- /dev/null +++ b/assets/js/b0a8b060.9aff46db.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkreact_rxjs_org=self.webpackChunkreact_rxjs_org||[]).push([[362],{3905:function(e,t,n){n.d(t,{Zo:function(){return p},kt:function(){return h}});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 r(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 i(e){for(var t=1;t=0||(a[n]=e[n]);return a}(e,t);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);for(o=0;o=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(a[n]=e[n])}return a}var s=o.createContext({}),d=function(e){var t=o.useContext(s),n=t;return e&&(n="function"==typeof e?e(t):i(i({},t),e)),n},p=function(e){var t=d(e.components);return o.createElement(s.Provider,{value:t},e.children)},u="mdxType",c={inlineCode:"code",wrapper:function(e){var t=e.children;return o.createElement(o.Fragment,{},t)}},m=o.forwardRef((function(e,t){var n=e.components,a=e.mdxType,r=e.originalType,s=e.parentName,p=l(e,["components","mdxType","originalType","parentName"]),u=d(n),m=a,h=u["".concat(s,".").concat(m)]||u[m]||c[m]||r;return n?o.createElement(h,i(i({ref:t},p),{},{components:n})):o.createElement(h,i({ref:t},p))}));function h(e,t){var n=arguments,a=t&&t.mdxType;if("string"==typeof e||a){var r=n.length,i=new Array(r);i[0]=m;var l={};for(var s in t)hasOwnProperty.call(t,s)&&(l[s]=t[s]);l.originalType=e,l[u]="string"==typeof e?e:a,i[1]=l;for(var d=2;d();\nconst [editTodo$, onEditTodo] = createSignal<{ id: number; text: string }>();\nconst [toggleTodo$, onToggleTodo] = createSignal();\nconst [deleteTodo$, onDeleteTodo] = createSignal();\n")),(0,r.kt)("h2",{id:"creating-a-single-stream-for-all-the-user-events"},"Creating a single stream for all the user events"),(0,r.kt)("p",null,"It would be very convenient to have a merged stream with all those events. However,\nif we did a traditional ",(0,r.kt)("inlineCode",{parentName:"p"},"merge"),", then it would be very challenging to know the\norigin of each event."),(0,r.kt)("p",null,"That's why ",(0,r.kt)("inlineCode",{parentName:"p"},"@react-rxjs/utils")," exposes the ",(0,r.kt)("a",{parentName:"p",href:"../api/utils/mergeWithKey"},(0,r.kt)("inlineCode",{parentName:"a"},"mergeWithKey")),"\noperator. Let's use it:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-tsx"},"const todoActions$ = mergeWithKey({\n add: newTodo$.pipe(map((text, id) => ({ id, text }))),\n edit: editTodo$,\n toggle: toggleTodo$.pipe(map(id => ({ id }))),\n delete: deleteTodo$.pipe(map(id => ({ id })))\n})\n")),(0,r.kt)("p",null,"Which is basically the same as doing this (but a lot shorter, of course \ud83d\ude04):"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-tsx"},'const todoActions$ = merge(\n newTodo$.pipe(map((text, id) => ({\n type: "add" as const,\n payload: { id, text },\n }))),\n editTodo$.pipe(map(payload => ({\n type: "edit" as const,\n payload,\n }))),\n toggleTodo$.pipe(map(id => ({\n type: "toggle" as const,\n payload: { id },\n }))),\n deleteTodo$.pipe(map(id => ({\n type: "delete" as const,\n payload: { id },\n }))),\n)\n')),(0,r.kt)("h2",{id:"creating-a-stream-for-each-todo"},"Creating a stream for each todo"),(0,r.kt)("p",null,"Now that we have put all the streams together, let's create a stream for\neach todo. And for that, we will be using another operator from ",(0,r.kt)("inlineCode",{parentName:"p"},"@react-rxjs/utils"),":\nthe ",(0,r.kt)("a",{parentName:"p",href:"../api/utils/partitionByKey"},(0,r.kt)("inlineCode",{parentName:"a"},"partitionByKey"))," operator: "),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-ts"},'type Todo = { id: number; text: string; done: boolean };\nconst [todosMap, keys$] = partitionByKey(\n todoActions$,\n event => event.payload.id,\n (event$, id) =>\n event$.pipe(\n takeWhile((event) => event.type !== "delete"),\n scan(\n (state, action) => {\n switch (action.type) {\n case "add":\n case "edit":\n return { ...state, text: action.payload.text };\n case "toggle":\n return { ...state, done: !state.done };\n default:\n return state;\n }\n },\n { id, text: "", done: false } as Todo\n )\n )\n)\n')),(0,r.kt)("p",null,"Now we have a function, ",(0,r.kt)("inlineCode",{parentName:"p"},"todosMap"),", that returns an Observable of events\nassociated with a given todo. ",(0,r.kt)("inlineCode",{parentName:"p"},"partitionByKey")," transforms the source observable in a way\nsimilar to the ",(0,r.kt)("inlineCode",{parentName:"p"},"groupBy")," operator that's exposed from RxJS. However, there are\nsome important differences:"),(0,r.kt)("ul",null,(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("inlineCode",{parentName:"li"},"partitionByKey")," gives you a function that returns an Observable, rather\nthan an Observable that emits Observables. It also provides an Observable\nthat emits the list of keys, whenever that list changes (",(0,r.kt)("inlineCode",{parentName:"li"},"keys$")," in the code\nabove)."),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("inlineCode",{parentName:"li"},"partitionByKey"),' has an optional third parameter which allows you to create\na complex inner stream that will become the "grouped" stream that is\nreturned.'),(0,r.kt)("li",{parentName:"ul"},"This returned stream is enhanced with a\n",(0,r.kt)("a",{parentName:"li",href:"../api/core/shareLatest"},(0,r.kt)("inlineCode",{parentName:"a"},"shareLatest"))," and ",(0,r.kt)("inlineCode",{parentName:"li"},"partitionByKey")," internally\nsubscribes to it as soon as it is created to ensure that the consumer always\nhas the latest value.")),(0,r.kt)("h2",{id:"collecting-the-groupedobservables"},"Collecting the GroupedObservables"),(0,r.kt)("p",null,"We now have a way of getting streams for each todo, and we have a stream\n(",(0,r.kt)("inlineCode",{parentName:"p"},"keys$"),") that represents the list of todos by their ids and emits whenever\none is added or deleted. We should also like a stream that emits whenever\nthe state of any todo changes, and gives us access to all of them.\n",(0,r.kt)("a",{parentName:"p",href:"../api/utils/combineKeys"},(0,r.kt)("inlineCode",{parentName:"a"},"combineKeys()"))," suits this purpose. Let's try it:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-ts"},"const todosMap$: Observable> = combineKeys(keys$, todosMap);\n")),(0,r.kt)("p",null,"And with this we are ready to start wiring things up."),(0,r.kt)("h2",{id:"wiring-up-a-basic-version"},"Wiring up a basic version"),(0,r.kt)("p",null,"Let's start with the top-level component:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-tsx"},"const [useTodos] = bind(todosMap$.pipe(map(todosMap => [...todosMap.values()])))\n\nfunction TodoList() {\n const todoList = useTodos()\n\n return (\n <>\n {/* */}\n {/* */}\n \n\n {todoList.map((todoItem) => (\n \n ))}\n \n );\n}\n")),(0,r.kt)("p",null,"Next, let's implement the ",(0,r.kt)("inlineCode",{parentName:"p"},"TodoItemCreator"),":"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-tsx"},"function TodoItemCreator() {\n const [inputValue, setInputValue] = useState('');\n\n const addItem = () => {\n onNewTodo(inputValue);\n setInputValue('');\n };\n\n const onChange = ({target: {value}}) => {\n setInputValue(value);\n };\n\n return (\n
\n \n \n
\n );\n}\n")),(0,r.kt)("p",null,"And finally, the ",(0,r.kt)("inlineCode",{parentName:"p"},"TodoItem")," component:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-tsx"},'function TodoItem({item}) {\n const editItemText = ({target: {value}}) => {\n onEditTodo(item.id, value)\n }\n\n const toggleItemCompletion = () => {\n onToggleTodo(item.id)\n }\n\n const deleteItem = () => {\n onDeleteTodo(item.id)\n }\n\n return (\n
\n \n \n \n
\n )\n}\n')),(0,r.kt)("p",null,"That's it! We have a basic version working."),(0,r.kt)("h2",{id:"cutting-react-out-of-the-state-management-game"},"Cutting React out of the state management game"),(0,r.kt)("p",null,"What we've done so far is pretty neat, but there are a lot of unnecessary\nrenders going on in our application. Editing any of the todos, for example,\ncauses the whole list to re-render. To those with experience in React\ndevelopment, this hardly seems noteworthy\u2014our state is our list of todos, it\nlives in our TodoList component, so of course it re-renders when that state\nchanges. With React-RxJS, we can do better. Before we proceed with the\nremaining features, let's relieve React of its state management\nresponsibilities altogether."),(0,r.kt)("p",null,"Take a look at the stream we've bound to the TodoList component:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-tsx"},"const [useTodos] = bind(todosMap$.pipe(map(todosMap => [...todosMap.values()])));\n")),(0,r.kt)("p",null,"This is just an ",(0,r.kt)("inlineCode",{parentName:"p"},"Observable"),", and it will emit every time any todo\ngets updated\u2014triggering a TodoList render. In fact TodoList only needs to know\nwhich todos to display; rendering them according to their properties can be\nleft up to the child component, TodoItem. Therefore let's bind a list of\n",(0,r.kt)("em",{parentName:"p"},"which")," todos exist. Luckily we already have a stream for that, returned above\nby ",(0,r.kt)("inlineCode",{parentName:"p"},"partitionByKey"),". So:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-tsx"},"const [useTodoIds] = bind(keys$);\n")),(0,r.kt)("p",null,"Simple! Now we edit our TodoList component to pass just the todo id:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-diff"}," function TodoList() {\n- const todoList = useTodos();\n+ const todoIds = useTodos();\n\n return (\n <>\n \n \n \n \n- {todoList.map((todoItem) => (\n- \n- ))}\n+ {todoIds.map((id) => (\n+ \n+ ))}\n \n );\n }\n")),(0,r.kt)("p",null,"and teach TodoItem to get its state from the stream corresponding to that id,\nrather than from its parent component:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-tsx"},"const TodoItem: React.FC<{ id: number }> = ({ id }) => {\n const item = useTodo(id);\n\n return( ... )\n}\n")),(0,r.kt)("h2",{id:"adding-filters"},"Adding filters"),(0,r.kt)("p",null,"As we already know, we will need to capture the filter selected by the user:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-ts"},'export enum FilterType {\n All = "all",\n Done = "done",\n Pending = "pending"\n}\nconst [selectedFilter$, onSelectFilter] = createSignal()\n')),(0,r.kt)("p",null,"Next, let's create a hook and a stream for the current filter:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-ts"},"const [useCurrentFilter, currentFilter$] = bind(\n selectedFilter$.pipe(startWith(FilterType.All))\n)\n")),(0,r.kt)("p",null,"Also, let's tell our TodoItems not to render if they've been filtered out:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-diff"}," const TodoItem: React.FC<{ id: number }> = ({ id }) => {\n const item = useTodo(id);\n+ const currentFilter = useCurrentFilter();\n \n return ( ... );\n }\n")),(0,r.kt)("p",null,"Time to implement the ",(0,r.kt)("inlineCode",{parentName:"p"},"TodoListFilters")," component:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-tsx"},"function TodoListFilters() {\n const filter = useCurrentFilter()\n\n const updateFilter = ({target: {value}}) => {\n onSelectFilter(value)\n };\n\n return (\n <>\n Filter:\n \n \n );\n}\n")),(0,r.kt)("h2",{id:"adding-stats"},"Adding stats"),(0,r.kt)("p",null,"We will be showing the following stats:"),(0,r.kt)("ul",null,(0,r.kt)("li",{parentName:"ul"},"Total number of todo items"),(0,r.kt)("li",{parentName:"ul"},"Total number of completed items"),(0,r.kt)("li",{parentName:"ul"},"Total number of uncompleted items"),(0,r.kt)("li",{parentName:"ul"},"Percentage of items completed")),(0,r.kt)("p",null,"Let's create a ",(0,r.kt)("inlineCode",{parentName:"p"},"useTodosStats")," for it:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-ts"},"const [useTodosStats] = bind(\n todosList$.pipe(map(todosList => {\n const nTotal = todosList.length\n const nCompleted = todosList.filter((item) => item.done).length\n const nUncompleted = nTotal - nCompleted\n const percentCompleted = \n nTotal === 0 ? 0 : Math.round((nCompleted / nTotal) * 100)\n\n return {\n nTotal,\n nCompleted,\n nUncompleted,\n percentCompleted,\n }\n }))\n)\n")),(0,r.kt)("p",null,"And now let's use this hook in the ",(0,r.kt)("inlineCode",{parentName:"p"},"TodoListStats")," component:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-tsx"},"function TodoListStats() {\n const { nTotal, nCompleted, nUncompleted, percentCompleted } = useTodosStats()\n\n return (\n
    \n
  • Total items: {nTotal}
  • \n
  • Items completed: {nCompleted}
  • \n
  • Items not completed: {nUncompleted}
  • \n
  • Percent completed: {percentCompleted}
  • \n
\n );\n}\n")),(0,r.kt)("h2",{id:"summary"},"Summary"),(0,r.kt)("p",null,"The result of this tutorial can be seen in this CodeSandbox:"),(0,r.kt)("iframe",{src:"https://codesandbox.io/embed/react-rxjs-basic-todos-nd8rn?fontsize=14&hidenavigation=1&theme=dark&view=editor",style:{width:"100%",height:"500px",border:0,borderRadius:"4px",overflow:"hidden"},title:"react-rxjs-github-issues-example",allow:"geolocation; microphone; camera; midi; vr; accelerometer; gyroscope; payment; ambient-light-sensor; encrypted-media; usb",sandbox:"allow-modals allow-forms allow-popups allow-scripts allow-same-origin"}))}h.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/b0a8b060.a3a7e39e.js b/assets/js/b0a8b060.a3a7e39e.js deleted file mode 100644 index f256853..0000000 --- a/assets/js/b0a8b060.a3a7e39e.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkreact_rxjs_org=self.webpackChunkreact_rxjs_org||[]).push([[362],{3905:function(e,t,n){n.d(t,{Zo:function(){return p},kt:function(){return h}});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 r(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 i(e){for(var t=1;t=0||(a[n]=e[n]);return a}(e,t);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);for(o=0;o=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(a[n]=e[n])}return a}var s=o.createContext({}),d=function(e){var t=o.useContext(s),n=t;return e&&(n="function"==typeof e?e(t):i(i({},t),e)),n},p=function(e){var t=d(e.components);return o.createElement(s.Provider,{value:t},e.children)},u="mdxType",c={inlineCode:"code",wrapper:function(e){var t=e.children;return o.createElement(o.Fragment,{},t)}},m=o.forwardRef((function(e,t){var n=e.components,a=e.mdxType,r=e.originalType,s=e.parentName,p=l(e,["components","mdxType","originalType","parentName"]),u=d(n),m=a,h=u["".concat(s,".").concat(m)]||u[m]||c[m]||r;return n?o.createElement(h,i(i({ref:t},p),{},{components:n})):o.createElement(h,i({ref:t},p))}));function h(e,t){var n=arguments,a=t&&t.mdxType;if("string"==typeof e||a){var r=n.length,i=new Array(r);i[0]=m;var l={};for(var s in t)hasOwnProperty.call(t,s)&&(l[s]=t[s]);l.originalType=e,l[u]="string"==typeof e?e:a,i[1]=l;for(var d=2;d();\nconst [editTodo$, onEditTodo] = createSignal<{ id: number; text: string }>();\nconst [toggleTodo$, onToggleTodo] = createSignal();\nconst [deleteTodo$, onDeleteTodo] = createSignal();\n")),(0,r.kt)("h2",{id:"creating-a-single-stream-for-all-the-user-events"},"Creating a single stream for all the user events"),(0,r.kt)("p",null,"It would be very convenient to have a merged stream with all those events. However,\nif we did a traditional ",(0,r.kt)("inlineCode",{parentName:"p"},"merge"),", then it would be very challenging to know the\norigin of each event."),(0,r.kt)("p",null,"That's why ",(0,r.kt)("inlineCode",{parentName:"p"},"@react-rxjs/utils")," exposes the ",(0,r.kt)("a",{parentName:"p",href:"../api/utils/mergeWithKey"},(0,r.kt)("inlineCode",{parentName:"a"},"mergeWithKey")),"\noperator. Let's use it:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-tsx"},"const todoActions$ = mergeWithKey({\n add: newTodo$.pipe(map((text, id) => ({ id, text }))),\n edit: editTodo$,\n toggle: toggleTodo$.pipe(map(id => ({ id }))),\n delete: deleteTodo$.pipe(map(id => ({ id })))\n})\n")),(0,r.kt)("p",null,"Which is basically the same as doing this (but a lot shorter, of course \ud83d\ude04):"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-tsx"},'const todoActions$ = merge(\n newTodo$.pipe(map(text, id) => ({\n type: "add" as const\n payload: { id, text },\n })),\n editTodo$.pipe(map(payload => ({\n type: "edit" as const,\n payload,\n }))),\n toggleTodo$.pipe(map(id => ({\n type: "toggle" as const,\n payload: { id },\n }))),\n deleteTodo$.pipe(map(id => ({\n type: "delete" as const,\n payload: { id },\n }))),\n)\n')),(0,r.kt)("h2",{id:"creating-a-stream-for-each-todo"},"Creating a stream for each todo"),(0,r.kt)("p",null,"Now that we have put all the streams together, let's create a stream for\neach todo. And for that, we will be using another operator from ",(0,r.kt)("inlineCode",{parentName:"p"},"@react-rxjs/utils"),":\nthe ",(0,r.kt)("a",{parentName:"p",href:"../api/utils/partitionByKey"},(0,r.kt)("inlineCode",{parentName:"a"},"partitionByKey"))," operator: "),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-ts"},'type Todo = { id: number; text: string; done: boolean };\nconst [todosMap, keys$] = partitionByKey(\n todoActions$,\n event => event.payload.id,\n (event$, id) =>\n event$.pipe(\n takeWhile((event) => event.type !== "delete"),\n scan(\n (state, action) => {\n switch (action.type) {\n case "add":\n case "edit":\n return { ...state, text: action.payload.text };\n case "toggle":\n return { ...state, done: !state.done };\n default:\n return state;\n }\n },\n { id, text: "", done: false } as Todo\n )\n )\n)\n')),(0,r.kt)("p",null,"Now we have a function, ",(0,r.kt)("inlineCode",{parentName:"p"},"todosMap"),", that returns an Observable of events\nassociated with a given todo. ",(0,r.kt)("inlineCode",{parentName:"p"},"partitionByKey")," transforms the source observable in a way\nsimilar to the ",(0,r.kt)("inlineCode",{parentName:"p"},"groupBy")," operator that's exposed from RxJS. However, there are\nsome important differences:"),(0,r.kt)("ul",null,(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("inlineCode",{parentName:"li"},"partitionByKey")," gives you a function that returns an Observable, rather\nthan an Observable that emits Observables. It also provides an Observable\nthat emits the list of keys, whenever that list changes (",(0,r.kt)("inlineCode",{parentName:"li"},"keys$")," in the code\nabove)."),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("inlineCode",{parentName:"li"},"partitionByKey"),' has an optional third parameter which allows you to create\na complex inner stream that will become the "grouped" stream that is\nreturned.'),(0,r.kt)("li",{parentName:"ul"},"This returned stream is enhanced with a\n",(0,r.kt)("a",{parentName:"li",href:"../api/core/shareLatest"},(0,r.kt)("inlineCode",{parentName:"a"},"shareLatest"))," and ",(0,r.kt)("inlineCode",{parentName:"li"},"partitionByKey")," internally\nsubscribes to it as soon as it is created to ensure that the consumer always\nhas the latest value.")),(0,r.kt)("h2",{id:"collecting-the-groupedobservables"},"Collecting the GroupedObservables"),(0,r.kt)("p",null,"We now have a way of getting streams for each todo, and we have a stream\n(",(0,r.kt)("inlineCode",{parentName:"p"},"keys$"),") that represents the list of todos by their ids and emits whenever\none is added or deleted. We should also like a stream that emits whenever\nthe state of any todo changes, and gives us access to all of them.\n",(0,r.kt)("a",{parentName:"p",href:"../api/utils/combineKeys"},(0,r.kt)("inlineCode",{parentName:"a"},"combineKeys()"))," suits this purpose. Let's try it:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-ts"},"const todosMap$: Observable> = combineKeys(keys$, todosMap);\n")),(0,r.kt)("p",null,"And with this we are ready to start wiring things up."),(0,r.kt)("h2",{id:"wiring-up-a-basic-version"},"Wiring up a basic version"),(0,r.kt)("p",null,"Let's start with the top-level component:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-tsx"},"const [useTodos] = bind(todosMap$.pipe(map(todosMap => [...todosMap.values()])))\n\nfunction TodoList() {\n const todoList = useTodos()\n\n return (\n <>\n {/* */}\n {/* */}\n \n\n {todoList.map((todoItem) => (\n \n ))}\n \n );\n}\n")),(0,r.kt)("p",null,"Next, let's implement the ",(0,r.kt)("inlineCode",{parentName:"p"},"TodoItemCreator"),":"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-tsx"},"function TodoItemCreator() {\n const [inputValue, setInputValue] = useState('');\n\n const addItem = () => {\n onNewTodo(inputValue);\n setInputValue('');\n };\n\n const onChange = ({target: {value}}) => {\n setInputValue(value);\n };\n\n return (\n
\n \n \n
\n );\n}\n")),(0,r.kt)("p",null,"And finally, the ",(0,r.kt)("inlineCode",{parentName:"p"},"TodoItem")," component:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-tsx"},'function TodoItem({item}) {\n const editItemText = ({target: {value}}) => {\n onEditTodo(item.id, value)\n }\n\n const toggleItemCompletion = () => {\n onToggleTodo(item.id)\n }\n\n const deleteItem = () => {\n onDeleteTodo(item.id)\n }\n\n return (\n
\n \n \n \n
\n )\n}\n')),(0,r.kt)("p",null,"That's it! We have a basic version working."),(0,r.kt)("h2",{id:"cutting-react-out-of-the-state-management-game"},"Cutting React out of the state management game"),(0,r.kt)("p",null,"What we've done so far is pretty neat, but there are a lot of unnecessary\nrenders going on in our application. Editing any of the todos, for example,\ncauses the whole list to re-render. To those with experience in React\ndevelopment, this hardly seems noteworthy\u2014our state is our list of todos, it\nlives in our TodoList component, so of course it re-renders when that state\nchanges. With React-RxJS, we can do better. Before we proceed with the\nremaining features, let's relieve React of its state management\nresponsibilities altogether."),(0,r.kt)("p",null,"Take a look at the stream we've bound to the TodoList component:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-tsx"},"const [useTodos] = bind(todosMap$.pipe(map(todosMap => [...todosMap.values()])));\n")),(0,r.kt)("p",null,"This is just an ",(0,r.kt)("inlineCode",{parentName:"p"},"Observable"),", and it will emit every time any todo\ngets updated\u2014triggering a TodoList render. In fact TodoList only needs to know\nwhich todos to display; rendering them according to their properties can be\nleft up to the child component, TodoItem. Therefore let's bind a list of\n",(0,r.kt)("em",{parentName:"p"},"which")," todos exist. Luckily we already have a stream for that, returned above\nby ",(0,r.kt)("inlineCode",{parentName:"p"},"partitionByKey"),". So:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-tsx"},"const [useTodoIds] = bind(keys$);\n")),(0,r.kt)("p",null,"Simple! Now we edit our TodoList component to pass just the todo id:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-diff"}," function TodoList() {\n- const todoList = useTodos();\n+ const todoIds = useTodos();\n\n return (\n <>\n \n \n \n \n- {todoList.map((todoItem) => (\n- \n- ))}\n+ {todoIds.map((id) => (\n+ \n+ ))}\n \n );\n }\n")),(0,r.kt)("p",null,"and teach TodoItem to get its state from the stream corresponding to that id,\nrather than from its parent component:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-tsx"},"const TodoItem: React.FC<{ id: number }> = ({ id }) => {\n const item = useTodo(id);\n\n return( ... )\n}\n")),(0,r.kt)("h2",{id:"adding-filters"},"Adding filters"),(0,r.kt)("p",null,"As we already know, we will need to capture the filter selected by the user:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-ts"},'export enum FilterType {\n All = "all",\n Done = "done",\n Pending = "pending"\n}\nconst [selectedFilter$, onSelectFilter] = createSignal()\n')),(0,r.kt)("p",null,"Next, let's create a hook and a stream for the current filter:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-ts"},"const [useCurrentFilter, currentFilter$] = bind(\n selectedFilter$.pipe(startWith(FilterType.All))\n)\n")),(0,r.kt)("p",null,"Also, let's tell our TodoItems not to render if they've been filtered out:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-diff"}," const TodoItem: React.FC<{ id: number }> = ({ id }) => {\n const item = useTodo(id);\n+ const currentFilter = useCurrentFilter();\n \n return ( ... );\n }\n")),(0,r.kt)("p",null,"Time to implement the ",(0,r.kt)("inlineCode",{parentName:"p"},"TodoListFilters")," component:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-tsx"},"function TodoListFilters() {\n const filter = useCurrentFilter()\n\n const updateFilter = ({target: {value}}) => {\n onSelectFilter(value)\n };\n\n return (\n <>\n Filter:\n \n \n );\n}\n")),(0,r.kt)("h2",{id:"adding-stats"},"Adding stats"),(0,r.kt)("p",null,"We will be showing the following stats:"),(0,r.kt)("ul",null,(0,r.kt)("li",{parentName:"ul"},"Total number of todo items"),(0,r.kt)("li",{parentName:"ul"},"Total number of completed items"),(0,r.kt)("li",{parentName:"ul"},"Total number of uncompleted items"),(0,r.kt)("li",{parentName:"ul"},"Percentage of items completed")),(0,r.kt)("p",null,"Let's create a ",(0,r.kt)("inlineCode",{parentName:"p"},"useTodosStats")," for it:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-ts"},"const [useTodosStats] = bind(\n todosList$.pipe(map(todosList => {\n const nTotal = todosList.length\n const nCompleted = todosList.filter((item) => item.done).length\n const nUncompleted = nTotal - nCompleted\n const percentCompleted = \n nTotal === 0 ? 0 : Math.round((nCompleted / nTotal) * 100)\n\n return {\n nTotal,\n nCompleted,\n nUncompleted,\n percentCompleted,\n }\n }))\n)\n")),(0,r.kt)("p",null,"And now let's use this hook in the ",(0,r.kt)("inlineCode",{parentName:"p"},"TodoListStats")," component:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-tsx"},"function TodoListStats() {\n const { nTotal, nCompleted, nUncompleted, percentCompleted } = useTodosStats()\n\n return (\n
    \n
  • Total items: {nTotal}
  • \n
  • Items completed: {nCompleted}
  • \n
  • Items not completed: {nUncompleted}
  • \n
  • Percent completed: {percentCompleted}
  • \n
\n );\n}\n")),(0,r.kt)("h2",{id:"summary"},"Summary"),(0,r.kt)("p",null,"The result of this tutorial can be seen in this CodeSandbox:"),(0,r.kt)("iframe",{src:"https://codesandbox.io/embed/react-rxjs-basic-todos-nd8rn?fontsize=14&hidenavigation=1&theme=dark&view=editor",style:{width:"100%",height:"500px",border:0,borderRadius:"4px",overflow:"hidden"},title:"react-rxjs-github-issues-example",allow:"geolocation; microphone; camera; midi; vr; accelerometer; gyroscope; payment; ambient-light-sensor; encrypted-media; usb",sandbox:"allow-modals allow-forms allow-popups allow-scripts allow-same-origin"}))}h.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/runtime~main.dcb1ca1e.js b/assets/js/runtime~main.1b49943f.js similarity index 98% rename from assets/js/runtime~main.dcb1ca1e.js rename to assets/js/runtime~main.1b49943f.js index abd1900..2531c29 100644 --- a/assets/js/runtime~main.dcb1ca1e.js +++ b/assets/js/runtime~main.1b49943f.js @@ -1 +1 @@ -!function(){"use strict";var e,t,r,n,f,o={},a={};function c(e){var t=a[e];if(void 0!==t)return t.exports;var r=a[e]={id:e,loaded:!1,exports:{}};return o[e].call(r.exports,r,r.exports,c),r.loaded=!0,r.exports}c.m=o,c.c=a,e=[],c.O=function(t,r,n,f){if(!r){var o=1/0;for(i=0;i=f)&&Object.keys(c.O).every((function(e){return c.O[e](r[d])}))?r.splice(d--,1):(a=!1,f0&&e[i-1][2]>f;i--)e[i]=e[i-1];e[i]=[r,n,f]},c.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return c.d(t,{a:t}),t},r=Object.getPrototypeOf?function(e){return Object.getPrototypeOf(e)}:function(e){return e.__proto__},c.t=function(e,n){if(1&n&&(e=this(e)),8&n)return e;if("object"==typeof e&&e){if(4&n&&e.__esModule)return e;if(16&n&&"function"==typeof e.then)return e}var f=Object.create(null);c.r(f);var o={};t=t||[null,r({}),r([]),r(r)];for(var a=2&n&&e;"object"==typeof a&&!~t.indexOf(a);a=r(a))Object.getOwnPropertyNames(a).forEach((function(t){o[t]=function(){return e[t]}}));return o.default=function(){return e},c.d(f,o),f},c.d=function(e,t){for(var r in t)c.o(t,r)&&!c.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},c.f={},c.e=function(e){return Promise.all(Object.keys(c.f).reduce((function(t,r){return c.f[r](e,t),t}),[]))},c.u=function(e){return"assets/js/"+({50:"f18181f9",53:"935f2afb",121:"bfa848fe",133:"99d6843e",142:"241ace95",153:"699cd95f",162:"d589d3a7",174:"765558b9",195:"c4f5d8e4",212:"ad7e8345",279:"7cbbb66b",362:"b0a8b060",386:"3dcf8b31",395:"c9fe6ade",427:"8a41b08e",506:"6027136a",514:"1be78505",528:"85c46f15",559:"109f1412",580:"874cf61f",597:"60e96e28",599:"dbd78864",635:"9b7b3dfa",658:"43cd4838",688:"85b51ac8",714:"eaea80b4",761:"8d97cf5b",797:"0e38392d",870:"c3324ffd",902:"a3966f72",905:"d5469669",918:"17896441",920:"1a4e3797",995:"a84f26d6"}[e]||e)+"."+{50:"90fe855a",53:"041b8d8c",121:"bc35a809",133:"07de87a0",142:"97436ccc",153:"400ecc78",162:"73ab6c32",174:"749a675f",195:"758cca4d",212:"6cda1f16",279:"e9b08e05",362:"a3a7e39e",385:"8b6d270c",386:"aa36801d",395:"7c7d6c8b",427:"c3f601d9",506:"8bdc969a",514:"d6936ca2",528:"cae1e87e",559:"6efad079",562:"84c7a6d2",580:"7418f8ed",597:"00c3a357",599:"3b8af3a8",635:"eac37d9a",658:"641d9235",688:"674e342c",714:"6795b362",761:"6d26ed2e",797:"7f0d2eb6",870:"6148451a",894:"8fd6eb53",902:"23681a34",905:"8f5099c5",918:"185927e2",920:"e36f4207",945:"b879be68",972:"1fc46c92",995:"ea9e7f45"}[e]+".js"},c.miniCssF=function(e){},c.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),c.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n={},f="react-rxjs-org:",c.l=function(e,t,r,o){if(n[e])n[e].push(t);else{var a,d;if(void 0!==r)for(var u=document.getElementsByTagName("script"),i=0;i=f)&&Object.keys(c.O).every((function(e){return c.O[e](r[d])}))?r.splice(d--,1):(a=!1,f0&&e[i-1][2]>f;i--)e[i]=e[i-1];e[i]=[r,n,f]},c.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return c.d(t,{a:t}),t},r=Object.getPrototypeOf?function(e){return Object.getPrototypeOf(e)}:function(e){return e.__proto__},c.t=function(e,n){if(1&n&&(e=this(e)),8&n)return e;if("object"==typeof e&&e){if(4&n&&e.__esModule)return e;if(16&n&&"function"==typeof e.then)return e}var f=Object.create(null);c.r(f);var o={};t=t||[null,r({}),r([]),r(r)];for(var a=2&n&&e;"object"==typeof a&&!~t.indexOf(a);a=r(a))Object.getOwnPropertyNames(a).forEach((function(t){o[t]=function(){return e[t]}}));return o.default=function(){return e},c.d(f,o),f},c.d=function(e,t){for(var r in t)c.o(t,r)&&!c.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},c.f={},c.e=function(e){return Promise.all(Object.keys(c.f).reduce((function(t,r){return c.f[r](e,t),t}),[]))},c.u=function(e){return"assets/js/"+({50:"f18181f9",53:"935f2afb",121:"bfa848fe",133:"99d6843e",142:"241ace95",153:"699cd95f",162:"d589d3a7",174:"765558b9",195:"c4f5d8e4",212:"ad7e8345",279:"7cbbb66b",362:"b0a8b060",386:"3dcf8b31",395:"c9fe6ade",427:"8a41b08e",506:"6027136a",514:"1be78505",528:"85c46f15",559:"109f1412",580:"874cf61f",597:"60e96e28",599:"dbd78864",635:"9b7b3dfa",658:"43cd4838",688:"85b51ac8",714:"eaea80b4",761:"8d97cf5b",797:"0e38392d",870:"c3324ffd",902:"a3966f72",905:"d5469669",918:"17896441",920:"1a4e3797",995:"a84f26d6"}[e]||e)+"."+{50:"90fe855a",53:"041b8d8c",121:"bc35a809",133:"07de87a0",142:"97436ccc",153:"400ecc78",162:"73ab6c32",174:"749a675f",195:"758cca4d",212:"6cda1f16",279:"e9b08e05",362:"9aff46db",385:"8b6d270c",386:"aa36801d",395:"7c7d6c8b",427:"c3f601d9",506:"8bdc969a",514:"d6936ca2",528:"cae1e87e",559:"6efad079",562:"84c7a6d2",580:"7418f8ed",597:"00c3a357",599:"3b8af3a8",635:"eac37d9a",658:"641d9235",688:"674e342c",714:"6795b362",761:"6d26ed2e",797:"7f0d2eb6",870:"6148451a",894:"8fd6eb53",902:"23681a34",905:"8f5099c5",918:"185927e2",920:"e36f4207",945:"b879be68",972:"1fc46c92",995:"ea9e7f45"}[e]+".js"},c.miniCssF=function(e){},c.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),c.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n={},f="react-rxjs-org:",c.l=function(e,t,r,o){if(n[e])n[e].push(t);else{var a,d;if(void 0!==r)for(var u=document.getElementsByTagName("script"),i=0;i StateObservable | React-RxJS - +

StateObservable

Represents an Observable state. It has the following properties:

  1. It's multicast: The subscription is shared with all the subscribers.
  2. It replays the last emitted value to every new subscriber.
  3. It doesn't propagate complete.
  4. When all its subscribers unsubscribe, it cleans up everything, unsubscribing from the source and resetting the latest value.
interface StateObservable<T> extends Observable<T> {
getRefCount: () => number
getValue: (filter?: (value: T) => boolean) => T | StatePromise<T>
}

Inherits from rxjs' Observable.

methods

getRefCount()

Gets the current number of subscribers.

function getRefCount(this: StateObservable): number

Returns

The current number of subscribers this StateObservable has.

getValue(filter)

function getValue(
this: StateObservable,
filter?: (value: T) => boolean,
): T | StatePromise<T>

Arguments

  • filter: (Optional) A function that will exclude value as a possible value returned by this function. Defaults to () => true (every value is returned)

Returns

Either:

  • The latest value, if this StateObservable has already emitted any.
  • A promise that will resolve the first value, if this StateObservable hasn't emitted any yet.
note
  • Calling this method will throw an error if this StateObservable doesn't have any subscription active.

  • The promise returned by this method will reject if the observable completes without emitting any value, or if all subscribers unsubscribe before the observable emits something.

DefaultedStateObservable

A StateObservable that was provided with a default value.

interface DefaultedStateObservable<T> extends StateObservable<T> {
getDefaultValue: () => T
getValue: (filter?: (value: T) => boolean) => T
}

Inherits from StateObservable and rxjs' Observable.

getDefaultValue()

Gets the default value set to this DefaultedStateObservable

function getDefaultValue(this: DefaultedStateObservable<T>): T

Returns

The default value of this DefaultedStateObservable

getValue(filter)

function getValue(this: StateObservable, filter?: (value: T) => boolean): T

Arguments

  • filter: (Optional) A function that will exclude value as a possible value returned by this function. Defaults to () => true (every value is returned). If the value is excluded, it will return the default value instead.

Returns

Either:

  • The latest value, if this DefaultedStateObservable has already emitted any.
  • The default value, if this DefaultedStateObservable hasn't emitted any yet.
note

The getValue() method of DefaultedStateObservable overrides the one from StateObservable.

This means it won't return a Promise and it won't throw errors as the original StateObservable does.

See also

- + \ No newline at end of file diff --git a/docs/api/core/bind.html b/docs/api/core/bind.html index 56ef4b9..876acd1 100644 --- a/docs/api/core/bind.html +++ b/docs/api/core/bind.html @@ -4,7 +4,7 @@ bind(observable) | React-RxJS - + @@ -23,7 +23,7 @@ while it's waiting for the first value. It's equivalent to useStateObservable of the resulting StateObservable.

  • The factory function that returns the StateObservable that the hook uses for the specific arguments. It can be used for composing other streams that depend on it.

  • Example

    const [useStory, getStory$] = bind((storyId: number) =>
    getStoryWithUpdates$(storyId),
    )

    const Story: React.FC<{ id: number }> = ({ id }) => {
    const story = useStory(id)

    return (
    <article>
    <h1>{story.title}</h1>
    <p>{story.description}</p>
    </article>
    )
    }

    See also

    - + \ No newline at end of file diff --git a/docs/api/core/removeSubscribe.html b/docs/api/core/removeSubscribe.html index b01ba51..358f6b4 100644 --- a/docs/api/core/removeSubscribe.html +++ b/docs/api/core/removeSubscribe.html @@ -4,7 +4,7 @@ <RemoveSubscribe /> | React-RxJS - + @@ -13,7 +13,7 @@ don't want some of its children to leak subscriptions on it.

    Example

    const user$ = state(/* ... */)

    function App() {
    const user = useStateObservable(user$)

    return (
    <Content>
    <Header>Application Example</Header>
    <RemoveSubscribe>
    {/* none of the routes will be able to use the top-level <Subscribe> */}
    <AppRoutes />
    </RemoveSubscribe>
    </Content>
    )
    }

    createRoot(rootElement).render(
    <Subscribe>
    <App />
    </Subscribe>,
    )
    note

    This component only prevents its children from leaking subscriptions to a parent <Subscribe />. If that Subscribe had a fallback, it will not prevent the from using the Suspense boundary.

    See also

    - + \ No newline at end of file diff --git a/docs/api/core/shareLatest.html b/docs/api/core/shareLatest.html index db3fb50..9cc0b9e 100644 --- a/docs/api/core/shareLatest.html +++ b/docs/api/core/shareLatest.html @@ -4,7 +4,7 @@ shareLatest() | React-RxJS - + @@ -12,7 +12,7 @@

    shareLatest()

    An RxJS pipeable operator which multicasts the source stream and replays the latest emitted value.

    It's a utility function kept for historical purposes. Since RxJS@^7.0.0 released, it's equivalent to:

    import { share } from 'rxjs/operators';

    function shareLatest<T>() {
    return share<T>({
    connector: () => new ReplaySubject(1)
    })
    }

    Returns

    MonoTypeOperatorFunction<T>: An Observable that shares the latest emitted value from the source Observable with all subscribers, and restarts the stream when it completes or errors.

    Example

    import { filter, map } from "rxjs/operators"
    import { shareLatest } from "@react-rxjs/core"

    const activePlanetName$ = planet$.pipe(
    filter((planet) => planet.isActive),
    map((planet) => planet.name),
    shareLatest(),
    )

    See also

    - + \ No newline at end of file diff --git a/docs/api/core/state.html b/docs/api/core/state.html index 826158a..e6b9d59 100644 --- a/docs/api/core/state.html +++ b/docs/api/core/state.html @@ -4,14 +4,14 @@ state(observable) | React-RxJS - +

    state(observable)

    Creates a StateObservable from the source Observable.

    function state<T>(
    observable: Observable<T>,
    defaultValue?: T,
    ): StateObservable<T>

    Arguments

    • observable: The source Observable
    • defaultValue: (Optional) value to emit when the source hasn't emitted yet.

    Returns

    The StateObservable derived from the source observable.

    The returned StateObservable emits the same values as the source Observable, with the following behavior:

    1. It shares the subscription to all the subscribers.
    2. It replays the last emitted value to new subscribers.
    3. It doesn't propagate complete.
    4. When all subscribers unsubscribe, it unsubscribes from the source and resets the latest value.

    Example

    import { timer, interval } from "rxjs/operators"
    import { state } from "@react-rxjs/core"

    const time$ = state(interval(1000))

    time$.subscribe({
    next: (v) => console.log(v), // Logs 0, 1, 2, ...
    })

    timer(5000)
    .pipe(switchMap(() => time$))
    .subscribe({
    next: (v) => console.log(v), // Logs 5, 6, 7, ...
    })

    Factory Overload

    Creates a factory of StateObservable, that caches the observables created for each key.

    function state<A extends unknown[], T>(
    getObservable: (...args: A) => Observable<T>,
    defaultValue?: T | ((...args: A) => T),
    ): (...args: A) => StateObservable<T>

    Arguments

    • getObservable: Factory of Observables. The arguments of this function will be the ones used in the hook.
    • defaultValue: (Optional) value to emit when the source hasn't emitted yet.

    Returns

    The factory function that returns the StateObservable for a given key.

    Example

    const getPrice$ = state((productId: number) =>
    getProductPriceFromSocket$(productId),
    )

    getPrice$("apples").subscribe({
    next: (v) => console.log(v),
    })

    See also

    - + \ No newline at end of file diff --git a/docs/api/core/subscribe.html b/docs/api/core/subscribe.html index 66e9a5b..6d0c5c9 100644 --- a/docs/api/core/subscribe.html +++ b/docs/api/core/subscribe.html @@ -4,7 +4,7 @@ <Subscribe /> | React-RxJS - + @@ -14,7 +14,7 @@ element until the suspended element resolves.

    const Subscribe: React.FC<{
    source$?: Observable<any>
    fallback?: JSX.Element
    }>

    Properties

    • source$: (Optional) Source Observable that the Component should subscribe to, before its children renders.
    • fallback: (Optional) The JSX Element to be rendered before the subscription is created. Default: null.
    note

    This Component doesn't trigger any updates if any of its subscription emits.

    Important

    This Component first mounts itself rendering null, subscribes to source$ and then it renders its children.

    See also

    - + \ No newline at end of file diff --git a/docs/api/core/suspense.html b/docs/api/core/suspense.html index 3f571f4..40c08b8 100644 --- a/docs/api/core/suspense.html +++ b/docs/api/core/suspense.html @@ -4,7 +4,7 @@ SUSPENSE | React-RxJS - + @@ -12,7 +12,7 @@

    SUSPENSE

    SUSPENSE is a special symbol that can be emitted from observables to let the React hook know that there is a value on its way, and that we want to leverage React Suspense while we are waiting for that value.

    const SUSPENSE: unique symbol

    Example

    import { concat, of } from "rxjs"
    import { switchMap } from "rxjs/operators"
    import { SUSPENSE } from "@react-rxjs/core"

    const story$ = selectedStoryId$.pipe(
    switchMap((id) => concat(of(SUSPENSE), getStory$(id))),
    )

    See also

    - + \ No newline at end of file diff --git a/docs/api/core/useStateObservable.html b/docs/api/core/useStateObservable.html index e57a4dc..72e80a3 100644 --- a/docs/api/core/useStateObservable.html +++ b/docs/api/core/useStateObservable.html @@ -4,14 +4,14 @@ useStateObservable(observable) | React-RxJS - +

    useStateObservable(observable)

    Gets the latest value from an observable returned by state.

    function useStateObservable<T>(
    observable: StateObservable<T>,
    ): Exclude<T, typeof SUSPENSE>

    Arguments

    • observable: The StateObservable to get the value from.

    Returns

    The latest emitted value of the Observable.

    If the Observable hasn't emitted a value yet, it will leverage React Suspense while it's waiting for the first value.

    Examples

    import { scan } from "rxjs/operators"
    import { state, useStateObservable } from "@react-rxjs/core"

    const counter$ = state(clicks$.pipe(startWith(0)), 0)

    function CounterDisplay() {
    const counter = useStateObservable(counter$)

    return <div>{counter}</div>
    }
    note

    It's important to note that the StateObservable must be created outside the React render function, otherwise it could run into an infinite loop.

    For observables that need an instance id, use the parametric overload of state. For observables that need other parameters as input, try to model these parameters as other StateObservables of your application and use RxJS Observable composition.

    With factory of observables

    const getStory$ = state((storyId: number) => getStoryWithUpdates$(storyId))

    const Story: React.FC<{ id: number }> = ({ id }) => {
    const story = useStateObservable(getStory$(id))

    return (
    <article>
    <h1>{story.title}</h1>
    <p>{story.description}</p>
    </article>
    )
    }
    note

    useStateObservable needs the observable passed as a parameter to already have a subscription active, otherwise it will throw a Missing Subscribe error.

    You can use the <Subscribe>{children}</Subscribe> component to help manage your subscriptions.

    See also

    - + \ No newline at end of file diff --git a/docs/api/utils/collect.html b/docs/api/utils/collect.html index c0b6cf2..23d61ec 100644 --- a/docs/api/utils/collect.html +++ b/docs/api/utils/collect.html @@ -4,7 +4,7 @@ collect(filter) | React-RxJS - + @@ -15,7 +15,7 @@ whether the inner Observable should be collected. Default: undefined.

    Returns

    CollectedObservable<Map<K, GroupedObservable<K, V>>> - An Observable that:

    • Emits a Map containing all the keys seen in the source grouped Observables so far, along with the grouped Observable for matches each key.
    • Has a function .get(key: K): Observable<V> that returns the Observable that matches the key parameter.

    See also

    - + \ No newline at end of file diff --git a/docs/api/utils/collectValues.html b/docs/api/utils/collectValues.html index f073a0f..f928826 100644 --- a/docs/api/utils/collectValues.html +++ b/docs/api/utils/collectValues.html @@ -4,7 +4,7 @@ collectValues() | React-RxJS - + @@ -13,7 +13,7 @@ combineKeys covers its intended use case.

    A pipeable operator that collects all the GroupedObservables emitted by the source and emits a Map with the latest values of the inner Observables.

    function collectValues<K, V>(): OperatorFunction<GroupedObservable<K, V>, Map<K, V>>

    Returns

    OperatorFunction<GroupedObservable<K, V>, Map<K, V>>: An Observable that emits a Map with the latest value for each key in the source grouped Observables.

    Example

    import { Subject } from 'rxjs'
    import { mapTo, scan, takeWhile } from 'rxjs/operators'
    import { collectValues, split } from '@react-rxjs/utils'

    const votesByKey$ = new Subject<{ key: string }>()
    const counters$ = votesByKey$.pipe(
    split(
    (vote) => vote.key,
    (votes$) =>
    votes$.pipe(
    mapTo(1),
    scan((count) => count + 1),
    takeWhile((count) => count < 3),
    ),
    ),
    collectValues(),
    )

    counters$.subscribe((counters) => {
    console.log("counters$:")
    counters.forEach((value, key) => {
    console.log(`${key}: ${value}`)
    })
    })

    votesByKey$.next({ key: "foo" })
    // > counters$:
    // > foo: 1

    votesByKey$.next({ key: "foo" })
    // > counters$:
    // > foo: 2

    votesByKey$.next({ key: "bar" })
    // > counters$:
    // > foo: 2
    // > bar: 1

    votesByKey$.next({ key: "foo" })
    // > counters$:
    // > bar: 1

    votesByKey$.next({ key: "bar" })
    // > counters$:
    // > bar: 2
    //
    votesByKey$.next({ key: "bar" })
    // > counters$:

    See also

    - + \ No newline at end of file diff --git a/docs/api/utils/combineKeys.html b/docs/api/utils/combineKeys.html index a069497..a561755 100644 --- a/docs/api/utils/combineKeys.html +++ b/docs/api/utils/combineKeys.html @@ -4,7 +4,7 @@ combineKeys() | React-RxJS - + @@ -16,7 +16,7 @@ you a stream that emits a map of this whole data structure whenever any of it changes. If you're careful about where you bind these you can save a lot of component updates:

    interface Pet {
    id: number;
    pet: string,
    pos: number;
    }

    const petNames = ["Fluffy", "Bella", "Nala", "Nocturne", "Teddy"];

    const [petUpdate$, updatePet] = createSignal<Pet>();

    const petRace$ = petUpdate$.pipe(startWith(
    ...petNames.map((pet, id): Pet => ({pet, id, pos: 1})),
    ));

    const [petByID, petIds$] = partitionByKey(petRace$, x => x.id)
    const keyMap$ = combineKeys(petIds$, petByID);

    const leadingPet$ = keyMap$.pipe(map(x => // map to pet with highest pos
    Array.from(x.entries())
    .sort(([k1,v1], [k2,v2]) => v2.pos - v1.pos)[0][1]
    ));

    const advancingPet$: Observable<Pet> = interval(1000).pipe(
    withLatestFrom(keyMap$),
    map(([_, x]) => x),
    takeWhile(x => {
    for (let [k,v] of x) {
    if (v.pos == 20) return false // win condition
    }
    return true;
    }),
    map((x: Map<number, Pet>) =>
    x.get(Math.floor(Math.random() * x.size)) as Pet),
    map(pet => ({...pet, pos: pet.pos + 1})), // increment position
    tap(updatePet),
    );

    const [usePetIDs] = bind(petIds$);
    const [usePetByID] = bind((petId:number) => petByID(petId));
    const [useLeader] = bind(leadingPet$, null);
    const [useAdvancingPet] = bind(advancingPet$, null);

    See also

    - + \ No newline at end of file diff --git a/docs/api/utils/contextBinder.html b/docs/api/utils/contextBinder.html index 2afcaa9..fb53fbd 100644 --- a/docs/api/utils/contextBinder.html +++ b/docs/api/utils/contextBinder.html @@ -4,7 +4,7 @@ contextBinder() | React-RxJS - + @@ -13,7 +13,7 @@ the results of the provided function (which can use hooks)

    export function contextBinder<A extends unknown[], T>(
    ...args: Array<() => any>
    ): typeof bind

    Arguments

    • ...args: A list of functions that its result will be bound to the first arguments within getObservable of the bind function enhanced by this function.

    Returns

    An enhanced bind function where it will have its first arguments bound to the return values of the input functions

    Example

    const MyContext = React.createContext<number>(0);

    const myContextBind = contextBinder(
    () => useContext(MyContext)
    );

    const [useValue, value$] = myContextBind(
    (myContextValue, prefix: string) =>
    of(prefix + ' ' + myContextValue)
    )

    const Component = () => {
    const contextDisplay = useValue('Current context value:'))

    return <div>{contextDisplay}</div>
    }
    - + \ No newline at end of file diff --git a/docs/api/utils/createKeyedSignal.html b/docs/api/utils/createKeyedSignal.html index 782e08f..dae8d15 100644 --- a/docs/api/utils/createKeyedSignal.html +++ b/docs/api/utils/createKeyedSignal.html @@ -4,7 +4,7 @@ createKeyedSignal() | React-RxJS - + @@ -13,7 +13,7 @@ of a Subject for each key.

    export function createKeyedSignal<A extends unknown[], T>(
    keySelector?: (signal: T) => K,
    mapper?: (...args: A) => T,
    ): [(key: K) => GroupedObservable<K, T>, (...args: A) => void]

    Arguments

    • keySelector?: (Optional) A function that extracts the key from the emitted value. If omitted, it will use the first argument as the key.

    • mapper?: (Optional) A function for mapping the arguments of the emitter function into the value of the Observable.

      Defaults to (v: Payload) => v

    Returns

    [1, 2]:

    1. A function that returns the observable for a given key

    2. The emitter function

    See also

    - + \ No newline at end of file diff --git a/docs/api/utils/createSignal.html b/docs/api/utils/createSignal.html index 988f89e..21dc4b0 100644 --- a/docs/api/utils/createSignal.html +++ b/docs/api/utils/createSignal.html @@ -4,7 +4,7 @@ createSignal() | React-RxJS - + @@ -12,7 +12,7 @@

    createSignal()

    Creates a Signal: it's like a subject, but with the consumer and the producer split.

    export function createSignal<A extends unknown[], T>(
    mapper?: (...args: A) => T,
    ): [Observable<T>, (...args: A) => void]

    Arguments

    • mapper?: (Optional) A function for mapping the arguments of the emitter function into the value of the Observable.

      Defaults to (v: Payload) => v

    Returns

    [1, 2]:

    1. The observable for the signal

    2. The emitter function

    Examples

    Void signal (no payload):

    const [buttonPresses$, pressButton] = createSignal();
    // ...
    <button onClick={() => pressButton()}>...</button>

    Taking a payload. Note that without the type parameter you'll get a void signal as above:

    const [itemComplete$, doCompleteItem] = createSignal<number>();

    // ...

    <button onClick={() => doCompleteItem(id)}>...</button>

    Mapping the emitter function's arguments to the resulting emission:

    const mapper = (id: number, status: Status) => ({ id, status });
    const [statusChange$, setStatus] = createSignal(mapper);
    // statusChange$ is Observable<{id: number, status: Status}>
    // setStatus is (id: number, status: Status) => void

    // ...

    <button onClick={() => setStatus(id, Status.Complete)}>...</button>

    See also

    - + \ No newline at end of file diff --git a/docs/api/utils/mergeWithKey.html b/docs/api/utils/mergeWithKey.html index 3cb4985..d73c8dd 100644 --- a/docs/api/utils/mergeWithKey.html +++ b/docs/api/utils/mergeWithKey.html @@ -4,7 +4,7 @@ mergeWithKey(inputObject) | React-RxJS - + @@ -13,7 +13,7 @@ which provides the key of the stream of that emission.

    function mergeWithKey<
    O extends { [P in keyof any]: ObservableInput<any> },
    OT extends {
    [K in keyof O]: O[K] extends ObservableInput<infer V>
    ? { type: K; payload: V }
    : unknown
    }
    >(inputObject: O, concurrent?: number, scheduler?: SchedulerLike):
    Observable<OT[keyof O]>

    Arguments

    • inputObject: An object that contains multiple streams, indexed by key.
    • concurrent: (Optional) Maximum number of input Observables being subscribed to concurrently. Default: Number.POSITIVE_INFINITY
    • scheduler: (Optional) The SchedulerLike to use for managing concurrency of input Observables. Default: null.

    Returns

    Observable<OT[keyof O]>: An observable that emits a flux-like object that contains 2 properties:

    • type: the key of the stream that has emitted.
    • payload: the emitted value.

    Example

    import { Subject } from "rxjs"
    import { scan, startWith } from 'rxjs/operators'
    import { mergeWithKey, createSignal } from '@react-rxjs/utils'

    const [inc$, doInc] = createSignal();
    const [dec$, doDec] = createSignal();
    const [resetTo$, doResetTo] = createSignal<number>();

    const counter$ = mergeWithKey({
    inc$,
    dec$,
    resetTo$,
    }).pipe(
    scan((acc, current) => {
    switch (current.type) {
    case "inc$":
    return acc + 1
    case "dec$":
    return acc - 1
    case "resetTo$":
    return current.payload
    default:
    return acc
    }
    }, 0),
    startWith(0),
    )

    See also

    - + \ No newline at end of file diff --git a/docs/api/utils/partitionByKey.html b/docs/api/utils/partitionByKey.html index c92f4a6..b5ec86a 100644 --- a/docs/api/utils/partitionByKey.html +++ b/docs/api/utils/partitionByKey.html @@ -4,7 +4,7 @@ partitionByKey() | React-RxJS - + @@ -13,7 +13,7 @@ each of these groups by using a map function.

    export function partitionByKey<T, K, R>(
    stream: Observable<T>,
    keySelector: (value: T) => K,
    streamSelector: (grouped: Observable<T>, key: K) => Observable<R>,
    ): [(key: K) => GroupedObservable<K, R>, Observable<K[]>]

    Arguments

    • stream: Input stream
    • keySelector: Function that specifies the key for each element in stream
    • streamSelector: Function to apply to each resulting group

    Returns

    [1, 2]:

    1. A function that accepts a key and returns a stream for the group of that key.

    2. A stream with the list of active keys

    Examples

    const source = interval(1000);
    const [getGroupByKey, keys$] = partitionByKey(
    source,
    x => x % 2 == 0 ? "even" : "odd",
    (groupedObservable$, key) => groupedObservable$.pipe(map(x => `${x} is ${key}`))
    );

    const [useEven, even$] = bind(getGroupByKey("even"));
    const [useOdd, odd$] = bind(getGroupByKey("odd"));
    const [useKeys] = bind(keys$);

    function MyComponent() {
    const odd = useOdd();
    const even = useEven();
    const keys = useKeys();

    return (
    <>
    <div>Your keys are: {keys.join(", ")}</div>
    <div>{odd}</div>
    <div>{even}</div>
    </>
    );
    }

    A more typical list example. The list component can bind the list of keys while the item component binds the stream for each item, eliminating unnecessary renders:

    interface Pet {
    id: number;
    pet: string,
    pos?: number;
    }

    const petNames = ["Fluffy", "Bella", "Nala", "Nocturne", "Teddy"]
    .map((pet, id): Pet => ({pet, id}));

    const [petUpdate$, updatePet] = createSignal<Pet>();

    // Let's line up our pets
    const petRace$ = merge(of(...petNames), petUpdate$);

    const [petByID, petIds$] = partitionByKey(
    petRace$,
    x => x.id,
    )

    const [usePetByID] = bind((id: number) => petByID(id));
    const [usePetIDs] = bind(petIds$);

    const PetItem = ({petID}: {petID: number}) => {
    const pet = usePetByID(petID);

    return (
    <li>
    <div style={{width:'100%', textAlign:'right'}}>
    {pet.pet}
    </div>
    <br />
    <div style={{textAlign:'left'}}>
    {'*'.repeat(pet.pos || 1)}
    </div>
    </li>
    );
    }

    const PetList = () => {
    const petIDs = usePetIDs();

    return (<ul>{petIDs.map(x => (<PetItem key={x} petID={x} />))}</ul>);
    }

    See also

    - + \ No newline at end of file diff --git a/docs/api/utils/selfDependant.html b/docs/api/utils/selfDependant.html index 9fbb774..c1e9225 100644 --- a/docs/api/utils/selfDependant.html +++ b/docs/api/utils/selfDependant.html @@ -4,13 +4,13 @@ selfDependant() | React-RxJS - +

    selfDependant()

    A utility for creating observables that have circular dependencies.

    function selfDependant<T>(): [Observable<T>, () => MonoTypeOperatorFunction<T>];

    Returns

    [1, 2]:

    1. The inner Subject as an Observable.

    2. A pipeable operator that taps into the inner Subject.

    Example

    import { merge, of, Subject } from 'rxjs'
    import { delay, map, share, switchMapTo, withLatestFrom } from 'rxjs/operators'
    import { selfDependant } from '@react-rxjs/utils'

    const [_resettableCounter$, connectResettableCounter] = selfDependant<number>()

    const clicks$ = new Subject()
    const inc$ = clicks$.pipe(
    withLatestFrom(_resettableCounter$),
    map((_, x) => x + 1),
    share(),
    )

    const delayedZero$ = of(0).pipe(delay(10_000))
    const reset$ = inc$.pipe(switchMapTo(delayedZero$))

    const resettableCounter$ = merge(inc$, reset$, of(0)).pipe(
    connectResettableCounter(),
    )
    - + \ No newline at end of file diff --git a/docs/api/utils/split.html b/docs/api/utils/split.html index 9c2c04f..b1a283d 100644 --- a/docs/api/utils/split.html +++ b/docs/api/utils/split.html @@ -4,7 +4,7 @@ split(keySelector) | React-RxJS - + @@ -15,7 +15,7 @@ are optional transformed by the stream selector function, if specified.

    Description

    split will subscribe to each grouped Observable and share the result to every inner subscriber of that group. This inner Observable can be mapped to another Observable through the streamSelector argument.

    See also

    - + \ No newline at end of file diff --git a/docs/api/utils/suspend.html b/docs/api/utils/suspend.html index f3c9624..eedf965 100644 --- a/docs/api/utils/suspend.html +++ b/docs/api/utils/suspend.html @@ -4,14 +4,14 @@ suspend(observable) | React-RxJS - +

    suspend(observable)

    A RxJS creation operator that prepends a SUSPENSE to the source Observable.

    function suspend<T>(source$: Observable<T>) => Observable<T | typeof SUSPENSE>

    Arguments

    • source$: The source Observable.

    Returns

    Observable<T | typeof SUSPENSE>: An Observable that emits SUSPENSE as its first value, followed by the values from the source Observable.

    Example

    import { switchMap } from 'rxjs/operators'
    import { suspend } from '@react-rxjs/utils'

    const story$ = selectedStoryId$.pipe(
    switchMap(id => suspend(getStory$(id))
    )

    See also

    - + \ No newline at end of file diff --git a/docs/api/utils/suspended.html b/docs/api/utils/suspended.html index c9ffbe5..84c11b6 100644 --- a/docs/api/utils/suspended.html +++ b/docs/api/utils/suspended.html @@ -4,14 +4,14 @@ suspended() | React-RxJS - +

    suspended()

    The pipeable version of suspend. Prepends a SUSPENSE to the source Observable.

    function suspended<T>(): OperatorFunction<T, T | typeof SUSPEND>

    Returns

    OperatorFunction<T, T | typeof SUSPEND>: An Observable that emits SUSPENSE as its first value, followed by the values from the source Observable.

    Example

    import { switchMap } from 'rxjs/operators'
    import { suspended } from '@react-rxjs/utils'

    const story$ = selectedStoryId$.pipe(
    switchMap((id) => getStory$(id).pipe(suspended())),
    )

    See also

    - + \ No newline at end of file diff --git a/docs/api/utils/switchMapSuspended.html b/docs/api/utils/switchMapSuspended.html index badaa1a..b29b933 100644 --- a/docs/api/utils/switchMapSuspended.html +++ b/docs/api/utils/switchMapSuspended.html @@ -4,7 +4,7 @@ switchMapSuspended() | React-RxJS - + @@ -12,7 +12,7 @@

    switchMapSuspended()

    Like switchMap, but applying a startWith(SUSPENSE) to the inner Observable.

    function switchMapSuspended<T, O extends ObservableInput<any>>(project: (value: T, index: number) => O): 
    OperatorFunction<T, ObservedValueOf<O> | typeof SUSPENSE>;

    Arguments

    • project: A function that, when applied to an item emitted by the source Observable, returns an Observable.

    Returns

    OperatorFunction<T, ObservedValueOf<O> | typeof SUSPENSE>: An Observable that emits the result of applying the projection function to each item emitted by the source Observable, and taking only the values from the most recently projected inner Observable, prepended with SUSPENSE.

    Example

    import { switchMapSuspended } from '@react-rxjs/utils'

    const story$ = selectedStoryId$.pipe(switchMapSuspended(getStory$))

    See also

    - + \ No newline at end of file diff --git a/docs/core-concepts.html b/docs/core-concepts.html index 183a63d..a530f75 100644 --- a/docs/core-concepts.html +++ b/docs/core-concepts.html @@ -4,13 +4,13 @@ Core Concepts | React-RxJS - +

    Core Concepts

    Push vs Pull

    Historically, React uses a pull-based architecture. This means that when React needs to re-render, it will call the render function of every affected component. This will return a new representation of the UI, which React can reconcile with the previous one. Any changes are then propagated to the DOM.

    This kind of behavior is called pull because the consumer (in this case, React), is the one that requests the new value.

    On the other hand, RxJS uses a push-based approach, where you declaratively define streams and their relationships, and RxJS will propagate every change from one stream to the next one.

    This is called push because now the producer of the state is responsible for handing the new value over to those that depend on it. This has a positive effect: only those entities that depend on the value that has changed will update, and it can be done without having to make comparisons or detect changes.

    Not only can this approach significantly improve performance, it also makes state management more declarative, in a way that can be read top-to-bottom.

    React-RxJS bridges the gap between these two behaviors, making it possible to declare a push-based application state that works flawlessly with pull-based React.

    Streams as state

    RxJS streams are used to represent events or changing values over time. They have an important property: Because of their declarative nature, they don't execute the effect until someone subscribes to it.

    import { Observable } from "rxjs"

    const first5Numbers = new Observable((obs) => {
    console.log("hello!")
    for (let i = 0; i < 5; i++) obs.next(i)
    obs.complete()
    })
    // Logs nothing

    first5Numbers.subscribe((n) => {
    console.log(n)
    })
    // Logs "hello!" followed by 0 1 2 3 4

    Not only that, but they are unicast: A new subscription is created for every new observer.

    import { interval } from "rxjs"
    import { take } from "rxjs/operators"

    const first5SpacedNumbers = interval(1000).pipe(take(5))

    first5SpacedNumbers.subscribe((v) => console.log("A", v))
    // Will start logging A1... A2...

    setTimeout(() => {
    first5SpacedNumbers.subscribe((v) => console.log("B", v))
    }, 2000)
    // Will continue with B1... A3... B2... A4

    This makes sense because you might want to have a different state for each subscription. However, this doesn't play nicely with React. In React, you have different components, and they all need to receive the same value. Moreover, if that value dispatches a call to a service, you'd only want to make one single call to be shared among all of the components.

    RxJS has an operator that helps with this, called share:

    import { interval } from "rxjs"
    import { take, share } from "rxjs/operators"

    const first5SpacedNumbers = interval(1000).pipe(take(5), share())

    first5SpacedNumbers.subscribe((v) => console.log("A", v))
    // Will start logging A1... A2...

    setTimeout(() => {
    first5SpacedNumbers.subscribe((v) => console.log("B", v))
    }, 2000)
    // Will continue with A3 B3... A4 B4...

    The technical term for this is that share multicasts the stream, so that it only makes one subscription to the source, and will propagate every change to all the subscriptions of the shared stream.

    However, this now has a different issue for React's use case: If you look closely at the last snippet, even though "B" subscribed when the last value of the stream was 2, it didn't receive that value. And it makes sense because the change to 2 was emitted in the past - "B" didn't receive that change because it subscribed later.

    As React is pull-based, it needs access to the latest value emitted from the stream when it needs to re-render. With the current model, it would have to wait until a new change is emitted in the stream before it can receive the new state, which wouldn't really work. Here's where React-RxJS comes into play.

    RxJS has another operator shareReplay which would cover this issue. However, it doesn't play nicely with the way that React works: when the source completes it will keep the last values in memory indefinitely, which would cause a possible memory leak.

    So that's why React-RxJS provides shareLatest. In essence, it addresses the issue of sharing the state between many components and keeping always the latest value, but without the additional issues that shareReplay exposes for this particular use case. So with React-RxJS our example would become:

    import { interval } from "rxjs"
    import { take } from "rxjs/operators"
    import { shareLatest } from "@react-rxjs/core"

    const first5SpacedNumbers = interval(1000).pipe(take(5), shareLatest())

    first5SpacedNumbers.subscribe((v) => console.log("A", v))
    // Will start logging A1... A2...

    setTimeout(() => {
    first5SpacedNumbers.subscribe((v) => console.log("B", v))
    }, 2000)
    // Will continue with B2... A3 B3... A4 B4...

    Now this stream would be ready to be consumed by React. shareLatest in a way turns a stream into a state entity. Something that owns a current value, while also providing a way to subscribe to future updates.

    The main function of React-RxJS, bind, uses this operator on every stream. bind is the function you need to use to get a React hook that will receive that value. This function not only adds shareLatest to the stream, but also applies a few more tricks to integrate with React, such as:

    • Leveraging Suspense, so that you can represent loading states from the streams.
    • Leveraging Error Boundaries to allow graceful error recovery.
    • Performance optimizations, making sure React doesn't update when it doesn't need to.
    • Manages a cache of parametric observables (when using the factory overload).

    If we use bind instead, our example will become:

    import { interval } from "rxjs"
    import { take } from "rxjs/operators"
    import { bind } from "@react-rxjs/core"

    const [useFirst5SpacedNumbers, first5SpacedNumbers$] = bind(
    interval(1000).pipe(take(5)),
    )

    useFirst5SpacedNumbers is a hook that will return just a number, which is shared for all components that use it.

    Something important to note, though, is that the subscription to the shared observable (in this case, first5SpacedNumbers$) must have an active subscription before the hook can execute. We can't rely on React renderer to make the initial subscription for us (the subscription which would trigger the side effect), because we can't rely on when rendering happens, nor if it will be interrupted or cancelled.

    React-RxJS provides different ways of addressing this: The most simple one is to declare the default value for that hook by using the optional argument in bind:

    import { interval } from "rxjs"
    import { take } from "rxjs/operators"
    import { bind } from "@react-rxjs/core"

    const [useFirst5SpacedNumbers, first5SpacedNumbers$] = bind(
    interval(1000).pipe(take(5)),
    0 // Default value
    )

    function NumberDisplay() {
    const number = useFirst5SpacedNumbers()

    return <div>{number}</div>;
    }

    function App() {
    return <NumberDisplay />
    }

    When a React-RxJS hook has a default value and no one is subscribed to its observable, on the first render it will return that value, and then it will safely subscribe to the source after mounting. If the underlying observable did have a subscription before the component was mounted, it will directly get the current value instead.

    If you don't give it a default value, you will need to make sure that observable has a subscription active before the Component that uses that hook is called. React-RxJS has a utility that helps with this: <Subscribe source$={stream}>{ children }</Subscribe> will render { children } only after subscribing to its source$. Subscribe also subscribes to all the observables used by its children (as if it were a React's Context), so in this case we can just omit source$

    import { interval } from "rxjs"
    import { take } from "rxjs/operators"
    import { bind, Subscribe } from "@react-rxjs/core"

    const [useFirst5SpacedNumbers, first5SpacedNumbers$] = bind(
    interval(1000).pipe(take(5))
    )

    function NumberDisplay() {
    const number = useFirst5SpacedNumbers()

    return <div>{number}</div>;
    }

    function App() {
    return <Subscribe>
    <NumberDisplay />
    </Subscribe>
    }

    Keep in mind that <Subscribe> will hold a subscription to the observables until it gets unmounted, you need to decide where to put these <Subscribe> boundaries on your application so that subscriptions get cleaned up properly.

    With the mental model of "streams as state", it's also worth noting that the observables returned by bind won't complete: If the source of that observable completes, it will keep the last value and replay it back to new subscribers, as a completion on the source means that there won't be more changes to that stream. Remember that if the subscriber count reaches 0, this state will be cleaned up, and the subscription will restart when a new observer subscribes later on.

    Composing streams

    As the stream returned by bind is shared, it can be easily composed with other streams.

    import { interval } from "rxjs"
    import { take } from "rxjs/operators"
    import { bind } from "@react-rxjs/core"

    const [useSeconds, second$] = bind(interval(1000))

    const [useLatestNSeconds, latestNSeconds$] = bind((n: number) =>
    second$.pipe(take(n)),
    )

    Composition is an important factor in RxJS streams. It's often recommended to break down streams into smaller chunks, that you can later compose into more complex interactions.

    Note that you might not need to use bind on every observable. bind only makes sense when you need to get a hook for that stream, or to create a factory of observables (basically a function that returns observables based on its arguments).

    Entry points

    Now, where does data for the state come from? Probably the first example that we might think in RxJS is something that fetches some data:

    import { ajax } from "rxjs/ajax"
    import { bind } from "@react-rxjs/core"

    const [useTodos, todo$] = bind(ajax.getJSON("/todos"))

    And of course, this will work: Any component can use useTodos to get the list of todos.

    However, there are some times where we need to use data coming directly from the user. This is where RxJS Subjects come into play. In React-RxJS we've abstracted this into signals, which separate the producer and the consumer of that subject.

    With a signal you can create an entry point for your streams. For example, in a local todos app, you can define your state as:

    import { scan } from "rxjs/operators"
    import { bind } from "@react-rxjs/core"
    import { createSignal } from "@react-rxjs/utils"

    const [newTodos$, postNewTodo] = createSignal();

    const [useTodoList, todoList$] = bind(
    newTodos$.pipe(
    scan((acc, todo) => [...acc, todo], [])
    ),
    []
    )

    And now the "TodoForm" component can directly call postNewTodo whenever the user creates a todo, and the change will be propagated down to the list.

    Keep in mind that bind doesn't do magic. If no one is subscribed to todoList$ (not even from the hook) then that stream won't be listening for changes on newTodos subject, and if a subscription happens late, the subject won't replay the todos created, so they would get lost.

    Instances

    There are many times where you need components to access a particular instance - Classic example is a list of posts. To help with that, bind can also take a factory function that returns an Observable for that instance.

    For example, if we have a list of posts, we might have an observable that has all of them in a dictionary:

    const [usePosts, posts$] = bind(service.getPost$()) // Dictionary<Post>

    Although from within each instance component we could theoretically call usePosts(), and then take the post that component actually needs, this would cause unnecessary re-renders when other instances change. We solve this by using the factory overload:

    const [usePost, post$] = bind((id: string) =>
    posts$.pipe(map((posts) => posts[id])),
    )

    And now the component can use usePost(id) by passing it's own id, and that component will re-render only when that post changes. The second parameter returned, post$, it's actually also a function so that it can be composed in other streams: post$(id) returns the observable instance that emits Posts for that specific id.

    Lastly, do not use this overload of bind as a way of calling the server. React-RxJS' mental model is that the observables are state, using bind to create a hook useCreateUser(userName, email) that makes the action of creating a new user won't work as you'd like to. Instead, create a signal and have an observable depend on that signal to send the request to the server.

    Suspense

    In an earlier example:

    import { ajax } from "rxjs/ajax"
    import { bind } from "@react-rxjs/core"

    const [useTodos, todo$] = bind(ajax.getJSON("/todos"))

    You might be wondering - how does this exactly work with React? If React is pull-based and it needs a value at the time it's re-rendering, this stream might not have a value until the ajax call is resolved.

    Well, React added a feature called Suspense. With Suspense, we can represent values that are not yet ready, and we can notify React when those values have been loaded.

    react-rxjs comes with full support for Suspense, and it treats it as a first-class citizen. This means that by default, using a hook from a stream that hasn't emitted any value will result in that hook suspending the component.

    Note that for this to work properly, you need to have proper Suspense boundaries throughout your component tree. If you don't want to use Suspense just yet, the solution is simple: Make sure that the stream always has a value. bind also takes an optional parameter for the default value, which guarantees that the hook won't invoke suspense:

    import { ajax } from "rxjs/ajax"
    import { bind } from "@react-rxjs/core"

    const [useTodos, todos$] = bind(
    ajax.getJSON("/todos"),
    null
    )

    Now useTodos will emit null immediately while it's fetching data (so that we can manually handle that), instead of suspending the component, and when the ajax call is resolved, it will emit the result of that call.

    When using Suspense, however, there's also another way to suspend a component with react-rxjs: by emitting SUSPENSE. For example, this can come in handy if you need to refresh the data because some filter has changed.

    Error boundaries

    React 16 added the concept of Error Boundaries: A way to catch errors in the component tree and show a fallback UI so it can be recovered from.

    React-RxJS is mindful of these, in a way that if one of the streams emits an error, the components that are subscribed to that stream will propagate that error to the nearest Error Boundary.

    We recommend creating Error Boundaries with react-error-boundary, because it creates a good abstraction to build them, by declaring a fallback component and recovery strategy, in a similar way to Suspense Boundaries.

    Let's take a look at an example:

    import { bind } from "@react-rxjs/core"
    import { interval } from "rxjs"
    import { map, startWith } from "rxjs/operators"
    import { ErrorBoundary } from "react-error-boundary"

    const [useTimedBomb, timedBomb$] = bind(
    interval(1000).pipe(
    map((v) => v + 1),
    startWith(0),
    map((v) => {
    if (v === 3) {
    throw new Error("boom")
    }
    return v
    }),
    ),
    )

    function Bomb() {
    const time = useTimedBomb()

    return <div>{time}</div>
    }

    function ErrorFallback({ error, componentStack, resetErrorBoundary }) {
    return (
    <div>
    <p>Something went wrong:</p>
    <pre>{error?.message}</pre>
    <pre>{componentStack}</pre>
    <button onClick={resetErrorBoundary}>Try again</button>
    </div>
    )
    }

    function App() {
    return (
    <div className="App">
    <ErrorBoundary FallbackComponent={ErrorFallback}>
    <Subscribe>
    <Bomb />
    </Subscribe>
    </ErrorBoundary>
    </div>
    )
    }

    In here, useTimedBomb will start counting from 0 and emit an error in the 3rd second. React-RxJS ensures that this error will be caught in the ErrorBoundary defined for the component that's using this stream, so the fallback UI will be shown.

    When a rxjs stream emits an error, the stream gets immediately closed. This way, if our strategy to recover from the error is to try again, when our Subscribe boundary resubscribes to the stream it will create a new subscription and start over again.

    In this case, after 3 seconds it will throw an error again, but in a real-world case this might be different, and you might need different recovery strategies depending on each case. react-error-boundary helps by providing a declarative way to define these strategies.

    - + \ No newline at end of file diff --git a/docs/getting-started.html b/docs/getting-started.html index c5d93a2..7606306 100644 --- a/docs/getting-started.html +++ b/docs/getting-started.html @@ -4,14 +4,14 @@ Getting Started | React-RxJS - +

    Getting Started

    Installation

    React-RxJS is published in npm as @react-rxjs/core

    npm i rxjs @react-rxjs/core @react-rxjs/utils

    or using yarn

    yarn add rxjs @react-rxjs/core @react-rxjs/utils

    Create a hook from an observable

    @react-rxjs/core exports a function called bind which is used to connect a stream to a hook.

    import { bind } from "@react-rxjs/core"
    import { createSignal } from "@react-rxjs/utils"

    // A signal is an entry point to react-rxjs. It's equivalent to using a subject
    const [textChange$, setText] = createSignal();

    const [useText, text$] = bind(textChange$, "")

    function TextInput() {
    const text = useText()

    return (
    <div>
    <input
    type="text"
    value={text}
    placeholder="Type something..."
    onChange={(e) => setText(e.target.value)}
    />
    <br />
    Echo: {text}
    </div>
    )
    }

    bind returns a tuple that contains the hook, plus the underlying shared observable so it can be used by other streams:

    import { map } from "rxjs/operators"
    import { bind, Subscribe } from "@react-rxjs/core"

    // Previously...
    // const [useText, text$] = bind(...);

    const [useCharCount, charCount$] = bind(
    text$.pipe(
    map((text) => text.length)
    )
    )

    function CharacterCount() {
    const count = useCharCount()

    return <>Character Count: {count}</>
    }

    Something to note is that a subscription on the underlying observable must be present before the hook is executed. We can use Subscribe to help us with it:

    function CharacterCounter() {
    return (
    <div>
    <Subscribe>
    <TextInput />
    <CharacterCount />
    </Subscribe>
    </div>
    )
    }

    The interactive result:

    Next steps

    We strongly recommend reading through core concepts to understand the mindset of this library.

    - + \ No newline at end of file diff --git a/docs/motivation.html b/docs/motivation.html index 3206807..901e619 100644 --- a/docs/motivation.html +++ b/docs/motivation.html @@ -4,7 +4,7 @@ Motivation | React-RxJS - + @@ -19,7 +19,7 @@ operator 🤷, but we will be able to declare the dynamic behavior of our state at the time of its declaration using RxJS streams. No stores. No context. Just reactive streams that integrate seamlessly with React.

    Working with Reactive solutions has many advantages, among them:

    • They provide loosely coupled solutions: reactive streams are only coupled to events that they directly depend on.
    • Avoiding unnecessary computations, which translates into optimal react updates.
    • Improving code navigability, by avoiding unnecessary layers of indirection.
    • If we compare them with Flux based architectures, they generate a lot less boilerplate.
    • By avoiding central stores we get code-splitting out of the box.
    - + \ No newline at end of file diff --git a/docs/tutorial/github-issues.html b/docs/tutorial/github-issues.html index fefcbf2..fa007b0 100644 --- a/docs/tutorial/github-issues.html +++ b/docs/tutorial/github-issues.html @@ -4,7 +4,7 @@ Github Issues Viewer | React-RxJS - + @@ -118,7 +118,7 @@ that are only done from the IssuesDetailsPage component, or the RxJS operators that are only being used for the IssuesDetailsPage, were also excluded from the main chunk.

    - + \ No newline at end of file diff --git a/docs/tutorial/todos.html b/docs/tutorial/todos.html index 99137c9..68714e7 100644 --- a/docs/tutorial/todos.html +++ b/docs/tutorial/todos.html @@ -4,7 +4,7 @@ Todo App | React-RxJS - + @@ -16,7 +16,7 @@ user. Let's create some Signals for this:

    const [newTodo$, onNewTodo] = createSignal<string>();
    const [editTodo$, onEditTodo] = createSignal<{ id: number; text: string }>();
    const [toggleTodo$, onToggleTodo] = createSignal<number>();
    const [deleteTodo$, onDeleteTodo] = createSignal<number>();

    Creating a single stream for all the user events

    It would be very convenient to have a merged stream with all those events. However, if we did a traditional merge, then it would be very challenging to know the origin of each event.

    That's why @react-rxjs/utils exposes the mergeWithKey -operator. Let's use it:

    const todoActions$ = mergeWithKey({
    add: newTodo$.pipe(map((text, id) => ({ id, text }))),
    edit: editTodo$,
    toggle: toggleTodo$.pipe(map(id => ({ id }))),
    delete: deleteTodo$.pipe(map(id => ({ id })))
    })

    Which is basically the same as doing this (but a lot shorter, of course 😄):

    const todoActions$ = merge(
    newTodo$.pipe(map(text, id) => ({
    type: "add" as const
    payload: { id, text },
    })),
    editTodo$.pipe(map(payload => ({
    type: "edit" as const,
    payload,
    }))),
    toggleTodo$.pipe(map(id => ({
    type: "toggle" as const,
    payload: { id },
    }))),
    deleteTodo$.pipe(map(id => ({
    type: "delete" as const,
    payload: { id },
    }))),
    )

    Creating a stream for each todo

    Now that we have put all the streams together, let's create a stream for +operator. Let's use it:

    const todoActions$ = mergeWithKey({
    add: newTodo$.pipe(map((text, id) => ({ id, text }))),
    edit: editTodo$,
    toggle: toggleTodo$.pipe(map(id => ({ id }))),
    delete: deleteTodo$.pipe(map(id => ({ id })))
    })

    Which is basically the same as doing this (but a lot shorter, of course 😄):

    const todoActions$ = merge(
    newTodo$.pipe(map((text, id) => ({
    type: "add" as const,
    payload: { id, text },
    }))),
    editTodo$.pipe(map(payload => ({
    type: "edit" as const,
    payload,
    }))),
    toggleTodo$.pipe(map(id => ({
    type: "toggle" as const,
    payload: { id },
    }))),
    deleteTodo$.pipe(map(id => ({
    type: "delete" as const,
    payload: { id },
    }))),
    )

    Creating a stream for each todo

    Now that we have put all the streams together, let's create a stream for each todo. And for that, we will be using another operator from @react-rxjs/utils: the partitionByKey operator:

    type Todo = { id: number; text: string; done: boolean };
    const [todosMap, keys$] = partitionByKey(
    todoActions$,
    event => event.payload.id,
    (event$, id) =>
    event$.pipe(
    takeWhile((event) => event.type !== "delete"),
    scan(
    (state, action) => {
    switch (action.type) {
    case "add":
    case "edit":
    return { ...state, text: action.payload.text };
    case "toggle":
    return { ...state, done: !state.done };
    default:
    return state;
    }
    },
    { id, text: "", done: false } as Todo
    )
    )
    )

    Now we have a function, todosMap, that returns an Observable of events associated with a given todo. partitionByKey transforms the source observable in a way @@ -47,7 +47,7 @@ which todos exist. Luckily we already have a stream for that, returned above by partitionByKey. So:

    const [useTodoIds] = bind(keys$);

    Simple! Now we edit our TodoList component to pass just the todo id:

     function TodoList() {
    - const todoList = useTodos();
    + const todoIds = useTodos();

    return (
    <>
    <TodoListStats />
    <TodoListFilters />
    <TodoItemCreator />

    - {todoList.map((todoItem) => (
    - <TodoItem key={todoItem.id} item={todoItem} />
    - ))}
    + {todoIds.map((id) => (
    + <TodoItem key={id} id={id} />
    + ))}
    </>
    );
    }

    and teach TodoItem to get its state from the stream corresponding to that id, rather than from its parent component:

    const TodoItem: React.FC<{ id: number }> = ({ id }) => {
    const item = useTodo(id);

    return( ... )
    }

    Adding filters

    As we already know, we will need to capture the filter selected by the user:

    export enum FilterType {
    All = "all",
    Done = "done",
    Pending = "pending"
    }
    const [selectedFilter$, onSelectFilter] = createSignal<FilterType>()

    Next, let's create a hook and a stream for the current filter:

    const [useCurrentFilter, currentFilter$] = bind(
    selectedFilter$.pipe(startWith(FilterType.All))
    )

    Also, let's tell our TodoItems not to render if they've been filtered out:

     const TodoItem: React.FC<{ id: number }> = ({ id }) => {
    const item = useTodo(id);
    + const currentFilter = useCurrentFilter();

    return ( ... );
    }

    Time to implement the TodoListFilters component:

    function TodoListFilters() {
    const filter = useCurrentFilter()

    const updateFilter = ({target: {value}}) => {
    onSelectFilter(value)
    };

    return (
    <>
    Filter:
    <select value={filter} onChange={updateFilter}>
    <option value={FilterType.All}>All</option>
    <option value={FilterType.Done}>Completed</option>
    <option value={FilterType.Pending}>Uncompleted</option>
    </select>
    </>
    );
    }

    Adding stats

    We will be showing the following stats:

    • Total number of todo items
    • Total number of completed items
    • Total number of uncompleted items
    • Percentage of items completed

    Let's create a useTodosStats for it:

    const [useTodosStats] = bind(
    todosList$.pipe(map(todosList => {
    const nTotal = todosList.length
    const nCompleted = todosList.filter((item) => item.done).length
    const nUncompleted = nTotal - nCompleted
    const percentCompleted =
    nTotal === 0 ? 0 : Math.round((nCompleted / nTotal) * 100)

    return {
    nTotal,
    nCompleted,
    nUncompleted,
    percentCompleted,
    }
    }))
    )

    And now let's use this hook in the TodoListStats component:

    function TodoListStats() {
    const { nTotal, nCompleted, nUncompleted, percentCompleted } = useTodosStats()

    return (
    <ul>
    <li>Total items: {nTotal}</li>
    <li>Items completed: {nCompleted}</li>
    <li>Items not completed: {nUncompleted}</li>
    <li>Percent completed: {percentCompleted}</li>
    </ul>
    );
    }

    Summary

    The result of this tutorial can be seen in this CodeSandbox:

    - + \ No newline at end of file diff --git a/index.html b/index.html index 558c1a0..857a2e0 100644 --- a/index.html +++ b/index.html @@ -4,13 +4,13 @@ React-RxJS | React-RxJS - +
    React-RxJS logo

    React-RxJS

    React bindings for RxJS

    Truly reactive

    React-RxJS allows you to express the dynamic behavior of your app's state completely at the time of its definition.

    Seamless React integration

    React-RxJS offers a hook-based API with first-class support for React.Suspense and Error Boundaries. Also, all hooks created with React-RxJS can be used for sharing state.

    Highly performant

    Modeling your state with observables enables a highly performant state propagation system based on forward referencing subscriptions.

    - + \ No newline at end of file diff --git a/search.html b/search.html index 1bdb942..6976a13 100644 --- a/search.html +++ b/search.html @@ -4,13 +4,13 @@ Search the documentation | React-RxJS - + - + \ No newline at end of file