Skip to content

Commit 851b7b9

Browse files
committed
feat: add useValue and useService
simpler hooks for simpler folk Signed-off-by: Yuval Datner <[email protected]>
1 parent d78bdb2 commit 851b7b9

File tree

5 files changed

+118
-3
lines changed

5 files changed

+118
-3
lines changed

src/RuntimeProvider.ts

+30-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"use client"
2+
import type * as Context from "@effect/data/Context"
23
import type { LazyArg } from "@effect/data/Function"
34
import { pipe } from "@effect/data/Function"
45
import * as Effect from "@effect/io/Effect"
@@ -8,6 +9,8 @@ import * as Scope from "@effect/io/Scope"
89
import type * as Stream from "@effect/stream/Stream"
910
import * as internalUseResult from "effect-react/internal/hooks/useResult"
1011
import * as internalUseResultCallback from "effect-react/internal/hooks/useResultCallback"
12+
import * as internalUseService from "effect-react/internal/hooks/useService"
13+
import * as internalUseValue from "effect-react/internal/hooks/useValue"
1114
import type * as ResultBag from "effect-react/ResultBag"
1215
import type { DependencyList } from "react"
1316
import { createContext } from "react"
@@ -35,6 +38,22 @@ export type UseResultCallback<R> = <Args extends Array<any>, R0 extends R, E, A>
3538
f: (...args: Args) => Stream.Stream<R0, E, A>
3639
) => readonly [ResultBag.ResultBag<E, A>, (...args: Args) => void]
3740

