Skip to content

Commit 4435461

Browse files
authored
feat(ItMulti): support for handling initializer functions as initial values (#71)
1 parent 57056b2 commit 4435461

File tree

5 files changed

+138
-16
lines changed

5 files changed

+138
-16
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -759,10 +759,10 @@ It's similar to [`<It>`](#it), only it works with any changeable number of async
759759
An array of values to iterate over simultaneously, which may include any mix of async iterables or plain (non async iterable) values. Source values may be added, removed or changed at any time and new iterations will be close and started accordingly as per [Iteration lifecycle](#iteration-lifecycle).
760760

761761
- `initialValues`:
762-
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.
762+
An _optional_ array of initial values or functions that return 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.
763763

764764
- `defaultInitialValue`:
765-
An _optional_ default starting value for every new async iterable in `values` if there is no corresponding one for it in the `initialValues` prop, defaults to `undefined`.
765+
An _optional_ default starting value for every new async iterable in `values` if there is no corresponding one for it in the `initialValues` prop, 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).
766766

767767
- `children`:
768768
A render function that is called on every progression in any of the running iterations, returning something to render for them. The function is called with an array of the combined iteration state objects of all sources currently given by the `values` prop (see [Iteration state properties breakdown](#iteration-state-properties-breakdown)).

spec/tests/IterateMulti.spec.tsx

Lines changed: 118 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ describe('`IterateMulti` hook', () => {
226226
});
227227

228228
it(
229-
gray("When given multiple iterables, some empty, reflects each's states correctly"),
229+
gray("When given multiple iterables, some empty, renders each's state correctly"),
230230
async () => {
231231
const renderFn = vi.fn() as Mock<
232232
IterateMultiProps<[AsyncIterable<'a'>, AsyncIterable<never>]>['children']
@@ -257,6 +257,123 @@ describe('`IterateMulti` hook', () => {
257257
}
258258
);
259259

260+
it(
261+
gray(
262+
'When given multiple iterables with a default initial value as a function, calls it once whenever a new iterable is added'
263+
),
264+
async () => {
265+
const channels = [
266+
new IteratorChannelTestHelper<string>(),
267+
new IteratorChannelTestHelper<string>(),
268+
];
269+
const initialValueFn = vi.fn(() => '___');
270+
const renderFn = vi.fn() as Mock<
271+
(nexts: IterationResultSet<AsyncIterable<string>[], [], '___'>) => any
272+
>;
273+
274+
const Component = ({ values }: { values: AsyncIterable<string>[] }) => (
275+
<IterateMulti values={values} defaultInitialValue={initialValueFn}>
276+
{renderFn.mockImplementation(() => (
277+
<div id="test-created-elem">Render count: {renderFn.mock.calls.length}</div>
278+
))}
279+
</IterateMulti>
280+
);
281+
282+
const rendered = render(<></>);
283+
284+
await act(() => rendered.rerender(<Component values={channels} />));
285+
expect(renderFn.mock.calls).lengthOf(1);
286+
expect(renderFn.mock.lastCall?.flat()).toStrictEqual([
287+
{ value: '___', pendingFirst: true, done: false, error: undefined },
288+
{ value: '___', pendingFirst: true, done: false, error: undefined },
289+
]);
290+
expect(rendered.container.innerHTML).toStrictEqual(
291+
`<div id="test-created-elem">Render count: 1</div>`
292+
);
293+
294+
await act(() => {
295+
channels[0].put('a');
296+
channels[1].put('b');
297+
});
298+
expect(renderFn.mock.calls).lengthOf(2);
299+
expect(renderFn.mock.lastCall?.flat()).toStrictEqual([
300+
{ value: 'a', pendingFirst: false, done: false, error: undefined },
301+
{ value: 'b', pendingFirst: false, done: false, error: undefined },
302+
]);
303+
expect(rendered.container.innerHTML).toStrictEqual(
304+
`<div id="test-created-elem">Render count: 2</div>`
305+
);
306+
307+
await act(() => {
308+
channels.push(new IteratorChannelTestHelper());
309+
rendered.rerender(<Component values={channels} />);
310+
});
311+
expect(renderFn.mock.calls).lengthOf(3);
312+
expect(renderFn.mock.lastCall?.flat()).toStrictEqual([
313+
{ value: 'a', pendingFirst: false, done: false, error: undefined },
314+
{ value: 'b', pendingFirst: false, done: false, error: undefined },
315+
{ value: '___', pendingFirst: true, done: false, error: undefined },
316+
]);
317+
expect(rendered.container.innerHTML).toStrictEqual(
318+
`<div id="test-created-elem">Render count: 3</div>`
319+
);
320+
expect(initialValueFn).toHaveBeenCalledTimes(3);
321+
}
322+
);
323+
324+
it(
325+
gray(
326+
'When given multiple iterables with initial values as a functions, calls each once whenever a corresponding iterable is added'
327+
),
328+
async () => {
329+
const channels = [new IteratorChannelTestHelper<string>()];
330+
const [initialValueFn1, initialValueFn2] = [vi.fn(), vi.fn()];
331+
const renderFn = vi.fn() as Mock<
332+
(nexts: IterationResultSet<AsyncIterable<string>[], ['_1_', '_2_']>) => any
333+
>;
334+
335+
const Component = ({ values }: { values: AsyncIterable<string>[] }) => (
336+
<IterateMulti
337+
values={values}
338+
initialValues={[
339+
initialValueFn1.mockImplementation(() => '_1_'),
340+
initialValueFn2.mockImplementation(() => '_2_'),
341+
]}
342+
>
343+
{renderFn.mockImplementation(() => (
344+
<div id="test-created-elem">Render count: {renderFn.mock.calls.length}</div>
345+
))}
346+
</IterateMulti>
347+
);
348+
349+
const rendered = render(<></>);
350+
351+
await act(() => rendered.rerender(<Component values={channels} />));
352+
expect(renderFn.mock.calls).lengthOf(1);
353+
expect(renderFn.mock.lastCall?.flat()).toStrictEqual([
354+
{ value: '_1_', pendingFirst: true, done: false, error: undefined },
355+
]);
356+
357+
await act(() => channels[0].put('a'));
358+
expect(renderFn.mock.calls).lengthOf(2);
359+
expect(renderFn.mock.lastCall?.flat()).toStrictEqual([
360+
{ value: 'a', pendingFirst: false, done: false, error: undefined },
361+
]);
362+
363+
await act(() => {
364+
channels.push(new IteratorChannelTestHelper());
365+
rendered.rerender(<Component values={channels} />);
366+
});
367+
expect(renderFn.mock.calls).lengthOf(3);
368+
expect(renderFn.mock.lastCall?.flat()).toStrictEqual([
369+
{ value: 'a', pendingFirst: false, done: false, error: undefined },
370+
{ value: '_2_', pendingFirst: true, done: false, error: undefined },
371+
]);
372+
expect(initialValueFn1).toHaveBeenCalledOnce();
373+
expect(initialValueFn2).toHaveBeenCalledOnce();
374+
}
375+
);
376+
260377
it(
261378
gray(
262379
"When given multiple iterables with corresponding initial values for some and a default initial value, correctly renders each's state and corresponding initial value or the default initial value if not present"

spec/tests/useAsyncIterMulti.spec.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ describe('`useAsyncIterMulti` hook', () => {
167167

168168
it(
169169
gray(
170-
'When given multiple iterables with a default initial value as a function, calls it once on every added source iterable'
170+
'When given multiple iterables with a default initial value as a function, calls it once whenever a new iterable is added'
171171
),
172172
async () => {
173173
const channels = [
@@ -215,7 +215,7 @@ describe('`useAsyncIterMulti` hook', () => {
215215

216216
it(
217217
gray(
218-
'When given multiple iterables with initial values as a functions, calls each once when a corresponding iterable is added'
218+
'When given multiple iterables with initial values as a functions, calls each once whenever a corresponding iterable is added'
219219
),
220220
async () => {
221221
const channels = [new IteratorChannelTestHelper<string>()];
@@ -277,8 +277,6 @@ describe('`useAsyncIterMulti` hook', () => {
277277
});
278278
});
279279

280-
renderedHook.result.current[0].value;
281-
282280
await act(() => {});
283281
expect(timesRerendered).toStrictEqual(1);
284282
expect(renderedHook.result.current).toStrictEqual([

src/IterateMulti/index.tsx

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { type ReactNode } from 'react';
22
import { type Writeable } from '../common/Writeable.js';
33
import { useAsyncIterMulti, type IterationResultSet } from '../useAsyncIterMulti/index.js';
4+
import { type MaybeFunction } from '../common/MaybeFunction.js';
45
import { type iterateFormatted } from '../iterateFormatted/index.js'; // eslint-disable-line @typescript-eslint/no-unused-vars
56

67
export { IterateMulti, type IterateMultiProps };
@@ -154,7 +155,7 @@ function IterateMulti<
154155
const TVals extends readonly unknown[],
155156
const TInitVals extends readonly unknown[] = readonly [],
156157
const TDefaultInitVal = undefined,
157-
>(props: IterateMultiProps<TVals, TInitVals, TDefaultInitVal>): ReactNode {
158+
>(props: IterateMultiProps<TVals, MaybeFunctions<TInitVals>, TDefaultInitVal>): ReactNode {
158159
const nexts = useAsyncIterMulti(props.values, {
159160
initialValues: props.initialValues,
160161
defaultInitialValue: props.defaultInitialValue,
@@ -181,18 +182,20 @@ type IterateMultiProps<
181182
values: TVals;
182183

183184
/**
184-
* An optional array of initial values. The values here will be the starting points for all the
185-
* async iterables from `values` (correspondingly by matching array positions) when they are
186-
* rendered by the `children` render function for the first time and for each while it is pending
187-
* its first yield. Async iterables from `values` that have no corresponding item in this array,
188-
* will fall back to the {@link IterateMultiProps.defaultInitialValue `defaultInitialValue`} prop
189-
* as the initial value.
185+
* An _optional_ array of initial values or functions that return initial values. These values
186+
* will be the starting points for all the async iterables from `values` (correspondingly by
187+
* matching array positions) when they are rendered by the `children` render function for the
188+
* first time and for each while it is pending its first yield. Async iterables from `values`
189+
* that have no corresponding item in this array, will fall back to the
190+
* {@link IterateMultiProps.defaultInitialValue `defaultInitialValue`} prop as the initial value.
190191
*/
191192
initialValues?: TInitVals;
192193

193194
/**
194195
* An _optional_ default starting value for every new async iterable in `values` if there is no
195-
* corresponding one for it in the `initialValues` prop, defaults to `undefined`.
196+
* corresponding one for it in the `initialValues` prop, defaults to `undefined`. You can pass
197+
* an actual value, or a function that returns a value (which the hook will call for every new
198+
* iterable added).
196199
*/
197200
defaultInitialValue?: TDefaultInitVal;
198201

@@ -210,3 +213,7 @@ type IterateMultiProps<
210213
iterationStates: IterationResultSet<Writeable<TVals>, Writeable<TInitVals>, TDefaultInitVal>
211214
) => ReactNode;
212215
};
216+
217+
type MaybeFunctions<T extends readonly unknown[]> = {
218+
[I in keyof T]: T[I] extends MaybeFunction<infer J> ? J : T[I];
219+
};

src/useAsyncIterMulti/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ export { useAsyncIterMulti, type IterationResult, type IterationResultSet };
156156
* function DynamicInputsComponent() {
157157
* const [inputs, setInputs] = useState<MaybeAsyncIterable<string>[]>([]);
158158
*
159-
* const states = useAsyncIterMulti(inputs);
159+
* const states = useAsyncIterMulti(inputs, { defaultInitialValue: '' });
160160
*
161161
* const addAsyncIterValue = () => {
162162
* const iterableValue = (async function* () {

0 commit comments

Comments
 (0)