From bfcc0978824c742ee56481b617564f7a1c2d1825 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Tue, 18 Feb 2025 15:25:46 +0100 Subject: [PATCH] usePromise --- packages/example/src/routes/promise.tsx | 46 +++++++-------- packages/reffuse/src/Reffuse.ts | 76 +++++++++++++++---------- 2 files changed, 64 insertions(+), 58 deletions(-) diff --git a/packages/example/src/routes/promise.tsx b/packages/example/src/routes/promise.tsx index 9d69d62..da200db 100644 --- a/packages/example/src/routes/promise.tsx +++ b/packages/example/src/routes/promise.tsx @@ -2,44 +2,36 @@ import { R } from "@/reffuse" import { HttpClient } from "@effect/platform" import { Text } from "@radix-ui/themes" import { createFileRoute } from "@tanstack/react-router" -import { Console, Effect } from "effect" -import { Suspense, use, useEffect, useMemo } from "react" +import { Effect, Schema } from "effect" +import { Suspense, use } from "react" export const Route = createFileRoute("/promise")({ component: RouteComponent }) + +const Result = Schema.Tuple(Schema.String) +type Result = typeof Result.Type + function RouteComponent() { + const promise = R.usePromise(HttpClient.HttpClient.pipe( + Effect.flatMap(client => client.get("https://www.uuidtools.com/api/generate/v4")), + HttpClient.withTracerPropagation(false), + Effect.flatMap(res => res.json), + Effect.flatMap(Schema.decodeUnknown(Result)), + + Effect.scoped, + )) + return ( Loading...}> - + ) } -function AsyncComponent() { - - // const runPromise = R.useRunPromise() - - // const promise = useMemo(() => HttpClient.HttpClient.pipe( - // Effect.flatMap(client => client.get("https://www.uuidtools.com/api/generate/v4")), - // HttpClient.withTracerPropagation(false), - // Effect.flatMap(res => res.json), - // Effect.tap(Console.log), - - // Effect.scoped, - // runPromise, - // ), [runPromise]) - - const promise = useMemo(() => new Promise((resolve => { - setTimeout(() => { resolve("prout") }, 500) - })), []) - - console.log("React.use invoked with:", promise); - const value = use(promise) - - - return
Hello "/tests"!
- +function AsyncComponent({ promise }: { readonly promise: Promise }) { + const [uuid] = use(promise) + return {uuid} } diff --git a/packages/reffuse/src/Reffuse.ts b/packages/reffuse/src/Reffuse.ts index 4ec7386..cc1755f 100644 --- a/packages/reffuse/src/Reffuse.ts +++ b/packages/reffuse/src/Reffuse.ts @@ -284,43 +284,57 @@ export class Reffuse { ]) } - // useSuspense( - // effect: Effect.Effect, - // deps?: React.DependencyList, - // options?: { readonly signal?: AbortSignal } & RenderOptions, - // ): A { - // const runPromise = this.useRunPromise() + usePromise( + effect: Effect.Effect, + deps?: React.DependencyList, + options?: { readonly signal?: AbortSignal } & RenderOptions, + ): Promise { + const runPromise = this.useRunPromise() - // const promise = React.useMemo(() => runPromise(effect, options), [ - // ...options?.doNotReExecuteOnRuntimeOrContextChange ? [] : [runPromise], - // ...(deps ?? []), - // ]) - // return React.use(promise) - // } + return React.useMemo(() => runPromise(effect, options), [ + ...options?.doNotReExecuteOnRuntimeOrContextChange ? [] : [runPromise], + ...(deps ?? []), + ]) + } - // useSuspenseScoped( - // effect: Effect.Effect, - // deps?: React.DependencyList, - // options?: { readonly signal?: AbortSignal } & RenderOptions & ScopeOptions, - // ): A { - // const runSync = this.useRunSync() - // const runPromise = this.useRunPromise() + usePromiseScoped( + effect: Effect.Effect, + deps?: React.DependencyList, + options?: { readonly signal?: AbortSignal } & RenderOptions & ScopeOptions, + ): Promise { + const runSync = this.useRunSync() + const runPromise = this.useRunPromise() - // const initialPromise = React.useMemo(() => runPromise(Effect.scoped(effect)), []) - // const [promise, setPromise] = React.useState(initialPromise) + // Calculate an initial version of the value so that it can be accessed during the first render + const initialScope = React.useMemo(() => runSync(Scope.make(options?.finalizerExecutionStrategy)), []) + const initialValue = React.useMemo(() => runPromise(Effect.provideService(effect, Scope.Scope, initialScope), options), []) - // React.useEffect(() => { - // const scope = runSync(Scope.make()) - // setPromise(runPromise(Effect.provideService(effect, Scope.Scope, scope), options)) + // Keep track of the state of the initial scope + const initialScopeClosed = React.useRef(false) - // return () => { runPromise(Scope.close(scope, Exit.void)) } - // }, [ - // ...options?.doNotReExecuteOnRuntimeOrContextChange ? [] : [runSync, runPromise], - // ...(deps ?? []), - // ]) + const [value, setValue] = React.useState(initialValue) - // return React.use(promise) - // } + React.useEffect(() => { + const closeInitialScopeIfNeeded = Scope.close(initialScope, Exit.void).pipe( + Effect.andThen(Effect.sync(() => { initialScopeClosed.current = true })), + Effect.when(() => !initialScopeClosed.current), + ) + + const scope = closeInitialScopeIfNeeded.pipe( + Effect.andThen(Scope.make(options?.finalizerExecutionStrategy)), + runSync, + ) + + setValue(runPromise(Effect.provideService(effect, Scope.Scope, initialScope), options)) + + return () => { runSync(Scope.close(scope, Exit.void)) } + }, [ + ...options?.doNotReExecuteOnRuntimeOrContextChange ? [] : [runSync, runPromise], + ...(deps ?? []), + ]) + + return value + } useRef(value: A): SubscriptionRef.SubscriptionRef {