21 Commits

Author SHA1 Message Date
Julien Valverdé
5fb22ad183 Tests
All checks were successful
Lint / lint (push) Successful in 11s
2025-08-08 00:00:34 +02:00
Julien Valverdé
8e4bcfe59d Tests
All checks were successful
Lint / lint (push) Successful in 11s
2025-08-07 22:29:52 +02:00
Julien Valverdé
c158cdef19 Untraced hooks
All checks were successful
Lint / lint (push) Successful in 12s
2025-08-07 07:42:34 +02:00
Julien Valverdé
fa90d9438b Tests
All checks were successful
Lint / lint (push) Successful in 13s
2025-08-07 06:41:30 +02:00
Julien Valverdé
f8b356ef39 Fix
All checks were successful
Lint / lint (push) Successful in 11s
2025-08-07 05:18:21 +02:00
Julien Valverdé
d38a5a4afd Example cleanup
All checks were successful
Lint / lint (push) Successful in 11s
2025-08-07 04:58:56 +02:00
Julien Valverdé
53bceb3a8a Traits fix
All checks were successful
Lint / lint (push) Successful in 11s
2025-08-07 04:21:43 +02:00
Julien Valverdé
d3afca85da Memoized fix
Some checks failed
Lint / lint (push) Failing after 11s
2025-08-07 03:20:41 +02:00
Julien Valverdé
9cfc7072df Component refactoring
Some checks failed
Lint / lint (push) Failing after 11s
2025-08-07 03:05:18 +02:00
Julien Valverdé
bb9c41deae Tests
All checks were successful
Lint / lint (push) Successful in 11s
2025-07-29 04:20:26 +02:00
Julien Valverdé
41b1396a58 Fix
All checks were successful
Lint / lint (push) Successful in 1m0s
2025-07-29 03:32:17 +02:00
Julien Valverdé
b2b002852c useInput
All checks were successful
Lint / lint (push) Successful in 12s
2025-07-29 02:57:18 +02:00
Julien Valverdé
ec8f9f2ddb Fix
All checks were successful
Lint / lint (push) Successful in 11s
2025-07-28 04:32:08 +02:00
Julien Valverdé
55ca8a0dd4 Refactoring
All checks were successful
Lint / lint (push) Successful in 11s
2025-07-28 04:25:41 +02:00
Julien Valverdé
6b39671d60 Fix
All checks were successful
Lint / lint (push) Successful in 11s
2025-07-28 04:20:04 +02:00
Julien Valverdé
bada57a591 Refactoring
All checks were successful
Lint / lint (push) Successful in 11s
2025-07-28 04:02:55 +02:00
Julien Valverdé
09ed773b96 Refactoring
All checks were successful
Lint / lint (push) Successful in 11s
2025-07-28 01:42:09 +02:00
Julien Valverdé
956a532195 Fix
All checks were successful
Lint / lint (push) Successful in 12s
2025-07-27 19:08:16 +02:00
Julien Valverdé
1c1659e82c Fix
All checks were successful
Lint / lint (push) Successful in 12s
2025-07-27 18:09:07 +02:00
Julien Valverdé
051226ebd4 SubscriptionSubRef refactoring
All checks were successful
Lint / lint (push) Successful in 12s
2025-07-26 01:35:02 +02:00
Julien Valverdé
35463d5607 Fix
All checks were successful
Lint / lint (push) Successful in 12s
2025-07-24 02:26:53 +02:00
32 changed files with 641 additions and 667 deletions

View File

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

View File

