Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(useAsyncIter): allow initial value to be a function, called once on mount #48

Merged
merged 3 commits into from
Jan 9, 2025
Merged
Show file tree
Hide file tree
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
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
3 changes: 3 additions & 0 deletions src/common/MaybeFunction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { MaybeFunction };

type MaybeFunction<T, TPossibleArgs extends unknown[] = []> = T | ((...args: TPossibleArgs) => T);
7 changes: 7 additions & 0 deletions src/common/callOrReturn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { type MaybeFunction } from './MaybeFunction.js';

export { callOrReturn };

function callOrReturn<T>(value: MaybeFunction<T>): T {
return typeof value !== 'function' ? value : (value as () => T)();
}
24 changes: 14 additions & 10 deletions src/useAsyncIter/index.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
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 MaybeFunction } from '../common/MaybeFunction.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 +63,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 +101,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: MaybeFunction<TInitVal>
): IterationResult<TVal, TInitVal>;
} = <
TVal extends
| undefined
Expand All @@ -112,19 +116,19 @@ const useAsyncIter: {
ExtractAsyncIterValue<TVal>
>;
},
TInitVal = undefined,
TInitVal,
>(
input: TVal,
initialVal: TInitVal
initialVal: MaybeFunction<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
Loading