Skip to content
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
74 changes: 58 additions & 16 deletions spec/tests/Iterate.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { it, describe, expect, afterEach } from 'vitest';
import { it, describe, expect, afterEach, vi, type Mock } from 'vitest';
import { gray } from 'colorette';
import { render, cleanup as cleanupMountedReactTrees, act } from '@testing-library/react';
import { Iterate, It, type IterationResult } from '../../src/index.js';
import { Iterate, It, iterateFormatted, type IterationResult } from '../../src/index.js';
import { asyncIterOf } from '../utils/asyncIterOf.js';
import { IteratorChannelTestHelper } from '../utils/IteratorChannelTestHelper.js';

Expand Down Expand Up @@ -664,36 +664,78 @@ describe('`Iterate` component', () => {
it(
gray('When given iterable yields consecutive identical values the hook will not re-render'),
async () => {
let timesRerendered = 0;
let lastRenderFnInput: undefined | IterationResult<string | undefined>;
const channel = new IteratorChannelTestHelper<string>();
const renderFn = vi.fn() as Mock<
(next: IterationResult<AsyncIterable<string | undefined>>) => any
>;

const rendered = render(
<Iterate value={channel}>
{next => {
timesRerendered++;
lastRenderFnInput = next;
return <div id="test-created-elem">Render count: {timesRerendered}</div>;
}}
{renderFn.mockImplementation(() => (
<div id="test-created-elem">Render count: {renderFn.mock.calls.length}</div>
))}
</Iterate>
);

for (let i = 0; i < 3; ++i) {
await act(() => channel.put('a'));
}

expect(timesRerendered).toStrictEqual(2);
expect(lastRenderFnInput).toStrictEqual({
value: 'a',
pendingFirst: false,
done: false,
error: undefined,
});
expect(renderFn.mock.calls).lengthOf(2);
expect(renderFn.mock.lastCall).toStrictEqual([
{ value: 'a', pendingFirst: false, done: false, error: undefined },
]);
expect(rendered.container.innerHTML).toStrictEqual(
'<div id="test-created-elem">Render count: 2</div>'
);
}
);

it(
gray(
'When given a `ReactAsyncIterable` yielding `undefined`s or `null`s that wraps an iter which originally yields non-nullable values, processes the `undefined`s and `null` values expected'
),
async () => {
const channel = new IteratorChannelTestHelper<string>();
const renderFn = vi.fn() as Mock<
(next: IterationResult<AsyncIterable<string | null | undefined>>) => any
>;

const buildContent = (iter: AsyncIterable<string>, formatInto: string | null | undefined) => {
return (
<Iterate value={iterateFormatted(iter, _ => formatInto)}>
{renderFn.mockImplementation(next => (
<div id="test-created-elem">{next.value + ''}</div>
))}
</Iterate>
);
};

const rendered = render(<></>);

rendered.rerender(buildContent(channel, ''));

await act(() => {
channel.put('a');
rendered.rerender(buildContent(channel, null));
});
expect(renderFn.mock.lastCall).toStrictEqual([
{ value: null, pendingFirst: false, done: false, error: undefined },
]);
expect(rendered.container.innerHTML).toStrictEqual('<div id="test-created-elem">null</div>');

await act(() => {
channel.put('b');
rendered.rerender(buildContent(channel, undefined));
});
expect(renderFn.mock.lastCall).toStrictEqual([
{ value: undefined, pendingFirst: false, done: false, error: undefined },
]);
expect(rendered.container.innerHTML).toStrictEqual(
'<div id="test-created-elem">undefined</div>'
);
}
);
});

const simulatedError = new Error('🚨 Simulated Error 🚨');
48 changes: 47 additions & 1 deletion spec/tests/useAsyncIter.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { it, describe, expect, afterEach } from 'vitest';
import { gray } from 'colorette';
import { cleanup as cleanupMountedReactTrees, act, renderHook } from '@testing-library/react';
import { useAsyncIter } from '../../src/index.js';
import { useAsyncIter, iterateFormatted } from '../../src/index.js';
import { asyncIterOf } from '../utils/asyncIterOf.js';
import { IteratorChannelTestHelper } from '../utils/IteratorChannelTestHelper.js';

Expand Down Expand Up @@ -489,6 +489,52 @@ describe('`useAsyncIter` hook', () => {
});
}
);

it(
gray(
'When given a `ReactAsyncIterable` yielding `undefined`s or `null`s that wraps an iter which originally yields non-nullable values, returns the `undefined`s and `null`s in the result as expected'
),
async () => {
const channel = new IteratorChannelTestHelper<string>();
let timesRerendered = 0;

const renderedHook = await act(() =>
renderHook(
({ formatInto }) => {
timesRerendered++;
return useAsyncIter(iterateFormatted(channel, _ => formatInto));
},
{
initialProps: { formatInto: '' as string | null | undefined },
}
)
);

await act(() => {
channel.put('a');
renderedHook.rerender({ formatInto: null });
});
expect(timesRerendered).toStrictEqual(3);
expect(renderedHook.result.current).toStrictEqual({
value: null,
pendingFirst: false,
done: false,
error: undefined,
});

await act(() => {
channel.put('b');
renderedHook.rerender({ formatInto: undefined });
});
expect(timesRerendered).toStrictEqual(5);
expect(renderedHook.result.current).toStrictEqual({
value: undefined,
pendingFirst: false,
done: false,
error: undefined,
});
}
);
});

