23 Commits

Author SHA1 Message Date
Julien Valverdé
2a78232ec7 Hooks
All checks were successful
Lint / lint (push) Successful in 14s
2025-07-01 20:12:17 +02:00
Julien Valverdé
19194d6677 Hook
Some checks failed
Lint / lint (push) Failing after 11s
2025-07-01 18:13:03 +02:00
Julien Valverdé
40871b793d Tests
All checks were successful
Lint / lint (push) Successful in 14s
2025-07-01 16:54:50 +02:00
Julien Valverdé
f079b90f28 Tests
All checks were successful
Lint / lint (push) Successful in 14s
2025-07-01 16:48:53 +02:00
Julien Valverdé
28b6e9276e Fix
All checks were successful
Lint / lint (push) Successful in 15s
2025-07-01 16:44:28 +02:00
Julien Valverdé
8025ec4a22 Fix
All checks were successful
Lint / lint (push) Successful in 15s
2025-07-01 16:31:30 +02:00
Julien Valverdé
02ee2c10cc Fix
All checks were successful
Lint / lint (push) Successful in 14s
2025-07-01 16:16:29 +02:00
Julien Valverdé
bb1a71f63b Scope refactoring
All checks were successful
Lint / lint (push) Successful in 15s
2025-07-01 15:59:58 +02:00
Julien Valverdé
a9448f55cf Fix
All checks were successful
Lint / lint (push) Successful in 13s
2025-07-01 13:32:25 +02:00
Julien Valverdé
c0f3073d20 Hook work
All checks were successful
Lint / lint (push) Successful in 15s
2025-07-01 13:30:50 +02:00
Julien Valverdé
8cfe186574 Fix
All checks were successful
Lint / lint (push) Successful in 14s
2025-07-01 00:46:59 +02:00
Julien Valverdé
625cecda27 Fix
All checks were successful
Lint / lint (push) Successful in 14s
2025-07-01 00:29:42 +02:00
Julien Valverdé
7cc0a68170 useMemoLayer
All checks were successful
Lint / lint (push) Successful in 15s
2025-07-01 00:23:45 +02:00
Julien Valverdé
8be1295e2f Layer tests
All checks were successful
Lint / lint (push) Successful in 15s
2025-07-01 00:11:34 +02:00
Julien Valverdé
a781be8f24 Working ref
All checks were successful
Lint / lint (push) Successful in 15s
2025-06-30 22:49:30 +02:00
Julien Valverdé
4913f5cc35 Tests
All checks were successful
Lint / lint (push) Successful in 14s
2025-06-30 22:07:38 +02:00
Julien Valverdé
2a37f843ca AsyncProvider
All checks were successful
Lint / lint (push) Successful in 15s
2025-06-30 22:04:03 +02:00
Julien Valverdé
78a3735038 Refactoring
All checks were successful
Lint / lint (push) Successful in 16s
2025-06-30 21:44:29 +02:00
Julien Valverdé
37d9400ada Component displayName
All checks were successful
Lint / lint (push) Successful in 14s
2025-06-30 00:37:16 +02:00
Julien Valverdé
2ef47bed70 Work
All checks were successful
Lint / lint (push) Successful in 14s
2025-06-29 23:00:33 +02:00
Julien Valverdé
2b78d4dc49 Cleanup
All checks were successful
Lint / lint (push) Successful in 15s
2025-06-29 19:35:43 +02:00
Julien Valverdé
6fa73ee33f Tests
All checks were successful
Lint / lint (push) Successful in 15s
2025-06-29 19:11:16 +02:00
Julien Valverdé
3ea4c81872 React component refactoring 2025-06-29 18:52:42 +02:00
10 changed files with 642 additions and 115 deletions

View File

