1 Commits

Author SHA1 Message Date
d40c30f837 Update dependency @effect/language-service to ^0.72.0
All checks were successful
Lint / lint (push) Successful in 42s
Test build / test-build (pull_request) Successful in 26s
2026-01-21 12:01:31 +00:00

View File

@@ -47,18 +47,16 @@ export declare namespace Component {
* Options that can be set on the component * Options that can be set on the component
*/ */
export interface Options { export interface Options {
/** Custom displayName for React DevTools and debugging. */ /** Custom displayName for React DevTools and debugging */
readonly displayName?: string readonly displayName?: string
/** /**
* Strategy used when executing finalizers on unmount/scope close. * Strategy used when executing finalizers on unmount/scope close
* @default ExecutionStrategy.sequential * @default ExecutionStrategy.sequential
*/ */
readonly finalizerExecutionStrategy: ExecutionStrategy.ExecutionStrategy readonly finalizerExecutionStrategy: ExecutionStrategy.ExecutionStrategy
/** /**
* Debounce time before executing finalizers after component unmount. * Debounce time before executing finalizers after component unmount
* Helps avoid unnecessary work during fast remount/remount cycles. * Helps avoid unnecessary work during fast remount/remount cycles
* @default "100 millis" * @default "100 millis"
*/ */
readonly finalizerExecutionDebounce: Duration.DurationInput readonly finalizerExecutionDebounce: Duration.DurationInput
@@ -340,17 +338,9 @@ export declare namespace make {
} }
/** /**
* Creates an Effect-FC Component following the same overloads and pipeline style as `Effect.fn`. * Creates an Effect-based React component.
* *
* This is the **recommended** way to define components. It supports: * Follows the `Effect.fn` API. Supports both generator syntax (recommended) and direct Effect composition.
* - Generator syntax (yield* style) — most ergonomic and readable
* - Direct Effect return (non-generator)
* - Chained transformation functions (like Effect.fn pipelines)
* - Optional tracing span with automatic `displayName`
*
* When you provide a `spanName` as the first argument, two things happen automatically:
* 1. A tracing span is created with that name (unless using `makeUntraced`)
* 2. The resulting React component gets `displayName = spanName`
*/ */
export const make: ( export const make: (
& make.Gen & make.Gen
@@ -381,15 +371,10 @@ export const make: (
} }
/** /**
* Same as `make`, but creates an **untraced** version no automatic tracing span is created. * Same as `make` but creates an untraced version (no automatic span created).
* Useful for very low-level utilities or when you want full control over tracing.
* *
* Follows the exact same API shape as `Effect.fnUntraced`. * Follows the `Effect.fnUntraced` API.
* Useful for:
* - Components where you want full manual control over tracing
* - Avoiding span noise in deeply nested UI
*
* When a string is provided as first argument, it is **only** used as the React component's `displayName`
* (no tracing span is created).
*/ */
export const makeUntraced: ( export const makeUntraced: (
& make.Gen & make.Gen
@@ -457,7 +442,7 @@ export const withOptions: {
* *
* // Route * // Route
* export const Route = createFileRoute("/")({ * export const Route = createFileRoute("/")({
* component: Component.withRuntime(HomePage, runtime.context) * component: withRuntime(HomePage, runtime.context),
* }) * })
* ``` * ```
* *
@@ -483,10 +468,6 @@ export const withRuntime: {
}) })
/**
* Service that keeps track of scopes associated with React components
* (used internally by the `useScope` hook).
*/
export class ScopeMap extends Effect.Service<ScopeMap>()("@effect-fc/Component/ScopeMap", { export class ScopeMap extends Effect.Service<ScopeMap>()("@effect-fc/Component/ScopeMap", {
effect: Effect.bind(Effect.Do, "ref", () => Ref.make(HashMap.empty<object, ScopeMap.Entry>())) effect: Effect.bind(Effect.Do, "ref", () => Ref.make(HashMap.empty<object, ScopeMap.Entry>()))
}) {} }) {}
@@ -506,14 +487,6 @@ export declare namespace useScope {
} }
} }
/**
* Hook that creates and manages a `Scope` for the current component instance.
*
* Automatically closes the scope whenever `deps` changes or the component unmounts.
*
* @param deps - dependency array like in `React.useEffect`
* @param options - finalizer execution control
*/
export const useScope = Effect.fnUntraced(function*( export const useScope = Effect.fnUntraced(function*(
deps: React.DependencyList, deps: React.DependencyList,
options?: useScope.Options, options?: useScope.Options,
@@ -522,40 +495,43 @@ export const useScope = Effect.fnUntraced(function*(
const runtimeRef = React.useRef<Runtime.Runtime<never>>(null!) const runtimeRef = React.useRef<Runtime.Runtime<never>>(null!)
runtimeRef.current = yield* Effect.runtime() runtimeRef.current = yield* Effect.runtime()
const { key, scope } = React.useMemo(() => Runtime.runSync(runtimeRef.current)(Effect.Do.pipe( const scopeMap = yield* ScopeMap as unknown as Effect.Effect<ScopeMap>
Effect.bind("scopeMapRef", () => Effect.map(
ScopeMap as unknown as Effect.Effect<ScopeMap>, const [key, scope] = React.useMemo(() => Runtime.runSync(runtimeRef.current)(Effect.andThen(
scopeMap => scopeMap.ref, Effect.all([Effect.succeed({}), scopeMap.ref]),
)), ([key, map]) => Effect.andThen(
Effect.let("key", () => ({})), Option.match(HashMap.get(map, key), {
Effect.bind("scope", () => Scope.make(options?.finalizerExecutionStrategy ?? defaultOptions.finalizerExecutionStrategy)), onSome: entry => Effect.succeed(entry.scope),
Effect.tap(({ scopeMapRef, key, scope }) => onNone: () => Effect.tap(
Ref.update(scopeMapRef, HashMap.set(key, { Scope.make(options?.finalizerExecutionStrategy ?? defaultOptions.finalizerExecutionStrategy),
scope => Ref.update(scopeMap.ref, HashMap.set(key, {
scope, scope,
closeFiber: Option.none(), closeFiber: Option.none(),
})) })),
),
}),
scope => [key, scope] as const,
), ),
// biome-ignore lint/correctness/useExhaustiveDependencies: use of React.DependencyList // biome-ignore lint/correctness/useExhaustiveDependencies: use of React.DependencyList
)), deps) )), deps)
// biome-ignore lint/correctness/useExhaustiveDependencies: only reactive on "key" // biome-ignore lint/correctness/useExhaustiveDependencies: only reactive on "key"
React.useEffect(() => Runtime.runSync(runtimeRef.current)((ScopeMap as unknown as Effect.Effect<ScopeMap>).pipe( React.useEffect(() => Runtime.runSync(runtimeRef.current)(scopeMap.ref.pipe(
Effect.map(scopeMap => scopeMap.ref),
Effect.tap(ref => ref.pipe(
Effect.andThen(HashMap.get(key)), Effect.andThen(HashMap.get(key)),
Effect.andThen(entry => Option.match(entry.closeFiber, { Effect.tap(entry => Option.match(entry.closeFiber, {
onSome: Fiber.interruptFork, onSome: fiber => Effect.andThen(
Ref.update(scopeMap.ref, HashMap.set(key, { ...entry, closeFiber: Option.none() })),
Fiber.interruptFork(fiber),
),
onNone: () => Effect.void, onNone: () => Effect.void,
})), })),
)), Effect.map(({ scope }) =>
Effect.map(ref =>
() => Runtime.runSync(runtimeRef.current)(Effect.andThen( () => Runtime.runSync(runtimeRef.current)(Effect.andThen(
Effect.sleep(options?.finalizerExecutionDebounce ?? defaultOptions.finalizerExecutionDebounce).pipe( Effect.forkDaemon(Effect.sleep(options?.finalizerExecutionDebounce ?? defaultOptions.finalizerExecutionDebounce).pipe(
Effect.andThen(Scope.close(scope, Exit.void)), Effect.andThen(Scope.close(scope, Exit.void)),
Effect.onExit(() => Ref.update(ref, HashMap.remove(key))), Effect.andThen(Ref.update(scopeMap.ref, HashMap.remove(key))),
Effect.forkDaemon, )),
), fiber => Ref.update(scopeMap.ref, HashMap.set(key, {
fiber => Ref.update(ref, HashMap.set(key, {
scope, scope,
closeFiber: Option.some(fiber), closeFiber: Option.some(fiber),
})), })),
@@ -566,9 +542,6 @@ export const useScope = Effect.fnUntraced(function*(
return scope return scope
}) })
/**
* Runs an effect and returns its result only once on component mount.
*/
export const useOnMount = Effect.fnUntraced(function* <A, E, R>( export const useOnMount = Effect.fnUntraced(function* <A, E, R>(
f: () => Effect.Effect<A, E, R> f: () => Effect.Effect<A, E, R>
): Effect.fn.Return<A, E, R> { ): Effect.fn.Return<A, E, R> {
@@ -580,11 +553,6 @@ export declare namespace useOnChange {
export interface Options extends useScope.Options {} export interface Options extends useScope.Options {}
} }
/**
* Runs an effect and returns its result whenever dependencies change.
*
* Provides its own `Scope` which closes whenever `deps` changes or the component unmounts.
*/
export const useOnChange = Effect.fnUntraced(function* <A, E, R>( export const useOnChange = Effect.fnUntraced(function* <A, E, R>(
f: () => Effect.Effect<A, E, R>, f: () => Effect.Effect<A, E, R>,
deps: React.DependencyList, deps: React.DependencyList,
@@ -606,11 +574,6 @@ export declare namespace useReactEffect {
} }
} }
/**
* Like `React.useEffect` but accepts an effect.
*
* Cleanup logic is handled through the `Scope` API rather than using imperative cleanup.
*/
export const useReactEffect = Effect.fnUntraced(function* <E, R>( export const useReactEffect = Effect.fnUntraced(function* <E, R>(
f: () => Effect.Effect<void, E, R>, f: () => Effect.Effect<void, E, R>,
deps?: React.DependencyList, deps?: React.DependencyList,
@@ -647,11 +610,6 @@ export declare namespace useReactLayoutEffect {
export interface Options extends useReactEffect.Options {} export interface Options extends useReactEffect.Options {}
} }
/**
* Like `React.useReactLayoutEffect` but accepts an effect.
*
* Cleanup logic is handled through the `Scope` API rather than using imperative cleanup.
*/
export const useReactLayoutEffect = Effect.fnUntraced(function* <E, R>( export const useReactLayoutEffect = Effect.fnUntraced(function* <E, R>(
f: () => Effect.Effect<void, E, R>, f: () => Effect.Effect<void, E, R>,
deps?: React.DependencyList, deps?: React.DependencyList,
@@ -662,27 +620,18 @@ export const useReactLayoutEffect = Effect.fnUntraced(function* <E, R>(
React.useLayoutEffect(() => runReactEffect(runtime, f, options), deps) React.useLayoutEffect(() => runReactEffect(runtime, f, options), deps)
}) })
/**
* Get a synchronous run function for the current runtime context.
*/
export const useRunSync = <R = never>(): Effect.Effect< export const useRunSync = <R = never>(): Effect.Effect<
<A, E = never>(effect: Effect.Effect<A, E, Scope.Scope | R>) => A, <A, E = never>(effect: Effect.Effect<A, E, Scope.Scope | R>) => A,
never, never,
Scope.Scope | R Scope.Scope | R
> => Effect.andThen(Effect.runtime(), Runtime.runSync) > => Effect.andThen(Effect.runtime(), Runtime.runSync)
/**
* Get a Promise-based run function for the current runtime context.
*/
export const useRunPromise = <R = never>(): Effect.Effect< export const useRunPromise = <R = never>(): Effect.Effect<
<A, E = never>(effect: Effect.Effect<A, E, Scope.Scope | R>) => Promise<A>, <A, E = never>(effect: Effect.Effect<A, E, Scope.Scope | R>) => Promise<A>,
never, never,
Scope.Scope | R Scope.Scope | R
> => Effect.andThen(Effect.runtime(), context => Runtime.runPromise(context)) > => Effect.andThen(Effect.runtime(), context => Runtime.runPromise(context))
/**
* Turns a function returning an effect into a memoized synchronous function.
*/
export const useCallbackSync = Effect.fnUntraced(function* <Args extends unknown[], A, E, R>( export const useCallbackSync = Effect.fnUntraced(function* <Args extends unknown[], A, E, R>(
f: (...args: Args) => Effect.Effect<A, E, R>, f: (...args: Args) => Effect.Effect<A, E, R>,
deps: React.DependencyList, deps: React.DependencyList,
@@ -695,9 +644,6 @@ export const useCallbackSync = Effect.fnUntraced(function* <Args extends unknown
return React.useCallback((...args: Args) => Runtime.runSync(runtimeRef.current)(f(...args)), deps) return React.useCallback((...args: Args) => Runtime.runSync(runtimeRef.current)(f(...args)), deps)
}) })
/**
* Turns a function returning an effect into a memoized Promise-based asynchronous function.
*/
export const useCallbackPromise = Effect.fnUntraced(function* <Args extends unknown[], A, E, R>( export const useCallbackPromise = Effect.fnUntraced(function* <Args extends unknown[], A, E, R>(
f: (...args: Args) => Effect.Effect<A, E, R>, f: (...args: Args) => Effect.Effect<A, E, R>,
deps: React.DependencyList, deps: React.DependencyList,
@@ -714,17 +660,10 @@ export declare namespace useContext {
export interface Options extends useOnChange.Options {} export interface Options extends useOnChange.Options {}
} }
/**
* Hook that constructs a layer and returns the created context.
*
* The layer gets reconstructed everytime `layer` changes, so make sure its value is stable.
*
* Building a layer containing asynchronous effects require the component calling this hook to be made async using `Async.async`.
*/
export const useContext = <ROut, E, RIn>( export const useContext = <ROut, E, RIn>(
layer: Layer.Layer<ROut, E, RIn>, layer: Layer.Layer<ROut, E, RIn>,
options?: useContext.Options, options?: useContext.Options,
): Effect.Effect<Context.Context<ROut>, E, Exclude<RIn, Scope.Scope>> => useOnChange(() => Effect.context<RIn>().pipe( ): Effect.Effect<Context.Context<ROut>, E, Scope.Scope | RIn> => useOnChange(() => Effect.context<RIn>().pipe(
Effect.map(context => ManagedRuntime.make(Layer.provide(layer, Layer.succeedContext(context)))), Effect.map(context => ManagedRuntime.make(Layer.provide(layer, Layer.succeedContext(context)))),
Effect.tap(runtime => Effect.addFinalizer(() => runtime.disposeEffect)), Effect.tap(runtime => Effect.addFinalizer(() => runtime.disposeEffect)),
Effect.andThen(runtime => runtime.runtimeEffect), Effect.andThen(runtime => runtime.runtimeEffect),