const simulatedError = new Error('🚨 Simulated Error 🚨');
50 changes: 49 additions & 1 deletion spec/tests/useAsyncIterMulti.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -652,7 +652,7 @@ describe('`useAsyncIterMulti` hook', () => {

it(
gray(
'When given "React Async Iterables", maintains the iteration states based on the original source iters they contain and applies the next given format functions correctly'
'When given `ReactAsyncIterables`, maintains the iteration states based on the original source iters they contain and applies the next given format functions correctly'
),
async () => {
const channel1 = new IteratorChannelTestHelper<string>();
Expand Down Expand Up @@ -722,6 +722,54 @@ describe('`useAsyncIterMulti` hook', () => {
]);
}
);

it(
gray(
'When given `ReactAsyncIterable`s yielding `undefined`s or `null`s that wrap iters which originally yield non-nullable values, returns the `undefined`s and `null`s in the results as expected'
),
async () => {
const channel1 = new IteratorChannelTestHelper<string>();
const channel2 = new IteratorChannelTestHelper<string>();
let timesRerendered = 0;

const renderedHook = await act(() =>
renderHook(
({ formatInto }) => {
timesRerendered++;
return useAsyncIterMulti([
iterateFormatted(channel1, _ => formatInto),
iterateFormatted(channel2, _ => formatInto),
]);
},
{
initialProps: { formatInto: '' as string | null | undefined },
}
)
);

await act(() => {
channel1.put('a');
channel2.put('a');
renderedHook.rerender({ formatInto: null });
});
expect(timesRerendered).toStrictEqual(3);
expect(renderedHook.result.current).toStrictEqual([
{ value: null, pendingFirst: false, done: false, error: undefined },
{ value: null, pendingFirst: false, done: false, error: undefined },
]);

await act(() => {
channel1.put('b');
channel2.put('b');
renderedHook.rerender({ formatInto: undefined });
});
expect(timesRerendered).toStrictEqual(5);
expect(renderedHook.result.current).toStrictEqual([
{ value: undefined, pendingFirst: false, done: false, error: undefined },
{ value: undefined, pendingFirst: false, done: false, error: undefined },
]);
}
);
});

const simulatedError1 = new Error('🚨 Simulated Error 1 🚨');
Expand Down
63 changes: 15 additions & 48 deletions src/useAsyncIter/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
reactAsyncIterSpecialInfoSymbol,
type ReactAsyncIterSpecialInfo,
} from '../common/ReactAsyncIterable.js';
import { iterateAsyncIterWithCallbacks } from '../common/iterateAsyncIterWithCallbacks.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

Expand Down Expand Up @@ -154,58 +155,24 @@ const useAsyncIter: {
}, [iterSourceRefToUse]);

useEffect(() => {
const iterator = iterSourceRefToUse[Symbol.asyncIterator]();
let iteratorClosedByConsumer = false;
let iterationIdx = 0;

(async () => {
let iterationIdx = 0;
return iterateAsyncIterWithCallbacks(iterSourceRefToUse, stateRef.current.value, next => {
const possibleGivenFormatFn =
latestInputRef.current?.[reactAsyncIterSpecialInfoSymbol]?.formatFn;

try {
for await (const value of { [Symbol.asyncIterator]: () => iterator }) {
if (!iteratorClosedByConsumer) {
const formattedValue =
latestInputRef.current?.[reactAsyncIterSpecialInfoSymbol]?.formatFn(
value,
iterationIdx++
) ?? (value as ExtractAsyncIterValue<TVal>);
const formattedValue = possibleGivenFormatFn
? possibleGivenFormatFn(next.value, iterationIdx++)
: (next.value as ExtractAsyncIterValue<TVal>);

if (!Object.is(formattedValue, stateRef.current.value)) {
stateRef.current = {
value: formattedValue,
pendingFirst: false,
done: false,
error: undefined,
};
rerender();
}
}
}
if (!iteratorClosedByConsumer) {
stateRef.current = {
value: stateRef.current.value,
pendingFirst: false,
done: true,
error: undefined,
};
rerender();
}
} catch (err) {
if (!iteratorClosedByConsumer) {
stateRef.current = {
value: stateRef.current.value,
pendingFirst: false,
done: true,
error: err,
};
rerender();
}
}
})();
stateRef.current = {
...next,
pendingFirst: false,
value: formattedValue,
};

return () => {
iteratorClosedByConsumer = true;
iterator.return?.();
};
rerender();
});
}, [iterSourceRefToUse]);

return stateRef.current;
Expand Down
2 changes: 1 addition & 1 deletion src/useAsyncIterMulti/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { isAsyncIter } from '../common/isAsyncIter.js';
import { type IterationResult } from '../useAsyncIter/index.js';
import { asyncIterSyncMap } from '../common/asyncIterSyncMap.js';
import { parseReactAsyncIterable } from '../common/ReactAsyncIterable.js';
import { iterateAsyncIterWithCallbacks } from './iterateAsyncIterWithCallbacks.js';
import { iterateAsyncIterWithCallbacks } from '../common/iterateAsyncIterWithCallbacks.js';

export { useAsyncIterMulti, type IterationResult, type IterationResultSet };

Expand Down
Loading