Compare commits
3 Commits
d40c30f837
...
2dcea4c81d
| Author | SHA1 | Date | |
|---|---|---|---|
| 2dcea4c81d | |||
|
|
581bab027c | ||
|
|
3be9d94aa8 |
@@ -47,16 +47,18 @@ 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
|
||||||
@@ -338,9 +340,17 @@ export declare namespace make {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an Effect-based React component.
|
* Creates an Effect-FC Component following the same overloads and pipeline style as `Effect.fn`.
|
||||||
*
|
*
|
||||||
* Follows the `Effect.fn` API. Supports both generator syntax (recommended) and direct Effect composition.
|
* This is the **recommended** way to define components. It supports:
|
||||||
|
* - 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
|
||||||
@@ -371,10 +381,15 @@ export const make: (
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Same as `make` but creates an untraced version (no automatic span created).
|
* Same as `make`, but creates an **untraced** version — no automatic tracing span is created.
|
||||||
* Useful for very low-level utilities or when you want full control over tracing.
|
|
||||||
*
|
*
|
||||||
* Follows the `Effect.fnUntraced` API.
|
* Follows the exact same API shape as `Effect.fnUntraced`.
|
||||||
|
* 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
|
||||||
@@ -442,7 +457,7 @@ export const withOptions: {
|
|||||||
*
|
*
|
||||||
* // Route
|
* // Route
|
||||||
* export const Route = createFileRoute("/")({
|
* export const Route = createFileRoute("/")({
|
||||||
* component: withRuntime(HomePage, runtime.context),
|
* component: Component.withRuntime(HomePage, runtime.context)
|
||||||
* })
|
* })
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
@@ -468,6 +483,10 @@ 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>()))
|
||||||
}) {}
|
}) {}
|
||||||
@@ -487,6 +506,14 @@ 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,
|
||||||
@@ -495,43 +522,40 @@ 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 scopeMap = yield* ScopeMap as unknown as Effect.Effect<ScopeMap>
|
const { key, scope } = React.useMemo(() => Runtime.runSync(runtimeRef.current)(Effect.Do.pipe(
|
||||||
|
Effect.bind("scopeMapRef", () => Effect.map(
|
||||||
const [key, scope] = React.useMemo(() => Runtime.runSync(runtimeRef.current)(Effect.andThen(
|
ScopeMap as unknown as Effect.Effect<ScopeMap>,
|
||||||
Effect.all([Effect.succeed({}), scopeMap.ref]),
|
scopeMap => scopeMap.ref,
|
||||||
([key, map]) => Effect.andThen(
|
)),
|
||||||
Option.match(HashMap.get(map, key), {
|
Effect.let("key", () => ({})),
|
||||||
onSome: entry => Effect.succeed(entry.scope),
|
Effect.bind("scope", () => Scope.make(options?.finalizerExecutionStrategy ?? defaultOptions.finalizerExecutionStrategy)),
|
||||||
onNone: () => Effect.tap(
|
Effect.tap(({ scopeMapRef, key, scope }) =>
|
||||||
Scope.make(options?.finalizerExecutionStrategy ?? defaultOptions.finalizerExecutionStrategy),
|
Ref.update(scopeMapRef, HashMap.set(key, {
|
||||||
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.ref.pipe(
|
React.useEffect(() => Runtime.runSync(runtimeRef.current)((ScopeMap as unknown as Effect.Effect<ScopeMap>).pipe(
|
||||||
|
Effect.map(scopeMap => scopeMap.ref),
|
||||||
|
Effect.tap(ref => ref.pipe(
|
||||||
Effect.andThen(HashMap.get(key)),
|
Effect.andThen(HashMap.get(key)),
|
||||||
Effect.tap(entry => Option.match(entry.closeFiber, {
|
Effect.andThen(entry => Option.match(entry.closeFiber, {
|
||||||
onSome: fiber => Effect.andThen(
|
onSome: Fiber.interruptFork,
|
||||||
Ref.update(scopeMap.ref, HashMap.set(key, { ...entry, closeFiber: Option.none() })),
|
|
||||||
Fiber.interruptFork(fiber),
|
|
||||||
),
|
|
||||||
onNone: () => Effect.void,
|
onNone: () => Effect.void,
|
||||||
})),
|
})),
|
||||||
Effect.map(({ scope }) =>
|
|
||||||
() => Runtime.runSync(runtimeRef.current)(Effect.andThen(
|
|
||||||
Effect.forkDaemon(Effect.sleep(options?.finalizerExecutionDebounce ?? defaultOptions.finalizerExecutionDebounce).pipe(
|
|
||||||
Effect.andThen(Scope.close(scope, Exit.void)),
|
|
||||||
Effect.andThen(Ref.update(scopeMap.ref, HashMap.remove(key))),
|
|
||||||
)),
|
)),
|
||||||
fiber => Ref.update(scopeMap.ref, HashMap.set(key, {
|
Effect.map(ref =>
|
||||||
|
() => Runtime.runSync(runtimeRef.current)(Effect.andThen(
|
||||||
|
Effect.sleep(options?.finalizerExecutionDebounce ?? defaultOptions.finalizerExecutionDebounce).pipe(
|
||||||
|
Effect.andThen(Scope.close(scope, Exit.void)),
|
||||||
|
Effect.onExit(() => Ref.update(ref, HashMap.remove(key))),
|
||||||
|
Effect.forkDaemon,
|
||||||
|
),
|
||||||
|
fiber => Ref.update(ref, HashMap.set(key, {
|
||||||
scope,
|
scope,
|
||||||
closeFiber: Option.some(fiber),
|
closeFiber: Option.some(fiber),
|
||||||
})),
|
})),
|
||||||
@@ -542,6 +566,9 @@ 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> {
|
||||||
@@ -553,6 +580,11 @@ 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,
|
||||||
@@ -574,6 +606,11 @@ 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,
|
||||||
@@ -610,6 +647,11 @@ 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,
|
||||||
@@ -620,18 +662,27 @@ 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,
|
||||||
@@ -644,6 +695,9 @@ 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,
|
||||||
@@ -660,10 +714,17 @@ 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, Scope.Scope | RIn> => useOnChange(() => Effect.context<RIn>().pipe(
|
): Effect.Effect<Context.Context<ROut>, E, Exclude<RIn, Scope.Scope>> => 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),
|
||||||
|
|||||||
Reference in New Issue
Block a user