Skip to content

Commit f81b65d

Browse files
committed
add deps repeater to hooks
1 parent c3cc5b1 commit f81b65d

File tree

2 files changed

+123
-59
lines changed

2 files changed

+123
-59
lines changed

src/__tests__/react-hooks.ts

Lines changed: 77 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,31 @@ import { Repeater } from "@repeaterjs/repeater";
22
import { act, renderHook } from "@testing-library/react-hooks";
33
import { useAsyncIter, useRepeater, useResult } from "../react-hooks";
44

5+
describe("useRepeater", () => {
6+
test("basic", async () => {
7+
const { result } = renderHook(() => {
8+
return useRepeater();
9+
});
10+
11+
const [repeater, push, stop] = result.current;
12+
expect(repeater).toBeDefined();
13+
expect(push).toBeDefined();
14+
expect(stop).toBeDefined();
15+
push(1);
16+
expect(await repeater.next()).toEqual({ value: 1, done: false });
17+
push(2);
18+
expect(await repeater.next()).toEqual({ value: 2, done: false });
19+
push(3);
20+
push(4);
21+
expect(await repeater.next()).toEqual({ value: 3, done: false });
22+
expect(await repeater.next()).toEqual({ value: 4, done: false });
23+
stop();
24+
expect(await repeater.next()).toEqual({ done: true });
25+
});
26+
});
27+
528
describe("useAsyncIter", () => {
6-
test("render", async () => {
29+
test("basic", async () => {
730
const callback = jest.fn(async function*(): AsyncIterableIterator<number> {
831
yield 1;
932
await new Promise((resolve) => setTimeout(resolve, 100));
@@ -40,14 +63,37 @@ describe("useAsyncIter", () => {
4063
return useAsyncIter(callback);
4164
});
4265

66+
expect(await result.current.next()).toEqual({ value: 1, done: false });
4367
unmount();
4468
expect(await result.current.next()).toEqual({ done: true });
4569
expect(callback).toHaveBeenCalledTimes(1);
4670
});
71+
72+
test("deps", async () => {
73+
const callback = jest.fn(async function*(
74+
deps: AsyncIterableIterator<[number]>
75+
): AsyncIterableIterator<number> {
76+
for await (const [num] of deps) {
77+
yield num ** 2;
78+
}
79+
});
80+
81+
const { result, rerender } = renderHook((props) => {
82+
return useAsyncIter(callback, [props.num]);
83+
}, {initialProps: {num: 1}});
84+
85+
expect(await result.current.next()).toEqual({ value: 1, done: false });
86+
rerender({ num: 2 });
87+
expect(await result.current.next()).toEqual({ value: 4, done: false });
88+
rerender({ num: 3 });
89+
rerender({ num: 4 });
90+
expect(await result.current.next()).toEqual({ value: 9, done: false });
91+
expect(await result.current.next()).toEqual({ value: 16, done: false });
92+
});
4793
});
4894

4995
describe("useResult", () => {
50-
test("render", async () => {
96+
test("basic", async () => {
5197
let push: (value: number) => Promise<void>;
5298
let stop: (() => void) & Promise<void>;
5399
const repeater = new Repeater(async (push1, stop1) => {
@@ -97,36 +143,41 @@ describe("useResult", () => {
97143
expect(result.current).toBeUndefined();
98144
await act(() => push(1));
99145
expect(result.current).toEqual({ value: 1, done: false });
100-
await act(() => push(2));
101-
expect(result.current).toEqual({ value: 2, done: false });
102-
await act(() => push(3));
103-
expect(result.current).toEqual({ value: 3, done: false });
104146
unmount();
105-
expect(result.current).toEqual({ value: 3, done: false });
106147
expect(callback).toHaveBeenCalledTimes(1);
107148
expect(returnSpy).toHaveBeenCalledTimes(1);
108149
});
109-
});
110150

111-
describe("useRepeater", () => {
112-
test("render", async () => {
113-
const { result } = renderHook(() => {
114-
return useRepeater();
151+
test("deps", async () => {
152+
const callback = jest.fn((deps) => {
153+
return new Repeater<number>(async (push1) => {
154+
const push = async (num: number) => {
155+
return act(() => push1(num));
156+
};
157+
158+
for await (const [num] of deps) {
159+
await push(num);
160+
}
161+
162+
return -1;
163+
});
115164
});
116165

117-
const [repeater, push, stop] = result.current;
118-
expect(repeater).toBeDefined();
119-
expect(push).toBeDefined();
120-
expect(stop).toBeDefined();
121-
push(1);
122-
push(2);
123-
push(3);
124-
push(4);
125-
stop();
126-
expect(await repeater.next()).toEqual({ value: 1, done: false });
127-
expect(await repeater.next()).toEqual({ value: 2, done: false });
128-
expect(await repeater.next()).toEqual({ value: 3, done: false });
129-
expect(await repeater.next()).toEqual({ value: 4, done: false });
130-
expect(await repeater.next()).toEqual({ done: true });
166+
const { result, rerender, waitForNextUpdate } = renderHook((props) => {
167+
return useResult(callback, [props.num]);
168+
}, { initialProps: { num: 1 } });
169+
170+
expect(result.current).toBeUndefined();
171+
await waitForNextUpdate();
172+
expect(result.current).toEqual({ value: 1, done: false });
173+
rerender({ num: 2 });
174+
expect(result.current).toEqual({ value: 1, done: false });
175+
await waitForNextUpdate();
176+
expect(result.current).toEqual({ value: 2, done: false });
177+
rerender({ num: 3 });
178+
expect(result.current).toEqual({ value: 2, done: false });
179+
await waitForNextUpdate();
180+
expect(result.current).toEqual({ value: 3, done: false });
181+
expect(callback).toHaveBeenCalledTimes(1);
131182
});
132183
});

src/react-hooks.ts

Lines changed: 46 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,61 @@
11
import { useEffect, useState } from "react";
22
import { Push, Repeater, RepeaterBuffer, Stop } from "@repeaterjs/repeater";
33

4-
export function useAsyncIter<T>(
5-
callback: () => AsyncIterableIterator<T>,
4+
// Repeaters are lazy, hooks are eager.
5+
// We need to return push and stop synchronously from the useRepeater hook so
6+
// we prime the repeater by calling next immediately.
7+
function createPrimedRepeater<T>(
8+
buffer?: RepeaterBuffer<T>,
9+
): [Repeater<T>, Push<T>, Stop] {
10+
let push: Push<T>;
11+
let stop: Stop;
12+
const repeater = new Repeater((push1, stop1) => {
13+
push = push1;
14+
stop = stop1;
15+
// this value is thrown away
16+
push(null as any);
17+
}, buffer);
18+
// pull and throw away the first value so the executor above runs
19+
repeater.next();
20+
return [repeater, push!, stop!];
21+
}
22+
23+
export function useRepeater<T>(
24+
buffer?: RepeaterBuffer<T>,
25+
): [Repeater<T>, Push<T>, Stop] {
26+
const [tuple] = useState(() => createPrimedRepeater(buffer));
27+
return tuple;
28+
}
29+
30+
export function useAsyncIter<T, TDeps extends any[]>(
31+
callback: (deps: AsyncIterableIterator<TDeps>) => AsyncIterableIterator<T>,
32+
deps: TDeps = [] as unknown as TDeps,
633
): AsyncIterableIterator<T> {
7-
const [iter] = useState(() => callback());
34+
const [repeater, push, stop] = useRepeater<TDeps>();
35+
const [iter] = useState(() => callback(repeater));
36+
useEffect(() => {
37+
push(deps);
38+
}, [...deps]);
39+
840
useEffect(
9-
() => () => {
10-
if (iter.return != null) {
11-
// TODO: handle return errors
12-
iter.return().catch();
13-
}
14-
},
41+
() => () => {
42+
stop();
43+
if (iter.return != null) {
44+
// TODO: handle return errors
45+
iter.return().catch();
46+
}
47+
},
1548
[],
1649
);
1750

1851
return iter;
1952
}
2053

21-
export function useResult<T>(
22-
callback: () => AsyncIterableIterator<T>,
54+
export function useResult<T, TDeps extends any[]>(
55+
callback: (deps: AsyncIterableIterator<TDeps>) => AsyncIterableIterator<T>,
56+
deps?: TDeps,
2357
): IteratorResult<T> | undefined {
24-
const iter = useAsyncIter(callback);
58+
const iter = useAsyncIter(callback, deps);
2559
const [result, setResult] = useState<IteratorResult<T>>();
2660
useEffect(() => {
2761
let mounted = true;
@@ -53,24 +87,3 @@ export function useResult<T>(
5387

5488
return result;
5589
}
56-
57-
export function useRepeater<T>(
58-
buffer?: RepeaterBuffer<T>,
59-
): [Repeater<T>, Push<T>, Stop] {
60-
let push: Push<T>;
61-
let stop: Stop;
62-
const [repeater] = useState(() => {
63-
const repeater = new Repeater((push1, stop1) => {
64-
push = push1;
65-
stop = stop1;
66-
}, buffer);
67-
68-
// We pull the first value so that the executor runs.
69-
repeater.next();
70-
// The first value (null) is thrown away.
71-
push(null as any);
72-
return repeater;
73-
});
74-
75-
return [repeater, push!, stop!];
76-
}

0 commit comments

Comments
 (0)