Compare commits

...

8 Commits

Author SHA1 Message Date
9c0892078a Update dependency @effect/language-service to ^0.48.0
All checks were successful
Lint / lint (push) Successful in 12s
Test build / test-build (pull_request) Successful in 18s
2025-10-23 12:01:28 +00:00
Julien Valverdé
874da0b963 Refactor component
All checks were successful
Lint / lint (push) Successful in 12s
2025-10-23 12:11:35 +02:00
Julien Valverdé
bb0579408d Fix
All checks were successful
Lint / lint (push) Successful in 12s
2025-10-23 10:49:00 +02:00
Julien Valverdé
b39c5946f9 Fix
All checks were successful
Lint / lint (push) Successful in 12s
2025-10-23 10:42:27 +02:00
Julien Valverdé
aaf494e27a Refactor component creation
All checks were successful
Lint / lint (push) Successful in 12s
2025-10-23 10:36:33 +02:00
Julien Valverdé
dbc5694b6d Fix
All checks were successful
Lint / lint (push) Successful in 13s
2025-10-23 09:48:37 +02:00
Julien Valverdé
86582de0c5 Fix
All checks were successful
Lint / lint (push) Successful in 12s
2025-10-23 02:30:32 +02:00
Julien Valverdé
72495bb9b5 Refactor useScope
All checks were successful
Lint / lint (push) Successful in 41s
2025-10-23 02:23:48 +02:00
10 changed files with 98 additions and 59 deletions

View File