@@ -1,26 +1,41 @@
import { Context, Effect, Effectable, ExecutionStrategy, Function, Predicate, Runtime, Scope, String, Tracer, type Utils } from "effect" import { Context, Effect, Effectable, ExecutionStrategy, Function, identity, Predicate, Runtime, Scope, String, Tracer, type Types } from "effect"
import * as React from "react" import * as React from "react"
import * as Hook from "./Hook.js" import { Hooks } from "./hooks/index.js"
import * as Memoized from "./Memoized.js" import * as Memoized from "./Memoized.js"
export const TypeId: unique symbol = Symbol.for("effect-fc/Component") export const TypeId: unique symbol = Symbol.for("effect-fc/Component")
export type TypeId = typeof TypeId export type TypeId = typeof TypeId
export interface Component<P extends {} = {}, E = never, R = never> export interface Component<F extends FunctionComponent, E, R>
extends Effect.Effect<React.FC<P>, never, Exclude<R, Scope.Scope>>, Component.Options { extends
Effect.Effect<F, never, Exclude<R, Scope.Scope>>,
Component.Options
{
new(_: never): {} new(_: never): {}
readonly [TypeId]: TypeId readonly [TypeId]: TypeId
readonly ["~FunctionComponent"]: F
readonly ["~Props"]: FunctionComponent.Props<F>
readonly ["~Success"]: FunctionComponent.Success<F>
readonly ["~Error"]: E
readonly ["~Context"]: R
/** @internal */ /** @internal */
makeFunctionComponent(runtimeRef: React.Ref<Runtime.Runtime<Exclude<R, Scope.Scope>>>, scope: Scope.Scope): React.FC<P> readonly body: (props: FunctionComponent.Props<F>) => Effect.Effect<FunctionComponent.Success<F>, E, R>
/** @internal */ /** @internal */
readonly body: (props: P) => Effect.Effect<React.ReactNode, E, R> makeFunctionComponent(
runtimeRef: React.Ref<Runtime.Runtime<Exclude<R, Scope.Scope>>>,
scope: Scope.Scope,
): F
} }
export namespace Component { export namespace Component {
export type Props<T> = T extends Component<infer P, infer _E, infer _R> ? P : never export type FunctionComponent<T extends Component<any, any, any>> = T extends Component<infer F, infer _E, infer _R> ? F : never
export type Error<T> = T extends Component<infer _P, infer E, infer _R> ? E : never export type Error<T extends Component<any, any, any>> = T extends Component<infer _F, infer E, infer _R> ? E : never
export type Context<T> = T extends Component<infer _P, infer _E, infer R> ? R : never export type Context<T extends Component<any, any, any>> = T extends Component<infer _F, infer _E, infer R> ? R : never
export type AsComponent<T extends Component<any, any, any>> = Component<FunctionComponent<T>, Error<T>, Context<T>>
export interface Options { export interface Options {
readonly displayName?: string readonly displayName?: string
@@ -29,18 +44,27 @@ export namespace Component {
} }
} }
export type FunctionComponent = (...args: readonly any[]) => React.ReactNode
export namespace FunctionComponent {
export type Props<T extends FunctionComponent> = T extends (...args: readonly [infer P, ...any[]]) => infer _A ? P : never
export type Success<T extends FunctionComponent> = T extends (...args: infer _Args) => infer A ? A : never
}
const ComponentProto = Object.freeze({ const ComponentProto = Object.freeze({
...Effectable.CommitPrototype, ...Effectable.CommitPrototype,
[TypeId]: TypeId, [TypeId]: TypeId,
commit: Effect.fn("Component")(function* <P extends {}, E, R>(this: Component<P, E, R>) { commit: Effect.fnUntraced(function* <F extends FunctionComponent, E, R>(
this: Component<F, E, R>
) {
const self = this const self = this
const runtimeRef = React.useRef<Runtime.Runtime<Exclude<R, Scope.Scope>>>(null!) const runtimeRef = React.useRef<Runtime.Runtime<Exclude<R, Scope.Scope>>>(null!)
runtimeRef.current = yield* Effect.runtime<Exclude<R, Scope.Scope>>() runtimeRef.current = yield* Effect.runtime<Exclude<R, Scope.Scope>>()
return React.useCallback(function ScopeProvider(props: P) { return React.useCallback(function ScopeProvider(props: FunctionComponent.Props<F>) {
const scope = Runtime.runSync(runtimeRef.current)(Hook.useScope( const scope = Runtime.runSync(runtimeRef.current)(Hooks.useScope(
Array.from( Array.from(
Context.omit(...nonReactiveTags)(runtimeRef.current.context).unsafeMap.values() Context.omit(...nonReactiveTags)(runtimeRef.current.context).unsafeMap.values()
), ),
@@ -48,7 +72,7 @@ const ComponentProto = Object.freeze({
)) ))
const FC = React.useMemo(() => { const FC = React.useMemo(() => {
const f = self.makeFunctionComponent(runtimeRef, scope) const f: React.FC<FunctionComponent.Props<F>> = self.makeFunctionComponent(runtimeRef, scope)
f.displayName = self.displayName ?? "Anonymous" f.displayName = self.displayName ?? "Anonymous"
return Memoized.isMemoized(self) return Memoized.isMemoized(self)
? React.memo(f, self.propsAreEqual) ? React.memo(f, self.propsAreEqual)
@@ -59,12 +83,12 @@ const ComponentProto = Object.freeze({
}, []) }, [])
}), }),
makeFunctionComponent <P extends {}, E, R>( makeFunctionComponent<F extends FunctionComponent, E, R>(
this: Component<P, E, R>, this: Component<F, E, R>,
runtimeRef: React.RefObject<Runtime.Runtime<Exclude<R, Scope.Scope>>>, runtimeRef: React.RefObject<Runtime.Runtime<Exclude<R, Scope.Scope>>>,
scope: Scope.Scope, scope: Scope.Scope,
): React.FC<P> { ) {
return (props: P) => Runtime.runSync(runtimeRef.current)( return (props: FunctionComponent.Props<F>) => Runtime.runSync(runtimeRef.current)(
Effect.provideService(this.body(props), Scope.Scope, scope) Effect.provideService(this.body(props), Scope.Scope, scope)
) )
}, },
@@ -78,277 +102,23 @@ const defaultOptions = {
const nonReactiveTags = [Tracer.ParentSpan] as const const nonReactiveTags = [Tracer.ParentSpan] as const
export const isComponent = (u: unknown): u is Component<{}, unknown, unknown> => Predicate.hasProperty(u, TypeId) export const isComponent = (u: unknown): u is Component<FunctionComponent, unknown, unknown> => Predicate.hasProperty(u, TypeId)
export namespace make { export const make = <Args extends readonly any[], A extends React.ReactNode, E, R>(
export type Gen = { body: (...args: Args) => Effect.Effect<A, E, R>
<Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, P extends {} = {}>( ): Component<(...args: Args) => A, E, R> => Object.setPrototypeOf(
body: (props: P) => Generator<Eff, React.ReactNode, never>,
): Component<
P,
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer E, infer _R>>] ? E : never,
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer _E, infer R>>] ? R : never
>
<Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, A, B extends Effect.Effect<React.ReactNode, any, any>, P extends {} = {}>(
body: (props: P) => Generator<Eff, A, never>,
a: (
_: Effect.Effect<
A,
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer E, infer _R>>] ? E : never,
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer _E, infer R>>] ? R : never
>,
props: NoInfer<P>,
) => B
): Component<P, Effect.Effect.Error<B>, Effect.Effect.Context<B>>
<Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, A, B, C extends Effect.Effect<React.ReactNode, any, any>, P extends {} = {}>(
body: (props: P) => Generator<Eff, A, never>,
a: (
_: Effect.Effect<
A,
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer E, infer _R>>] ? E : never,
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer _E, infer R>>] ? R : never
>,
props: NoInfer<P>,
) => B,
b: (_: B, props: NoInfer<P>) => C,
): Component<P, Effect.Effect.Error<C>, Effect.Effect.Context<C>>
<Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, A, B, C, D extends Effect.Effect<React.ReactNode, any, any>, P extends {} = {}>(
body: (props: P) => Generator<Eff, A, never>,
a: (
_: Effect.Effect<
A,
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer E, infer _R>>] ? E : never,
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer _E, infer R>>] ? R : never
>,
props: NoInfer<P>,
) => B,
b: (_: B, props: NoInfer<P>) => C,
c: (_: C, props: NoInfer<P>) => D,
): Component<P, Effect.Effect.Error<D>, Effect.Effect.Context<D>>
<Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, A, B, C, D, E extends Effect.Effect<React.ReactNode, any, any>, P extends {} = {}>(
body: (props: P) => Generator<Eff, A, never>,
a: (
_: Effect.Effect<
A,
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer E, infer _R>>] ? E : never,
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer _E, infer R>>] ? R : never
>,
props: NoInfer<P>,
) => B,
b: (_: B, props: NoInfer<P>) => C,
c: (_: C, props: NoInfer<P>) => D,
d: (_: D, props: NoInfer<P>) => E,
): Component<P, Effect.Effect.Error<E>, Effect.Effect.Context<E>>
<Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, A, B, C, D, E, F extends Effect.Effect<React.ReactNode, any, any>, P extends {} = {}>(
body: (props: P) => Generator<Eff, A, never>,
a: (
_: Effect.Effect<
A,
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer E, infer _R>>] ? E : never,
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer _E, infer R>>] ? R : never
>,
props: NoInfer<P>,
) => B,
b: (_: B, props: NoInfer<P>) => C,
c: (_: C, props: NoInfer<P>) => D,
d: (_: D, props: NoInfer<P>) => E,
e: (_: E, props: NoInfer<P>) => F,
): Component<P, Effect.Effect.Error<F>, Effect.Effect.Context<F>>
<Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, A, B, C, D, E, F, G extends Effect.Effect<React.ReactNode, any, any>, P extends {} = {}>(
body: (props: P) => Generator<Eff, A, never>,
a: (
_: Effect.Effect<
A,
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer E, infer _R>>] ? E : never,
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer _E, infer R>>] ? R : never
>,
props: NoInfer<P>,
) => B,
b: (_: B, props: NoInfer<P>) => C,
c: (_: C, props: NoInfer<P>) => D,
d: (_: D, props: NoInfer<P>) => E,
e: (_: E, props: NoInfer<P>) => F,
f: (_: F, props: NoInfer<P>) => G,
): Component<P, Effect.Effect.Error<G>, Effect.Effect.Context<G>>
<Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, A, B, C, D, E, F, G, H extends Effect.Effect<React.ReactNode, any, any>, P extends {} = {}>(
body: (props: P) => Generator<Eff, A, never>,
a: (
_: Effect.Effect<
A,
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer E, infer _R>>] ? E : never,
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer _E, infer R>>] ? R : never
>,
props: NoInfer<P>,
) => B,
b: (_: B, props: NoInfer<P>) => C,
c: (_: C, props: NoInfer<P>) => D,
d: (_: D, props: NoInfer<P>) => E,
e: (_: E, props: NoInfer<P>) => F,
f: (_: F, props: NoInfer<P>) => G,
g: (_: G, props: NoInfer<P>) => H,
): Component<P, Effect.Effect.Error<H>, Effect.Effect.Context<H>>
<Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, A, B, C, D, E, F, G, H, I extends Effect.Effect<React.ReactNode, any, any>, P extends {} = {}>(
body: (props: P) => Generator<Eff, A, never>,
a: (
_: Effect.Effect<
A,
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer E, infer _R>>] ? E : never,
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer _E, infer R>>] ? R : never
>,
props: NoInfer<P>,
) => B,
b: (_: B, props: NoInfer<P>) => C,
c: (_: C, props: NoInfer<P>) => D,
d: (_: D, props: NoInfer<P>) => E,
e: (_: E, props: NoInfer<P>) => F,
f: (_: F, props: NoInfer<P>) => G,
g: (_: G, props: NoInfer<P>) => H,
h: (_: H, props: NoInfer<P>) => I,
): Component<P, Effect.Effect.Error<I>, Effect.Effect.Context<I>>
<Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, A, B, C, D, E, F, G, H, I, J extends Effect.Effect<React.ReactNode, any, any>, P extends {} = {}>(
body: (props: P) => Generator<Eff, A, never>,
a: (
_: Effect.Effect<
A,
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer E, infer _R>>] ? E : never,
[Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer _E, infer R>>] ? R : never
>,
props: NoInfer<P>,
) => B,
b: (_: B, props: NoInfer<P>) => C,
c: (_: C, props: NoInfer<P>) => D,
d: (_: D, props: NoInfer<P>) => E,
e: (_: E, props: NoInfer<P>) => F,
f: (_: F, props: NoInfer<P>) => G,
g: (_: G, props: NoInfer<P>) => H,
h: (_: H, props: NoInfer<P>) => I,
i: (_: I, props: NoInfer<P>) => J,
): Component<P, Effect.Effect.Error<J>, Effect.Effect.Context<J>>
}
export type NonGen = {
<Eff extends Effect.Effect<React.ReactNode, any, any>, P extends {} = {}>(
body: (props: P) => Eff
): Component<P, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, P extends {} = {}>(
body: (props: P) => A,
a: (_: A, props: NoInfer<P>) => Eff,
): Component<P, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, B, P extends {} = {}>(
body: (props: P) => A,
a: (_: A, props: NoInfer<P>) => B,
b: (_: B, props: NoInfer<P>) => Eff,
): Component<P, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, B, C, P extends {} = {}>(
body: (props: P) => A,
a: (_: A, props: NoInfer<P>) => B,
b: (_: B, props: NoInfer<P>) => C,
c: (_: C, props: NoInfer<P>) => Eff,
): Component<P, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, B, C, D, P extends {} = {}>(
body: (props: P) => A,
a: (_: A, props: NoInfer<P>) => B,
b: (_: B, props: NoInfer<P>) => C,
c: (_: C, props: NoInfer<P>) => D,
d: (_: D, props: NoInfer<P>) => Eff,
): Component<P, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, B, C, D, E, P extends {} = {}>(
body: (props: P) => A,
a: (_: A, props: NoInfer<P>) => B,
b: (_: B, props: NoInfer<P>) => C,
c: (_: C, props: NoInfer<P>) => D,
d: (_: D, props: NoInfer<P>) => E,
e: (_: E, props: NoInfer<P>) => Eff,
): Component<P, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, B, C, D, E, F, P extends {} = {}>(
body: (props: P) => A,
a: (_: A, props: NoInfer<P>) => B,
b: (_: B, props: NoInfer<P>) => C,
c: (_: C, props: NoInfer<P>) => D,
d: (_: D, props: NoInfer<P>) => E,
e: (_: E, props: NoInfer<P>) => F,
f: (_: F, props: NoInfer<P>) => Eff,
): Component<P, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, B, C, D, E, F, G, P extends {} = {}>(
body: (props: P) => A,
a: (_: A, props: NoInfer<P>) => B,
b: (_: B, props: NoInfer<P>) => C,
c: (_: C, props: NoInfer<P>) => D,
d: (_: D, props: NoInfer<P>) => E,
e: (_: E, props: NoInfer<P>) => F,
f: (_: F, props: NoInfer<P>) => G,
g: (_: G, props: NoInfer<P>) => Eff,
): Component<P, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, B, C, D, E, F, G, H, P extends {} = {}>(
body: (props: P) => A,
a: (_: A, props: NoInfer<P>) => B,
b: (_: B, props: NoInfer<P>) => C,
c: (_: C, props: NoInfer<P>) => D,
d: (_: D, props: NoInfer<P>) => E,
e: (_: E, props: NoInfer<P>) => F,
f: (_: F, props: NoInfer<P>) => G,
g: (_: G, props: NoInfer<P>) => H,
h: (_: H, props: NoInfer<P>) => Eff,
): Component<P, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
<Eff extends Effect.Effect<React.ReactNode, any, any>, A, B, C, D, E, F, G, H, I, P extends {} = {}>(
body: (props: P) => A,
a: (_: A, props: NoInfer<P>) => B,
b: (_: B, props: NoInfer<P>) => C,
c: (_: C, props: NoInfer<P>) => D,
d: (_: D, props: NoInfer<P>) => E,
e: (_: E, props: NoInfer<P>) => F,
f: (_: F, props: NoInfer<P>) => G,
g: (_: G, props: NoInfer<P>) => H,
h: (_: H, props: NoInfer<P>) => I,
i: (_: I, props: NoInfer<P>) => Eff,
): Component<P, Effect.Effect.Error<Eff>, Effect.Effect.Context<Eff>>
}
}
export const make: (
& make.Gen
& make.NonGen
& ((
spanName: string,
spanOptions?: Tracer.SpanOptions,
) => make.Gen & make.NonGen)
) = (spanNameOrBody: Function | string, ...pipeables: any[]): any => {
if (typeof spanNameOrBody !== "string") {
const displayName = displayNameFromBody(spanNameOrBody)
return Object.setPrototypeOf(
Object.assign(function() {}, defaultOptions, {
body: displayName
? Effect.fn(displayName)(spanNameOrBody as any, ...pipeables as [])
: Effect.fn(spanNameOrBody as any, ...pipeables),
displayName,
}),
ComponentProto,
)
}
else {
const spanOptions = pipeables[0]
return (body: any, ...pipeables: any[]) => Object.setPrototypeOf(
Object.assign(function() {}, defaultOptions, {
body: Effect.fn(spanNameOrBody, spanOptions)(body, ...pipeables as []),
displayName: displayNameFromBody(body) ?? spanNameOrBody,
}),
ComponentProto,
)
}
}
export const makeUntraced: make.Gen & make.NonGen = (
body: Function,
...pipeables: any[]
) => Object.setPrototypeOf(
Object.assign(function() {}, defaultOptions, { Object.assign(function() {}, defaultOptions, {
body: Effect.fnUntraced(body as any, ...pipeables as []), body,
displayName: displayNameFromBody(body), displayName: !String.isEmpty(body.name) ? body.name : undefined,
}), }),
ComponentProto, ComponentProto,
) )
const displayNameFromBody = (body: Function) => !String.isEmpty(body.name) ? body.name : undefined export const withFunctionComponentSignature: {
<F extends FunctionComponent>(): <T extends Component<any, any, any>>(self: T) =>
& Omit<T, keyof Component.AsComponent<T>>
& Component<F, Component.Error<T>, Component.Context<T>>
} = () => identity
export const withOptions: { export const withOptions: {
<T extends Component<any, any, any>>( <T extends Component<any, any, any>>(
@@ -367,17 +137,17 @@ export const withOptions: {
)) ))
export const withRuntime: { export const withRuntime: {
<P extends {}, E, R>( <F extends FunctionComponent, E, R>(
context: React.Context<Runtime.Runtime<R>>, context: React.Context<Runtime.Runtime<R>>,
): (self: Component<P, E, R>) => React.FC<P> ): (self: Component<F, E, Types.NoInfer<R>>) => F
<P extends {}, E, R>( <F extends FunctionComponent, E, R>(
self: Component<P, E, R>, self: Component<F, E, Types.NoInfer<R>>,
context: React.Context<Runtime.Runtime<R>>, context: React.Context<Runtime.Runtime<R>>,
): React.FC<P> ): F
} = Function.dual(2, <P extends {}, E, R>( } = Function.dual(2, <F extends FunctionComponent, E, R>(
self: Component<P, E, R>, self: Component<F, E, R>,
context: React.Context<Runtime.Runtime<R>>, context: React.Context<Runtime.Runtime<R>>,
): React.FC<P> => function WithRuntime(props) { ) => function WithRuntime(props: FunctionComponent.Props<F>) {
return React.createElement( return React.createElement(
Runtime.runSync(React.useContext(context))(self), Runtime.runSync(React.useContext(context))(self),
props, props,

View File

@@ -1,302 +0,0 @@
import { type Context, Effect, Equivalence, ExecutionStrategy, Exit, type Layer, Option, pipe, PubSub, Ref, Runtime, Scope, Stream, SubscriptionRef } from "effect"
import * as React from "react"
import { SetStateAction } from "./types/index.js"
export interface ScopeOptions {
readonly finalizerExecutionMode?: "sync" | "fork"
readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy
}
export const useScope: {
(
deps: React.DependencyList,
options?: ScopeOptions,
): Effect.Effect<Scope.Scope>
} = Effect.fn("useScope")(function*(deps, options) {
const runtime = yield* Effect.runtime()
const [isInitialRun, initialScope] = React.useMemo(() => Runtime.runSync(runtime)(Effect.all([
Ref.make(true),
Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential),
])), [])
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: () => Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential).pipe(
Effect.tap(scope => Effect.sync(() => setScope(scope))),
Effect.map(scope => () => closeScope(scope, runtime, options)),
),
})
), deps)
return scope
})
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 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.fn("useCallbackSync")(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.fn("useCallbackPromise")(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 useMemo: {
<A, E, R>(
factory: () => Effect.Effect<A, E, R>,
deps: React.DependencyList,
): Effect.Effect<A, E, R>
} = Effect.fn("useMemo")(function* <A, E, R>(
factory: () => Effect.Effect<A, E, R>,
deps: React.DependencyList,
) {
const runtime = yield* Effect.runtime()
return yield* React.useMemo(() => Runtime.runSync(runtime)(Effect.cached(factory())), deps)
})
export const useOnce: {
<A, E, R>(factory: () => Effect.Effect<A, E, R>): Effect.Effect<A, E, R>
} = Effect.fn("useOnce")(function* <A, E, R>(
factory: () => Effect.Effect<A, E, R>
) {
return yield* useMemo(factory, [])
})
export const useEffect: {
<E, R>(
effect: () => Effect.Effect<void, E, R>,
deps?: React.DependencyList,
options?: ScopeOptions,
): Effect.Effect<void, never, Exclude<R, Scope.Scope>>
} = Effect.fn("useEffect")(function* <E, R>(
effect: () => Effect.Effect<void, E, R>,
deps?: React.DependencyList,
options?: ScopeOptions,
) {
const runtime = yield* Effect.runtime<Exclude<R, Scope.Scope>>()
React.useEffect(() => Effect.Do.pipe(
Effect.bind("scope", () => Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)),
Effect.bind("exit", ({ scope }) => Effect.exit(Effect.provideService(effect(), Scope.Scope, scope))),
Effect.map(({ scope }) =>
() => closeScope(scope, runtime, options)
),
Runtime.runSync(runtime),
), deps)
})
export const useLayoutEffect: {
<E, R>(
effect: () => Effect.Effect<void, E, R>,
deps?: React.DependencyList,
options?: ScopeOptions,
): Effect.Effect<void, never, Exclude<R, Scope.Scope>>
} = Effect.fn("useLayoutEffect")(function* <E, R>(
effect: () => Effect.Effect<void, E, R>,
deps?: React.DependencyList,
options?: ScopeOptions,
) {
const runtime = yield* Effect.runtime<Exclude<R, Scope.Scope>>()
React.useLayoutEffect(() => Effect.Do.pipe(
Effect.bind("scope", () => Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)),
Effect.bind("exit", ({ scope }) => Effect.exit(Effect.provideService(effect(), Scope.Scope, scope))),
Effect.map(({ scope }) =>
() => closeScope(scope, runtime, options)
),
Runtime.runSync(runtime),
), 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.fn("useFork")(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 ?? ExecutionStrategy.sequential)
)
Runtime.runFork(runtime)(Effect.provideService(effect(), Scope.Scope, scope), { ...options, scope })
return () => closeScope(scope, runtime, {
...options,
finalizerExecutionMode: options?.finalizerExecutionMode ?? "fork",
})
}, deps)
})
export const useContext: {
<ROut, E, RIn>(
layer: Layer.Layer<ROut, E, RIn>,
options?: ScopeOptions,
): Effect.Effect<Context.Context<ROut>, E, Exclude<RIn, Scope.Scope>>
} = Effect.fn("useContext")(function* <ROut, E, RIn>(
layer: Layer.Layer<ROut, E, RIn>,
options?: ScopeOptions,
) {
const scope = yield* useScope([layer], options)
return yield* useMemo(() => Effect.provideService(
Effect.provide(Effect.context<ROut>(), layer),
Scope.Scope,
scope,
), [scope])
})
export const useRefFromReactiveValue: {
<A>(value: A): Effect.Effect<SubscriptionRef.SubscriptionRef<A>>
} = Effect.fn("useRefFromReactiveValue")(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.fn("useSubscribeRefs")(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, Equivalence.strict())),
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.fn("useRefState")(function* <A>(ref: SubscriptionRef.SubscriptionRef<A>) {
const [reactStateValue, setReactStateValue] = React.useState(yield* useOnce(() => ref))
yield* useFork(() => Stream.runForEach(
Stream.changesWith(ref.changes, Equivalence.strict()),
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.fn("useStreamFromReactiveValues")(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.fn("useSubscribeStream")(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, Equivalence.strict()),
v => Effect.sync(() => setReactStateValue(Option.some(v))),
), [stream])
return reactStateValue as Option.Some<A>
})

View File

@@ -25,22 +25,22 @@ export const isMemoized = (u: unknown): u is Memoized<unknown> => Predicate.hasP
export const memo = <T extends Component.Component<any, any, any>>( export const memo = <T extends Component.Component<any, any, any>>(
self: T self: T
): T & Memoized<Component.Component.Props<T>> => Object.setPrototypeOf( ): T & Memoized<Component.FunctionComponent.Props<Component.Component.FunctionComponent<T>>> => Object.setPrototypeOf(
Object.assign(function() {}, self, MemoizedProto), Object.assign(function() {}, self),
Object.getPrototypeOf(self), Object.freeze({ ...Object.getPrototypeOf(self), ...MemoizedProto }),
) )
export const withOptions: { export const withOptions: {
<T extends Component.Component<any, any, any> & Memoized<any>>( <T extends Component.Component<any, any, any> & Memoized<any>>(
options: Partial<Memoized.Options<Component.Component.Props<T>>> options: Partial<Memoized.Options<Component.FunctionComponent.Props<Component.Component.FunctionComponent<T>>>>
): (self: T) => T ): (self: T) => T
<T extends Component.Component<any, any, any> & Memoized<any>>( <T extends Component.Component<any, any, any> & Memoized<any>>(
self: T, self: T,
options: Partial<Memoized.Options<Component.Component.Props<T>>>, options: Partial<Memoized.Options<Component.FunctionComponent.Props<Component.Component.FunctionComponent<T>>>>,
): T ): T
} = Function.dual(2, <T extends Component.Component<any, any, any> & Memoized<any>>( } = Function.dual(2, <T extends Component.Component<any, any, any> & Memoized<any>>(
self: T, self: T,
options: Partial<Memoized.Options<Component.Component.Props<T>>>, options: Partial<Memoized.Options<Component.FunctionComponent.Props<Component.Component.FunctionComponent<T>>>>,
): T => Object.setPrototypeOf( ): T => Object.setPrototypeOf(
Object.assign(function() {}, self, options), Object.assign(function() {}, self, options),
Object.getPrototypeOf(self), Object.getPrototypeOf(self),

View File

@@ -1,7 +1,6 @@
import { Effect, Function, Predicate, Runtime, Scope } from "effect" import { Effect, Function, Predicate, Runtime, Scope } from "effect"
import * as React from "react" import * as React from "react"
import type * as Component from "./Component.js" import type * as Component from "./Component.js"
import type { ExcludeKeys } from "./utils.js"
export const TypeId: unique symbol = Symbol.for("effect-fc/Suspense") export const TypeId: unique symbol = Symbol.for("effect-fc/Suspense")
@@ -22,16 +21,17 @@ export namespace Suspense {
const SuspenseProto = Object.freeze({ const SuspenseProto = Object.freeze({
[TypeId]: TypeId, [TypeId]: TypeId,
makeFunctionComponent(
this: Component.Component<any, any, any> & Suspense, makeFunctionComponent<F extends Component.FunctionComponent, E, R>(
runtimeRef: React.RefObject<Runtime.Runtime<any>>, this: Component.Component<F, E, R> & Suspense,
runtimeRef: React.RefObject<Runtime.Runtime<Exclude<R, Scope.Scope>>>,
scope: Scope.Scope, scope: Scope.Scope,
): React.FC<any> { ) {
const SuspenseInner = (props: { readonly promise: Promise<React.ReactNode> }) => React.use(props.promise) const SuspenseInner = (props: { readonly promise: Promise<React.ReactNode> }) => React.use(props.promise)
return ({ fallback, name, ...props }: Suspense.Props) => { return ({ fallback, name, ...props }: Suspense.Props) => {
const promise = Runtime.runPromise(runtimeRef.current)( const promise = Runtime.runPromise(runtimeRef.current)(
Effect.provideService(this.body(props), Scope.Scope, scope) Effect.provideService(this.body(props as Component.FunctionComponent.Props<F>), Scope.Scope, scope)
) )
return React.createElement( return React.createElement(
@@ -46,15 +46,22 @@ const SuspenseProto = Object.freeze({
export const isSuspense = (u: unknown): u is Suspense => Predicate.hasProperty(u, TypeId) export const isSuspense = (u: unknown): u is Suspense => Predicate.hasProperty(u, TypeId)
export const suspense = <T extends Component.Component<P, any, any>, P extends {}>( export const suspense = <T extends Component.Component<any, any, any>>(
self: T & Component.Component<ExcludeKeys<P, keyof Suspense.Props>, any, any> self: T
): ( ): (
& Omit<T, keyof Component.Component<P, Component.Component.Error<T>, Component.Component.Context<T>>> & Omit<T, keyof Component.Component.AsComponent<T>>
& Component.Component<P & Suspense.Props, Component.Component.Error<T>, Component.Component.Context<T>> & Component.Component<
Component.Component.FunctionComponent<T> extends (...args: readonly [infer P, ...infer Args]) => infer A
? A extends React.ReactNode
? (...args: readonly [props: P & Suspense.Props, ...Args]) => A
: never
: never,
Component.Component.Error<T>,
Component.Component.Context<T>>
& Suspense & Suspense
) => Object.setPrototypeOf( ) => Object.setPrototypeOf(
Object.assign(function() {}, self, SuspenseProto), Object.assign(function() {}, self),
Object.getPrototypeOf(self), Object.freeze({ ...Object.getPrototypeOf(self), ...SuspenseProto }),
) )
export const withOptions: { export const withOptions: {

View File

@@ -0,0 +1,7 @@
import type { ExecutionStrategy } from "effect"
export interface ScopeOptions {
readonly finalizerExecutionMode?: "sync" | "fork"
readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy
}

View File

@@ -0,0 +1,15 @@
export * from "./ScopeOptions.js"
export * from "./useCallbackPromise.js"
export * from "./useCallbackSync.js"
export * from "./useContext.js"
export * from "./useEffect.js"
export * from "./useInput.js"
export * from "./useLayoutEffect.js"
export * from "./useMemo.js"
export * from "./useOnce.js"
export * from "./useRefFromReactiveValue.js"
export * from "./useRefState.js"
export * from "./useScope.js"
export * from "./useStreamFromReactiveValues.js"
export * from "./useSubscribeRefs.js"
export * from "./useSubscribeStream.js"

View File

@@ -0,0 +1,18 @@
import { Exit, Runtime, Scope } from "effect"
import type { ScopeOptions } from "./ScopeOptions.js"
export 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
}
}

View File

@@ -0,0 +1,18 @@
import { Effect, Runtime } from "effect"
import * as React from "react"
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 runtimeRef = React.useRef<Runtime.Runtime<R>>(null!)
runtimeRef.current = yield* Effect.runtime<R>()
return React.useCallback((...args: Args) => Runtime.runPromise(runtimeRef.current)(callback(...args)), deps)
})

View File

@@ -0,0 +1,18 @@
import { Effect, Runtime } from "effect"
import * as React from "react"
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 runtimeRef = React.useRef<Runtime.Runtime<R>>(null!)
runtimeRef.current = yield* Effect.runtime<R>()
return React.useCallback((...args: Args) => Runtime.runSync(runtimeRef.current)(callback(...args)), deps)
})

View File

@@ -0,0 +1,23 @@
import { type Context, Effect, type Layer, Scope } from "effect"
import type { ScopeOptions } from "./ScopeOptions.js"
import { useMemo } from "./useMemo.js"
import { useScope } from "./useScope.js"
export const useContext: {
<ROut, E, RIn>(
layer: Layer.Layer<ROut, E, RIn>,
options?: ScopeOptions,
): Effect.Effect<Context.Context<ROut>, E, Exclude<RIn, Scope.Scope>>
} = Effect.fnUntraced(function* <ROut, E, RIn>(
layer: Layer.Layer<ROut, E, RIn>,
options?: ScopeOptions,
) {
const scope = yield* useScope([layer], options)
return yield* useMemo(() => Effect.provideService(
Effect.provide(Effect.context<ROut>(), layer),
Scope.Scope,
scope,
), [scope])
})

View File

@@ -0,0 +1,28 @@
import { Effect, ExecutionStrategy, Runtime, Scope } from "effect"
import * as React from "react"
import type { ScopeOptions } from "./ScopeOptions.js"
import { closeScope } from "./internal.js"
export const useEffect: {
<E, R>(
effect: () => Effect.Effect<void, E, R>,
deps?: React.DependencyList,
options?: ScopeOptions,
): Effect.Effect<void, never, Exclude<R, Scope.Scope>>
} = Effect.fnUntraced(function* <E, R>(
effect: () => Effect.Effect<void, E, R>,
deps?: React.DependencyList,
options?: ScopeOptions,
) {
const runtime = yield* Effect.runtime<Exclude<R, Scope.Scope>>()
React.useEffect(() => Effect.Do.pipe(
Effect.bind("scope", () => Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)),
Effect.bind("exit", ({ scope }) => Effect.exit(Effect.provideService(effect(), Scope.Scope, scope))),
Effect.map(({ scope }) =>
() => closeScope(scope, runtime, options)
),
Runtime.runSync(runtime),
), deps)
})

View File

@@ -0,0 +1,31 @@
import { Effect, ExecutionStrategy, Runtime, Scope } from "effect"
import * as React from "react"
import { closeScope } from "./internal.js"
import type { ScopeOptions } from "./ScopeOptions.js"
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 ?? ExecutionStrategy.sequential)
)
Runtime.runFork(runtime)(Effect.provideService(effect(), Scope.Scope, scope), { ...options, scope })
return () => closeScope(scope, runtime, {
...options,
finalizerExecutionMode: options?.finalizerExecutionMode ?? "fork",
})
}, deps)
})

View File

@@ -0,0 +1,59 @@
import { type Duration, Effect, flow, identity, Option, type ParseResult, Ref, Schema, Stream, SubscriptionRef, type Types } from "effect"
import * as React from "react"
import { useCallbackSync } from "./useCallbackSync.js"
import { useFork } from "./useFork.js"
import { useOnce } from "./useOnce.js"
import { useSubscribeRefs } from "./useSubscribeRefs.js"
export namespace useInput {
export interface Options<A, R> {
readonly ref: SubscriptionRef.SubscriptionRef<A>
readonly schema: Schema.Schema<Types.NoInfer<A>, string, R>
readonly debounce?: Duration.Duration
}
export interface Result {
readonly value: string
readonly onChange: React.ChangeEventHandler<HTMLInputElement | HTMLTextAreaElement>
readonly error: Option.Option<ParseResult.ParseError>
}
}
export const useInput: {
<A, R>(options: useInput.Options<A, R>): Effect.Effect<useInput.Result, ParseResult.ParseError, R>
} = Effect.fnUntraced(function* <A, R>(options: useInput.Options<A, R>) {
const internalRef = yield* useOnce(() => options.ref.pipe(
Effect.andThen(Schema.encode(options.schema)),
Effect.andThen(SubscriptionRef.make),
))
const [error, setError] = React.useState(Option.none<ParseResult.ParseError>())
yield* useFork(() => Effect.all([
Stream.runForEach(options.ref.changes, upstreamValue =>
Effect.andThen(internalRef, internalValue =>
upstreamValue !== internalValue
? Effect.andThen(Schema.encode(options.schema)(upstreamValue), v => Ref.set(internalRef, v))
: Effect.void
)
),
Stream.runForEach(
internalRef.changes.pipe(options.debounce ? Stream.debounce(options.debounce) : identity),
flow(
Schema.decode(options.schema),
Effect.andThen(v => Ref.set(options.ref, v)),
Effect.andThen(() => setError(Option.none())),
Effect.catchTag("ParseError", e => Effect.sync(() => setError(Option.some(e)))),
),
),
], { concurrency: "unbounded" }), [options.ref, options.schema, options.debounce, internalRef])
const [value] = yield* useSubscribeRefs(internalRef)
const onChange = yield* useCallbackSync((e: React.ChangeEvent<HTMLInputElement>) => Ref.set(
internalRef,
e.target.value,
), [internalRef])
return { value, onChange, error }
})

View File

@@ -0,0 +1,28 @@
import { Effect, ExecutionStrategy, Runtime, Scope } from "effect"
import * as React from "react"
import type { ScopeOptions } from "./ScopeOptions.js"
import { closeScope } from "./internal.js"
export const useLayoutEffect: {
<E, R>(
effect: () => Effect.Effect<void, E, R>,
deps?: React.DependencyList,
options?: ScopeOptions,
): Effect.Effect<void, never, Exclude<R, Scope.Scope>>
} = Effect.fnUntraced(function* <E, R>(
effect: () => Effect.Effect<void, E, R>,
deps?: React.DependencyList,
options?: ScopeOptions,
) {
const runtime = yield* Effect.runtime<Exclude<R, Scope.Scope>>()
React.useLayoutEffect(() => Effect.Do.pipe(
Effect.bind("scope", () => Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)),
Effect.bind("exit", ({ scope }) => Effect.exit(Effect.provideService(effect(), Scope.Scope, scope))),
Effect.map(({ scope }) =>
() => closeScope(scope, runtime, options)
),
Runtime.runSync(runtime),
), deps)
})

View File

@@ -0,0 +1,16 @@
import { Effect, Runtime } from "effect"
import * as React from "react"
export const useMemo: {
<A, E, R>(
factory: () => Effect.Effect<A, E, R>,
deps: React.DependencyList,
): Effect.Effect<A, E, R>
} = Effect.fnUntraced(function* <A, E, R>(
factory: () => Effect.Effect<A, E, R>,
deps: React.DependencyList,
) {
const runtime = yield* Effect.runtime()
return yield* React.useMemo(() => Runtime.runSync(runtime)(Effect.cached(factory())), deps)
})

View File

@@ -0,0 +1,11 @@
import { Effect } from "effect"
import { useMemo } from "./useMemo.js"
export const useOnce: {
<A, E, R>(factory: () => Effect.Effect<A, E, R>): Effect.Effect<A, E, R>
} = Effect.fnUntraced(function* <A, E, R>(
factory: () => Effect.Effect<A, E, R>
) {
return yield* useMemo(factory, [])
})

View File

@@ -0,0 +1,12 @@
import { Effect, Ref, SubscriptionRef } from "effect"
import { useEffect } from "./useEffect.js"
import { useOnce } from "./useOnce.js"
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
})

View File

@@ -0,0 +1,28 @@
import { Effect, Equivalence, Ref, Stream, SubscriptionRef } from "effect"
import * as React from "react"
import { SetStateAction } from "../../types/index.js"
import { useCallbackSync } from "./useCallbackSync.js"
import { useFork } from "./useFork.js"
import { useOnce } from "./useOnce.js"
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, Equivalence.strict()),
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]
})

