Skip to content

Commit 08a664d

Browse files
authored
feat(useAsyncIterState): support setting an initial value via an argument (#46)
1 parent 392b2e9 commit 08a664d

File tree

3 files changed

+74
-24
lines changed

3 files changed

+74
-24
lines changed

spec/tests/useAsyncIterState.spec.tsx

+37-8
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,43 @@ afterEach(() => {
1414
});
1515

1616
describe('`useAsyncIterState` hook', () => {
17+
it(gray("The state iterable's `.current.value` property is read-only"), async () => {
18+
const [values] = renderHook(() => useAsyncIterState<string>()).result.current;
19+
20+
expect(() => {
21+
(values.value as any).current = '...';
22+
}).toThrow(TypeError);
23+
});
24+
25+
describe(
26+
gray(
27+
"When a non-`undefined` initial value is given, it's set as the starting value for the iterable's `.value.current` property"
28+
),
29+
() => {
30+
for (const [desc, initVal] of [
31+
['As a plain value', { initial: true } as const],
32+
['As a function', () => ({ initial: true }) as const],
33+
] as const) {
34+
it(gray(desc), async () => {
35+
const [values, setValue] = renderHook(() =>
36+
useAsyncIterState<string, { initial: true }>(initVal)
37+
).result.current;
38+
39+
const currentValues = [values.value.current];
40+
const yieldPromise = pipe(values, asyncIterTakeFirst());
41+
42+
await act(() => {
43+
setValue('a');
44+
currentValues.push(values.value.current);
45+
});
46+
47+
expect(await yieldPromise).toStrictEqual('a');
48+
expect(currentValues).toStrictEqual([{ initial: true }, 'a']);
49+
});
50+
}
51+
}
52+
);
53+
1754
it(gray('The returned iterable can be async-iterated upon successfully'), async () => {
1855
const [values, setValue] = renderHook(() => useAsyncIterState<string>()).result.current;
1956

@@ -243,12 +280,4 @@ describe('`useAsyncIterState` hook', () => {
243280
expect(currentValues).toStrictEqual([undefined, 'a', 'b', 'c']);
244281
}
245282
);
246-
247-
it(gray("The state iterable's `.current.value` property is read-only"), async () => {
248-
const [values] = renderHook(() => useAsyncIterState<number>()).result.current;
249-
250-
expect(() => {
251-
(values.value as any).current = `CAN'T DO THIS...`;
252-
}).toThrow(TypeError);
253-
});
254283
});

src/useAsyncIterState/IterableChannel.ts

+11-7
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,22 @@ import { promiseWithResolvers } from '../common/promiseWithResolvers.js';
33

44
export { IterableChannel, type AsyncIterableSubject };
55

6-
class IterableChannel<T> {
6+
class IterableChannel<T, TInit = T> {
77
#isClosed = false;
88
#nextIteration = promiseWithResolvers<IteratorResult<T, void>>();
9-
#currentValue: T | undefined;
9+
#currentValue: T | TInit;
1010

11-
put(update: T | ((prevState: T | undefined) => T)): void {
11+
constructor(initialValue: TInit) {
12+
this.#currentValue = initialValue;
13+
}
14+
15+
put(update: T | ((prevState: T | TInit) => T)): void {
1216
if (!this.#isClosed) {
1317
const value =
1418
typeof update !== 'function'
1519
? update
1620
: (() => {
17-
const updateFnTypePatched = update as (prevState: T | undefined) => T;
21+
const updateFnTypePatched = update as (prevState: T | TInit) => T;
1822
return updateFnTypePatched(this.#currentValue);
1923
})();
2024

@@ -32,7 +36,7 @@ class IterableChannel<T> {
3236
this.#nextIteration.resolve({ done: true, value: undefined });
3337
}
3438

35-
values: AsyncIterableSubject<T> = {
39+
values: AsyncIterableSubject<T, TInit> = {
3640
value: (() => {
3741
const self = this;
3842
return {
@@ -65,11 +69,11 @@ class IterableChannel<T> {
6569
* meaning that multiple iterators can be consumed (iterated) simultaneously and each one would pick up
6670
* the same values as others the moment they were generated through state updates.
6771
*/
68-
type AsyncIterableSubject<T> = {
72+
type AsyncIterableSubject<T, TInit> = {
6973
/**
7074
* A React Ref-like object whose inner `current` property shows the most up to date state value.
7175
*/
72-
value: Readonly<MutableRefObject<T | undefined>>;
76+
value: Readonly<MutableRefObject<T | TInit>>;
7377

7478
/**
7579
* Returns an async iterator to iterate over. All iterators returned by this share the same source

src/useAsyncIterState/index.ts

+26-9
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,6 @@ export { useAsyncIterState, type AsyncIterStateResult, type AsyncIterableSubject
3636
*
3737
* ---
3838
*
39-
*
40-
*
4139
* The returned async iterable can be passed over to any level down the component tree and rendered
4240
* using `<Iterate>`, `useAsyncIter`, and so on. It also contains a `.current.value` property which shows
4341
* the current up to date state value at all times. Use this any case you just need to read the immediate
@@ -88,19 +86,38 @@ export { useAsyncIterState, type AsyncIterStateResult, type AsyncIterableSubject
8886
* ---
8987
*
9088
* @template TVal the type of state to be set and yielded by returned iterable.
89+
* @template TInitVal The type of the starting value for the state iterable's `.current.value` property.
90+
*
91+
* @param initialValue Any optional starting value for the state iterable's `.current.value` property, defaults to `undefined`.
9192
*
9293
* @returns a stateful async iterable and a function with which to yield an update, both maintain stable references across re-renders.
9394
*
9495
* @see {@link Iterate `<Iterate>`}
9596
*/
96-
function useAsyncIterState<TVal>(): AsyncIterStateResult<TVal> {
97+
function useAsyncIterState<TVal>(): AsyncIterStateResult<TVal, undefined>;
98+
99+
function useAsyncIterState<TVal>(
100+
initialValue: TVal | (() => TVal)
101+
): AsyncIterStateResult<TVal, TVal>;
102+
103+
function useAsyncIterState<TVal, TInitVal = undefined>(
104+
initialValue: TInitVal | (() => TInitVal)
105+
): AsyncIterStateResult<TVal, TInitVal>;
106+
107+
function useAsyncIterState<TVal, TInitVal>(
108+
initialValue?: TInitVal | (() => TInitVal)
109+
): AsyncIterStateResult<TVal, TInitVal> {
97110
const ref = useRef<{
98-
channel: IterableChannel<TVal>;
99-
result: AsyncIterStateResult<TVal>;
111+
channel: IterableChannel<TVal, TInitVal>;
112+
result: AsyncIterStateResult<TVal, TInitVal>;
100113
}>();
101114

102115
ref.current ??= (() => {
103-
const channel = new IterableChannel<TVal>();
116+
const initialValueDetermined =
117+
typeof initialValue !== 'function' ? initialValue : (initialValue as () => TInitVal)();
118+
119+
const channel = new IterableChannel<TVal, TInitVal>(initialValueDetermined as TInitVal);
120+
104121
return {
105122
channel,
106123
result: [channel.values, newVal => channel.put(newVal)],
@@ -123,7 +140,7 @@ function useAsyncIterState<TVal>(): AsyncIterStateResult<TVal> {
123140
*
124141
* @see {@link useAsyncIterState `useAsyncIterState`}
125142
*/
126-
type AsyncIterStateResult<TVal> = [
143+
type AsyncIterStateResult<TVal, TInitVal> = [
127144
/**
128145
* A stateful async iterable which yields every updated value following a state update.
129146
*
@@ -133,11 +150,11 @@ type AsyncIterStateResult<TVal> = [
133150
* meaning multiple iterators can be consumed (iterated) simultaneously, each one picking up the
134151
* same values as others the moment they were generated through state updates.
135152
*/
136-
values: AsyncIterableSubject<TVal>,
153+
values: AsyncIterableSubject<TVal, TInitVal>,
137154

138155
/**
139156
* A function which updates the state, causing the paired async iterable to yield the updated state
140157
* value and immediately sets its `.current.value` property to the latest state.
141158
*/
142-
setValue: (update: TVal | ((prevState: TVal | undefined) => TVal)) => void,
159+
setValue: (update: TVal | ((prevState: TVal | TInitVal) => TVal)) => void,
143160
];

0 commit comments

Comments
 (0)