0.1.10 (#13)
All checks were successful
Publish / publish (push) Successful in 22s
Lint / lint (push) Successful in 13s

Co-authored-by: Julien Valverdé <julien.valverde@mailo.com>
Reviewed-on: https://gitea:3000/Thilawyn/reffuse/pulls/13
This commit was merged in pull request #13.
This commit is contained in:
Julien Valverdé
2025-05-11 19:21:06 +02:00
parent bc8c96635c
commit 64943deaab
15 changed files with 563 additions and 310 deletions

View File

@@ -11,41 +11,41 @@
"preview": "vite preview"
},
"devDependencies": {
"@eslint/js": "^9.25.1",
"@tanstack/react-router": "^1.117.1",
"@tanstack/react-router-devtools": "^1.117.1",
"@tanstack/router-plugin": "^1.117.2",
"@eslint/js": "^9.26.0",
"@tanstack/react-router": "^1.120.2",
"@tanstack/react-router-devtools": "^1.120.2",
"@tanstack/router-plugin": "^1.120.2",
"@thilawyn/thilaschema": "^0.1.4",
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.2",
"@types/react": "^19.1.3",
"@types/react-dom": "^19.1.3",
"@vitejs/plugin-react": "^4.4.1",
"eslint": "^9.25.1",
"eslint": "^9.26.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.0.0",
"globals": "^16.1.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"typescript-eslint": "^8.31.0",
"vite": "^6.3.3"
"typescript-eslint": "^8.32.0",
"vite": "^6.3.5"
},
"dependencies": {
"@effect/platform": "^0.80.14",
"@effect/platform-browser": "^0.60.5",
"@effect/platform": "^0.80.21",
"@effect/platform-browser": "^0.60.12",
"@radix-ui/themes": "^3.2.1",
"@reffuse/extension-lazyref": "workspace:*",
"@reffuse/extension-query": "workspace:*",
"@typed/async-data": "^0.13.1",
"@typed/id": "^0.17.2",
"@typed/lazy-ref": "^0.3.3",
"effect": "^3.14.14",
"lucide-react": "^0.503.0",
"effect": "^3.14.21",
"lucide-react": "^0.508.0",
"mobx": "^6.13.7",
"reffuse": "workspace:*"
},
"overrides": {
"effect": "^3.14.14",
"@effect/platform": "^0.80.14",
"@effect/platform-browser": "^0.60.5",
"effect": "^3.14.21",
"@effect/platform": "^0.80.21",
"@effect/platform-browser": "^0.60.12",
"@typed/lazy-ref": "^0.3.3",
"@typed/async-data": "^0.13.1"
}

View File

