Skip to content

Commit

Permalink
feat(useAsyncIter): allow initial value to be a function, called once…
Browse files Browse the repository at this point in the history
… on mount
  • Loading branch information
shtaif committed Jan 9, 2025
1 parent 054ea94 commit 1db7dd4
Show file tree
Hide file tree
Showing 3 changed files with 45 additions and 11 deletions.
28 changes: 27 additions & 1 deletion spec/tests/useAsyncIter.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 { useAsyncIter, iterateFormatted } from '../../src/index.js';
Expand Down Expand Up @@ -404,6 +404,32 @@ describe('`useAsyncIter` hook', () => {
}
);

it(
gray(
'When given an initial value as a function, calls it once on mount and uses its result as the initial value correctly'
),
async () => {
const channel = new IteratorChannelTestHelper<string>();
const initValFn = vi.fn(() => '_');

const renderedHook = await act(() => renderHook(() => useAsyncIter(channel, initValFn)));
const results = [renderedHook.result.current];

await act(() => renderedHook.rerender());
results.push(renderedHook.result.current);

await act(() => channel.put('a'));
results.push(renderedHook.result.current);

expect(initValFn).toHaveBeenCalledOnce();
expect(results).toStrictEqual([
{ value: '_', pendingFirst: true, done: false, error: undefined },
{ value: '_', pendingFirst: true, done: false, error: undefined },
{ value: 'a', pendingFirst: false, done: false, error: undefined },
]);
}
);

it(gray('When unmounted will close the last active iterator it held'), async () => {
const channel = new IteratorChannelTestHelper<string>();

Expand Down
5 changes: 5 additions & 0 deletions src/common/callOrReturn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export { callOrReturn };

function callOrReturn<T>(value: T | (() => T)): T {
return typeof value !== 'function' ? value : (value as () => T)();
}
23 changes: 13 additions & 10 deletions src/useAsyncIter/index.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import { useRef, useMemo, useEffect } from 'react';
import { useMemo, useEffect } from 'react';
import { useLatest } from '../common/hooks/useLatest.js';
import { isAsyncIter } from '../common/isAsyncIter.js';
import { useSimpleRerender } from '../common/hooks/useSimpleRerender.js';
import { useRefWithInitialValue } from '../common/hooks/useRefWithInitialValue.js';
import { type ExtractAsyncIterValue } from '../common/ExtractAsyncIterValue.js';
import {
reactAsyncIterSpecialInfoSymbol,
type ReactAsyncIterSpecialInfo,
} from '../common/ReactAsyncIterable.js';
import { iterateAsyncIterWithCallbacks } from '../common/iterateAsyncIterWithCallbacks.js';
import { callOrReturn } from '../common/callOrReturn.js';
import { type Iterate } from '../Iterate/index.js'; // eslint-disable-line @typescript-eslint/no-unused-vars
import { type iterateFormatted } from '../iterateFormatted/index.js'; // eslint-disable-line @typescript-eslint/no-unused-vars

export { useAsyncIter, type IterationResult };

// TODO: The initial values should be able to be given as functions, having them called once on mount

/**
* `useAsyncIter` hooks up a single async iterable value to your component and its lifecycle.
*
Expand Down Expand Up @@ -62,7 +62,7 @@ export { useAsyncIter, type IterationResult };
* @template TInitVal The type of the initial value, defaults to `undefined`.
*
* @param input Any async iterable or plain value.
* @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`.
* @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).
*
* @returns An object with properties reflecting the current state of the iterated async iterable or plain value provided via `input` (see {@link IterationResult `IterationResult`}).
*
Expand Down Expand Up @@ -100,7 +100,10 @@ export { useAsyncIter, type IterationResult };
*/
const useAsyncIter: {
<TVal>(input: TVal, initialVal?: undefined): IterationResult<TVal>;
<TVal, TInitVal>(input: TVal, initialVal: TInitVal): IterationResult<TVal, TInitVal>;
<TVal, TInitVal>(
input: TVal,
initialVal: TInitVal | (() => TInitVal)
): IterationResult<TVal, TInitVal>;
} = <
TVal extends
| undefined
Expand All @@ -112,19 +115,19 @@ const useAsyncIter: {
ExtractAsyncIterValue<TVal>
>;
},
TInitVal = undefined,
TInitVal,
>(
input: TVal,
initialVal: TInitVal
initialVal: TInitVal | (() => TInitVal)
): IterationResult<TVal, TInitVal> => {
const rerender = useSimpleRerender();

const stateRef = useRef<IterationResult<TVal, TInitVal>>({
value: initialVal as any,
const stateRef = useRefWithInitialValue<IterationResult<TVal, TInitVal>>(() => ({
value: callOrReturn(initialVal) as any,
pendingFirst: true,
done: false,
error: undefined,
});
}));

const latestInputRef = useLatest(input);

Expand Down

0 comments on commit 1db7dd4

Please sign in to comment.