@@ -5,7 +5,7 @@
"name": "@effect-fc/monorepo", "name": "@effect-fc/monorepo",
"devDependencies": { "devDependencies": {
"@biomejs/biome": "^2.2.5", "@biomejs/biome": "^2.2.5",
"@effect/language-service": "^0.46.0", "@effect/language-service": "^0.48.0",
"@types/bun": "^1.2.23", "@types/bun": "^1.2.23",
"npm-check-updates": "^19.0.0", "npm-check-updates": "^19.0.0",
"npm-sort": "^0.0.4", "npm-sort": "^0.0.4",
@@ -135,7 +135,7 @@
"@effect-fc/example": ["@effect-fc/example@workspace:packages/example"], "@effect-fc/example": ["@effect-fc/example@workspace:packages/example"],
"@effect/language-service": ["@effect/language-service@0.46.0", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-eWMuy/RNvDMdhi8NJ/pfHS1UHd5R7adXlO4ClRYMgF6cUqN6FdXw1HgJHF7gJILVPD0Mdo/XQYNJ5gZbsdaImg=="], "@effect/language-service": ["@effect/language-service@0.48.0", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-u7DTPoGFFeDGSdomjY5C2nCGNWSisxpYSqHp3dlSG8kCZh5cay+166bveHRYvuJSJS5yomdkPTJwjwrqMmT7Og=="],
"@effect/platform": ["@effect/platform@0.92.1", "", { "dependencies": { "find-my-way-ts": "^0.1.6", "msgpackr": "^1.11.4", "multipasta": "^0.2.7" }, "peerDependencies": { "effect": "^3.18.1" } }, "sha512-XXWCBVwyhaKZISN7aM1fv/3fWDGyxr84ObywnUrL8aHvJLoIeskWFAP/fqw3c5MFCrJ3ZV97RWLbv6JiBQugdg=="], "@effect/platform": ["@effect/platform@0.92.1", "", { "dependencies": { "find-my-way-ts": "^0.1.6", "msgpackr": "^1.11.4", "multipasta": "^0.2.7" }, "peerDependencies": { "effect": "^3.18.1" } }, "sha512-XXWCBVwyhaKZISN7aM1fv/3fWDGyxr84ObywnUrL8aHvJLoIeskWFAP/fqw3c5MFCrJ3ZV97RWLbv6JiBQugdg=="],

View File

@@ -16,7 +16,7 @@
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "^2.2.5", "@biomejs/biome": "^2.2.5",
"@effect/language-service": "^0.46.0", "@effect/language-service": "^0.48.0",
"@types/bun": "^1.2.23", "@types/bun": "^1.2.23",
"npm-check-updates": "^19.0.0", "npm-check-updates": "^19.0.0",
"npm-sort": "^0.0.4", "npm-sort": "^0.0.4",

View File

@@ -1,10 +1,10 @@
/** biome-ignore-all lint/complexity/useArrowFunction: necessary for class prototypes */ /** biome-ignore-all lint/complexity/useArrowFunction: necessary for class prototypes */
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 * as Component from "./Component.js"
export const TypeId: unique symbol = Symbol.for("effect-fc/Async") export const TypeId: unique symbol = Symbol.for("effect-fc/Async/Async")
export type TypeId = typeof TypeId export type TypeId = typeof TypeId
export interface Async extends Async.Options { export interface Async extends Async.Options {
@@ -26,13 +26,15 @@ const SuspenseProto = Object.freeze({
makeFunctionComponent<P extends {}, A extends React.ReactNode, E, R>( makeFunctionComponent<P extends {}, A extends React.ReactNode, E, R>(
this: Component.Component<P, A, E, R> & Async, this: Component.Component<P, A, E, R> & Async,
runtimeRef: React.RefObject<Runtime.Runtime<Exclude<R, Scope.Scope>>>, runtimeRef: React.RefObject<Runtime.Runtime<Exclude<R, Scope.Scope>>>,
scope: Scope.Scope,
) { ) {
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 }: Async.Props) => { return ({ fallback, name, ...props }: Async.Props) => {
const promise = Runtime.runPromise(runtimeRef.current)( const promise = Runtime.runPromise(runtimeRef.current)(
Effect.provideService(this.body(props as P), Scope.Scope, scope) Effect.andThen(
Component.useScope([], this),
scope => Effect.provideService(this.body(props as P), Scope.Scope, scope),
)
) )
return React.createElement( return React.createElement(

View File

@@ -1,11 +1,11 @@
/** biome-ignore-all lint/complexity/noBannedTypes: {} is the default type for React props */ /** biome-ignore-all lint/complexity/noBannedTypes: {} is the default type for React props */
/** biome-ignore-all lint/complexity/useArrowFunction: necessary for class prototypes */ /** biome-ignore-all lint/complexity/useArrowFunction: necessary for class prototypes */
import { Context, Effect, Effectable, ExecutionStrategy, Exit, Function, Layer, ManagedRuntime, Predicate, Ref, Runtime, Scope, Tracer, type Types, type Utils } from "effect" import { Context, Effect, Effectable, Equivalence, ExecutionStrategy, Exit, Fiber, Function, HashMap, Layer, ManagedRuntime, Option, Predicate, Ref, Runtime, Scope, Tracer, type Types, type Utils } from "effect"
import * as React from "react" import * as React from "react"
import { Memoized } from "./index.js" import { Memoized } from "./index.js"
export const TypeId: unique symbol = Symbol.for("effect-fc/Component") export const TypeId: unique symbol = Symbol.for("effect-fc/Component/Component")
export type TypeId = typeof TypeId export type TypeId = typeof TypeId
export interface Component<P extends {}, A extends React.ReactNode, E, R> export interface Component<P extends {}, A extends React.ReactNode, E, R>
@@ -25,8 +25,7 @@ extends
/** @internal */ /** @internal */
makeFunctionComponent( makeFunctionComponent(
runtimeRef: React.Ref<Runtime.Runtime<Exclude<R, Scope.Scope>>>, runtimeRef: React.Ref<Runtime.Runtime<Exclude<R, Scope.Scope>>>
scope: Scope.Scope,
): (props: P) => A ): (props: P) => A
} }
@@ -58,38 +57,33 @@ const ComponentProto = Object.freeze({
commit: Effect.fnUntraced(function* <P extends {}, A extends React.ReactNode, E, R>( commit: Effect.fnUntraced(function* <P extends {}, A extends React.ReactNode, E, R>(
this: Component<P, A, E, R> this: Component<P, A, E, R>
) { ) {
const self = this
// biome-ignore lint/style/noNonNullAssertion: React ref initialization // biome-ignore lint/style/noNonNullAssertion: React ref initialization
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.useRef(function ScopeProvider(props: P) { return yield* React.useState(() => Runtime.runSync(runtimeRef.current)(Effect.cachedFunction(
const scope = Runtime.runSync(runtimeRef.current)(useScope( (_services: readonly any[]) => Effect.sync(() => {
Array.from( const f: React.FC<P> = this.makeFunctionComponent(runtimeRef)
Context.omit(...nonReactiveTags)(runtimeRef.current.context).unsafeMap.values() f.displayName = this.displayName ?? "Anonymous"
), return Memoized.isMemoized(this)
self, ? React.memo(f, this.propsAreEqual)
))
const FC = React.useMemo(() => {
const f: React.FC<P> = self.makeFunctionComponent(runtimeRef, scope)
f.displayName = self.displayName ?? "Anonymous"
return Memoized.isMemoized(self)
? React.memo(f, self.propsAreEqual)
: f : f
}, [scope]) }),
Equivalence.array(Equivalence.strict()),
return React.createElement(FC, props) )))[0](Array.from(
}).current Context.omit(...nonReactiveTags)(runtimeRef.current.context).unsafeMap.values()
))
}), }),
makeFunctionComponent<P extends {}, A extends React.ReactNode, E, R>( makeFunctionComponent<P extends {}, A extends React.ReactNode, E, R>(
this: Component<P, A, E, R>, this: Component<P, A, E, R>,
runtimeRef: React.RefObject<Runtime.Runtime<Exclude<R, Scope.Scope>>>, runtimeRef: React.RefObject<Runtime.Runtime<Exclude<R, Scope.Scope>>>,
scope: Scope.Scope,
) { ) {
return (props: P) => Runtime.runSync(runtimeRef.current)( return (props: P) => Runtime.runSync(runtimeRef.current)(
Effect.provideService(this.body(props), Scope.Scope, scope) Effect.andThen(
useScope([], this),
scope => Effect.provideService(this.body(props), Scope.Scope, scope),
)
) )
}, },
} as const) } as const)
@@ -413,35 +407,69 @@ export const withRuntime: {
}) })
export class ScopeMap extends Effect.Service<ScopeMap>()("effect-fc/Component/ScopeMap", {
effect: Effect.bind(Effect.Do, "ref", () => Ref.make(HashMap.empty<string, ScopeMap.Entry>()))
}) {}
export namespace ScopeMap {
export interface Entry {
readonly scope: Scope.CloseableScope
readonly closeFiber: Option.Option<Fiber.RuntimeFiber<void>>
}
}
export const useScope: { export const useScope: {
( (
deps: React.DependencyList, deps: React.DependencyList,
options?: ScopeOptions, options?: ScopeOptions,
): Effect.Effect<Scope.Scope> ): Effect.Effect<Scope.Scope>
} = Effect.fnUntraced(function*(deps, options) { } = Effect.fnUntraced(function*(deps, options) {
const runtime = yield* Effect.runtime() // biome-ignore lint/style/noNonNullAssertion: context initialization
const runtimeRef = React.useRef<Runtime.Runtime<never>>(null!)
runtimeRef.current = yield* Effect.runtime()
// biome-ignore lint/correctness/useExhaustiveDependencies: no reactivity needed const key = React.useId()
const [isInitialRun, initialScope] = React.useMemo(() => Runtime.runSync(runtime)(Effect.all([ const scopeMap = yield* ScopeMap as unknown as Effect.Effect<ScopeMap>
Ref.make(true),
Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential),
])), [])
const [scope, setScope] = React.useState(initialScope)
React.useEffect(() => Runtime.runSync(runtime)( const scope = React.useMemo(() => Runtime.runSync(runtimeRef.current)(Effect.andThen(
Effect.if(isInitialRun, { scopeMap.ref,
onTrue: () => Effect.as( map => Option.match(HashMap.get(map, key), {
Ref.set(isInitialRun, false), onSome: entry => Effect.succeed(entry.scope),
() => closeScope(scope, runtime, options), onNone: () => Effect.tap(
Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential),
scope => Ref.update(scopeMap.ref, HashMap.set(key, {
scope,
closeFiber: Option.none(),
}))
), ),
}),
onFalse: () => Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential).pipe(
Effect.tap(scope => Effect.sync(() => setScope(scope))),
Effect.map(scope => () => closeScope(scope, runtime, options)),
),
})
// biome-ignore lint/correctness/useExhaustiveDependencies: use of React.DependencyList // biome-ignore lint/correctness/useExhaustiveDependencies: use of React.DependencyList
), deps) )), deps)
// biome-ignore lint/correctness/useExhaustiveDependencies: only reactive on "scope"
React.useEffect(() => Runtime.runSync(runtimeRef.current)(scopeMap.ref.pipe(
Effect.andThen(HashMap.get(key)),
Effect.tap(entry => Option.match(entry.closeFiber, {
onSome: fiber => Effect.andThen(
Ref.update(scopeMap.ref, HashMap.set(key, { ...entry, closeFiber: Option.none() })),
Fiber.interruptFork(fiber),
),
onNone: () => Effect.void,
})),
Effect.map(({ scope }) =>
() => Runtime.runSync(runtimeRef.current)(Effect.andThen(
Effect.forkDaemon(Effect.sleep("100 millis").pipe(
Effect.andThen(Scope.close(scope, Exit.void)),
Effect.andThen(Ref.update(scopeMap.ref, HashMap.remove(key))),
)),
fiber => Ref.update(scopeMap.ref, HashMap.set(key, {
scope,
closeFiber: Option.some(fiber),
})),
))
),
)), [scope])
return scope return scope
}) })