@@ -1,7 +1,7 @@
import { AlertDialog, Button, Flex, Text } from "@radix-ui/themes"
import { Cause, Console, Effect, Either, flow, Match, Option, Stream } from "effect"
import { useState } from "react"
import { AppQueryErrorHandler } from "./query"
import { AppQueryClient } from "./query"
import { R } from "./reffuse"
@@ -9,8 +9,8 @@ export function VQueryErrorHandler() {
const [open, setOpen] = useState(false)
const error = R.useSubscribeStream(
R.useMemo(() => AppQueryErrorHandler.pipe(
Effect.map(handler => handler.errors.pipe(
R.useMemo(() => AppQueryClient.pipe(
Effect.map(client => client.errorHandler.errors.pipe(
Stream.changes,
Stream.tap(Console.error),
Stream.tap(() => Effect.sync(() => setOpen(true))),

View File

@@ -3,12 +3,11 @@ import { Clipboard, Geolocation, Permissions } from "@effect/platform-browser"
import { LazyRefExtension } from "@reffuse/extension-lazyref"
import { QueryExtension } from "@reffuse/extension-query"
import { Reffuse, ReffuseContext } from "reffuse"
import { AppQueryClient, AppQueryErrorHandler } from "./query"
import { AppQueryClient } from "./query"
export const RootContext = ReffuseContext.make<
| AppQueryClient
| AppQueryErrorHandler
| Clipboard.Clipboard
| Geolocation.Geolocation
| Permissions.Permissions

View File

@@ -19,6 +19,7 @@ import { Route as LazyrefImport } from './routes/lazyref'
import { Route as CountImport } from './routes/count'
import { Route as BlankImport } from './routes/blank'
import { Route as IndexImport } from './routes/index'
import { Route as StreamsPullImport } from './routes/streams/pull'
import { Route as QueryUsequeryImport } from './routes/query/usequery'
import { Route as QueryUsemutationImport } from './routes/query/usemutation'
import { Route as QueryServiceImport } from './routes/query/service'
@@ -73,6 +74,12 @@ const IndexRoute = IndexImport.update({
getParentRoute: () => rootRoute,
} as any)
const StreamsPullRoute = StreamsPullImport.update({
id: '/streams/pull',
path: '/streams/pull',
getParentRoute: () => rootRoute,
} as any)
const QueryUsequeryRoute = QueryUsequeryImport.update({
id: '/query/usequery',
path: '/query/usequery',
@@ -172,6 +179,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof QueryUsequeryImport
parentRoute: typeof rootRoute
}
'/streams/pull': {
id: '/streams/pull'
path: '/streams/pull'
fullPath: '/streams/pull'
preLoaderRoute: typeof StreamsPullImport
parentRoute: typeof rootRoute
}
}
}
@@ -189,6 +203,7 @@ export interface FileRoutesByFullPath {
'/query/service': typeof QueryServiceRoute
'/query/usemutation': typeof QueryUsemutationRoute
'/query/usequery': typeof QueryUsequeryRoute
'/streams/pull': typeof StreamsPullRoute
}
export interface FileRoutesByTo {
@@ -203,6 +218,7 @@ export interface FileRoutesByTo {
'/query/service': typeof QueryServiceRoute
'/query/usemutation': typeof QueryUsemutationRoute
'/query/usequery': typeof QueryUsequeryRoute
'/streams/pull': typeof StreamsPullRoute
}
export interface FileRoutesById {
@@ -218,6 +234,7 @@ export interface FileRoutesById {
'/query/service': typeof QueryServiceRoute
'/query/usemutation': typeof QueryUsemutationRoute
'/query/usequery': typeof QueryUsequeryRoute
'/streams/pull': typeof StreamsPullRoute
}
export interface FileRouteTypes {
@@ -234,6 +251,7 @@ export interface FileRouteTypes {
| '/query/service'
| '/query/usemutation'
| '/query/usequery'
| '/streams/pull'
fileRoutesByTo: FileRoutesByTo
to:
| '/'
@@ -247,6 +265,7 @@ export interface FileRouteTypes {
| '/query/service'
| '/query/usemutation'
| '/query/usequery'
| '/streams/pull'
id:
| '__root__'
| '/'
@@ -260,6 +279,7 @@ export interface FileRouteTypes {
| '/query/service'
| '/query/usemutation'
| '/query/usequery'
| '/streams/pull'
fileRoutesById: FileRoutesById
}
@@ -275,6 +295,7 @@ export interface RootRouteChildren {
QueryServiceRoute: typeof QueryServiceRoute
QueryUsemutationRoute: typeof QueryUsemutationRoute
QueryUsequeryRoute: typeof QueryUsequeryRoute
StreamsPullRoute: typeof StreamsPullRoute
}
const rootRouteChildren: RootRouteChildren = {
@@ -289,6 +310,7 @@ const rootRouteChildren: RootRouteChildren = {
QueryServiceRoute: QueryServiceRoute,
QueryUsemutationRoute: QueryUsemutationRoute,
QueryUsequeryRoute: QueryUsequeryRoute,
StreamsPullRoute: StreamsPullRoute,
}
export const routeTree = rootRoute
@@ -311,7 +333,8 @@ export const routeTree = rootRoute
"/todos",
"/query/service",
"/query/usemutation",
"/query/usequery"
"/query/usequery",
"/streams/pull"
]
},
"/": {
@@ -346,6 +369,9 @@ export const routeTree = rootRoute
},
"/query/usequery": {
"filePath": "query/usequery.tsx"
},
"/streams/pull": {
"filePath": "streams/pull.tsx"
}
}
}

View File

@@ -1,6 +1,6 @@
import { R } from "@/reffuse"
import { createFileRoute } from "@tanstack/react-router"
import { Ref } from "effect"
import { Effect, Ref } from "effect"
export const Route = createFileRoute("/count")({
@@ -11,14 +11,13 @@ function Count() {
const runSync = R.useRunSync()
const countRef = R.useRef(0)
const [count] = R.useRefState(countRef)
const countRef = R.useRef(() => Effect.succeed(0))
const [count] = R.useSubscribeRefs(countRef)
return (
<div className="container mx-auto">
{/* <button onClick={() => setCount((count) => count + 1)}> */}
<button onClick={() => Ref.update(countRef, count => count + 1).pipe(runSync)}>
<button onClick={() => runSync(Ref.update(countRef, count => count + 1))}>
count is {count}
</button>
</div>

View File

@@ -14,7 +14,7 @@ export const Route = createFileRoute("/query/service")({
function RouteComponent() {
const query = R.useQuery({
key: R.useStreamFromValues(["uuid4", 10 as number]),
key: R.useStreamFromReactiveValues(["uuid4", 10 as number]),
query: ([, count]) => Console.log(`Querying ${ count } IDs...`).pipe(
Effect.andThen(Effect.sleep("500 millis")),
Effect.andThen(HttpClient.get(`https://www.uuidtools.com/api/generate/v4/count/${ count }`)),

View File

@@ -20,7 +20,7 @@ function RouteComponent() {
const [count, setCount] = useState(1)
const query = R.useQuery({
key: R.useStreamFromValues(["uuid4", count]),
key: R.useStreamFromReactiveValues(["uuid4", count]),
query: ([, count]) => Console.log(`Querying ${ count } IDs...`).pipe(
Effect.andThen(Effect.sleep("500 millis")),
Effect.andThen(HttpClient.get(`https://www.uuidtools.com/api/generate/v4/count/${ count }`)),

View File

@@ -0,0 +1,34 @@
import { R } from "@/reffuse"
import { Button, Flex, Text } from "@radix-ui/themes"
import { createFileRoute } from "@tanstack/react-router"
import { Chunk, Effect, Exit, Option, Queue, Random, Scope, Stream } from "effect"
import { useMemo, useState } from "react"
export const Route = createFileRoute("/streams/pull")({
component: RouteComponent
})
function RouteComponent() {
const stream = useMemo(() => Stream.repeatEffect(Random.nextInt), [])
const streamScope = R.useScope([stream], { finalizerExecutionMode: "fork" })
const queue = R.useMemo(() => Effect.provideService(Stream.toQueueOfElements(stream), Scope.Scope, streamScope), [streamScope])
const [value, setValue] = useState(Option.none<number>())
const pullLatest = R.useCallbackSync(() => Queue.takeAll(queue).pipe(
Effect.flatMap(Chunk.last),
Effect.flatMap(Exit.matchEffect({
onSuccess: Effect.succeed,
onFailure: Effect.fail,
})),
Effect.tap(v => Effect.sync(() => setValue(Option.some(v)))),
), [queue])
return (
<Flex direction="column" align="center" gap="2">
{Option.isSome(value) && <Text>{value.value}</Text>}
<Button onClick={pullLatest}>Pull latest</Button>
</Flex>
)
}

View File

@@ -2,7 +2,11 @@ import { R } from "@/reffuse"
import { Button, Flex, Text } from "@radix-ui/themes"
import { createFileRoute } from "@tanstack/react-router"
import { GetRandomValues, makeUuid4 } from "@typed/id"
import { Console, Effect, Ref } from "effect"
import { Console, Effect, Option, Scope } from "effect"
import { useEffect, useState } from "react"
const makeUuid = Effect.provide(makeUuid4, GetRandomValues.CryptoRandom)
export const Route = createFileRoute("/tests")({
@@ -10,48 +14,34 @@ export const Route = createFileRoute("/tests")({
})
function RouteComponent() {
const deepRef = R.useRef({ value: "poulet" })
const deepValueRef = R.useSubRef(deepRef, ["value"])
const runSync = R.useRunSync()
// const value = R.useMemoScoped(Effect.addFinalizer(() => Console.log("cleanup")).pipe(
// Effect.andThen(makeUuid4),
// Effect.provide(GetRandomValues.CryptoRandom),
// ), [])
// console.log(value)
R.useFork(() => Effect.addFinalizer(() => Console.log("cleanup")).pipe(
Effect.andThen(Console.log("ouient")),
Effect.delay("1 second"),
const [uuid, setUuid] = useState(R.useMemo(() => makeUuid, []))
const generateUuid = R.useCallbackSync(() => makeUuid.pipe(
Effect.tap(v => Effect.sync(() => setUuid(v)))
), [])
const uuidStream = R.useStreamFromReactiveValues([uuid])
const uuidStreamLatestValue = R.useSubscribeStream(uuidStream)
const uuidRef = R.useRef("none")
const anotherRef = R.useRef(69)
const logValue = R.useCallbackSync(Effect.fn(function*(value: string) {
yield* Effect.log(value)
}), [])
const generateUuid = R.useCallbackSync(() => makeUuid4.pipe(
Effect.provide(GetRandomValues.CryptoRandom),
Effect.tap(v => Ref.set(uuidRef, v)),
Effect.tap(v => Ref.set(deepValueRef, v)),
), [])
const scope = R.useScope([uuid])
useEffect(() => Effect.addFinalizer(() => Console.log("Scope cleanup!")).pipe(
Effect.andThen(Console.log("Scope changed")),
Effect.provideService(Scope.Scope, scope),
runSync,
), [scope, runSync])
return (
<Flex direction="row" justify="center" align="center" gap="2">
<R.SubscribeRefs refs={[uuidRef, anotherRef]}>
{(uuid, anotherRef) => <Text>{uuid} / {anotherRef}</Text>}
</R.SubscribeRefs>
<R.SubscribeRefs refs={[deepRef, deepValueRef]}>
{(deep, deepValue) => <Text>{JSON.stringify(deep)} / {deepValue}</Text>}
</R.SubscribeRefs>
<Button onClick={() => logValue("test")}>Log value</Button>
<Button onClick={() => generateUuid()}>Generate UUID</Button>
<Flex direction="column" justify="center" align="center" gap="2">
<Text>{uuid}</Text>
<Button onClick={generateUuid}>Generate UUID</Button>
<Text>
{Option.match(uuidStreamLatestValue, {
onSome: ([v]) => v,
onNone: () => <></>,
})}
</Text>
</Flex>
)
}

View File

@@ -16,7 +16,7 @@ export function VTodos() {
), [])
const todosRef = R.useMemo(() => TodosState.TodosState.pipe(Effect.map(state => state.todos)), [])
const [todos] = R.useRefState(todosRef)
const [todos] = R.useSubscribeRefs(todosRef)
return (

View File

@@ -1,4 +1,4 @@
import { Cause, Context, Effect, identity, Layer, Queue, Stream } from "effect"
import { Cause, Context, Effect, identity, Layer, PubSub, Stream } from "effect"
import type { Mutable } from "effect/Types"
@@ -36,17 +36,17 @@ export const Service = <Self, HandledE = never>() => (
const TagClass = Context.Tag(id)() as ServiceResult<Self, Id, FallbackA, HandledE>
(TagClass as Mutable<typeof TagClass>).Live = Layer.effect(TagClass, Effect.gen(function*() {
const queue = yield* Queue.unbounded<Cause.Cause<HandledE>>()
const errors = Stream.fromQueue(queue)
const pubsub = yield* PubSub.unbounded<Cause.Cause<HandledE>>()
const errors = Stream.fromPubSub(pubsub)
const handle = <A, E, R>(
self: Effect.Effect<A, E, R>
): Effect.Effect<A | FallbackA, Exclude<E, HandledE>, R> => f(
self as unknown as Effect.Effect<never, HandledE, never>,
(failure: HandledE) => Queue.offer(queue, Cause.fail(failure)).pipe(
(failure: HandledE) => PubSub.publish(pubsub, Cause.fail(failure)).pipe(
Effect.andThen(Effect.failCause(Cause.empty))
),
(defect: unknown) => Queue.offer(queue, Cause.die(defect)).pipe(
(defect: unknown) => PubSub.publish(pubsub, Cause.die(defect)).pipe(
Effect.andThen(Effect.failCause(Cause.empty))
),
)

View File

@@ -1,6 +1,6 @@
{
"name": "reffuse",
"version": "0.1.9",
"version": "0.1.10",
"type": "module",
"files": [
"./README.md",

View File

@@ -1,4 +1,4 @@
import { type Context, Effect, ExecutionStrategy, Exit, type Fiber, type Layer, Option, pipe, Pipeable, Queue, Ref, Runtime, Scope, Stream, SubscriptionRef } from "effect"
import { type Context, Effect, ExecutionStrategy, Exit, type Fiber, type Layer, Match, Option, pipe, Pipeable, PubSub, Ref, Runtime, Scope, Stream, SubscriptionRef } from "effect"
import * as React from "react"
import * as ReffuseContext from "./ReffuseContext.js"
import * as ReffuseRuntime from "./ReffuseRuntime.js"
@@ -14,6 +14,10 @@ export interface ScopeOptions {
readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy
}
export interface UseScopeOptions extends RenderOptions, ScopeOptions {
readonly finalizerExecutionMode?: "sync" | "fork"
}
export type RefsA<T extends readonly SubscriptionRef.SubscriptionRef<any>[]> = {
[K in keyof T]: Effect.Effect.Success<T[K]>
}
@@ -88,6 +92,55 @@ export abstract class ReffuseNamespace<R> {
), [runtime, context])
}
useScope<R>(
this: ReffuseNamespace<R>,
deps: React.DependencyList = [],
options?: UseScopeOptions,
): Scope.Scope {
const runSync = this.useRunSync()
const runFork = this.useRunFork()
const [isInitialRun, initialScope] = React.useMemo(() => runSync(Effect.all([
Ref.make(true),
Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential),
])), [])
const [scope, setScope] = React.useState(initialScope)
React.useEffect(() => isInitialRun.pipe(
Effect.if({
onTrue: () => Effect.as(
Ref.set(isInitialRun, false),
() => Scope.close(initialScope, Exit.void).pipe(
effect => Match.value(options?.finalizerExecutionMode ?? "sync").pipe(
Match.when("sync", () => { runSync(effect) }),
Match.when("fork", () => { runFork(effect) }),
Match.exhaustive,
)
),
),
onFalse: () => Scope.make(options?.finalizerExecutionStrategy).pipe(
Effect.tap(v => Effect.sync(() => setScope(v))),
Effect.map(v => () => Scope.close(v, Exit.void).pipe(
effect => Match.value(options?.finalizerExecutionMode ?? "sync").pipe(
Match.when("sync", () => { runSync(effect) }),
Match.when("fork", () => { runFork(effect) }),
Match.exhaustive,
)
)),
),
}),
runSync,
), [
...options?.doNotReExecuteOnRuntimeOrContextChange ? [] : [runSync, runFork],
...deps,
])
return scope
}
/**
* Reffuse equivalent to `React.useMemo`.
*
@@ -111,53 +164,6 @@ export abstract class ReffuseNamespace<R> {
])
}
useMemoScoped<A, E, R>(
this: ReffuseNamespace<R>,
effect: () => Effect.Effect<A, E, R | Scope.Scope>,
deps: React.DependencyList,
options?: RenderOptions & ScopeOptions,
): A {
const runSync = this.useRunSync()
const [isInitialRun, initialScope, initialValue] = React.useMemo(() => Effect.Do.pipe(
Effect.bind("isInitialRun", () => Ref.make(true)),
Effect.bind("scope", () => Scope.make(options?.finalizerExecutionStrategy)),
Effect.bind("value", ({ scope }) => Effect.provideService(effect(), Scope.Scope, scope)),
Effect.map(({ isInitialRun, scope, value }) => [isInitialRun, scope, value] as const),
runSync,
), [])
const [value, setValue] = React.useState(initialValue)
React.useEffect(() => isInitialRun.pipe(
Effect.if({
onTrue: () => Ref.set(isInitialRun, false).pipe(
Effect.map(() =>
() => runSync(Scope.close(initialScope, Exit.void))
)
),
onFalse: () => Effect.Do.pipe(
Effect.bind("scope", () => Scope.make(options?.finalizerExecutionStrategy)),
Effect.bind("value", ({ scope }) => Effect.provideService(effect(), Scope.Scope, scope)),
Effect.tap(({ value }) =>
Effect.sync(() => setValue(value))
),
Effect.map(({ scope }) =>
() => runSync(Scope.close(scope, Exit.void))
),
),
}),
runSync,
), [
...options?.doNotReExecuteOnRuntimeOrContextChange ? [] : [runSync],
...deps,
])
return value
}
/**
* Reffuse equivalent to `React.useEffect`.
*
@@ -378,17 +384,26 @@ export abstract class ReffuseNamespace<R> {
])
}
useRef<A, R>(
useRef<A, E, R>(
this: ReffuseNamespace<R>,
value: A,
initialValue: () => Effect.Effect<A, E, R>,
): SubscriptionRef.SubscriptionRef<A> {
return this.useMemo(
() => SubscriptionRef.make(value),
() => Effect.flatMap(initialValue(), SubscriptionRef.make),
[],
{ doNotReExecuteOnRuntimeOrContextChange: true }, // Do not recreate the ref when the context changes
)
}
useRefFromReactiveValue<A, R>(
this: ReffuseNamespace<R>,
value: A,
): SubscriptionRef.SubscriptionRef<A> {
const ref = this.useRef(() => Effect.succeed(value))
this.useEffect(() => Ref.set(ref, value), [value], { doNotReExecuteOnRuntimeOrContextChange: true })
return ref
}
useSubRef<B, const P extends PropertyPath.Paths<B>, R>(
this: ReffuseNamespace<R>,
parent: SubscriptionRef.SubscriptionRef<B>,
@@ -455,32 +470,61 @@ export abstract class ReffuseNamespace<R> {
return [reactStateValue, setValue]
}
useStreamFromValues<const A extends React.DependencyList, R>(
useStreamFromReactiveValues<const A extends React.DependencyList, R>(
this: ReffuseNamespace<R>,
values: A,
): Stream.Stream<A> {
const [queue, stream] = this.useMemo(() => Queue.unbounded<A>().pipe(
Effect.map(queue => [queue, Stream.fromQueue(queue)] as const)
), [])
const scope = this.useScope()
this.useEffect(() => Queue.offer(queue, values), values)
const { latest, pubsub, stream } = this.useMemo(() => Effect.Do.pipe(
Effect.bind("latest", () => Ref.make(values)),
Effect.bind("pubsub", () => Effect.acquireRelease(PubSub.unbounded<A>(), PubSub.shutdown)),
Effect.let("stream", ({ latest, pubsub }) => Ref.get(latest).pipe(
Effect.flatMap(a => Effect.map(
Stream.fromPubSub(pubsub, { scoped: true }),
s => Stream.concat(Stream.make(a), s),
)),
Stream.unwrapScoped,
)),
Effect.provideService(Scope.Scope, scope),
), [scope], { doNotReExecuteOnRuntimeOrContextChange: true })
this.useEffect(() => Ref.set(latest, values).pipe(
Effect.andThen(PubSub.publish(pubsub, values)),
Effect.unlessEffect(PubSub.isShutdown(pubsub)),
), values, { doNotReExecuteOnRuntimeOrContextChange: true })
return stream
}
useSubscribeStream<A, InitialA extends A | undefined, E, R>(
useSubscribeStream<A, E, R>(
this: ReffuseNamespace<R>,
stream: Stream.Stream<A, E, R>,
initialValue?: InitialA,
): InitialA extends A ? Option.Some<A> : Option.Option<A> {
const [reactStateValue, setReactStateValue] = React.useState<Option.Option<A>>(Option.fromNullable(initialValue))
): Option.Option<A>
useSubscribeStream<A, E, IE, R>(
this: ReffuseNamespace<R>,
stream: Stream.Stream<A, E, R>,
initialValue: () => Effect.Effect<A, IE, R>,
): Option.Some<A>
useSubscribeStream<A, E, IE, R>(
this: ReffuseNamespace<R>,
stream: Stream.Stream<A, E, R>,
initialValue?: () => Effect.Effect<A, IE, R>,
): Option.Option<A> {
const [reactStateValue, setReactStateValue] = React.useState(this.useMemo(
() => initialValue
? Effect.map(initialValue(), Option.some)
: Effect.succeed(Option.none()),
[],
{ doNotReExecuteOnRuntimeOrContextChange: true },
))
this.useFork(() => Stream.runForEach(
Stream.changesWith(stream, (x, y) => x === y),
v => Effect.sync(() => setReactStateValue(Option.some(v))),
), [stream])
return reactStateValue as InitialA extends A ? Option.Some<A> : Option.Option<A>
return reactStateValue
}
@@ -518,15 +562,30 @@ export abstract class ReffuseNamespace<R> {
return props.children(this.useRefState(props.ref))
}
SubscribeStream<A, InitialA extends A | undefined, E, R>(
SubscribeStream<A, E, R>(
this: ReffuseNamespace<R>,
props: {
readonly stream: Stream.Stream<A, E, R>
readonly initialValue?: InitialA
readonly children: (latestValue: InitialA extends A ? Option.Some<A> : Option.Option<A>) => React.ReactNode
readonly children: (latestValue: Option.Option<A>) => React.ReactNode
},
): React.ReactNode
SubscribeStream<A, E, IE, R>(
this: ReffuseNamespace<R>,
props: {
readonly stream: Stream.Stream<A, E, R>
readonly initialValue: () => Effect.Effect<A, IE, R>
readonly children: (latestValue: Option.Some<A>) => React.ReactNode
},
): React.ReactNode
SubscribeStream<A, E, IE, R>(
this: ReffuseNamespace<R>,
props: {
readonly stream: Stream.Stream<A, E, R>
readonly initialValue?: () => Effect.Effect<A, IE, R>
readonly children: (latestValue: Option.Some<A>) => React.ReactNode
},
): React.ReactNode {
return props.children(this.useSubscribeStream(props.stream, props.initialValue))
return props.children(this.useSubscribeStream(props.stream, props.initialValue as () => Effect.Effect<A, IE, R>))
}
}