View File

@@ -0,0 +1,36 @@
import { Effect, ExecutionStrategy, Ref, Runtime, Scope } from "effect"
import * as React from "react"
import type { ScopeOptions } from "./ScopeOptions.js"
import { closeScope } from "./internal.js"
export const useScope: {
(
deps: React.DependencyList,
options?: ScopeOptions,
): Effect.Effect<Scope.Scope>
} = Effect.fnUntraced(function*(deps, options) {
const runtime = yield* Effect.runtime()
const [isInitialRun, initialScope] = React.useMemo(() => Runtime.runSync(runtime)(Effect.all([
Ref.make(true),
Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential),
])), [])
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: () => Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential).pipe(
Effect.tap(scope => Effect.sync(() => setScope(scope))),
Effect.map(scope => () => closeScope(scope, runtime, options)),
),
})
), deps)
return scope
})

View File

@@ -0,0 +1,30 @@
import { Effect, PubSub, Ref, Scope, Stream } from "effect"
import * as React from "react"
import { useEffect } from "./useEffect.js"
import { useOnce } from "./useOnce.js"
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
})

View File

@@ -0,0 +1,27 @@
import { Effect, Equivalence, pipe, Stream, SubscriptionRef } from "effect"
import * as React from "react"
import { useFork } from "./useFork.js"
import { useOnce } from "./useOnce.js"
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, Equivalence.strict())),
streams => Stream.zipLatestAll(...streams),
Stream.runForEach(v =>
Effect.sync(() => setReactStateValue(v))
),
), refs)
return reactStateValue as any
})

