Skip to content

Commit bfc1f09

Browse files
authored
feat(Iterate): support initial value in function form (#49)
* allow initial value to be a function, called once on mount * feat: introduce `MaybeFunction` type to clean up places accepting a value-or-function types of inputs * feat(Iterate): support initial value in function form and update type definitions * improve TODO comments to implement corresponding behaviors for `useAsyncIterMulti` and `IterateMulti` as well
1 parent 9a7e9e4 commit bfc1f09

File tree

4 files changed

+61
-9
lines changed

4 files changed

+61
-9
lines changed

spec/tests/Iterate.spec.tsx

+44
Original file line numberDiff line numberDiff line change
@@ -580,6 +580,50 @@ describe('`Iterate` component', () => {
580580
}
581581
);
582582

583+
it(
584+
gray(
585+
'When given an initial value as a function, calls it once on mount and uses its result as the initial value correctly'
586+
),
587+
async () => {
588+
const channel = new IteratorChannelTestHelper<string>();
589+
const initValFn = vi.fn(() => '_');
590+
const renderFn = vi.fn() as Mock<
591+
(next: IterationResult<AsyncIterable<string>, string>) => any
592+
>;
593+
594+
const Component = (props: { value: AsyncIterable<string> }) => (
595+
<Iterate value={props.value} initialValue={initValFn}>
596+
{renderFn.mockImplementation(() => (
597+
<div id="test-created-elem">Render count: {renderFn.mock.calls.length}</div>
598+
))}
599+
</Iterate>
600+
);
601+
602+
const rendered = render(<></>);
603+
604+
await act(() => rendered.rerender(<Component value={channel} />));
605+
const renderedHtmls = [rendered.container.innerHTML];
606+
607+
await act(() => rendered.rerender(<Component value={channel} />));
608+
renderedHtmls.push(rendered.container.innerHTML);
609+
610+
await act(() => channel.put('a'));
611+
renderedHtmls.push(rendered.container.innerHTML);
612+
613+
expect(initValFn).toHaveBeenCalledOnce();
614+
expect(renderFn.mock.calls).toStrictEqual([
615+
[{ value: '_', pendingFirst: true, done: false, error: undefined }],
616+
[{ value: '_', pendingFirst: true, done: false, error: undefined }],
617+
[{ value: 'a', pendingFirst: false, done: false, error: undefined }],
618+
]);
619+
expect(renderedHtmls).toStrictEqual([
620+
'<div id="test-created-elem">Render count: 1</div>',
621+
'<div id="test-created-elem">Render count: 2</div>',
622+
'<div id="test-created-elem">Render count: 3</div>',
623+
]);
624+
}
625+
);
626+
583627
it(gray('When unmounted will close the last active iterator it held'), async () => {
584628
let lastRenderFnInput: undefined | IterationResult<string | undefined>;
585629

src/Iterate/index.tsx

+14-8
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { type ReactNode } from 'react';
2+
import { type MaybeFunction } from '../common/MaybeFunction.js';
3+
import { type MaybeAsyncIterable } from '../MaybeAsyncIterable/index.js';
24
import { useAsyncIter, type IterationResult } from '../useAsyncIter/index.js';
35

46
export { Iterate, type IterateProps };
@@ -110,7 +112,7 @@ function Iterate<TVal, TInitialVal = undefined>(props: IterateProps<TVal, TIniti
110112
const propsBetterTyped = props as IteratePropsWithRenderFunction<TVal, TInitialVal>;
111113
const next = useAsyncIter(
112114
propsBetterTyped.value,
113-
propsBetterTyped.initialValue as TInitialVal
115+
propsBetterTyped.initialValue as NonNullable<typeof propsBetterTyped.initialValue>
114116
);
115117
return propsBetterTyped.children(next);
116118
})()
@@ -143,16 +145,18 @@ type IteratePropsWithRenderFunction<TVal, TInitialVal = undefined> = {
143145
*/
144146
value: TVal;
145147
/**
146-
* An optional initial value, defaults to `undefined`. Will be the value provided inside the child
147-
* render function when `<Iterate>` first renders on being mounted and while it's pending its first
148-
* value to be yielded.
148+
* An optional starting value, defaults to `undefined`. Will be the value inserted into the child render
149+
* function when `<Iterate>` first renders during mount and while it's pending its first value to be
150+
* yielded.
151+
*
152+
* You can pass an actual value, or a function that returns a value (which `<Iterate>` will call once during mounting).
149153
*/
150-
initialValue?: TInitialVal;
154+
initialValue?: MaybeFunction<TInitialVal>;
151155
/**
152156
* A render function that is called for each step of the iteration, returning something to render
153157
* out of it.
154158
*
155-
* @param nextIterationState - The current state of the iteration, including the yielded value, whether iteration is complete, any associated error, etc. (see {@link IterationResult `IterationResult`})
159+
* @param nextIterationState - The current state of the iteration, including the yielded value, whether iteration is complete, any associated error, etc. (see {@link IterationResult `IterationResult`}).
156160
* @returns The content to render for the current iteration state.
157161
*
158162
* @see {@link IterateProps `IterateProps`}
@@ -169,10 +173,12 @@ type IteratePropsWithNoRenderFunction = {
169173
value?: undefined;
170174
/**
171175
* An optional initial value, defaults to `undefined`.
176+
*
177+
* You can pass an actual value, or a function that returns a value (which `<Iterate>` will call once during mounting).
172178
*/
173-
initialValue?: ReactNode;
179+
initialValue?: MaybeFunction<ReactNode>;
174180
/**
175181
* The source value to render from, either an async iterable to iterate over of a plain value.
176182
*/
177-
children: ReactNode | AsyncIterable<ReactNode>;
183+
children: MaybeAsyncIterable<ReactNode>;
178184
};

src/IterateMulti/index.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { type iterateFormatted } from '../iterateFormatted/index.js'; // eslint-
55

66
export { IterateMulti, type IterateMultiProps };
77

8+
// TODO: The initial values should be able to be given in function/s form, with consideration for iterable sources that could be added in dynamically.
9+
810
/**
911
* The `<IterateMulti>` helper component (also exported as `<ItMulti>`) is used to combine and render
1012
* any number of async iterables (or plain non-iterable values) directly onto a piece of UI.

src/useAsyncIterMulti/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { iterateAsyncIterWithCallbacks } from '../common/iterateAsyncIterWithCal
99

1010
export { useAsyncIterMulti, type IterationResult, type IterationResultSet };
1111

12-
// TODO: The initial values should be able to be given as functions, having them called once on mount if so
12+
// TODO: The initial values should be able to be given in function/s form, with consideration for iterable sources that could be added in dynamically.
1313

1414
/**
1515
* `useAsyncIterMulti` hooks up multiple async iterables to your component and its lifecycle, letting

0 commit comments

Comments
 (0)