41+
/**
42+
* @since 1.0.0
43+
* @category hooks
44+
*/
45+
export type UseValue<R> = <R0 extends R, A>(
46+
stream: Stream.Stream<R0, never, A>,
47+
initial: A,
48+
deps: DependencyList
49+
) => A
50+
51+
/**
52+
* @since 1.0.0
53+
* @category hooks
54+
*/
55+
export type UseService<R> = <Tag extends Context.ValidTagsById<R>>(tag: Tag) => Context.Tag.Service<Tag>
56+
3857
/**
3958
* @since 1.0.0
4059
* @category models
@@ -43,6 +62,8 @@ export interface ReactEffectBag<R> {
4362
readonly RuntimeContext: React.Context<Runtime.Runtime<R>>
4463
readonly useResultCallback: UseResultCallback<R>
4564
readonly useResult: UseResult<R>
65+
readonly useValue: UseValue<R>
66+
readonly useService: UseService<R>
4667
}
4768

4869
/**
@@ -65,7 +86,9 @@ export const makeFromLayer = <R, E>(
6586
return {
6687
RuntimeContext,
6788
useResultCallback: internalUseResultCallback.make(RuntimeContext),
68-
useResult: internalUseResult.make(RuntimeContext)
89+
useResult: internalUseResult.make(RuntimeContext),
90+
useValue: internalUseValue.make(RuntimeContext),
91+
useService: internalUseService.make(RuntimeContext)
6992
}
7093
}
7194

@@ -81,7 +104,9 @@ export const makeFromRuntime = <R>(
81104
return {
82105
RuntimeContext,
83106
useResultCallback: internalUseResultCallback.make(RuntimeContext),
84-
useResult: internalUseResult.make(RuntimeContext)
107+
useResult: internalUseResult.make(RuntimeContext),
108+
useValue: internalUseValue.make(RuntimeContext),
109+
useService: internalUseService.make(RuntimeContext)
85110
}
86111
}
87112

@@ -95,6 +120,8 @@ export const makeFromRuntimeContext = <R>(
95120
return {
96121
RuntimeContext,
97122
useResultCallback: internalUseResultCallback.make(RuntimeContext),
98-
useResult: internalUseResult.make(RuntimeContext)
123+
useResult: internalUseResult.make(RuntimeContext),
124+
useValue: internalUseValue.make(RuntimeContext),
125+
useService: internalUseService.make(RuntimeContext)
99126
}
100127
}

src/internal/hooks/useService.ts

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import * as Context from "@effect/data/Context"
2+
import type * as RuntimeProvider from "effect-react/RuntimeProvider"
3+
import { useContext, useRef } from "react"
4+
5+
export const make: <R>(
6+
runtimeContext: RuntimeProvider.RuntimeContext<R>
7+
) => RuntimeProvider.UseService<R> = <R>(runtimeContext: RuntimeProvider.RuntimeContext<R>) => {
8+
return <Tag extends Context.ValidTagsById<R>>(tag: Tag) => {
9+
const runtime = useContext(runtimeContext)
10+
const serviceRef = useRef(Context.get(runtime.context, tag))
11+
12+
return serviceRef.current
13+
}
14+
}

src/internal/hooks/useValue.ts

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import * as Option from "@effect/data/Option"
2+
import type * as Stream from "@effect/stream/Stream"
3+
import * as internalUseResult from "effect-react/internal/hooks/useResult"
4+
import * as Result from "effect-react/Result"
5+
import type * as RuntimeProvider from "effect-react/RuntimeProvider"
6+
import { type DependencyList, useRef } from "react"
7+
8+
export const make: <R>(
9+
runtimeContext: RuntimeProvider.RuntimeContext<R>
10+
) => RuntimeProvider.UseValue<R> = <R>(runtimeContext: RuntimeProvider.RuntimeContext<R>) => {
11+
const useResult = internalUseResult.make(runtimeContext)
12+
return <R0 extends R, A>(stream: Stream.Stream<R0, never, A>, initial: A, deps: DependencyList) => {
13+
const { result } = useResult(() => stream, deps)
14+
const initialRef = useRef(initial)
15+
16+
return Option.getOrElse(Result.getValue(result), () => initialRef.current)
17+
}
18+
}

test/hooks/useService.ts

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import * as Context from "@effect/data/Context"
2+
import * as Layer from "@effect/io/Layer"
3+
import { renderHook } from "@testing-library/react"
4+
import * as RuntimeProvider from "effect-react/RuntimeProvider"
5+
import { describe, expect, it } from "vitest"
6+
7+
interface Foo {
8+
value: number
9+
}
10+
const foo = Context.Tag<Foo>()
11+
12+
const { useService } = RuntimeProvider.makeFromLayer(Layer.succeed(foo, { value: 1 }))
13+
14+
describe("useService", () => {
15+
it("should get service", async () => {
16+
const { result } = renderHook(() => useService(foo))
17+
expect(result.current.value).toBe(1)
18+
})
19+
})

test/hooks/useValue.ts

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import * as Context from "@effect/data/Context"
2+
import * as Effect from "@effect/io/Effect"
3+
import * as Layer from "@effect/io/Layer"
4+
import * as Stream from "@effect/stream/Stream"
5+
import { renderHook, waitFor } from "@testing-library/react"
6+
import * as RuntimeProvider from "effect-react/RuntimeProvider"
7+
import { describe, expect, it } from "vitest"
8+
9+
interface Foo {
10+
value: number
11+
}
12+
const foo = Context.Tag<Foo>()
13+
14+
const { useValue } = RuntimeProvider.makeFromLayer(Layer.succeed(foo, { value: 1 }))
15+
16+
describe("useValue", () => {
17+
it("should run effects", async () => {
18+
const testEffect = Effect.succeed(1)
19+
const { result } = renderHook(() => useValue(testEffect, 0, []))
20+
expect(result.current).toBe(0)
21+
await waitFor(() => expect(result.current).toBe(1))
22+
})
23+
24+
it("should provide context", async () => {
25+
const testEffect = Effect.map(foo, (_) => _.value)
26+
const { result } = renderHook(() => useValue(testEffect, 0, []))
27+
expect(result.current).toBe(0)
28+
await waitFor(() => expect(result.current).toBe(1))
29+
})
30+
31+
it("should run streams", async () => {
32+
const testStream = Stream.succeed(1)
33+
const { result } = renderHook(() => useValue(testStream, 0, []))
34+
expect(result.current).toBe(0)
35+
await waitFor(() => expect(result.current).toBe(1))
36+
})
37+
})

0 commit comments

Comments
 (0)