This commit is contained in:
@@ -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>(
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
29
packages/effect-components/src/ReactManagedRuntime.ts
Normal file
29
packages/effect-components/src/ReactManagedRuntime.ts
Normal 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"
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -1,44 +1,56 @@
|
|||||||
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(
|
||||||
const [state, setState] = React.useState("value")
|
Effect.fn(function*() {
|
||||||
|
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>
|
||||||
<TextField.Root
|
<TextField.Root
|
||||||
value={state}
|
value={state}
|
||||||
onChange={e => setState(e.target.value)}
|
onChange={e => setState(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</>
|
</>
|
||||||
})
|
}),
|
||||||
console.log(MyTestComponent)
|
|
||||||
|
ReactComponent.withDisplayName("MyTestComponent"),
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user