Refactoring
All checks were successful
Lint / lint (push) Successful in 16s

This commit is contained in:
Julien Valverdé
2025-06-30 21:44:29 +02:00
parent 37d9400ada
commit 78a3735038
5 changed files with 112 additions and 53 deletions

View File

@@ -1,23 +1,30 @@
import { Context, Effect, Runtime, Tracer } from "effect" import { Context, Effect, Function, Runtime, 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"
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 readonly displayName?: string
} }
export const nonReactiveTags = [Tracer.ParentSpan] as const 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 = {}>(self: ReactComponent<E, R, P>): Effect.Effect<React.FC<P>, never, R>
self: ReactComponent<P, E, R>, } = Effect.fnUntraced(function* <E, R, P>(
options?: ReactHook.ScopeOptions, self: ReactComponent<E, R, P>
): Effect.Effect<React.FC<P>, never, R>
} = Effect.fnUntraced(function* useFC<P, E, R>(
self: ReactComponent<P, E, R>
) { ) {
const runtime = yield* Effect.runtime<R>() const runtime = yield* Effect.runtime<R>()
@@ -31,14 +38,24 @@ export const useFC: {
}) })
export const use: { export const use: {
<P, E, R>( <E, R, P = {}>(
self: ReactComponent<P, E, R>, self: ReactComponent<E, R, P>,
fn: (Component: React.FC<P>) => React.ReactNode, fn: (Component: React.FC<P>) => React.ReactNode,
): Effect.Effect<React.ReactNode, never, R> ): Effect.Effect<React.ReactNode, never, R>
} = Effect.fnUntraced(function* use(self, fn) { } = Effect.fnUntraced(function*(self, fn) {
return fn(yield* useFC(self)) return fn(yield* useFC(self))
}) })
export const withRuntime: {
<E, R, P = {}>(context: React.Context<Runtime.Runtime<R>>): (self: ReactComponent<E, R, P>) => React.FC<P>
<E, R, P = {}>(self: ReactComponent<E, R, P>, context: React.Context<Runtime.Runtime<R>>): React.FC<P>
} = Function.dual(2, <E, R, P extends {}>(
self: ReactComponent<E, R, P>,
context: React.Context<Runtime.Runtime<R>>,
): React.FC<P> => function WithRuntime(props) {
const runtime = React.useContext(context)
return React.createElement(Runtime.runSync(runtime)(useFC(self)), props)
})
// export const useFC: { // export const useFC: {
// <P, E, R>( // <P, E, R>(

View File

@@ -13,7 +13,7 @@ export const useMemo: {
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, never, 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,
) { ) {
@@ -23,7 +23,7 @@ export const useMemo: {
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, never, 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, [])
@@ -31,16 +31,16 @@ export const useOnce: {
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 +64,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(

View File

@@ -0,0 +1,29 @@
import { Effect, type Layer, ManagedRuntime, 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 SyncProviderProps<R, ER> {
readonly runtime: ReactManagedRuntime<R, ER>
readonly children?: React.ReactNode
}
export const SyncProvider = <R, ER>(
props: SyncProviderProps<R, ER>
): React.ReactNode => React.createElement(props.runtime.context, {
value: React.useMemo(() => Effect.runSync(props.runtime.runtime.runtimeEffect), [props.runtime]),
children: props.children,
})
SyncProvider.displayName = "ReactManagedRuntimeSyncProvider"

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

@@ -1,36 +1,46 @@
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 { Console, Effect, Layer, pipe, SubscriptionRef } from "effect"
import { ReactComponent, ReactHook } from "effect-components" import { ReactComponent, ReactHook, ReactManagedRuntime } from "effect-components"
import * as React from "react" import * as React from "react"
class TestService extends Effect.Service<TestService>()("TestService", {
effect: Effect.bind(Effect.Do, "ref", () => SubscriptionRef.make("value")),
}) {}
const runtime = ReactManagedRuntime.make(Layer.empty)
export const Route = createFileRoute("/effect-component-tests")({ export const Route = createFileRoute("/effect-component-tests")({
component: RouteComponent, component: RouteComponent,
}) })
function RouteComponent() { function RouteComponent() {
const runtime = React.useMemo(() => ManagedRuntime.make(Layer.empty), []) return (
<ReactManagedRuntime.SyncProvider runtime={runtime}>
return <> <MyRoute />
{runtime.runSync(ReactComponent.use(MyTestComponent, Component => ( </ReactManagedRuntime.SyncProvider>
<Component /> )
)))}
</>
} }
const MyRoute = pipe(
Effect.fn(function*() {
return yield* ReactComponent.use(MyTestComponent, C => <C />)
}),
ReactComponent.withDisplayName("MyRoute"),
ReactComponent.withRuntime(runtime.context),
)
class TestService extends Effect.Service<TestService>()("TestService", {
effect: Effect.bind(Effect.Do, "ref", () => SubscriptionRef.make("value")),
}) {}
const MyTestComponent = Effect.fn(function* MyTestComponent(props?: { readonly value?: string }) { const MyTestComponent = pipe(
Effect.fn(function*() {
const [state, setState] = React.useState("value") const [state, setState] = React.useState("value")
// yield* ReactHook.useEffect(() => Effect.andThen( yield* ReactHook.useEffect(() => Effect.andThen(
// Effect.addFinalizer(() => Console.log("MyTestComponent umounted")), Effect.addFinalizer(() => Console.log("MyTestComponent umounted")),
// Console.log("MyTestComponent mounted"), Console.log("MyTestComponent mounted"),
// ), []) ), [])
return <> return <>
<Box> <Box>
@@ -40,5 +50,7 @@ const MyTestComponent = Effect.fn(function* MyTestComponent(props?: { readonly v
/> />
</Box> </Box>
</> </>
}) }),
console.log(MyTestComponent)
ReactComponent.withDisplayName("MyTestComponent"),
)