From 419bc927598d97d6a9868da18324a24bcdb2565b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Oliva?= Date: Thu, 6 May 2021 23:26:23 +0200 Subject: [PATCH 1/3] start restructure --- docs/features.md | 11 +++++++++++ docs/{getting-started.md => quick-start.md} | 2 +- sidebars.js | 3 +-- src/pages/index.js | 2 +- 4 files changed, 14 insertions(+), 4 deletions(-) create mode 100644 docs/features.md rename docs/{getting-started.md => quick-start.md} (98%) diff --git a/docs/features.md b/docs/features.md new file mode 100644 index 0000000..bd1060a --- /dev/null +++ b/docs/features.md @@ -0,0 +1,11 @@ +--- +title: Features +--- + +- Build truly reactive state. +- Makes code-splitting simple. +- First-class support for React Suspense. +- First-class support for Error boundaries. +- Built in Typescript. +- Extremely light: 3.4kB parsed, 1.5kB gziped. +- Thin API. \ No newline at end of file diff --git a/docs/getting-started.md b/docs/quick-start.md similarity index 98% rename from docs/getting-started.md rename to docs/quick-start.md index d3732a7..1a7d052 100644 --- a/docs/getting-started.md +++ b/docs/quick-start.md @@ -1,5 +1,5 @@ --- -title: Getting Started +title: Quick Start --- import CharacterCounter from "./examples/CharacterCounter" diff --git a/sidebars.js b/sidebars.js index 1c2bbd6..b530fb3 100644 --- a/sidebars.js +++ b/sidebars.js @@ -1,7 +1,6 @@ module.exports = { someSidebar: { - Introduction: ["motivation", "core-concepts", "getting-started"], - Tutorial: ["tutorial/github-issues", "tutorial/todos"], + Introduction: ["motivation", "quick-start", "features"], "API Reference": [ { type: "category", diff --git a/src/pages/index.js b/src/pages/index.js index 0613a4e..b05e7a8 100644 --- a/src/pages/index.js +++ b/src/pages/index.js @@ -112,7 +112,7 @@ function Home() { "button button--outline button--secondary button--lg", styles.getStarted, )} - to={useBaseUrl("docs/getting-started")} + to={useBaseUrl("docs/motivation")} > Get Started From 38d74ea55d54accc20509d7e3238b7dd40851028 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Oliva?= Date: Thu, 6 May 2021 23:43:27 +0200 Subject: [PATCH 2/3] rework quick start --- docs/features.md | 7 ++++- docs/quick-start.md | 67 ++++++++++++++++++++------------------------- 2 files changed, 36 insertions(+), 38 deletions(-) diff --git a/docs/features.md b/docs/features.md index bd1060a..f8fb6da 100644 --- a/docs/features.md +++ b/docs/features.md @@ -4,8 +4,13 @@ title: Features - Build truly reactive state. - Makes code-splitting simple. +- Simplifies code navigability. - First-class support for React Suspense. - First-class support for Error boundaries. +- No centralized store. +- No state context provider. - Built in Typescript. +- Works with React DOM and React Native. +- No external dependencies. - Extremely light: 3.4kB parsed, 1.5kB gziped. -- Thin API. \ No newline at end of file +- Thin API. diff --git a/docs/quick-start.md b/docs/quick-start.md index 1a7d052..0852cb5 100644 --- a/docs/quick-start.md +++ b/docs/quick-start.md @@ -7,32 +7,42 @@ import BrowserOnly from '@docusaurus/BrowserOnly'; ## Installation -React-RxJS is published in npm as `@react-rxjs/core` +React-RxJS is published in npm as `@react-rxjs/core`. +We also publish a recommended `@react-rxjs/utils` package with some useful functions to build reactive state streams. ```sh -npm i rxjs @react-rxjs/core +npm i rxjs @react-rxjs/core @react-rxjs/utils ``` or using yarn ```sh -yarn add rxjs @react-rxjs/core +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. +## Usage ```tsx -import { bind } from "@react-rxjs/core" +import { map } from "rxjs/operators" +import { bind, Subscribe } 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(); +// bind returns a hook to get the value of the observable. const [useText, text$] = bind(textChange$, "") +// And it also returns an observable so we can compose this state with other observables +// in here we're making a derived state by calculating the text$'s length. +const [useCharCount, charCount$] = bind( + text$.pipe( + map((text) => text.length) + ) +) + function TextInput() { + // Just use the hook! const text = useText() return ( @@ -41,48 +51,30 @@ function TextInput() { type="text" value={text} placeholder="Type something..." - onChange={(e) => setText(e.target.value)} + onChange={ + // And trigger the signal + (e) => setText(e.target.value) + } />
Echo: {text} ) } -``` - -`bind` returns a tuple that contains the hook, plus the underlying shared observable so it can be used by other streams: - -```tsx -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: - -```tsx function CharacterCounter() { + // `Subscribe` manages the subscription lifetime of its children return ( -
- - - - -
+ + + + ) } ``` @@ -93,7 +85,8 @@ The interactive result: {() => } -## Next steps +## There's more! + +This is just a simple example of two components sharing a synchronous state. -We strongly recommend reading through [core concepts](core-concepts.md) to -understand the mindset of this library. +React-RxJS gets even more fun when you start using asynchronous state, leveraging Suspense and enhancing code-splitting! \ No newline at end of file From ea6be452bc855380e55f89432b1bb20a5269c6b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Oliva?= Date: Fri, 7 May 2021 00:32:16 +0200 Subject: [PATCH 3/3] recipes/invalidate-query --- docs/examples/InvalidateQuery.tsx | 74 +++++++++++++++++++++++++++++++ docs/features.md | 5 +-- docs/quick-start.md | 6 +-- docs/recipes/invalidate-query.md | 71 +++++++++++++++++++++++++++++ sidebars.js | 1 + 5 files changed, 151 insertions(+), 6 deletions(-) create mode 100644 docs/examples/InvalidateQuery.tsx create mode 100644 docs/recipes/invalidate-query.md diff --git a/docs/examples/InvalidateQuery.tsx b/docs/examples/InvalidateQuery.tsx new file mode 100644 index 0000000..11859b1 --- /dev/null +++ b/docs/examples/InvalidateQuery.tsx @@ -0,0 +1,74 @@ +import { bind, Subscribe } from "@react-rxjs/core" +import { createSignal } from "@react-rxjs/utils" +import React, { useRef } from "react" +import { concat, defer } from "rxjs" +import { concatMap, switchMap } from "rxjs/operators" + +const { getTodos, postTodo } = (() => { + let todos = [ + { + id: 0, + title: "Grocery shopping", + }, + ] + + return { + getTodos: async () => todos, + postTodo: async (todo) => { + todos = [ + ...todos, + { + id: todos[todos.length - 1].id + 1, + title: todo, + }, + ] + }, + } +})() + +const [todoPost$, addTodo] = createSignal() + +const todoResult$ = todoPost$.pipe(concatMap(postTodo)) + +const [useTodos] = bind( + // When do we need to request todos? + concat( + // 1. One single time when starting + defer(getTodos), + // 2. Every time we have created a new todo + todoResult$.pipe(switchMap(getTodos)), + ), + [], +) + +function Todos() { + const todos = useTodos() + + const ref = useRef() + const handleAddClick = () => { + addTodo(ref.current!.value) + ref.current!.value = "" + ref.current!.focus() + } + + return ( +
+ + + +
    + {todos.map((todo) => ( +
  • {todo.title}
  • + ))} +
+
+ ) +} + +export default function InvalidateQuery() { + return ( + Loading...}> + + + ) +} diff --git a/docs/features.md b/docs/features.md index f8fb6da..ea76f4c 100644 --- a/docs/features.md +++ b/docs/features.md @@ -7,10 +7,9 @@ title: Features - Simplifies code navigability. - First-class support for React Suspense. - First-class support for Error boundaries. -- No centralized store. -- No state context provider. +- Completely decentralized store. - Built in Typescript. -- Works with React DOM and React Native. +- Works with any React renderer (React DOM, React Native, etc.). - No external dependencies. - Extremely light: 3.4kB parsed, 1.5kB gziped. - Thin API. diff --git a/docs/quick-start.md b/docs/quick-start.md index 0852cb5..93f9f43 100644 --- a/docs/quick-start.md +++ b/docs/quick-start.md @@ -28,7 +28,7 @@ import { bind, Subscribe } 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 [textChange$, setText] = createSignal() // bind returns a hook to get the value of the observable. const [useText, text$] = bind(textChange$, "") @@ -79,7 +79,7 @@ function CharacterCounter() { } ``` -The interactive result: +### Interactive result {() => } @@ -89,4 +89,4 @@ The interactive result: This is just a simple example of two components sharing a synchronous state. -React-RxJS gets even more fun when you start using asynchronous state, leveraging Suspense and enhancing code-splitting! \ No newline at end of file +React-RxJS gets even more fun when you start using asynchronous state, leveraging Suspense and enhancing code-splitting! diff --git a/docs/recipes/invalidate-query.md b/docs/recipes/invalidate-query.md new file mode 100644 index 0000000..dac5ee9 --- /dev/null +++ b/docs/recipes/invalidate-query.md @@ -0,0 +1,71 @@ +--- +title: Invalidate Query +--- + +import InvalidateQuery from "../examples/InvalidateQuery" +import BrowserOnly from '@docusaurus/BrowserOnly'; + +```tsx +import { bind, Subscribe } from "@react-rxjs/core" +import { createSignal } from "@react-rxjs/utils" +import { concat, defer } from "rxjs" +import { concatMap, switchMap } from "rxjs/operators" +import { getTodos, postTodo } from "../my-api" + +const [todoPost$, addTodo] = createSignal() + +const todoResult$ = todoPost$.pipe( + concatMap(postTodo) +) + +const [useTodos] = bind( + // When do we need to request todos? + concat( + // 1. One single time when starting + defer(getTodos), + // 2. Every time we have created a new todo + todoResult$.pipe( + switchMap(getTodos) + ) + ), + [] +) + +function Todos() { + const todos = useTodos() + + const ref = useRef() + const handleAddClick = () => { + addTodo(ref.current!.value) + ref.current!.value = "" + ref.current!.focus() + } + + return ( +
+ + + +
    + {todos.map((todo) => ( +
  • {todo.title}
  • + ))} +
+
+ ) +} + +function App() { + return ( + + + + ) +} +``` + +### Interactive result + + + {() => } + diff --git a/sidebars.js b/sidebars.js index b530fb3..8ff77d4 100644 --- a/sidebars.js +++ b/sidebars.js @@ -1,6 +1,7 @@ module.exports = { someSidebar: { Introduction: ["motivation", "quick-start", "features"], + Recipes: ["recipes/invalidate-query"], "API Reference": [ { type: "category",