View File

@@ -0,0 +1,31 @@
import { Effect, Equivalence, Option, Stream } from "effect"
import * as React from "react"
import { useFork } from "./useFork.js"
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, Equivalence.strict()),
v => Effect.sync(() => setReactStateValue(Option.some(v))),
), [stream])
return reactStateValue as Option.Some<A>
})

View File

@@ -0,0 +1,2 @@
export * from "./Hooks/index.js"
export * as Hooks from "./Hooks/index.js"

View File

@@ -1,5 +1,4 @@
export * as Component from "./Component.js" export * as Component from "./Component.js"
export * as Hook from "./Hook.js"
export * as Memoized from "./Memoized.js" export * as Memoized from "./Memoized.js"
export * as ReactManagedRuntime from "./ReactManagedRuntime.js" export * as ReactManagedRuntime from "./ReactManagedRuntime.js"
export * as Suspense from "./Suspense.js" export * as Suspense from "./Suspense.js"

View File

@@ -5,7 +5,8 @@ import * as PropertyPath from "./PropertyPath.js"
export const SubscriptionSubRefTypeId: unique symbol = Symbol.for("effect-fc/types/SubscriptionSubRef") export const SubscriptionSubRefTypeId: unique symbol = Symbol.for("effect-fc/types/SubscriptionSubRef")
export type SubscriptionSubRefTypeId = typeof SubscriptionSubRefTypeId export type SubscriptionSubRefTypeId = typeof SubscriptionSubRefTypeId
export interface SubscriptionSubRef<in out A, in out B> extends SubscriptionSubRef.Variance<A, B>, SubscriptionRef.SubscriptionRef<A> { export interface SubscriptionSubRef<in out A, in out B extends SubscriptionRef.SubscriptionRef<any>>
extends SubscriptionSubRef.Variance<A, B>, SubscriptionRef.SubscriptionRef<A> {
readonly parent: SubscriptionRef.SubscriptionRef<B> readonly parent: SubscriptionRef.SubscriptionRef<B>
readonly [Unify.typeSymbol]?: unknown readonly [Unify.typeSymbol]?: unknown
@@ -36,7 +37,8 @@ const synchronizedRefVariance = { _A: (_: any) => _ }
const subscriptionRefVariance = { _A: (_: any) => _ } const subscriptionRefVariance = { _A: (_: any) => _ }
const subscriptionSubRefVariance = { _A: (_: any) => _, _B: (_: any) => _ } const subscriptionSubRefVariance = { _A: (_: any) => _, _B: (_: any) => _ }
class SubscriptionSubRefImpl<in out A, in out B> extends Effectable.Class<A> implements SubscriptionSubRef<A, B> { class SubscriptionSubRefImpl<in out A, in out B extends SubscriptionRef.SubscriptionRef<any>>
extends Effectable.Class<A> implements SubscriptionSubRef<A, B> {
readonly [Readable.TypeId]: Readable.TypeId = Readable.TypeId readonly [Readable.TypeId]: Readable.TypeId = Readable.TypeId
readonly [Subscribable.TypeId]: Subscribable.TypeId = Subscribable.TypeId readonly [Subscribable.TypeId]: Subscribable.TypeId = Subscribable.TypeId
readonly [Ref.RefTypeId] = refVariance readonly [Ref.RefTypeId] = refVariance
@@ -47,9 +49,9 @@ class SubscriptionSubRefImpl<in out A, in out B> extends Effectable.Class<A> imp
readonly get: Effect.Effect<A> readonly get: Effect.Effect<A>
constructor( constructor(
readonly parent: SubscriptionRef.SubscriptionRef<B>, readonly parent: B,
readonly getter: (parentValue: B) => A, readonly getter: (parentValue: Effect.Effect.Success<B>) => A,
readonly setter: (parentValue: B, value: A) => B, readonly setter: (parentValue: Effect.Effect.Success<B>, value: A) => Effect.Effect.Success<B>,
) { ) {
super() super()
this.get = Effect.map(this.parent, this.getter) this.get = Effect.map(this.parent, this.getter)
@@ -60,12 +62,11 @@ class SubscriptionSubRefImpl<in out A, in out B> extends Effectable.Class<A> imp
} }
get changes(): Stream.Stream<A> { get changes(): Stream.Stream<A> {
return this.get.pipe( return Stream.unwrap(
Effect.map(a => this.parent.changes.pipe( Effect.map(this.get, a => Stream.concat(
Stream.map(this.getter), Stream.make(a),
s => Stream.concat(Stream.make(a), s), Stream.map(this.parent.changes, this.getter),
)), ))
Stream.unwrap,
) )
} }
@@ -75,7 +76,7 @@ class SubscriptionSubRefImpl<in out A, in out B> extends Effectable.Class<A> imp
modifyEffect<C, E, R>(f: (a: A) => Effect.Effect<readonly [C, A], E, R>): Effect.Effect<C, E, R> { modifyEffect<C, E, R>(f: (a: A) => Effect.Effect<readonly [C, A], E, R>): Effect.Effect<C, E, R> {
return Effect.Do.pipe( return Effect.Do.pipe(
Effect.bind("b", () => Ref.get(this.parent)), Effect.bind("b", (): Effect.Effect<Effect.Effect.Success<B>> => this.parent),
Effect.bind("ca", ({ b }) => f(this.getter(b))), Effect.bind("ca", ({ b }) => f(this.getter(b))),
Effect.tap(({ b, ca: [, a] }) => Ref.set(this.parent, this.setter(b, a))), Effect.tap(({ b, ca: [, a] }) => Ref.set(this.parent, this.setter(b, a))),
Effect.map(({ ca: [c] }) => c), Effect.map(({ ca: [c] }) => c),
@@ -84,28 +85,46 @@ class SubscriptionSubRefImpl<in out A, in out B> extends Effectable.Class<A> imp
} }
export const makeFromGetSet = <A, B>( export const makeFromGetSet = <A, B extends SubscriptionRef.SubscriptionRef<any>>(
parent: SubscriptionRef.SubscriptionRef<B>, parent: B,
options: { options: {
readonly get: (parentValue: B) => A readonly get: (parentValue: Effect.Effect.Success<B>) => A
readonly set: (parentValue: B, value: A) => B readonly set: (parentValue: Effect.Effect.Success<B>, value: A) => Effect.Effect.Success<B>
}, },
): SubscriptionSubRef<A, B> => new SubscriptionSubRefImpl(parent, options.get, options.set) ): SubscriptionSubRef<A, B> => new SubscriptionSubRefImpl(parent, options.get, options.set)
export const makeFromPath = <B, const P extends PropertyPath.Paths<B>>( export const makeFromPath = <
parent: SubscriptionRef.SubscriptionRef<B>, B extends SubscriptionRef.SubscriptionRef<any>,
const P extends PropertyPath.Paths<Effect.Effect.Success<B>>,
>(
parent: B,
path: P, path: P,
): SubscriptionSubRef<PropertyPath.ValueFromPath<B, P>, B> => new SubscriptionSubRefImpl( ): SubscriptionSubRef<PropertyPath.ValueFromPath<Effect.Effect.Success<B>, P>, B> => new SubscriptionSubRefImpl(
parent, parent,
parentValue => Option.getOrThrow(PropertyPath.get(parentValue, path)), parentValue => Option.getOrThrow(PropertyPath.get(parentValue, path)),
(parentValue, value) => Option.getOrThrow(PropertyPath.immutableSet(parentValue, path, value)), (parentValue, value) => Option.getOrThrow(PropertyPath.immutableSet(parentValue, path, value)),
) )
export const makeFromChunkRef = <A>( export const makeFromChunkRef: {
parent: SubscriptionRef.SubscriptionRef<Chunk.Chunk<A>>, <B extends SubscriptionRef.SubscriptionRef<Chunk.NonEmptyChunk<any>>>(
parent: B,
index: number,
): SubscriptionSubRef<
Effect.Effect.Success<B> extends Chunk.NonEmptyChunk<infer A> ? A : never,
B
>
<B extends SubscriptionRef.SubscriptionRef<Chunk.Chunk<any>>>(
parent: B,
index: number,
): SubscriptionSubRef<
Effect.Effect.Success<B> extends Chunk.Chunk<infer A> ? A : never,
B
>
} = (
parent: SubscriptionRef.SubscriptionRef<Chunk.Chunk<any>>,
index: number, index: number,
): SubscriptionSubRef<A, Chunk.Chunk<A>> => new SubscriptionSubRefImpl( ) => new SubscriptionSubRefImpl(
parent, parent,
parentValue => Chunk.unsafeGet(parentValue, index), parentValue => Chunk.unsafeGet(parentValue, index),
(parentValue, value) => Chunk.replace(parentValue, index, value), (parentValue, value) => Chunk.replace(parentValue, index, value),
) ) as any

View File

@@ -0,0 +1,22 @@
import { TextField } from "@radix-ui/themes"
import { Effect, Schema } from "effect"
import { Component } from "effect-fc"
import { useInput } from "effect-fc/hooks"
export namespace TextInput {
export interface Props<A, R> extends Omit<useInput.Options<A, R>, "schema">, TextField.RootProps {}
}
export const TextInput = <A, R>(schema: Schema.Schema<A, string, R>) => Component.make(
Effect.fnUntraced(function*(props: TextInput.Props<A, R>) {
const input = yield* useInput({ ...props, schema })
return (
<TextField.Root
{...props}
>
</TextField.Root>
)
})
)

View File

@@ -2,13 +2,14 @@ import { runtime } from "@/runtime"
import { Flex, Text, TextField } from "@radix-ui/themes" import { Flex, Text, TextField } from "@radix-ui/themes"
import { createFileRoute } from "@tanstack/react-router" import { createFileRoute } from "@tanstack/react-router"
import { GetRandomValues, makeUuid4 } from "@typed/id" import { GetRandomValues, makeUuid4 } from "@typed/id"
import { Effect } from "effect" import { Effect, Types } from "effect"
import { Component, Hook, Memoized, Suspense } from "effect-fc" import { Component, Memoized, Suspense } from "effect-fc"
import { Hooks } from "effect-fc/hooks"
import * as React from "react" import * as React from "react"
// Generator version // Generator version
const RouteComponent = Component.make(function* AsyncRendering() { const RouteComponent = Component.make(Effect.fnUntraced(function* AsyncRendering() {
const MemoizedAsyncComponentFC = yield* MemoizedAsyncComponent const MemoizedAsyncComponentFC = yield* MemoizedAsyncComponent
const AsyncComponentFC = yield* AsyncComponent const AsyncComponentFC = yield* AsyncComponent
const [input, setInput] = React.useState("") const [input, setInput] = React.useState("")
@@ -24,7 +25,7 @@ const RouteComponent = Component.make(function* AsyncRendering() {
<AsyncComponentFC /> <AsyncComponentFC />
</Flex> </Flex>
) )
}).pipe( })).pipe(
Component.withRuntime(runtime.context) Component.withRuntime(runtime.context)
) )
@@ -50,7 +51,7 @@ const RouteComponent = Component.make(function* AsyncRendering() {
// ) // )
class AsyncComponent extends Component.make(function* AsyncComponent() { class AsyncComponent extends Component.make(Effect.fnUntraced(function* AsyncComponent() {
const SubComponentFC = yield* SubComponent const SubComponentFC = yield* SubComponent
yield* Effect.sleep("500 millis") // Async operation yield* Effect.sleep("500 millis") // Async operation
@@ -62,16 +63,18 @@ class AsyncComponent extends Component.make(function* AsyncComponent() {
<SubComponentFC /> <SubComponentFC />
</Flex> </Flex>
) )
}).pipe( })).pipe(
Suspense.suspense, // Suspense.suspense,
Suspense.withOptions({ defaultFallback: <p>Loading...</p> }), // Suspense.withOptions({ defaultFallback: <p>Loading...</p> }),
) {} ) {}
const AsyncComponent2 = Suspense.withOptions(Suspense.suspense(AsyncComponent), {})
type T = Types.Simplify<typeof AsyncComponent2>
class MemoizedAsyncComponent extends Memoized.memo(AsyncComponent) {} class MemoizedAsyncComponent extends Memoized.memo(AsyncComponent) {}
class SubComponent extends Component.make(function* SubComponent() { class SubComponent extends Component.make(Effect.fnUntraced(function* SubComponent() {
const [state] = React.useState(yield* Hook.useOnce(() => Effect.provide(makeUuid4, GetRandomValues.CryptoRandom))) const [state] = React.useState(yield* Hooks.useOnce(() => Effect.provide(makeUuid4, GetRandomValues.CryptoRandom)))
return <Text>{state}</Text> return <Text>{state}</Text>
}) {} })) {}
export const Route = createFileRoute("/dev/async-rendering")({ export const Route = createFileRoute("/dev/async-rendering")({
component: RouteComponent component: RouteComponent

View File

@@ -7,7 +7,7 @@ import { Component, Memoized } from "effect-fc"
import * as React from "react" import * as React from "react"
const RouteComponent = Component.make(function* RouteComponent() { const RouteComponent = Component.make(Effect.fnUntraced(function* RouteComponent() {
const [value, setValue] = React.useState("") const [value, setValue] = React.useState("")
return ( return (
@@ -21,14 +21,14 @@ const RouteComponent = Component.make(function* RouteComponent() {
{yield* Effect.map(MemoizedSubComponent, FC => <FC />)} {yield* Effect.map(MemoizedSubComponent, FC => <FC />)}
</Flex> </Flex>
) )
}).pipe( })).pipe(
Component.withRuntime(runtime.context) Component.withRuntime(runtime.context)
) )
class SubComponent extends Component.make(function* SubComponent() { class SubComponent extends Component.make(Effect.fnUntraced(function* SubComponent() {
const id = yield* makeUuid4.pipe(Effect.provide(GetRandomValues.CryptoRandom)) const id = yield* makeUuid4.pipe(Effect.provide(GetRandomValues.CryptoRandom))
return <Text>{id}</Text> return <Text>{id}</Text>
}) {} })) {}
class MemoizedSubComponent extends Memoized.memo(SubComponent) {} class MemoizedSubComponent extends Memoized.memo(SubComponent) {}

View File

@@ -3,18 +3,19 @@ import { Todos } from "@/todo/Todos"
import { TodosState } from "@/todo/TodosState.service" import { TodosState } from "@/todo/TodosState.service"
import { createFileRoute } from "@tanstack/react-router" import { createFileRoute } from "@tanstack/react-router"
import { Effect } from "effect" import { Effect } from "effect"
import { Component, Hook } from "effect-fc" import { Component } from "effect-fc"
import { Hooks } from "effect-fc/hooks"
const TodosStateLive = TodosState.Default("todos") const TodosStateLive = TodosState.Default("todos")
export const Route = createFileRoute("/")({ export const Route = createFileRoute("/")({
component: Component.make(function* Index() { component: Component.make(Effect.fnUntraced(function* Index() {
return yield* Todos.pipe( return yield* Todos.pipe(
Effect.map(FC => <FC />), Effect.map(FC => <FC />),
Effect.provide(yield* Hook.useContext(TodosStateLive, { finalizerExecutionMode: "fork" })), Effect.provide(yield* Hooks.useContext(TodosStateLive, { finalizerExecutionMode: "fork" })),
) )
}).pipe( })).pipe(
Component.withRuntime(runtime.context) Component.withRuntime(runtime.context)
) )
}) })

View File

@@ -1,8 +1,9 @@
import * as Domain from "@/domain" import * as Domain from "@/domain"
import { Box, Button, Flex, IconButton, TextArea } from "@radix-ui/themes" import { Box, Button, Callout, Flex, IconButton, Text, TextArea } from "@radix-ui/themes"
import { GetRandomValues, makeUuid4 } from "@typed/id" import { GetRandomValues, makeUuid4 } from "@typed/id"
import { Chunk, Effect, Match, Option, Ref, Runtime, SubscriptionRef } from "effect" import { Chunk, Effect, Match, Option, ParseResult, Ref, Runtime, Schema, SubscriptionRef } from "effect"
import { Component, Hook, Memoized } from "effect-fc" import { Component, Memoized } from "effect-fc"
import { Hooks } from "effect-fc/hooks"
import { SubscriptionSubRef } from "effect-fc/types" import { SubscriptionSubRef } from "effect-fc/types"
import { FaArrowDown, FaArrowUp } from "react-icons/fa" import { FaArrowDown, FaArrowUp } from "react-icons/fa"
import { FaDeleteLeft } from "react-icons/fa6" import { FaDeleteLeft } from "react-icons/fa6"
@@ -24,11 +25,11 @@ export type TodoProps = (
| { readonly _tag: "edit", readonly index: number } | { readonly _tag: "edit", readonly index: number }
) )
export class Todo extends Component.make(function* Todo(props: TodoProps) { export class Todo extends Component.make(Effect.fnUntraced(function* Todo(props: TodoProps) {
const runtime = yield* Effect.runtime() const runtime = yield* Effect.runtime()
const state = yield* TodosState const state = yield* TodosState
const [ref, contentRef] = yield* Hook.useMemo(() => Match.value(props).pipe( const [ref, contentRef] = yield* Hooks.useMemo(() => Match.value(props).pipe(
Match.tag("new", () => Effect.andThen(makeTodo, SubscriptionRef.make)), Match.tag("new", () => Effect.andThen(makeTodo, SubscriptionRef.make)),
Match.tag("edit", ({ index }) => Effect.succeed(SubscriptionSubRef.makeFromChunkRef(state.ref, index))), Match.tag("edit", ({ index }) => Effect.succeed(SubscriptionSubRef.makeFromChunkRef(state.ref, index))),
Match.exhaustive, Match.exhaustive,
@@ -39,15 +40,26 @@ export class Todo extends Component.make(function* Todo(props: TodoProps) {
] as const), ] as const),
), [props._tag, props.index]) ), [props._tag, props.index])
const [content, size] = yield* Hook.useSubscribeRefs(contentRef, state.sizeRef) const [size] = yield* Hooks.useSubscribeRefs(state.sizeRef)
const contentInput = yield* Hooks.useInput({ ref: contentRef, schema: Schema.Any })
return ( return (
<Flex direction="column" align="stretch" gap="2"> <Flex direction="column" align="stretch" gap="2">
{Option.isSome(contentInput.error) &&
<Callout.Root color="red">
<Callout.Text>
{ParseResult.ArrayFormatter.formatErrorSync(contentInput.error.value).map(e => <>
<Text>&bull; {e.message}</Text><br />
</>)}
</Callout.Text>
</Callout.Root>
}
<Flex direction="row" align="center" gap="2"> <Flex direction="row" align="center" gap="2">
<Box flexGrow="1"> <Box flexGrow="1">
<TextArea <TextArea
value={content} value={contentInput.value}
onChange={e => Runtime.runSync(runtime)(Ref.set(contentRef, e.target.value))} onChange={contentInput.onChange}
/> />
</Box> </Box>
@@ -110,6 +122,6 @@ export class Todo extends Component.make(function* Todo(props: TodoProps) {
} }
</Flex> </Flex>
) )
}).pipe( })).pipe(
Memoized.memo Memoized.memo
) {} ) {}

View File

@@ -1,15 +1,16 @@
import { Container, Flex, Heading } from "@radix-ui/themes" import { Container, Flex, Heading } from "@radix-ui/themes"
import { Chunk, Console, Effect } from "effect" import { Chunk, Console, Effect } from "effect"
import { Component, Hook } from "effect-fc" import { Component } from "effect-fc"
import { Hooks } from "effect-fc/hooks"
import { Todo } from "./Todo" import { Todo } from "./Todo"
import { TodosState } from "./TodosState.service" import { TodosState } from "./TodosState.service"
export class Todos extends Component.make(function* Todos() { export class Todos extends Component.make(Effect.fnUntraced(function* Todos() {
const state = yield* TodosState const state = yield* TodosState
const [todos] = yield* Hook.useSubscribeRefs(state.ref) const [todos] = yield* Hooks.useSubscribeRefs(state.ref)
yield* Hook.useOnce(() => Effect.andThen( yield* Hooks.useOnce(() => Effect.andThen(
Console.log("Todos mounted"), Console.log("Todos mounted"),
Effect.addFinalizer(() => Console.log("Todos unmounted")), Effect.addFinalizer(() => Console.log("Todos unmounted")),
)) ))
@@ -29,4 +30,4 @@ export class Todos extends Component.make(function* Todos() {
</Flex> </Flex>
</Container> </Container>
) )
}) {} })) {}