Fix useScope
All checks were successful
Lint / lint (push) Successful in 17s

This commit is contained in:
Julien Valverdé
2025-10-23 14:31:51 +02:00
parent 874da0b963
commit cba42bfa52
3 changed files with 93 additions and 18 deletions

View File

@@ -408,7 +408,7 @@ export const withRuntime: {
export class ScopeMap extends Effect.Service<ScopeMap>()("effect-fc/Component/ScopeMap", { export class ScopeMap extends Effect.Service<ScopeMap>()("effect-fc/Component/ScopeMap", {
effect: Effect.bind(Effect.Do, "ref", () => Ref.make(HashMap.empty<string, ScopeMap.Entry>())) effect: Effect.bind(Effect.Do, "ref", () => Ref.make(HashMap.empty<object, ScopeMap.Entry>()))
}) {} }) {}
export namespace ScopeMap { export namespace ScopeMap {
@@ -429,12 +429,12 @@ export const useScope: {
const runtimeRef = React.useRef<Runtime.Runtime<never>>(null!) const runtimeRef = React.useRef<Runtime.Runtime<never>>(null!)
runtimeRef.current = yield* Effect.runtime() runtimeRef.current = yield* Effect.runtime()
const key = React.useId()
const scopeMap = yield* ScopeMap as unknown as Effect.Effect<ScopeMap> const scopeMap = yield* ScopeMap as unknown as Effect.Effect<ScopeMap>
const scope = React.useMemo(() => Runtime.runSync(runtimeRef.current)(Effect.andThen( const [key, scope] = React.useMemo(() => Runtime.runSync(runtimeRef.current)(Effect.andThen(
scopeMap.ref, Effect.all([Effect.succeed({}), scopeMap.ref]),
map => Option.match(HashMap.get(map, key), { ([key, map]) => Effect.andThen(
Option.match(HashMap.get(map, key), {
onSome: entry => Effect.succeed(entry.scope), onSome: entry => Effect.succeed(entry.scope),
onNone: () => Effect.tap( onNone: () => Effect.tap(
Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential), Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential),
@@ -444,10 +444,12 @@ export const useScope: {
})) }))
), ),
}), }),
scope => [key, scope] as const,
),
// 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" // biome-ignore lint/correctness/useExhaustiveDependencies: only reactive on "key"
React.useEffect(() => Runtime.runSync(runtimeRef.current)(scopeMap.ref.pipe( React.useEffect(() => Runtime.runSync(runtimeRef.current)(scopeMap.ref.pipe(
Effect.andThen(HashMap.get(key)), Effect.andThen(HashMap.get(key)),
Effect.tap(entry => Option.match(entry.closeFiber, { Effect.tap(entry => Option.match(entry.closeFiber, {
@@ -469,7 +471,7 @@ export const useScope: {
})), })),
)) ))
), ),
)), [scope]) )), [key])
return scope return scope
}) })

View File

