Skip to content

Commit

Permalink
support initializer functions for init values provided to `useAsyncIt…
Browse files Browse the repository at this point in the history
…erMulti` hook
  • Loading branch information
shtaif committed Jan 27, 2025
1 parent f5e214b commit 3084466
Show file tree
Hide file tree
Showing 3 changed files with 137 additions and 17 deletions.
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 @@ 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'
),
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 @@ describe('`useAsyncIterMulti` hook', () => {
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 @@ describe('`useAsyncIterMulti` hook', () => {
{ 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];
};

0 comments on commit 3084466

Please sign in to comment.