Skip to content

Commit 9a7e9e4

Browse files
authored
feat(useAsyncIter): allow initial value to be a function, called once on mount (#48)
* allow initial value to be a function, called once on mount * introduce `MaybeFunction` type to clean up places accepting a value-or-function types of inputs
1 parent fbf9e21 commit 9a7e9e4

File tree

4 files changed

+51
-11
lines changed

4 files changed

+51
-11
lines changed

spec/tests/useAsyncIter.spec.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { it, describe, expect, afterEach } from 'vitest';
1+
import { it, describe, expect, afterEach, vi } from 'vitest';
22
import { gray } from 'colorette';
33
import { cleanup as cleanupMountedReactTrees, act, renderHook } from '@testing-library/react';
44
import { useAsyncIter, iterateFormatted } from '../../src/index.js';
@@ -404,6 +404,32 @@ describe('`useAsyncIter` hook', () => {
404404
}
405405
);
406406

407+
it(
408+
gray(
409+
'When given an initial value as a function, calls it once on mount and uses its result as the initial value correctly'
410+
),
411+
async () => {
412+
const channel = new IteratorChannelTestHelper<string>();
413+
const initValFn = vi.fn(() => '_');
414+
415+
const renderedHook = await act(() => renderHook(() => useAsyncIter(channel, initValFn)));
416+
const results = [renderedHook.result.current];
417+
418+
await act(() => renderedHook.rerender());
419+
results.push(renderedHook.result.current);
420+
421+
await act(() => channel.put('a'));
422+
results.push(renderedHook.result.current);
423+
424+
expect(initValFn).toHaveBeenCalledOnce();
425+
expect(results).toStrictEqual([
426+
{ value: '_', pendingFirst: true, done: false, error: undefined },
427+
{ value: '_', pendingFirst: true, done: false, error: undefined },
428+
{ value: 'a', pendingFirst: false, done: false, error: undefined },
429+
]);
430+
}
431+
);
432+
407433
it(gray('When unmounted will close the last active iterator it held'), async () => {
408434
const channel = new IteratorChannelTestHelper<string>();
409435

src/common/MaybeFunction.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export { MaybeFunction };
2+
3+
type MaybeFunction<T, TPossibleArgs extends unknown[] = []> = T | ((...args: TPossibleArgs) => T);

src/common/callOrReturn.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { type MaybeFunction } from './MaybeFunction.js';
2+
3+
export { callOrReturn };
4+
5+
function callOrReturn<T>(value: MaybeFunction<T>): T {
6+
return typeof value !== 'function' ? value : (value as () => T)();
7+
}

src/useAsyncIter/index.ts

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,21 @@
1-
import { useRef, useMemo, useEffect } from 'react';
1+
import { useMemo, useEffect } from 'react';
22
import { useLatest } from '../common/hooks/useLatest.js';
33
import { isAsyncIter } from '../common/isAsyncIter.js';
44
import { useSimpleRerender } from '../common/hooks/useSimpleRerender.js';
5+
import { useRefWithInitialValue } from '../common/hooks/useRefWithInitialValue.js';
6+
import { type MaybeFunction } from '../common/MaybeFunction.js';
57
import { type ExtractAsyncIterValue } from '../common/ExtractAsyncIterValue.js';
68
import {
79
reactAsyncIterSpecialInfoSymbol,
810
type ReactAsyncIterSpecialInfo,
911
} from '../common/ReactAsyncIterable.js';
1012
import { iterateAsyncIterWithCallbacks } from '../common/iterateAsyncIterWithCallbacks.js';
13+
import { callOrReturn } from '../common/callOrReturn.js';
1114
import { type Iterate } from '../Iterate/index.js'; // eslint-disable-line @typescript-eslint/no-unused-vars
1215
import { type iterateFormatted } from '../iterateFormatted/index.js'; // eslint-disable-line @typescript-eslint/no-unused-vars
1316

1417
export { useAsyncIter, type IterationResult };
1518

16-
// TODO: The initial values should be able to be given as functions, having them called once on mount
17-
1819
/**
1920
* `useAsyncIter` hooks up a single async iterable value to your component and its lifecycle.
2021
*
@@ -62,7 +63,7 @@ export { useAsyncIter, type IterationResult };
6263
* @template TInitVal The type of the initial value, defaults to `undefined`.
6364
*
6465
* @param input Any async iterable or plain value.
65-
* @param initialVal Any initial value for the hook to return prior to resolving the ___first emission___ of the ___first given___ async iterable, defaults to `undefined`.
66+
* @param initialVal Any optional starting value for the hook to return prior to the ___first yield___ of the ___first given___ async iterable, defaults to `undefined`. You can pass an actual value, or a function that returns a value (which the hook will call once during mounting).
6667
*
6768
* @returns An object with properties reflecting the current state of the iterated async iterable or plain value provided via `input` (see {@link IterationResult `IterationResult`}).
6869
*
@@ -100,7 +101,10 @@ export { useAsyncIter, type IterationResult };
100101
*/
101102
const useAsyncIter: {
102103
<TVal>(input: TVal, initialVal?: undefined): IterationResult<TVal>;
103-
<TVal, TInitVal>(input: TVal, initialVal: TInitVal): IterationResult<TVal, TInitVal>;
104+
<TVal, TInitVal>(
105+
input: TVal,
106+
initialVal: MaybeFunction<TInitVal>
107+
): IterationResult<TVal, TInitVal>;
104108
} = <
105109
TVal extends
106110
| undefined
@@ -112,19 +116,19 @@ const useAsyncIter: {
112116
ExtractAsyncIterValue<TVal>
113117
>;
114118
},
115-
TInitVal = undefined,
119+
TInitVal,
116120
>(
117121
input: TVal,
118-
initialVal: TInitVal
122+
initialVal: MaybeFunction<TInitVal>
119123
): IterationResult<TVal, TInitVal> => {
120124
const rerender = useSimpleRerender();
121125

122-
const stateRef = useRef<IterationResult<TVal, TInitVal>>({
123-
value: initialVal as any,
126+
const stateRef = useRefWithInitialValue<IterationResult<TVal, TInitVal>>(() => ({
127+
value: callOrReturn(initialVal) as any,
124128
pendingFirst: true,
125129
done: false,
126130
error: undefined,
127-
});
131+
}));
128132

129133
const latestInputRef = useLatest(input);
130134

0 commit comments

Comments
 (0)