View File

@@ -9,7 +9,7 @@ import * as SubscriptionRef from "./SubscriptionRef.js"
import * as SubscriptionSubRef from "./SubscriptionSubRef.js" import * as SubscriptionSubRef from "./SubscriptionSubRef.js"
export const FormTypeId: unique symbol = Symbol.for("effect-fc/Form") export const FormTypeId: unique symbol = Symbol.for("effect-fc/Form/Form")
export type FormTypeId = typeof FormTypeId export type FormTypeId = typeof FormTypeId
export interface Form<in out A, in out I = A, out R = never, in out SA = void, in out SE = A, out SR = never> export interface Form<in out A, in out I = A, out R = never, in out SA = void, in out SE = A, out SR = never>

View File

@@ -3,7 +3,7 @@ import { type Equivalence, Function, Predicate } from "effect"
import type * as Component from "./Component.js" import type * as Component from "./Component.js"
export const TypeId: unique symbol = Symbol.for("effect-fc/Memoized") export const TypeId: unique symbol = Symbol.for("effect-fc/Memoized/Memoized")
export type TypeId = typeof TypeId export type TypeId = typeof TypeId
export interface Memoized<P> extends Memoized.Options<P> { export interface Memoized<P> extends Memoized.Options<P> {

View File

@@ -1,9 +1,10 @@
/** biome-ignore-all lint/complexity/useArrowFunction: necessary for class prototypes */ /** biome-ignore-all lint/complexity/useArrowFunction: necessary for class prototypes */
import { Effect, type Layer, ManagedRuntime, Predicate, type Runtime } from "effect" import { Effect, Layer, ManagedRuntime, Predicate, type Runtime } from "effect"
import * as React from "react" import * as React from "react"
import * as Component from "./Component.js"
export const TypeId: unique symbol = Symbol.for("effect-fc/ReactRuntime") export const TypeId: unique symbol = Symbol.for("effect-fc/ReactRuntime/ReactRuntime")
export type TypeId = typeof TypeId export type TypeId = typeof TypeId
export interface ReactRuntime<R, ER> { export interface ReactRuntime<R, ER> {
@@ -21,9 +22,12 @@ export const isReactRuntime = (u: unknown): u is ReactRuntime<unknown, unknown>
export const make = <R, ER>( export const make = <R, ER>(
layer: Layer.Layer<R, ER>, layer: Layer.Layer<R, ER>,
memoMap?: Layer.MemoMap, memoMap?: Layer.MemoMap,
): ReactRuntime<R, ER> => Object.setPrototypeOf( ): ReactRuntime<R | Component.ScopeMap, ER> => Object.setPrototypeOf(
Object.assign(function() {}, { Object.assign(function() {}, {
runtime: ManagedRuntime.make(layer, memoMap), runtime: ManagedRuntime.make(
Layer.merge(layer, Component.ScopeMap.Default),
memoMap,
),
// biome-ignore lint/style/noNonNullAssertion: context initialization // biome-ignore lint/style/noNonNullAssertion: context initialization
context: React.createContext<Runtime.Runtime<R>>(null!), context: React.createContext<Runtime.Runtime<R>>(null!),
}), }),

View File

@@ -2,7 +2,7 @@ import { Chunk, Effect, Effectable, Option, Predicate, Readable, Ref, Stream, Su
import * as PropertyPath from "./PropertyPath.js" import * as PropertyPath from "./PropertyPath.js"
export const SubscriptionSubRefTypeId: unique symbol = Symbol.for("effect-fc/SubscriptionSubRef") export const SubscriptionSubRefTypeId: unique symbol = Symbol.for("effect-fc/SubscriptionSubRef/SubscriptionSubRef")
export type SubscriptionSubRefTypeId = typeof SubscriptionSubRefTypeId export type SubscriptionSubRefTypeId = typeof SubscriptionSubRefTypeId
export interface SubscriptionSubRef<in out A, in out B extends SubscriptionRef.SubscriptionRef<any>> export interface SubscriptionSubRef<in out A, in out B extends SubscriptionRef.SubscriptionRef<any>>

View File

@@ -54,6 +54,11 @@ class RegisterFormView extends Component.makeUntraced("RegisterFormView")(functi
const TextFieldFormInputFC = yield* TextFieldFormInput const TextFieldFormInputFC = yield* TextFieldFormInput
yield* Component.useOnMount(() => Effect.gen(function*() {
yield* Effect.addFinalizer(() => Console.log("RegisterFormView unmounted"))
yield* Console.log("RegisterFormView mounted")
}))
return ( return (
<Container width="300"> <Container width="300">
@@ -87,7 +92,7 @@ class RegisterFormView extends Component.makeUntraced("RegisterFormView")(functi
const RegisterPage = Component.makeUntraced("RegisterPage")(function*() { const RegisterPage = Component.makeUntraced("RegisterPage")(function*() {
const RegisterFormViewFC = yield* Effect.provide( const RegisterFormViewFC = yield* Effect.provide(
RegisterFormView, RegisterFormView,
yield* Component.useContext(RegisterForm.Default, { finalizerExecutionMode: "fork" }), yield* Component.useContext(RegisterForm.Default),
) )
return <RegisterFormViewFC /> return <RegisterFormViewFC />

View File

@@ -11,7 +11,7 @@ const TodosStateLive = TodosState.Default("todos")
const Index = Component.makeUntraced("Index")(function*() { const Index = Component.makeUntraced("Index")(function*() {
const TodosFC = yield* Effect.provide( const TodosFC = yield* Effect.provide(
Todos, Todos,
yield* Component.useContext(TodosStateLive, { finalizerExecutionMode: "fork" }), yield* Component.useContext(TodosStateLive),
) )
return <TodosFC /> return <TodosFC />