Skip to content
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

Support for IterateMulti to handle getting initial values as initializer functions #71

Merged
merged 1 commit into from
Jan 28, 2025
Merged
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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -759,10 +759,10 @@ It's similar to [`<It>`](#it), only it works with any changeable number of async
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).

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

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

- `children`:
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)).
119 changes: 118 additions & 1 deletion spec/tests/IterateMulti.spec.tsx
Original file line number Diff line number Diff line change
@@ -226,7 +226,7 @@ describe('`IterateMulti` hook', () => {
});

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

it(
gray(
'When given multiple iterables with a default initial value as a function, calls it once whenever a new iterable is added'
),
async () => {
const channels = [
new IteratorChannelTestHelper<string>(),
new IteratorChannelTestHelper<string>(),
];
const initialValueFn = vi.fn(() => '___');
const renderFn = vi.fn() as Mock<
(nexts: IterationResultSet<AsyncIterable<string>[], [], '___'>) => any
>;

const Component = ({ values }: { values: AsyncIterable<string>[] }) => (
<IterateMulti values={values} defaultInitialValue={initialValueFn}>
{renderFn.mockImplementation(() => (
<div id="test-created-elem">Render count: {renderFn.mock.calls.length}</div>
))}
</IterateMulti>
);

const rendered = render(<></>);

await act(() => rendered.rerender(<Component values={channels} />));
expect(renderFn.mock.calls).lengthOf(1);
expect(renderFn.mock.lastCall?.flat()).toStrictEqual([
{ value: '___', pendingFirst: true, done: false, error: undefined },
{ value: '___', pendingFirst: true, done: false, error: undefined },
]);
expect(rendered.container.innerHTML).toStrictEqual(
`<div id="test-created-elem">Render count: 1</div>`
);

await act(() => {
channels[0].put('a');
channels[1].put('b');
});
expect(renderFn.mock.calls).lengthOf(2);
expect(renderFn.mock.lastCall?.flat()).toStrictEqual([
{ value: 'a', pendingFirst: false, done: false, error: undefined },
{ value: 'b', pendingFirst: false, done: false, error: undefined },
]);
expect(rendered.container.innerHTML).toStrictEqual(
`<div id="test-created-elem">Render count: 2</div>`
);

await act(() => {
channels.push(new IteratorChannelTestHelper());
rendered.rerender(<Component values={channels} />);
});
expect(renderFn.mock.calls).lengthOf(3);
expect(renderFn.mock.lastCall?.flat()).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(rendered.container.innerHTML).toStrictEqual(
`<div id="test-created-elem">Render count: 3</div>`
);
expect(initialValueFn).toHaveBeenCalledTimes(3);
}
);

it(
gray(
'When given multiple iterables with initial values as a functions, calls each once whenever a corresponding iterable is added'
),
async () => {
const channels = [new IteratorChannelTestHelper<string>()];
const [initialValueFn1, initialValueFn2] = [vi.fn(), vi.fn()];
const renderFn = vi.fn() as Mock<
(nexts: IterationResultSet<AsyncIterable<string>[], ['_1_', '_2_']>) => any
>;

const Component = ({ values }: { values: AsyncIterable<string>[] }) => (
<IterateMulti
values={values}
initialValues={[
initialValueFn1.mockImplementation(() => '_1_'),
initialValueFn2.mockImplementation(() => '_2_'),
]}
>
{renderFn.mockImplementation(() => (
<div id="test-created-elem">Render count: {renderFn.mock.calls.length}</div>
))}
</IterateMulti>
);

const rendered = render(<></>);

await act(() => rendered.rerender(<Component values={channels} />));
expect(renderFn.mock.calls).lengthOf(1);
expect(renderFn.mock.lastCall?.flat()).toStrictEqual([
{ value: '_1_', pendingFirst: true, done: false, error: undefined },
]);

await act(() => channels[0].put('a'));
expect(renderFn.mock.calls).lengthOf(2);
expect(renderFn.mock.lastCall?.flat()).toStrictEqual([
{ value: 'a', pendingFirst: false, done: false, error: undefined },
]);

await act(() => {
channels.push(new IteratorChannelTestHelper());
rendered.rerender(<Component values={channels} />);
});
expect(renderFn.mock.calls).lengthOf(3);
expect(renderFn.mock.lastCall?.flat()).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, correctly renders each's state and corresponding initial value or the default initial value if not present"
6 changes: 2 additions & 4 deletions spec/tests/useAsyncIterMulti.spec.ts
Original file line number Diff line number Diff line change
@@ -167,7 +167,7 @@ describe('`useAsyncIterMulti` hook', () => {

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

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

renderedHook.result.current[0].value;

await act(() => {});
expect(timesRerendered).toStrictEqual(1);
expect(renderedHook.result.current).toStrictEqual([
23 changes: 15 additions & 8 deletions src/IterateMulti/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { type ReactNode } from 'react';
import { type Writeable } from '../common/Writeable.js';
import { useAsyncIterMulti, type IterationResultSet } from '../useAsyncIterMulti/index.js';
import { type MaybeFunction } from '../common/MaybeFunction.js';
import { type iterateFormatted } from '../iterateFormatted/index.js'; // eslint-disable-line @typescript-eslint/no-unused-vars

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

/**
* An optional array of initial values. The values here will be the starting points for all the
* async iterables from `values` (correspondingly by matching array positions) when 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 corresponding item in this array,
* will fall back to the {@link IterateMultiProps.defaultInitialValue `defaultInitialValue`} prop
* as the initial value.
* An _optional_ array of initial values or functions that return initial values. These values
* will be the starting points for all the async iterables from `values` (correspondingly by
* matching array positions) when 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 corresponding item in this array, will fall back to the
* {@link IterateMultiProps.defaultInitialValue `defaultInitialValue`} prop as the initial value.
*/
initialValues?: TInitVals;

/**
* 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`.
* 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).
*/
defaultInitialValue?: TDefaultInitVal;

@@ -210,3 +213,7 @@ type IterateMultiProps<
iterationStates: IterationResultSet<Writeable<TVals>, Writeable<TInitVals>, TDefaultInitVal>
) => ReactNode;
};

type MaybeFunctions<T extends readonly unknown[]> = {
[I in keyof T]: T[I] extends MaybeFunction<infer J> ? J : T[I];
};
2 changes: 1 addition & 1 deletion src/useAsyncIterMulti/index.ts
Original file line number Diff line number Diff line change
@@ -156,7 +156,7 @@ export { useAsyncIterMulti, type IterationResult, type IterationResultSet };
* function DynamicInputsComponent() {
* const [inputs, setInputs] = useState<MaybeAsyncIterable<string>[]>([]);
*
* const states = useAsyncIterMulti(inputs);
* const states = useAsyncIterMulti(inputs, { defaultInitialValue: '' });
*
* const addAsyncIterValue = () => {
* const iterableValue = (async function* () {
Loading