From 3ce7db2c366679f31e35f35168ab3cbdb9e125cc Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 21 Sep 2023 14:59:45 +1200 Subject: [PATCH] add suspense hooks --- .changeset/red-apples-notice.md | 5 ++ docs/rx-react/index.ts.md | 28 ++++++++ packages/rx-react/src/index.ts | 113 +++++++++++++++++++++++++++++++- 3 files changed, 145 insertions(+), 1 deletion(-) create mode 100644 .changeset/red-apples-notice.md diff --git a/.changeset/red-apples-notice.md b/.changeset/red-apples-notice.md new file mode 100644 index 0000000..48d7e98 --- /dev/null +++ b/.changeset/red-apples-notice.md @@ -0,0 +1,5 @@ +--- +"@effect-rx/rx-react": patch +--- + +add suspense hooks diff --git a/docs/rx-react/index.ts.md b/docs/rx-react/index.ts.md index 75060db..30399e0 100644 --- a/docs/rx-react/index.ts.md +++ b/docs/rx-react/index.ts.md @@ -17,6 +17,8 @@ Added in v1.0.0 - [hooks](#hooks) - [useRefreshRx](#userefreshrx) - [useRx](#userx) + - [useRxSuspense](#userxsuspense) + - [useRxSuspenseSuccess](#userxsuspensesuccess) - [useRxValue](#userxvalue) - [useSetRx](#usesetrx) @@ -58,6 +60,32 @@ export declare const useRx: ( Added in v1.0.0 +## useRxSuspense + +**Signature** + +```ts +export declare const useRxSuspense: ( + rx: Rx.Rx>, + options?: { readonly suspendOnWaiting?: boolean } +) => { readonly isWaiting: boolean; readonly value: Result.Success | Result.Failure } +``` + +Added in v1.0.0 + +## useRxSuspenseSuccess + +**Signature** + +```ts +export declare const useRxSuspenseSuccess: ( + rx: Rx.Rx>, + options?: { readonly suspendOnWaiting?: boolean } +) => { readonly isWaiting: boolean; readonly value: A } +``` + +Added in v1.0.0 + ## useRxValue **Signature** diff --git a/packages/rx-react/src/index.ts b/packages/rx-react/src/index.ts index 16150de..cf95e21 100644 --- a/packages/rx-react/src/index.ts +++ b/packages/rx-react/src/index.ts @@ -2,7 +2,10 @@ * @since 1.0.0 */ import * as Registry from "@effect-rx/rx/Registry" -import type * as Rx from "@effect-rx/rx/Rx" +import * as Result from "@effect-rx/rx/Result" +import * as Rx from "@effect-rx/rx/Rx" +import { globalValue } from "@effect/data/GlobalValue" +import * as Cause from "@effect/io/Cause" import * as React from "react" export * as Registry from "@effect-rx/rx/Registry" @@ -86,3 +89,111 @@ export const useRx = (rx: Rx.Writable): readonly [value: R, setOrUpd useRxValue(rx), useSetRx(rx) ] as const + +type SuspenseResult = + | { + readonly _tag: "Suspended" + readonly promise: Promise + } + | { + readonly _tag: "Value" + readonly isWaiting: boolean + readonly value: Result.Success | Result.Failure + } + +const suspenseCache = globalValue("@effect-rx/rx-react/suspenseCache", () => new Map, () => void>()) +const suspenseRx = Rx.family((rx: Rx.Rx>) => + Rx.readable((get, ctx): SuspenseResult => { + const result = get(rx) + const value = Result.noWaiting(result) + if (value._tag === "Initial") { + return { + _tag: "Suspended", + promise: new Promise((resolve) => { + ctx.addFinalizer(() => { + resolve() + const unmount = suspenseCache.get(rx) + if (unmount) { + unmount() + suspenseCache.delete(rx) + } + }) + }) + } as const + } + const isWaiting = Result.isWaiting(result) + return { _tag: "Value", isWaiting, value } as const + }) +) +const suspenseRxWaiting = Rx.family((rx: Rx.Rx>) => + Rx.readable((get, ctx): SuspenseResult => { + const result = get(rx) + if (result._tag === "Waiting" || result._tag === "Initial") { + return { + _tag: "Suspended", + promise: new Promise((resolve) => { + ctx.addFinalizer(() => { + resolve() + const unmount = suspenseCache.get(rx) + if (unmount) { + unmount() + suspenseCache.delete(rx) + } + }) + }) + } as const + } + return { _tag: "Value", isWaiting: false, value: result } as const + }) +) + +/** + * @since 1.0.0 + * @category hooks + */ +export const useRxSuspense = ( + rx: Rx.Rx>, + options?: { readonly suspendOnWaiting?: boolean } +): { + readonly isWaiting: boolean + readonly value: Result.Success | Result.Failure +} => { + const registry = React.useContext(RegistryContext) + const resultRx = options?.suspendOnWaiting ? suspenseRxWaiting(rx) : suspenseRx(rx) + const result = useRxValue(resultRx) + if (result._tag === "Suspended") { + if (!suspenseCache.has(resultRx)) { + suspenseCache.set(resultRx, registry.mount(resultRx)) + } + throw result.promise + } + return result +} + +/** + * @since 1.0.0 + * @category hooks + */ +export const useRxSuspenseSuccess = ( + rx: Rx.Rx>, + options?: { readonly suspendOnWaiting?: boolean } +): { + readonly isWaiting: boolean + readonly value: A +} => { + const registry = React.useContext(RegistryContext) + const resultRx = options?.suspendOnWaiting ? suspenseRxWaiting(rx) : suspenseRx(rx) + const result = useRxValue(resultRx) + if (result._tag === "Suspended") { + if (!suspenseCache.has(resultRx)) { + suspenseCache.set(resultRx, registry.mount(resultRx)) + } + throw result.promise + } else if (result.value._tag === "Failure") { + throw Cause.squash(result.value.cause) + } + return { + isWaiting: result.isWaiting, + value: result.value.value + } +}