Skip to content

Commit 57056b2

Browse files
authored
feat(useAsyncIterMulti): support getting initial values as initializer functions (#70)
1 parent d9260c1 commit 57056b2

File tree

3 files changed

+137
-17
lines changed

3 files changed

+137
-17
lines changed

README.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -990,11 +990,10 @@ const [nextNum, nextStr, nextArr] = useAsyncIterMulti([numberIter, stringIter, a
990990
An _optional_ object with properties:
991991

992992
- `initialValues`:
993-
An _optional_ array of initial values. The values here will be the starting points for all the async iterables from `values` (by corresponding array positions) while they are rendered by the `children` render function __for the first time__ and for each while it is __pending its first yield__. Async iterables from `values` that have no initial value corresponding to them will assume `undefined` as initial value.
993+
An _optional_ array of initial values or functions that return initial values. The values will be the starting points for all the async iterables from `values` (by corresponding array positions) __for the first time__ and for each while it is __pending its first yield__. For async iterables from `values` that have no corresponding item here the provided `opts.defaultInitialValue` will be used as fallback.
994994

995995
- `defaultInitialValue`:
996-
An _optional_ default starting value for every new async iterable in `values` if there is no corresponding one for it in `opts.initialValues`, defaults to `undefined`.
997-
996+
An _optional_ default starting value for every new async iterable in `values` if there is no corresponding one for it in `opts.initialValues`, defaults to `undefined`. You can pass an actual value, or a function that returns a value (which the hook will call for every new iterable added).
998997

999998
### Returns
1000999

spec/tests/useAsyncIterMulti.spec.ts

Lines changed: 112 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { it, describe, expect, afterEach } from 'vitest';
1+
import { it, describe, expect, afterEach, vi } from 'vitest';
22
import { gray } from 'colorette';
33
import { cleanup as cleanupMountedReactTrees, act, renderHook } from '@testing-library/react';
44
import { iterateFormatted, useAsyncIterMulti } from '../../src/index.js';
@@ -165,6 +165,99 @@ describe('`useAsyncIterMulti` hook', () => {
165165
}
166166
);
167167

168+
it(
169+
gray(
170+
'When given multiple iterables with a default initial value as a function, calls it once on every added source iterable'
171+
),
172+
async () => {
173+
const channels = [
174+
new IteratorChannelTestHelper<string>(),
175+
new IteratorChannelTestHelper<string>(),
176+
];
177+
const initialValueFn = vi.fn(() => '___');
178+
let timesRerendered = 0;
179+
180+
const renderedHook = renderHook(() => {
181+
timesRerendered++;
182+
return useAsyncIterMulti(channels, { defaultInitialValue: initialValueFn });
183+
});
184+
185+
await act(() => {});
186+
expect(timesRerendered).toStrictEqual(1);
187+
expect(renderedHook.result.current).toStrictEqual([
188+
{ value: '___', pendingFirst: true, done: false, error: undefined },
189+
{ value: '___', pendingFirst: true, done: false, error: undefined },
190+
]);
191+
192+
await act(() => {
193+
channels[0].put('a');
194+
channels[1].put('b');
195+
});
196+
expect(timesRerendered).toStrictEqual(2);
197+
expect(renderedHook.result.current).toStrictEqual([
198+
{ value: 'a', pendingFirst: false, done: false, error: undefined },
199+
{ value: 'b', pendingFirst: false, done: false, error: undefined },
200+
]);
201+
202+
await act(() => {
203+
channels.push(new IteratorChannelTestHelper());
204+
renderedHook.rerender();
205+
});
206+
expect(timesRerendered).toStrictEqual(3);
207+
expect(renderedHook.result.current).toStrictEqual([
208+
{ value: 'a', pendingFirst: false, done: false, error: undefined },
209+
{ value: 'b', pendingFirst: false, done: false, error: undefined },
210+
{ value: '___', pendingFirst: true, done: false, error: undefined },
211+
]);
212+
expect(initialValueFn).toHaveBeenCalledTimes(3);
213+
}
214+
);
215+
216+
it(
217+
gray(
218+
'When given multiple iterables with initial values as a functions, calls each once when a corresponding iterable is added'
219+
),
220+
async () => {
221+
const channels = [new IteratorChannelTestHelper<string>()];
222+
const [initialValueFn1, initialValueFn2] = [vi.fn(), vi.fn()];
223+
let timesRerendered = 0;
224+
225+
const renderedHook = renderHook(() => {
226+
timesRerendered++;
227+
return useAsyncIterMulti(channels, {
228+
initialValues: [
229+
initialValueFn1.mockImplementation(() => '_1_'),
230+
initialValueFn2.mockImplementation(() => '_2_'),
231+
],
232+
});
233+
});
234+
235+
await act(() => {});
236+
expect(timesRerendered).toStrictEqual(1);
237+
expect(renderedHook.result.current).toStrictEqual([
238+
{ value: '_1_', pendingFirst: true, done: false, error: undefined },
239+
]);
240+
241+
await act(() => channels[0].put('a'));
242+
expect(timesRerendered).toStrictEqual(2);
243+
expect(renderedHook.result.current).toStrictEqual([
244+
{ value: 'a', pendingFirst: false, done: false, error: undefined },
245+
]);
246+
247+
await act(() => {
248+
channels.push(new IteratorChannelTestHelper());
249+
renderedHook.rerender();
250+
});
251+
expect(timesRerendered).toStrictEqual(3);
252+
expect(renderedHook.result.current).toStrictEqual([
253+
{ value: 'a', pendingFirst: false, done: false, error: undefined },
254+
{ value: '_2_', pendingFirst: true, done: false, error: undefined },
255+
]);
256+
expect(initialValueFn1).toHaveBeenCalledOnce();
257+
expect(initialValueFn2).toHaveBeenCalledOnce();
258+
}
259+
);
260+
168261
it(
169262
gray(
170263
"When given multiple iterables with corresponding initial values for some and a default initial value, reflects each's state correctly, starting with its corresponding initial value or the default initial value if not present"
@@ -173,14 +266,19 @@ describe('`useAsyncIterMulti` hook', () => {
173266
const channels = [
174267
new IteratorChannelTestHelper<string>(),
175268
new IteratorChannelTestHelper<string>(),
176-
] as const;
269+
];
177270
let timesRerendered = 0;
178271

179272
const renderedHook = renderHook(() => {
180273
timesRerendered++;
181-
return useAsyncIterMulti(channels, { initialValues: ['_1_'], defaultInitialValue: '___' });
274+
return useAsyncIterMulti(channels, {
275+
initialValues: [() => '_1_' as const] as const,
276+
defaultInitialValue: () => '___' as const,
277+
});
182278
});
183279

280+
renderedHook.result.current[0].value;
281+
184282
await act(() => {});
185283
expect(timesRerendered).toStrictEqual(1);
186284
expect(renderedHook.result.current).toStrictEqual([
@@ -201,6 +299,17 @@ describe('`useAsyncIterMulti` hook', () => {
201299
{ value: 'a', pendingFirst: false, done: false, error: undefined },
202300
{ value: 'b', pendingFirst: false, done: false, error: undefined },
203301
]);
302+
303+
await act(() => {
304+
channels.push(new IteratorChannelTestHelper());
305+
renderedHook.rerender();
306+
});
307+
expect(timesRerendered).toStrictEqual(4);
308+
expect(renderedHook.result.current).toStrictEqual([
309+
{ value: 'a', pendingFirst: false, done: false, error: undefined },
310+
{ value: 'b', pendingFirst: false, done: false, error: undefined },
311+
{ value: '___', pendingFirst: true, done: false, error: undefined },
312+
]);
204313
}
205314
);
206315

src/useAsyncIterMulti/index.ts

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { useRefWithInitialValue } from '../common/hooks/useRefWithInitialValue.j
44
import { isAsyncIter } from '../common/isAsyncIter.js';
55
import { type IterationResult } from '../useAsyncIter/index.js';
66
import { type AsyncIterableSubject } from '../AsyncIterableSubject/index.js';
7+
import { type MaybeFunction } from '../common/MaybeFunction.js';
8+
import { callOrReturn } from '../common/callOrReturn.js';
79
import { asyncIterSyncMap } from '../common/asyncIterSyncMap.js';
810
import { parseReactAsyncIterable } from '../common/ReactAsyncIterable.js';
911
import { iterateAsyncIterWithCallbacks } from '../common/iterateAsyncIterWithCallbacks.js';
@@ -79,8 +81,8 @@ export { useAsyncIterMulti, type IterationResult, type IterationResultSet };
7981
*
8082
* @param inputs An array of zero or more async iterable or plain values (mixable).
8183
* @param {object} opts An _optional_ object with options.
82-
* @param opts.initialValues An _optional_ array of initial values, each item of which is a starting value for the async iterable from `inputs` on the same array position. For every async iterable that has no corresponding item in this array, it would use the provided `opts.defaultInitialValue` as fallback.
83-
* @param opts.defaultInitialValue An _optional_ default starting value for every new async iterable in `inputs` if there is no corresponding one for it in `opts.initialValues`, defaults to `undefined`.
84+
* @param opts.initialValues An _optional_ array of initial values or functions that return initial values, each item of which is a starting value for the async iterable from `inputs` on the same array position. For every async iterable that has no corresponding item here, the provided `opts.defaultInitialValue` will be used as fallback.
85+
* @param opts.defaultInitialValue An _optional_ default starting value for every new async iterable in `inputs` if there is no corresponding one for it in `opts.initialValues`, defaults to `undefined`. You can pass an actual value, or a function that returns a value (which the hook will call for every new iterable added).
8486
*
8587
* @returns An array of objects that provide up-to-date information about each input's current value, completion status, whether it's still waiting for its first value and so on, correspondingly with the order in which they appear on `inputs` (see {@link IterationResultSet `IterationResultSet`}).
8688
*
@@ -206,12 +208,12 @@ function useAsyncIterMulti<
206208
initialValues?: TInitValues;
207209
defaultInitialValue?: TDefaultInitValue;
208210
}
209-
): IterationResultSet<TValues, TInitValues, TDefaultInitValue> {
211+
): IterationResultSet<TValues, MaybeFunctions<TInitValues>, TDefaultInitValue> {
210212
const update = useSimpleRerender();
211213

212214
const ref = useRefWithInitialValue(() => ({
213215
currDiffCompId: 0,
214-
prevResults: [] as IterationResultSet<TValues, TInitValues, TDefaultInitValue>,
216+
prevResults: [] as IterationResultSet<TValues, MaybeFunctions<TInitValues>, TDefaultInitValue>,
215217
activeItersMap: new Map<
216218
AsyncIterable<unknown>,
217219
{
@@ -233,8 +235,10 @@ function useAsyncIterMulti<
233235
};
234236
}, []);
235237

236-
const initialValues = opts?.initialValues ?? [];
237-
const defaultInitialValue = opts?.defaultInitialValue;
238+
const optsNormed = {
239+
initialValues: opts?.initialValues ?? [],
240+
defaultInitialValue: opts?.defaultInitialValue,
241+
};
238242

239243
const nextDiffCompId = (ref.current.currDiffCompId = ref.current.currDiffCompId === 0 ? 1 : 0);
240244
let numOfPrevRunItersPreserved = 0;
@@ -279,9 +283,11 @@ function useAsyncIterMulti<
279283
startingValue =
280284
i < prevResults.length
281285
? prevResults[i].value
282-
: i < initialValues.length
283-
? initialValues[i]
284-
: defaultInitialValue;
286+
: callOrReturn(
287+
i < optsNormed.initialValues.length
288+
? optsNormed.initialValues[i]
289+
: optsNormed.defaultInitialValue
290+
);
285291
pendingFirst = true;
286292
}
287293

@@ -305,7 +311,7 @@ function useAsyncIterMulti<
305311
activeItersMap.set(baseIter, newIterState);
306312

307313
return newIterState.currState;
308-
}) as IterationResultSet<TValues, TInitValues, TDefaultInitValue>;
314+
}) as IterationResultSet<TValues, MaybeFunctions<TInitValues>, TDefaultInitValue>;
309315

310316
const numOfPrevRunItersDisappeared = numOfPrevRunIters - numOfPrevRunItersPreserved;
311317

@@ -322,7 +328,9 @@ function useAsyncIterMulti<
322328
}
323329
}
324330

325-
return (ref.current.prevResults = nextResults);
331+
ref.current.prevResults = nextResults;
332+
333+
return nextResults;
326334
}
327335

328336
type IterationResultSet<
@@ -335,3 +343,7 @@ type IterationResultSet<
335343
I extends keyof TInitValues ? TInitValues[I] : TDefaultInitValue
336344
>;
337345
};
346+
347+
type MaybeFunctions<T extends readonly unknown[]> = {
348+
[I in keyof T]: T[I] extends MaybeFunction<infer J> ? J : T[I];
349+
};

0 commit comments

Comments
 (0)