@@ -13,6 +13,7 @@ import { Route as FormRouteImport } from './routes/form'
import { Route as BlankRouteImport } from './routes/blank' import { Route as BlankRouteImport } from './routes/blank'
import { Route as IndexRouteImport } from './routes/index' import { Route as IndexRouteImport } from './routes/index'
import { Route as DevMemoRouteImport } from './routes/dev/memo' import { Route as DevMemoRouteImport } from './routes/dev/memo'
import { Route as DevContextRouteImport } from './routes/dev/context'
import { Route as DevAsyncRenderingRouteImport } from './routes/dev/async-rendering' import { Route as DevAsyncRenderingRouteImport } from './routes/dev/async-rendering'
const FormRoute = FormRouteImport.update({ const FormRoute = FormRouteImport.update({
@@ -35,6 +36,11 @@ const DevMemoRoute = DevMemoRouteImport.update({
path: '/dev/memo', path: '/dev/memo',
getParentRoute: () => rootRouteImport, getParentRoute: () => rootRouteImport,
} as any) } as any)
const DevContextRoute = DevContextRouteImport.update({
id: '/dev/context',
path: '/dev/context',
getParentRoute: () => rootRouteImport,
} as any)
const DevAsyncRenderingRoute = DevAsyncRenderingRouteImport.update({ const DevAsyncRenderingRoute = DevAsyncRenderingRouteImport.update({
id: '/dev/async-rendering', id: '/dev/async-rendering',
path: '/dev/async-rendering', path: '/dev/async-rendering',
@@ -46,6 +52,7 @@ export interface FileRoutesByFullPath {
'/blank': typeof BlankRoute '/blank': typeof BlankRoute
'/form': typeof FormRoute '/form': typeof FormRoute
'/dev/async-rendering': typeof DevAsyncRenderingRoute '/dev/async-rendering': typeof DevAsyncRenderingRoute
'/dev/context': typeof DevContextRoute
'/dev/memo': typeof DevMemoRoute '/dev/memo': typeof DevMemoRoute
} }
export interface FileRoutesByTo { export interface FileRoutesByTo {
@@ -53,6 +60,7 @@ export interface FileRoutesByTo {
'/blank': typeof BlankRoute '/blank': typeof BlankRoute
'/form': typeof FormRoute '/form': typeof FormRoute
'/dev/async-rendering': typeof DevAsyncRenderingRoute '/dev/async-rendering': typeof DevAsyncRenderingRoute
'/dev/context': typeof DevContextRoute
'/dev/memo': typeof DevMemoRoute '/dev/memo': typeof DevMemoRoute
} }
export interface FileRoutesById { export interface FileRoutesById {
@@ -61,19 +69,33 @@ export interface FileRoutesById {
'/blank': typeof BlankRoute '/blank': typeof BlankRoute
'/form': typeof FormRoute '/form': typeof FormRoute
'/dev/async-rendering': typeof DevAsyncRenderingRoute '/dev/async-rendering': typeof DevAsyncRenderingRoute
'/dev/context': typeof DevContextRoute
'/dev/memo': typeof DevMemoRoute '/dev/memo': typeof DevMemoRoute
} }
export interface FileRouteTypes { export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/' | '/blank' | '/form' | '/dev/async-rendering' | '/dev/memo' fullPaths:
| '/'
| '/blank'
| '/form'
| '/dev/async-rendering'
| '/dev/context'
| '/dev/memo'
fileRoutesByTo: FileRoutesByTo fileRoutesByTo: FileRoutesByTo
to: '/' | '/blank' | '/form' | '/dev/async-rendering' | '/dev/memo' to:
| '/'
| '/blank'
| '/form'
| '/dev/async-rendering'
| '/dev/context'
| '/dev/memo'
id: id:
| '__root__' | '__root__'
| '/' | '/'
| '/blank' | '/blank'
| '/form' | '/form'
| '/dev/async-rendering' | '/dev/async-rendering'
| '/dev/context'
| '/dev/memo' | '/dev/memo'
fileRoutesById: FileRoutesById fileRoutesById: FileRoutesById
} }
@@ -82,6 +104,7 @@ export interface RootRouteChildren {
BlankRoute: typeof BlankRoute BlankRoute: typeof BlankRoute
FormRoute: typeof FormRoute FormRoute: typeof FormRoute
DevAsyncRenderingRoute: typeof DevAsyncRenderingRoute DevAsyncRenderingRoute: typeof DevAsyncRenderingRoute
DevContextRoute: typeof DevContextRoute
DevMemoRoute: typeof DevMemoRoute DevMemoRoute: typeof DevMemoRoute
} }
@@ -115,6 +138,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof DevMemoRouteImport preLoaderRoute: typeof DevMemoRouteImport
parentRoute: typeof rootRouteImport parentRoute: typeof rootRouteImport
} }
'/dev/context': {
id: '/dev/context'
path: '/dev/context'
fullPath: '/dev/context'
preLoaderRoute: typeof DevContextRouteImport
parentRoute: typeof rootRouteImport
}
'/dev/async-rendering': { '/dev/async-rendering': {
id: '/dev/async-rendering' id: '/dev/async-rendering'
path: '/dev/async-rendering' path: '/dev/async-rendering'
@@ -130,6 +160,7 @@ const rootRouteChildren: RootRouteChildren = {
BlankRoute: BlankRoute, BlankRoute: BlankRoute,
FormRoute: FormRoute, FormRoute: FormRoute,
DevAsyncRenderingRoute: DevAsyncRenderingRoute, DevAsyncRenderingRoute: DevAsyncRenderingRoute,
DevContextRoute: DevContextRoute,
DevMemoRoute: DevMemoRoute, DevMemoRoute: DevMemoRoute,
} }
export const routeTree = rootRouteImport export const routeTree = rootRouteImport

View File

@@ -0,0 +1,42 @@
import { Container, Flex, Text, TextField } from "@radix-ui/themes"
import { createFileRoute } from "@tanstack/react-router"
import { Console, Effect } from "effect"
import { Component } from "effect-fc"
import * as React from "react"
import { runtime } from "@/runtime"
class SubService extends Effect.Service<SubService>()("SubService", {
effect: (value: string) => Effect.succeed({ value })
}) {}
const SubComponent = Component.makeUntraced("SubComponent")(function*() {
const service = yield* SubService
yield* Component.useOnMount(() => Effect.gen(function*() {
yield* Effect.addFinalizer(() => Console.log("SubComponent unmounted"))
yield* Console.log("SubComponent mounted")
}))
return <Text>{service.value}</Text>
})
const ContextView = Component.makeUntraced("ContextView")(function*() {
const [serviceValue, setServiceValue] = React.useState("test")
const SubServiceLayer = React.useMemo(() => SubService.Default(serviceValue), [serviceValue])
const SubComponentFC = yield* Effect.provide(SubComponent, yield* Component.useContext(SubServiceLayer))
return (
<Container>
<Flex direction="column" align="center">
<TextField.Root value={serviceValue} onChange={e => setServiceValue(e.target.value)} />
<SubComponentFC />
</Flex>
</Container>
)
}).pipe(
Component.withRuntime(runtime.context)
)
export const Route = createFileRoute("/dev/context")({
component: ContextView
})