From 30844667c7ab3330d9968963b5941cfa28e8964f Mon Sep 17 00:00:00 2001 From: Dor Shtaif Date: Mon, 27 Jan 2025 17:31:47 +0200 Subject: [PATCH] support initializer functions for init values provided to `useAsyncIterMulti` hook --- README.md | 5 +- spec/tests/useAsyncIterMulti.spec.ts | 115 ++++++++++++++++++++++++++- src/useAsyncIterMulti/index.ts | 34 +++++--- 3 files changed, 137 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 79f7bc5..2028094 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/spec/tests/useAsyncIterMulti.spec.ts b/spec/tests/useAsyncIterMulti.spec.ts index 04da407..e94538c 100644 --- a/spec/tests/useAsyncIterMulti.spec.ts +++ b/spec/tests/useAsyncIterMulti.spec.ts @@ -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'; @@ -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(), + new IteratorChannelTestHelper(), + ]; + 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()]; + 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" @@ -173,14 +266,19 @@ describe('`useAsyncIterMulti` hook', () => { const channels = [ new IteratorChannelTestHelper(), new IteratorChannelTestHelper(), - ] 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; + await act(() => {}); expect(timesRerendered).toStrictEqual(1); expect(renderedHook.result.current).toStrictEqual([ @@ -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 }, + ]); } ); diff --git a/src/useAsyncIterMulti/index.ts b/src/useAsyncIterMulti/index.ts index 972f363..5e96953 100644 --- a/src/useAsyncIterMulti/index.ts +++ b/src/useAsyncIterMulti/index.ts @@ -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'; @@ -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`}). * @@ -206,12 +208,12 @@ function useAsyncIterMulti< initialValues?: TInitValues; defaultInitialValue?: TDefaultInitValue; } -): IterationResultSet { +): IterationResultSet, TDefaultInitValue> { const update = useSimpleRerender(); const ref = useRefWithInitialValue(() => ({ currDiffCompId: 0, - prevResults: [] as IterationResultSet, + prevResults: [] as IterationResultSet, TDefaultInitValue>, activeItersMap: new Map< AsyncIterable, { @@ -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; @@ -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; } @@ -305,7 +311,7 @@ function useAsyncIterMulti< activeItersMap.set(baseIter, newIterState); return newIterState.currState; - }) as IterationResultSet; + }) as IterationResultSet, TDefaultInitValue>; const numOfPrevRunItersDisappeared = numOfPrevRunIters - numOfPrevRunItersPreserved; @@ -322,7 +328,9 @@ function useAsyncIterMulti< } } - return (ref.current.prevResults = nextResults); + ref.current.prevResults = nextResults; + + return nextResults; } type IterationResultSet< @@ -335,3 +343,7 @@ type IterationResultSet< I extends keyof TInitValues ? TInitValues[I] : TDefaultInitValue >; }; + +type MaybeFunctions = { + [I in keyof T]: T[I] extends MaybeFunction ? J : T[I]; +};