@@ -16,6 +16,10 @@
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
"default": "./dist/index.js" "default": "./dist/index.js"
}, },
"./types": {
"types": "./dist/types/index.d.ts",
"default": "./dist/types/index.js"
},
"./*": { "./*": {
"types": "./dist/*.d.ts", "types": "./dist/*.d.ts",
"default": "./dist/*.js" "default": "./dist/*.js"

View File

@@ -1,89 +1,72 @@
import { Context, Effect, ExecutionStrategy, Exit, Ref, Runtime, Scope, Tracer } from "effect" import { Context, Effect, Function, Runtime, Scope, Tracer } from "effect"
import type { Mutable } from "effect/Types"
import * as React from "react" import * as React from "react"
import * as ReactHook from "./ReactHook.js" import * as ReactHook from "./ReactHook.js"
export interface ReactComponent<P, E, R> { export interface ReactComponent<E, R, P> {
(props: P): Effect.Effect<React.ReactNode, E, R> (props: P): Effect.Effect<React.ReactNode, E, R>
readonly displayName?: string
} }
export const nonReactiveTags = [Tracer.ParentSpan] as const
export const withDisplayName: {
<C extends ReactComponent<any, any, any>>(displayName: string): (self: C) => C
<C extends ReactComponent<any, any, any>>(self: C, displayName: string): C
} = Function.dual(2, <C extends ReactComponent<any, any, any>>(
self: C,
displayName: string,
): C => {
(self as Mutable<C>).displayName = displayName
return self
})
export const useFC: { export const useFC: {
<P, E, R>( <E, R, P extends {} = {}>(
self: ReactComponent<P, E, R>, self: ReactComponent<E, R, P>,
options?: ReactHook.ScopeOptions, options?: ReactHook.ScopeOptions,
): Effect.Effect<React.FC<P>, never, Exclude<R, Scope.Scope>> ): Effect.Effect<React.FC<P>, never, Exclude<R, Scope.Scope>>
} = Effect.fn(function* useFC<P, E, R>( } = Effect.fnUntraced(function* <E, R, P extends {}>(
self: ReactComponent<P, E, R>, self: ReactComponent<E, R, P>,
options?: ReactHook.ScopeOptions, options?: ReactHook.ScopeOptions,
) { ) {
const runtime = yield* Effect.runtime<Exclude<R, Scope.Scope>>() const runtime = yield* Effect.runtime<Exclude<R, Scope.Scope>>()
return React.useCallback((props: P) => Runtime.runSync(runtime)( return React.useMemo(() => function ScopeProvider(props: P) {
self(props) as Effect.Effect<React.ReactNode, E, Exclude<R, Scope.Scope>> const scope = Runtime.runSync(runtime)(ReactHook.useScope(options))
), [])
const FC = React.useMemo(() => {
const f = (props: P) => Runtime.runSync(runtime)(
Effect.provideService(self(props), Scope.Scope, scope)
)
if (self.displayName) f.displayName = self.displayName
return f
}, [scope])
return React.createElement(FC, props)
}, Array.from(
Context.omit(...nonReactiveTags)(runtime.context).unsafeMap.values()
))
}) })
export const use: { export const use: {
<P, E, R>( <E, R, P extends {} = {}>(
self: ReactComponent<P, E, R>, self: ReactComponent<E, R, P>,
fn: (Component: React.FC<P>) => React.ReactNode, fn: (Component: React.FC<P>) => React.ReactNode,
options?: ReactHook.ScopeOptions, options?: ReactHook.ScopeOptions,
): Effect.Effect<React.ReactNode, never, Exclude<R, Scope.Scope>> ): Effect.Effect<React.ReactNode, never, Exclude<R, Scope.Scope>>
} = Effect.fn(function* use<P, E, R>( } = Effect.fnUntraced(function*(self, fn, options) {
self: ReactComponent<P, E, R>,
fn: (Component: React.FC<P>) => React.ReactNode,
options?: ReactHook.ScopeOptions,
) {
return fn(yield* useFC(self, options)) return fn(yield* useFC(self, options))
}) })
export const withRuntime: {
const FC = <P, E, R>( <E, R, P extends {} = {}>(context: React.Context<Runtime.Runtime<R>>): (self: ReactComponent<E, R, P>) => React.FC<P>
self: ReactComponent<P, E, R>, <E, R, P extends {} = {}>(self: ReactComponent<E, R, P>, context: React.Context<Runtime.Runtime<R>>): React.FC<P>
runtime: Runtime.Runtime<R>, } = Function.dual(2, <E, R, P extends {}>(
props: P, self: ReactComponent<E, R, P>,
options?: ReactHook.ScopeOptions, context: React.Context<Runtime.Runtime<R>>,
): React.ReactNode => { ): React.FC<P> => function WithRuntime(props) {
const [isInitialRun, initialScope] = React.useMemo(() => Runtime.runSync(runtime)( const runtime = React.useContext(context)
Effect.all([Ref.make(true), makeScope(options)]) return React.createElement(Runtime.runSync(runtime)(useFC(self)), props)
), []) })
const [scope, setScope] = React.useState(initialScope)
React.useEffect(() => Runtime.runSync(runtime)(
Effect.if(isInitialRun, {
onTrue: () => Effect.as(
Ref.set(isInitialRun, false),
() => closeScope(scope, runtime, options),
),
onFalse: () => makeScope(options).pipe(
Effect.tap(scope => Effect.sync(() => setScope(scope))),
Effect.map(scope => () => closeScope(scope, runtime, options)),
),
})
), [])
return React.useMemo(() => Runtime.runSync(runtime)(
Effect.provideService(self(props), Scope.Scope, scope)
), [
props,
...Array.from(Context.omit(Tracer.ParentSpan)(runtime.context).unsafeMap.values()),
])
}
const makeScope = (options?: ReactHook.ScopeOptions) => Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)
const closeScope = (
scope: Scope.CloseableScope,
runtime: Runtime.Runtime<never>,
options?: ReactHook.ScopeOptions,
) => {
switch (options?.finalizerExecutionMode ?? "sync") {
case "sync":
Runtime.runSync(runtime)(Scope.close(scope, Exit.void))
break
case "fork":
Runtime.runFork(runtime)(Scope.close(scope, Exit.void))
break
}
}

View File

@@ -1,5 +1,6 @@
import { Effect, ExecutionStrategy, Runtime, Scope } from "effect" import { type Context, Effect, ExecutionStrategy, Exit, type Layer, Option, pipe, PubSub, Ref, Runtime, Scope, Stream, SubscriptionRef } from "effect"
import * as React from "react" import * as React from "react"
import { SetStateAction } from "./types/index.js"
export interface ScopeOptions { export interface ScopeOptions {
@@ -8,39 +9,121 @@ export interface ScopeOptions {
} }
export const useScope: {
(options?: ScopeOptions): Effect.Effect<Scope.Scope>
} = Effect.fnUntraced(function* (options?: ScopeOptions) {
const runtime = yield* Effect.runtime()
const [isInitialRun, initialScope] = React.useMemo(() => Runtime.runSync(runtime)(
Effect.all([Ref.make(true), makeScope(options)])
), [])
const [scope, setScope] = React.useState(initialScope)
React.useEffect(() => Runtime.runSync(runtime)(
Effect.if(isInitialRun, {
onTrue: () => Effect.as(
Ref.set(isInitialRun, false),
() => closeScope(scope, runtime, options),
),
onFalse: () => makeScope(options).pipe(
Effect.tap(scope => Effect.sync(() => setScope(scope))),
Effect.map(scope => () => closeScope(scope, runtime, options)),
),
})
), [])
return scope
})
const makeScope = (options?: ScopeOptions) => Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)
const closeScope = (
scope: Scope.CloseableScope,
runtime: Runtime.Runtime<never>,
options?: ScopeOptions,
) => {
switch (options?.finalizerExecutionMode ?? "sync") {
case "sync":
Runtime.runSync(runtime)(Scope.close(scope, Exit.void))
break
case "fork":
Runtime.runFork(runtime)(Scope.close(scope, Exit.void))
break
}
}
export const useMemo: { export const useMemo: {
<A, E, R>( <A, E, R>(
factory: () => Effect.Effect<A, E, R>, factory: () => Effect.Effect<A, E, R>,
deps: React.DependencyList, deps: React.DependencyList,
): Effect.Effect<A, never, R> ): Effect.Effect<A, E, R>
} = Effect.fnUntraced(function* useMemo<A, E, R>( } = Effect.fnUntraced(function* <A, E, R>(
factory: () => Effect.Effect<A, E, R>, factory: () => Effect.Effect<A, E, R>,
deps: React.DependencyList, deps: React.DependencyList,
) { ) {
const runtime = yield* Effect.runtime<R>() const runtime = yield* Effect.runtime()
return React.useMemo(() => Runtime.runSync(runtime)(factory()), deps) return yield* React.useMemo(() => Runtime.runSync(runtime)(Effect.cached(factory())), deps)
}) })
export const useOnce: { export const useOnce: {
<A, E, R>(factory: () => Effect.Effect<A, E, R>): Effect.Effect<A, never, R> <A, E, R>(factory: () => Effect.Effect<A, E, R>): Effect.Effect<A, E, R>
} = Effect.fnUntraced(function* useOnce<A, E, R>( } = Effect.fnUntraced(function* <A, E, R>(
factory: () => Effect.Effect<A, E, R> factory: () => Effect.Effect<A, E, R>
) { ) {
return yield* useMemo(factory, []) return yield* useMemo(factory, [])
}) })
export const useMemoLayer: {
<ROut, E, RIn>(
layer: Layer.Layer<ROut, E, RIn>
): Effect.Effect<Context.Context<ROut>, E, RIn>
} = Effect.fnUntraced(function* <ROut, E, RIn>(
layer: Layer.Layer<ROut, E, RIn>
) {
return yield* useMemo(() => Effect.provide(Effect.context<ROut>(), layer), [layer])
})
export const useCallbackSync: {
<Args extends unknown[], A, E, R>(
callback: (...args: Args) => Effect.Effect<A, E, R>,
deps: React.DependencyList,
): Effect.Effect<(...args: Args) => A, never, R>
} = Effect.fnUntraced(function* <Args extends unknown[], A, E, R>(
callback: (...args: Args) => Effect.Effect<A, E, R>,
deps: React.DependencyList,
) {
const runtime = yield* Effect.runtime<R>()
return React.useCallback((...args: Args) => Runtime.runSync(runtime)(callback(...args)), deps)
})
export const useCallbackPromise: {
<Args extends unknown[], A, E, R>(
callback: (...args: Args) => Effect.Effect<A, E, R>,
deps: React.DependencyList,
): Effect.Effect<(...args: Args) => Promise<A>, never, R>
} = Effect.fnUntraced(function* <Args extends unknown[], A, E, R>(
callback: (...args: Args) => Effect.Effect<A, E, R>,
deps: React.DependencyList,
) {
const runtime = yield* Effect.runtime<R>()
return React.useCallback((...args: Args) => Runtime.runPromise(runtime)(callback(...args)), deps)
})
export const useEffect: { export const useEffect: {
<E, R>( <E, R>(
effect: () => Effect.Effect<void, E, R | Scope.Scope>, effect: () => Effect.Effect<void, E, R>,
deps?: React.DependencyList, deps?: React.DependencyList,
options?: ScopeOptions, options?: ScopeOptions,
): Effect.Effect<void, never, R> ): Effect.Effect<void, never, Exclude<R, Scope.Scope>>
} = Effect.fnUntraced(function* useEffect<E, R>( } = Effect.fnUntraced(function* <E, R>(
effect: () => Effect.Effect<void, E, R | Scope.Scope>, effect: () => Effect.Effect<void, E, R>,
deps?: React.DependencyList, deps?: React.DependencyList,
options?: ScopeOptions, options?: ScopeOptions,
) { ) {
const runtime = yield* Effect.runtime<R>() const runtime = yield* Effect.runtime<Exclude<R, Scope.Scope>>()
React.useEffect(() => { React.useEffect(() => {
const { scope, exit } = Effect.Do.pipe( const { scope, exit } = Effect.Do.pipe(
@@ -64,16 +147,16 @@ export const useEffect: {
export const useLayoutEffect: { export const useLayoutEffect: {
<E, R>( <E, R>(
effect: () => Effect.Effect<void, E, R | Scope.Scope>, effect: () => Effect.Effect<void, E, R>,
deps?: React.DependencyList, deps?: React.DependencyList,
options?: ScopeOptions, options?: ScopeOptions,
): Effect.Effect<void, never, R> ): Effect.Effect<void, never, Exclude<R, Scope.Scope>>
} = Effect.fnUntraced(function* useLayoutEffect<E, R>( } = Effect.fnUntraced(function* <E, R>(
effect: () => Effect.Effect<void, E, R | Scope.Scope>, effect: () => Effect.Effect<void, E, R>,
deps?: React.DependencyList, deps?: React.DependencyList,
options?: ScopeOptions, options?: ScopeOptions,
) { ) {
const runtime = yield* Effect.runtime<R>() const runtime = yield* Effect.runtime<Exclude<R, Scope.Scope>>()
React.useLayoutEffect(() => { React.useLayoutEffect(() => {
const { scope, exit } = Effect.Do.pipe( const { scope, exit } = Effect.Do.pipe(
@@ -94,3 +177,141 @@ export const useLayoutEffect: {
} }
}, deps) }, deps)
}) })
export const useFork: {
<E, R>(
effect: () => Effect.Effect<void, E, R>,
deps?: React.DependencyList,
options?: Runtime.RunForkOptions & ScopeOptions,
): Effect.Effect<void, never, Exclude<R, Scope.Scope>>
} = Effect.fnUntraced(function* <E, R>(
effect: () => Effect.Effect<void, E, R>,
deps?: React.DependencyList,
options?: Runtime.RunForkOptions & ScopeOptions,
) {
const runtime = yield* Effect.runtime<Exclude<R, Scope.Scope>>()
React.useEffect(() => {
const scope = Runtime.runSync(runtime)(options?.scope
? Scope.fork(options.scope, options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)
: Scope.make(options?.finalizerExecutionStrategy)
)
Runtime.runFork(runtime)(Effect.provideService(effect(), Scope.Scope, scope), { ...options, scope })
return () => {
switch (options?.finalizerExecutionMode ?? "fork") {
case "sync":
Runtime.runSync(runtime)(Scope.close(scope, Exit.void))
break
case "fork":
Runtime.runFork(runtime)(Scope.close(scope, Exit.void))
break
}
}
}, deps)
})
export const useRefFromReactiveValue: {
<A>(value: A): Effect.Effect<SubscriptionRef.SubscriptionRef<A>>
} = Effect.fnUntraced(function*(value) {
const ref = yield* useOnce(() => SubscriptionRef.make(value))
yield* useEffect(() => Ref.set(ref, value), [value])
return ref
})
export const useSubscribeRefs: {
<const Refs extends readonly SubscriptionRef.SubscriptionRef<any>[]>(
...refs: Refs
): Effect.Effect<{ [K in keyof Refs]: Effect.Effect.Success<Refs[K]> }>
} = Effect.fnUntraced(function* <const Refs extends readonly SubscriptionRef.SubscriptionRef<any>[]>(
...refs: Refs
) {
const [reactStateValue, setReactStateValue] = React.useState(yield* useOnce(() =>
Effect.all(refs as readonly SubscriptionRef.SubscriptionRef<any>[])
))
yield* useFork(() => pipe(
refs.map(ref => Stream.changesWith(ref.changes, (x, y) => x === y)),
streams => Stream.zipLatestAll(...streams),
Stream.runForEach(v =>
Effect.sync(() => setReactStateValue(v))
),
), refs)
return reactStateValue as any
})
export const useRefState: {
<A>(
ref: SubscriptionRef.SubscriptionRef<A>
): Effect.Effect<readonly [A, React.Dispatch<React.SetStateAction<A>>]>
} = Effect.fnUntraced(function* <A>(ref: SubscriptionRef.SubscriptionRef<A>) {
const [reactStateValue, setReactStateValue] = React.useState(yield* useOnce(() => ref))
yield* useFork(() => Stream.runForEach(
Stream.changesWith(ref.changes, (x, y) => x === y),
v => Effect.sync(() => setReactStateValue(v)),
), [ref])
const setValue = yield* useCallbackSync((setStateAction: React.SetStateAction<A>) =>
Ref.update(ref, prevState =>
SetStateAction.value(setStateAction, prevState)
),
[ref])
return [reactStateValue, setValue]
})
export const useStreamFromReactiveValues: {
<const A extends React.DependencyList>(
values: A
): Effect.Effect<Stream.Stream<A>, never, Scope.Scope>
} = Effect.fnUntraced(function* <const A extends React.DependencyList>(values: A) {
const { latest, pubsub, stream } = yield* useOnce(() => Effect.Do.pipe(
Effect.bind("latest", () => Ref.make(values)),
Effect.bind("pubsub", () => Effect.acquireRelease(PubSub.unbounded<A>(), PubSub.shutdown)),
Effect.let("stream", ({ latest, pubsub }) => latest.pipe(
Effect.flatMap(a => Effect.map(
Stream.fromPubSub(pubsub, { scoped: true }),
s => Stream.concat(Stream.make(a), s),
)),
Stream.unwrapScoped,
)),
))
yield* useEffect(() => Ref.set(latest, values).pipe(
Effect.andThen(PubSub.publish(pubsub, values)),
Effect.unlessEffect(PubSub.isShutdown(pubsub)),
), values)
return stream
})
export const useSubscribeStream: {
<A, E, R>(
stream: Stream.Stream<A, E, R>
): Effect.Effect<Option.Option<A>, never, R>
<A extends NonNullable<unknown>, E, R>(
stream: Stream.Stream<A, E, R>,
initialValue: A,
): Effect.Effect<Option.Some<A>, never, R>
} = Effect.fnUntraced(function* <A extends NonNullable<unknown>, E, R>(
stream: Stream.Stream<A, E, R>,
initialValue?: A,
) {
const [reactStateValue, setReactStateValue] = React.useState(
React.useMemo(() => initialValue
? Option.some(initialValue)
: Option.none(),
[])
)
yield* useFork(() => Stream.runForEach(
Stream.changesWith(stream, (x, y) => x === y),
v => Effect.sync(() => setReactStateValue(Option.some(v))),
), [stream])
return reactStateValue as Option.Some<A>
})

View File

@@ -0,0 +1,47 @@
import { Effect, type Layer, ManagedRuntime, type Runtime } from "effect"
import * as React from "react"
export interface ReactManagedRuntime<R, ER> {
readonly runtime: ManagedRuntime.ManagedRuntime<R, ER>
readonly context: React.Context<Runtime.Runtime<R>>
}
export const make = <R, ER>(
layer: Layer.Layer<R, ER>,
memoMap?: Layer.MemoMap,
): ReactManagedRuntime<R, ER> => ({
runtime: ManagedRuntime.make(layer, memoMap),
context: React.createContext<Runtime.Runtime<R>>(null!),
})
export interface AsyncProviderProps<R, ER> extends React.SuspenseProps {
readonly runtime: ReactManagedRuntime<R, ER>
readonly children?: React.ReactNode
}
export function AsyncProvider<R, ER>(
{ runtime, children, ...suspenseProps }: AsyncProviderProps<R, ER>
): React.ReactNode {
const promise = React.useMemo(() => Effect.runPromise(runtime.runtime.runtimeEffect), [runtime])
return React.createElement(
React.Suspense,
suspenseProps,
React.createElement(AsyncProviderInner<R, ER>, { runtime, promise, children }),
)
}
interface AsyncProviderInnerProps<R, ER> {
readonly runtime: ReactManagedRuntime<R, ER>
readonly promise: Promise<Runtime.Runtime<R>>
readonly children?: React.ReactNode
}
function AsyncProviderInner<R, ER>(
{ runtime, promise, children }: AsyncProviderInnerProps<R, ER>
): React.ReactNode {
const value = React.use(promise)
return React.createElement(runtime.context, { value }, children)
}

View File

@@ -1,2 +1,3 @@
export * as ReactComponent from "./ReactComponent.js" export * as ReactComponent from "./ReactComponent.js"
export * as ReactHook from "./ReactHook.js" export * as ReactHook from "./ReactHook.js"
export * as ReactManagedRuntime from "./ReactManagedRuntime.js"

View File

@@ -0,0 +1,99 @@
import { Array, Function, Option, Predicate } from "effect"
type Prev = readonly [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
export type Paths<T, D extends number = 5, Seen = never> = [] | (
D extends never ? [] :
T extends Seen ? [] :
T extends readonly any[] ? ArrayPaths<T, D, Seen | T> :
T extends object ? ObjectPaths<T, D, Seen | T> :
never
)
export type ArrayPaths<T extends readonly any[], D extends number, Seen> = {
[K in keyof T as K extends number ? K : never]:
| [K]
| [K, ...Paths<T[K], Prev[D], Seen>]
} extends infer O
? O[keyof O]
: never
export type ObjectPaths<T extends object, D extends number, Seen> = {
[K in keyof T as K extends string | number | symbol ? K : never]-?:
NonNullable<T[K]> extends infer V
? [K] | [K, ...Paths<V, Prev[D], Seen>]
: never
} extends infer O
? O[keyof O]
: never
export type ValueFromPath<T, P extends any[]> = P extends [infer Head, ...infer Tail]
? Head extends keyof T
? ValueFromPath<T[Head], Tail>
: T extends readonly any[]
? Head extends number
? ValueFromPath<T[number], Tail>
: never
: never
: T
export type AnyKey = string | number | symbol
export type AnyPath = readonly AnyKey[]
export const unsafeGet: {
<T, const P extends Paths<T>>(path: P): (self: T) => ValueFromPath<T, P>
<T, const P extends Paths<T>>(self: T, path: P): ValueFromPath<T, P>
} = Function.dual(2, <T, const P extends Paths<T>>(self: T, path: P): ValueFromPath<T, P> =>
path.reduce((acc: any, key: any) => acc?.[key], self)
)
export const get: {
<T, const P extends Paths<T>>(path: P): (self: T) => Option.Option<ValueFromPath<T, P>>
<T, const P extends Paths<T>>(self: T, path: P): Option.Option<ValueFromPath<T, P>>
} = Function.dual(2, <T, const P extends Paths<T>>(self: T, path: P): Option.Option<ValueFromPath<T, P>> =>
path.reduce(
(acc: Option.Option<any>, key: any): Option.Option<any> => Option.isSome(acc)
? Predicate.hasProperty(acc.value, key)
? Option.some(acc.value[key])
: Option.none()
: acc,
Option.some(self),
)
)
export const immutableSet: {
<T, const P extends Paths<T>>(path: P, value: ValueFromPath<T, P>): (self: T) => ValueFromPath<T, P>
<T, const P extends Paths<T>>(self: T, path: P, value: ValueFromPath<T, P>): Option.Option<T>
} = Function.dual(3, <T, const P extends Paths<T>>(self: T, path: P, value: ValueFromPath<T, P>): Option.Option<T> => {
const key = Array.head(path as AnyPath)
if (Option.isNone(key))
return Option.some(value as T)
if (!Predicate.hasProperty(self, key.value))
return Option.none()
const child = immutableSet<any, any>(self[key.value], Option.getOrThrow(Array.tail(path as AnyPath)), value)
if (Option.isNone(child))
return child
if (Array.isArray(self))
return typeof key.value === "number"
? Option.some([
...self.slice(0, key.value),
child.value,
...self.slice(key.value + 1),
] as T)
: Option.none()
if (typeof self === "object")
return Option.some(
Object.assign(
Object.create(Object.getPrototypeOf(self)),
{ ...self, [key.value]: child.value },
)
)
return Option.none()
})

View File

@@ -0,0 +1,12 @@
import { Function } from "effect"
import type * as React from "react"
export const value: {
<S>(prevState: S): (self: React.SetStateAction<S>) => S
<S>(self: React.SetStateAction<S>, prevState: S): S
} = Function.dual(2, <S>(self: React.SetStateAction<S>, prevState: S): S =>
typeof self === "function"
? (self as (prevState: S) => S)(prevState)
: self
)

View File

@@ -0,0 +1,100 @@
import { Effect, Effectable, Option, Readable, Ref, Stream, Subscribable, SubscriptionRef, SynchronizedRef, type Types, type Unify } from "effect"
import * as PropertyPath from "./PropertyPath.js"
export const SubscriptionSubRefTypeId: unique symbol = Symbol.for("reffuse/types/SubscriptionSubRef")
export type SubscriptionSubRefTypeId = typeof SubscriptionSubRefTypeId
export interface SubscriptionSubRef<in out A, in out B> extends SubscriptionSubRef.Variance<A, B>, SubscriptionRef.SubscriptionRef<A> {
readonly parent: SubscriptionRef.SubscriptionRef<B>
readonly [Unify.typeSymbol]?: unknown
readonly [Unify.unifySymbol]?: SubscriptionSubRefUnify<this>
readonly [Unify.ignoreSymbol]?: SubscriptionSubRefUnifyIgnore
}
export declare namespace SubscriptionSubRef {
export interface Variance<in out A, in out B> {
readonly [SubscriptionSubRefTypeId]: {
readonly _A: Types.Invariant<A>
readonly _B: Types.Invariant<B>
}
}
}
export interface SubscriptionSubRefUnify<A extends { [Unify.typeSymbol]?: any }> extends SubscriptionRef.SubscriptionRefUnify<A> {
SubscriptionSubRef?: () => Extract<A[Unify.typeSymbol], SubscriptionSubRef<any, any>>
}
export interface SubscriptionSubRefUnifyIgnore extends SubscriptionRef.SubscriptionRefUnifyIgnore {
SubscriptionRef?: true
}
const refVariance = { _A: (_: any) => _ }
const synchronizedRefVariance = { _A: (_: any) => _ }
const subscriptionRefVariance = { _A: (_: any) => _ }
const subscriptionSubRefVariance = { _A: (_: any) => _, _B: (_: any) => _ }
class SubscriptionSubRefImpl<in out A, in out B> extends Effectable.Class<A> implements SubscriptionSubRef<A, B> {
readonly [Readable.TypeId]: Readable.TypeId = Readable.TypeId
readonly [Subscribable.TypeId]: Subscribable.TypeId = Subscribable.TypeId
readonly [Ref.RefTypeId] = refVariance
readonly [SynchronizedRef.SynchronizedRefTypeId] = synchronizedRefVariance
readonly [SubscriptionRef.SubscriptionRefTypeId] = subscriptionRefVariance
readonly [SubscriptionSubRefTypeId] = subscriptionSubRefVariance
readonly get: Effect.Effect<A>
constructor(
readonly parent: SubscriptionRef.SubscriptionRef<B>,
readonly getter: (parentValue: B) => A,
readonly setter: (parentValue: B, value: A) => B,
) {
super()
this.get = Effect.map(Ref.get(this.parent), this.getter)
}
commit() {
return this.get
}
get changes(): Stream.Stream<A> {
return this.get.pipe(
Effect.map(a => this.parent.changes.pipe(
Stream.map(this.getter),
s => Stream.concat(Stream.make(a), s),
)),
Stream.unwrap,
)
}
modify<C>(f: (a: A) => readonly [C, A]): Effect.Effect<C> {
return this.modifyEffect(a => Effect.succeed(f(a)))
}
modifyEffect<C, E, R>(f: (a: A) => Effect.Effect<readonly [C, A], E, R>): Effect.Effect<C, E, R> {
return Effect.Do.pipe(
Effect.bind("b", () => Ref.get(this.parent)),
Effect.bind("ca", ({ b }) => f(this.getter(b))),
Effect.tap(({ b, ca: [, a] }) => Ref.set(this.parent, this.setter(b, a))),
Effect.map(({ ca: [c] }) => c),
)
}
}
export const makeFromGetSet = <A, B>(
parent: SubscriptionRef.SubscriptionRef<B>,
getter: (parentValue: B) => A,
setter: (parentValue: B, value: A) => B,
): SubscriptionSubRef<A, B> => new SubscriptionSubRefImpl(parent, getter, setter)
export const makeFromPath = <B, const P extends PropertyPath.Paths<B>>(
parent: SubscriptionRef.SubscriptionRef<B>,
path: P,
): SubscriptionSubRef<PropertyPath.ValueFromPath<B, P>, B> => new SubscriptionSubRefImpl(
parent,
parentValue => Option.getOrThrow(PropertyPath.get(parentValue, path)),
(parentValue, value) => Option.getOrThrow(PropertyPath.immutableSet(parentValue, path, value)),
)

View File

@@ -0,0 +1,3 @@
export * as PropertyPath from "./PropertyPath.js"
export * as SetStateAction from "./SetStateAction.js"
export * as SubscriptionSubRef from "./SubscriptionSubRef.js"

View File

@@ -1,8 +1,26 @@
import { Box, Text, TextField } from "@radix-ui/themes" import { Box, TextField } from "@radix-ui/themes"
import { createFileRoute } from "@tanstack/react-router" import { createFileRoute } from "@tanstack/react-router"
import { Console, Effect, Layer, ManagedRuntime, SubscriptionRef } from "effect" import { Array, Console, Effect, Layer, pipe, Ref, Runtime, SubscriptionRef } from "effect"
import { ReactComponent, ReactHook } from "effect-components" import { ReactComponent, ReactHook, ReactManagedRuntime } from "effect-components"
import * as React from "react"
const LogLive = Layer.scopedDiscard(Effect.acquireRelease(
Console.log("Runtime built."),
() => Console.log("Runtime destroyed."),
))
class TestService extends Effect.Service<TestService>()("TestService", {
effect: Effect.bind(Effect.Do, "ref", () => SubscriptionRef.make("value")),
}) {}
class SubService extends Effect.Service<SubService>()("SubService", {
effect: Effect.bind(Effect.Do, "ref", () => SubscriptionRef.make("subvalue")),
}) {}
const runtime = ReactManagedRuntime.make(Layer.empty.pipe(
Layer.provideMerge(LogLive),
Layer.provideMerge(TestService.Default),
))
export const Route = createFileRoute("/effect-component-tests")({ export const Route = createFileRoute("/effect-component-tests")({
@@ -10,39 +28,78 @@ export const Route = createFileRoute("/effect-component-tests")({
}) })
function RouteComponent() { function RouteComponent() {
const runtime = React.useMemo(() => ManagedRuntime.make(Layer.empty), []) return (
<ReactManagedRuntime.AsyncProvider runtime={runtime}>
return <> <MyRoute />
{runtime.runSync(ReactComponent.use(MyTestComponent, Component => ( </ReactManagedRuntime.AsyncProvider>
<Component /> )
)).pipe(
Effect.scoped
))}
</>
} }
const MyRoute = pipe(
Effect.fn(function*() {
const runtime = yield* Effect.runtime()
class TestService extends Effect.Service<TestService>()("TestService", { const service = yield* TestService
effect: Effect.bind(Effect.Do, "ref", () => SubscriptionRef.make("value")), const [value] = yield* ReactHook.useSubscribeRefs(service.ref)
}) {}
const MyTestComponent = Effect.fn(function* MyTestComponent(props?: { readonly value?: string }) { // const MyTestComponentFC = yield* Effect.provide(
const [state, setState] = React.useState("value") // ReactComponent.useFC(MyTestComponent),
const effectValue = yield* Effect.succeed(`state: ${ state }`) // yield* ReactHook.useMemoLayer(SubService.Default),
// )
yield* ReactHook.useOnce(() => Effect.andThen(
Effect.addFinalizer(() => Console.log("MyTestComponent umounted")),
Console.log("MyTestComponent mounted"),
))
return <> return <>
<Text>{effectValue}</Text>
<Box> <Box>
<TextField.Root <TextField.Root
value={state} value={value}
onChange={e => setState(e.target.value)} onChange={e => Runtime.runSync(runtime)(Ref.set(service.ref, e.target.value))}
/>
</Box>
{/* {yield* ReactComponent.use(MyTestComponent, C => <C />).pipe(
Effect.provide(yield* ReactHook.useMemoLayer(SubService.Default))
)} */}
{/* {Array.range(0, 3).map(k =>
<MyTestComponentFC key={k} />
)} */}
{yield* pipe(
Array.range(0, 3),
Array.map(k => ReactComponent.use(MyTestComponent, FC =>
<FC key={k} />
)),
Effect.all,
Effect.provide(yield* ReactHook.useMemoLayer(SubService.Default)),
)}
</>
}),
ReactComponent.withDisplayName("MyRoute"),
ReactComponent.withRuntime(runtime.context),
)
const MyTestComponent = pipe(
Effect.fn(function*() {
const runtime = yield* Effect.runtime()
const service = yield* SubService
const [value] = yield* ReactHook.useSubscribeRefs(service.ref)
// yield* ReactHook.useMemo(() => Effect.andThen(
// Effect.addFinalizer(() => Console.log("MyTestComponent umounted")),
// Console.log("MyTestComponent mounted"),
// ), [])
return <>
<Box>
<TextField.Root
value={value}
onChange={e => Runtime.runSync(runtime)(Ref.set(service.ref, e.target.value))}
/> />
</Box> </Box>
</> </>
}) }),
ReactComponent.withDisplayName("MyTestComponent"),
)