From e71239b90304664994c1024ae0d35c7130e16af5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Tue, 18 Feb 2025 22:28:49 +0100 Subject: [PATCH] usePromiseScoped --- packages/example/src/routes/promise.tsx | 4 +- packages/example/src/routes/tests.tsx | 22 ++------- packages/reffuse/src/Reffuse.ts | 61 +++++++------------------ packages/reffuse/src/ReffuseContext.tsx | 1 + 4 files changed, 21 insertions(+), 67 deletions(-) diff --git a/packages/example/src/routes/promise.tsx b/packages/example/src/routes/promise.tsx index da200db..d5c9b57 100644 --- a/packages/example/src/routes/promise.tsx +++ b/packages/example/src/routes/promise.tsx @@ -15,13 +15,11 @@ const Result = Schema.Tuple(Schema.String) type Result = typeof Result.Type function RouteComponent() { - const promise = R.usePromise(HttpClient.HttpClient.pipe( + const promise = R.usePromiseScoped(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 ( diff --git a/packages/example/src/routes/tests.tsx b/packages/example/src/routes/tests.tsx index 6961e11..82b6919 100644 --- a/packages/example/src/routes/tests.tsx +++ b/packages/example/src/routes/tests.tsx @@ -1,8 +1,6 @@ import { R } from "@/reffuse" import { createFileRoute } from "@tanstack/react-router" -import { GetRandomValues, makeUuid4 } from "@typed/id" import { Console, Effect } from "effect" -import { useMemo, useState } from "react" export const Route = createFileRoute("/tests")({ @@ -16,24 +14,10 @@ function RouteComponent() { // ), []) // console.log(value) - // R.useFork(Effect.addFinalizer(() => Console.log("cleanup")).pipe( - // Effect.andThen(Console.log("ouient")), - // Effect.delay("1 second"), - // )) - - const runPromise = R.useRunPromise() - const [, setValue] = useState("") - - const promise = useMemo(() => makeUuid4.pipe( - Effect.provide(GetRandomValues.CryptoRandom), - Effect.tap(id => Effect.sync(() => setValue(id))), - Effect.andThen(Console.log), + R.useFork(Effect.addFinalizer(() => Console.log("cleanup")).pipe( + Effect.andThen(Console.log("ouient")), Effect.delay("1 second"), - - runPromise, - ), [runPromise]) - - console.log(promise) + )) return
Hello "/tests"!
diff --git a/packages/reffuse/src/Reffuse.ts b/packages/reffuse/src/Reffuse.ts index cc1755f..f3d56ec 100644 --- a/packages/reffuse/src/Reffuse.ts +++ b/packages/reffuse/src/Reffuse.ts @@ -5,6 +5,7 @@ import * as ReffuseRuntime from "./ReffuseRuntime.js" import * as SetStateAction from "./SetStateAction.js" +// MAYBE: make it an Effect and match the R parameter? export class Reffuse { constructor( @@ -305,29 +306,25 @@ export class Reffuse { const runSync = this.useRunSync() const runPromise = this.useRunPromise() - // 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), []) - - // Keep track of the state of the initial scope - const initialScopeClosed = React.useRef(false) - - const [value, setValue] = React.useState(initialValue) + const [value, setValue] = React.useState(new Promise(() => {})) React.useEffect(() => { - const closeInitialScopeIfNeeded = Scope.close(initialScope, Exit.void).pipe( - Effect.andThen(Effect.sync(() => { initialScopeClosed.current = true })), - Effect.when(() => !initialScopeClosed.current), - ) + const controller = new AbortController() + const signal = AbortSignal.any([ + controller.signal, + ...options?.signal ? [options.signal] : [], + ]) - const scope = closeInitialScopeIfNeeded.pipe( - Effect.andThen(Scope.make(options?.finalizerExecutionStrategy)), - runSync, - ) + const scope = runSync(Scope.make(options?.finalizerExecutionStrategy)) + setValue(runPromise(Effect.provideService(effect, Scope.Scope, scope), { + ...options, + signal, + })) - setValue(runPromise(Effect.provideService(effect, Scope.Scope, initialScope), options)) - - return () => { runSync(Scope.close(scope, Exit.void)) } + return () => { + controller.abort() + runSync(Scope.close(scope, Exit.void)) + } }, [ ...options?.doNotReExecuteOnRuntimeOrContextChange ? [] : [runSync, runPromise], ...(deps ?? []), @@ -371,32 +368,6 @@ export class Reffuse { return [reactStateValue, setValue] } - /** - * Binds the state of a `LazyRef` from the `@typed/lazy-ref` package to the state of the React component. - * - * Returns a [value, setter] tuple just like `React.useState` and triggers a re-render everytime the value held by the ref changes. - * - * Note that the rules of React's immutable state still apply: updating a ref with the same value will not trigger a re-render. - */ - // useLazyRefState(ref: LazyRef.LazyRef): [A, React.Dispatch>] { - // const runSync = this.useRunSync() - - // const initialState = React.useMemo(() => runSync(ref), []) - // const [reactStateValue, setReactStateValue] = React.useState(initialState) - - // this.useFork(Stream.runForEach(ref.changes, v => Effect.sync(() => - // setReactStateValue(v) - // )), [ref]) - - // const setValue = React.useCallback((setStateAction: React.SetStateAction) => - // runSync(LazyRef.update(ref, prevState => - // SetStateAction.value(setStateAction, prevState) - // )), - // [ref]) - - // return [reactStateValue, setValue] - // } - } diff --git a/packages/reffuse/src/ReffuseContext.tsx b/packages/reffuse/src/ReffuseContext.tsx index dbca682..a7b49a5 100644 --- a/packages/reffuse/src/ReffuseContext.tsx +++ b/packages/reffuse/src/ReffuseContext.tsx @@ -3,6 +3,7 @@ import React from "react" import * as ReffuseRuntime from "./ReffuseRuntime.js" +// TODO: merge this with the Provider, just like React 19 contexts export class ReffuseContext { readonly Context = React.createContext>(null!)