Skip to content

Support initializer functions for init values provided to useAsyncIterMulti hook #70

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Merged
merged 2 commits into from
Jan 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -990,11 +990,10 @@ const [nextNum, nextStr, nextArr] = useAsyncIterMulti([numberIter, stringIter, a
An _optional_ object with properties:

- `initialValues`:
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.
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.

- `defaultInitialValue`:
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`.

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).

### Returns

Expand Down
115 changes: 112 additions & 3 deletions spec/tests/useAsyncIterMulti.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { it, describe, expect, afterEach } from 'vitest';
import { it, describe, expect, afterEach, vi } from 'vitest';
import { gray } from 'colorette';
import { cleanup as cleanupMountedReactTrees, act, renderHook } from '@testing-library/react';
import { iterateFormatted, useAsyncIterMulti } from '../../src/index.js';
Expand Down Expand Up @@ -165,6 +165,99 @@
}
);

it(
gray(
'When given multiple iterables with a default initial value as a function, calls it once on every added source iterable'
),
async () => {
const channels = [
new IteratorChannelTestHelper<string>(),
new IteratorChannelTestHelper<string>(),
];
const initialValueFn = vi.fn(() => '___');
let timesRerendered = 0;

const renderedHook = renderHook(() => {
timesRerendered++;
return useAsyncIterMulti(channels, { defaultInitialValue: initialValueFn });
});

await act(() => {});
expect(timesRerendered).toStrictEqual(1);
expect(renderedHook.result.current).toStrictEqual([
{ value: '___', pendingFirst: true, done: false, error: undefined },
{ value: '___', pendingFirst: true, done: false, error: undefined },
]);

await act(() => {
channels[0].put('a');
channels[1].put('b');
});
expect(timesRerendered).toStrictEqual(2);
expect(renderedHook.result.current).toStrictEqual([
{ value: 'a', pendingFirst: false, done: false, error: undefined },
{ value: 'b', pendingFirst: false, done: false, error: undefined },
]);

await act(() => {
channels.push(new IteratorChannelTestHelper());
renderedHook.rerender();
});
expect(timesRerendered).toStrictEqual(3);
expect(renderedHook.result.current).toStrictEqual([
{ value: 'a', pendingFirst: false, done: false, error: undefined },
{ value: 'b', pendingFirst: false, done: false, error: undefined },
{ value: '___', pendingFirst: true, done: false, error: undefined },
]);
expect(initialValueFn).toHaveBeenCalledTimes(3);
}
);

it(
gray(
'When given multiple iterables with initial values as a functions, calls each once when a corresponding iterable is added'
),
async () => {
const channels = [new IteratorChannelTestHelper<string>()];
const [initialValueFn1, initialValueFn2] = [vi.fn(), vi.fn()];
let timesRerendered = 0;

const renderedHook = renderHook(() => {
timesRerendered++;
return useAsyncIterMulti(channels, {
initialValues: [
initialValueFn1.mockImplementation(() => '_1_'),
initialValueFn2.mockImplementation(() => '_2_'),
],
});
});

await act(() => {});
expect(timesRerendered).toStrictEqual(1);
expect(renderedHook.result.current).toStrictEqual([
{ value: '_1_', pendingFirst: true, done: false, error: undefined },
]);

await act(() => channels[0].put('a'));
expect(timesRerendered).toStrictEqual(2);
expect(renderedHook.result.current).toStrictEqual([
{ value: 'a', pendingFirst: false, done: false, error: undefined },
]);

await act(() => {
channels.push(new IteratorChannelTestHelper());
renderedHook.rerender();
});
expect(timesRerendered).toStrictEqual(3);
expect(renderedHook.result.current).toStrictEqual([
{ value: 'a', pendingFirst: false, done: false, error: undefined },
{ value: '_2_', pendingFirst: true, done: false, error: undefined },
]);
expect(initialValueFn1).toHaveBeenCalledOnce();
expect(initialValueFn2).toHaveBeenCalledOnce();
}
);

it(
gray(
"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"
Expand All @@ -173,14 +266,19 @@
const channels = [
new IteratorChannelTestHelper<string>(),
new IteratorChannelTestHelper<string>(),
] as const;
];
let timesRerendered = 0;

const renderedHook = renderHook(() => {
timesRerendered++;
return useAsyncIterMulti(channels, { initialValues: ['_1_'], defaultInitialValue: '___' });
return useAsyncIterMulti(channels, {
initialValues: [() => '_1_' as const] as const,
defaultInitialValue: () => '___' as const,
});
});

renderedHook.result.current[0].value;

Check warning on line 280 in spec/tests/useAsyncIterMulti.spec.ts

View workflow job for this annotation

GitHub Actions / lint_check

Expected an assignment or function call and instead saw an expression

await act(() => {});
expect(timesRerendered).toStrictEqual(1);
expect(renderedHook.result.current).toStrictEqual([
Expand All @@ -201,6 +299,17 @@
{ value: 'a', pendingFirst: false, done: false, error: undefined },
{ value: 'b', pendingFirst: false, done: false, error: undefined },
]);

await act(() => {
channels.push(new IteratorChannelTestHelper());
renderedHook.rerender();
});
expect(timesRerendered).toStrictEqual(4);
expect(renderedHook.result.current).toStrictEqual([
{ value: 'a', pendingFirst: false, done: false, error: undefined },
{ value: 'b', pendingFirst: false, done: false, error: undefined },
{ value: '___', pendingFirst: true, done: false, error: undefined },
]);
}
);

Expand Down
34 changes: 23 additions & 11 deletions src/useAsyncIterMulti/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { useRefWithInitialValue } from '../common/hooks/useRefWithInitialValue.j
import { isAsyncIter } from '../common/isAsyncIter.js';
import { type IterationResult } from '../useAsyncIter/index.js';
import { type AsyncIterableSubject } from '../AsyncIterableSubject/index.js';
import { type MaybeFunction } from '../common/MaybeFunction.js';
import { callOrReturn } from '../common/callOrReturn.js';
import { asyncIterSyncMap } from '../common/asyncIterSyncMap.js';
import { parseReactAsyncIterable } from '../common/ReactAsyncIterable.js';
import { iterateAsyncIterWithCallbacks } from '../common/iterateAsyncIterWithCallbacks.js';
Expand Down Expand Up @@ -79,8 +81,8 @@ export { useAsyncIterMulti, type IterationResult, type IterationResultSet };
*
* @param inputs An array of zero or more async iterable or plain values (mixable).
* @param {object} opts An _optional_ object with options.
* @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.
* @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`.
* @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.
* @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).
*
* @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`}).
*
Expand Down Expand Up @@ -206,12 +208,12 @@ function useAsyncIterMulti<
initialValues?: TInitValues;
defaultInitialValue?: TDefaultInitValue;
}
): IterationResultSet<TValues, TInitValues, TDefaultInitValue> {
): IterationResultSet<TValues, MaybeFunctions<TInitValues>, TDefaultInitValue> {
const update = useSimpleRerender();

const ref = useRefWithInitialValue(() => ({
currDiffCompId: 0,
prevResults: [] as IterationResultSet<TValues, TInitValues, TDefaultInitValue>,
prevResults: [] as IterationResultSet<TValues, MaybeFunctions<TInitValues>, TDefaultInitValue>,
activeItersMap: new Map<
AsyncIterable<unknown>,
{
Expand All @@ -233,8 +235,10 @@ function useAsyncIterMulti<
};
}, []);

const initialValues = opts?.initialValues ?? [];
const defaultInitialValue = opts?.defaultInitialValue;
const optsNormed = {
initialValues: opts?.initialValues ?? [],
defaultInitialValue: opts?.defaultInitialValue,
};

const nextDiffCompId = (ref.current.currDiffCompId = ref.current.currDiffCompId === 0 ? 1 : 0);
let numOfPrevRunItersPreserved = 0;
Expand Down Expand Up @@ -279,9 +283,11 @@ function useAsyncIterMulti<
startingValue =
i < prevResults.length
? prevResults[i].value
: i < initialValues.length
? initialValues[i]
: defaultInitialValue;
: callOrReturn(
i < optsNormed.initialValues.length
? optsNormed.initialValues[i]
: optsNormed.defaultInitialValue
);
pendingFirst = true;
}

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

return newIterState.currState;
}) as IterationResultSet<TValues, TInitValues, TDefaultInitValue>;
}) as IterationResultSet<TValues, MaybeFunctions<TInitValues>, TDefaultInitValue>;

const numOfPrevRunItersDisappeared = numOfPrevRunIters - numOfPrevRunItersPreserved;

Expand All @@ -322,7 +328,9 @@ function useAsyncIterMulti<
}
}

return (ref.current.prevResults = nextResults);
ref.current.prevResults = nextResults;

return nextResults;
}

type IterationResultSet<
Expand All @@ -335,3 +343,7 @@ type IterationResultSet<
I extends keyof TInitValues ? TInitValues[I] : TDefaultInitValue
>;
};

type MaybeFunctions<T extends readonly unknown[]> = {
[I in keyof T]: T[I] extends MaybeFunction<infer J> ? J : T[I];
};
Loading