Skip to content

Commit 23ad98c

Browse files
authored
fix: first yielding wrongly ignored if yielded value is identical to the last one stored before (#36)
1 parent 9685abb commit 23ad98c

File tree

4 files changed

+156
-7
lines changed

4 files changed

+156
-7
lines changed

spec/tests/Iterate.spec.tsx

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -662,7 +662,9 @@ describe('`Iterate` component', () => {
662662
);
663663

664664
it(
665-
gray('When given iterable yields consecutive identical values the hook will not re-render'),
665+
gray(
666+
'When given iterable yields consecutive identical values after the first, the component will not re-render'
667+
),
666668
async () => {
667669
const channel = new IteratorChannelTestHelper<string>();
668670
const renderFn = vi.fn() as Mock<
@@ -691,6 +693,61 @@ describe('`Iterate` component', () => {
691693
}
692694
);
693695

696+
it(
697+
gray(
698+
"When given iterable's first yield is identical to the previous value, the component does re-render"
699+
),
700+
async () => {
701+
const renderFn = vi.fn() as Mock<
702+
(next: IterationResult<AsyncIterable<string | undefined>>) => any
703+
>;
704+
const channel1 = new IteratorChannelTestHelper<string>();
705+
706+
const Component = (props: { value: AsyncIterable<string> }) => {
707+
return (
708+
<Iterate value={props.value}>
709+
{renderFn.mockImplementation(() => (
710+
<div id="test-created-elem">Render count: {renderFn.mock.calls.length}</div>
711+
))}
712+
</Iterate>
713+
);
714+
};
715+
716+
const rendered = await act(() => render(<Component value={channel1} />));
717+
718+
await act(() => channel1.put('a'));
719+
720+
const channel2 = new IteratorChannelTestHelper<string>();
721+
await act(() => rendered.rerender(<Component value={channel2} />));
722+
expect(renderFn.mock.calls).lengthOf(3);
723+
expect(renderFn.mock.lastCall).toStrictEqual([
724+
{
725+
value: 'a',
726+
pendingFirst: true,
727+
done: false,
728+
error: undefined,
729+
},
730+
]);
731+
expect(rendered.container.innerHTML).toStrictEqual(
732+
'<div id="test-created-elem">Render count: 3</div>'
733+
);
734+
735+
await act(() => channel2.put('a'));
736+
expect(renderFn.mock.calls).lengthOf(4);
737+
expect(renderFn.mock.lastCall).toStrictEqual([
738+
{
739+
value: 'a',
740+
pendingFirst: false,
741+
done: false,
742+
error: undefined,
743+
},
744+
]);
745+
expect(rendered.container.innerHTML).toStrictEqual(
746+
'<div id="test-created-elem">Render count: 4</div>'
747+
);
748+
}
749+
);
750+
694751
it(
695752
gray(
696753
'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'

spec/tests/useAsyncIter.spec.ts

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -466,7 +466,9 @@ describe('`useAsyncIter` hook', () => {
466466
);
467467

468468
it(
469-
gray('When given iterable yields consecutive identical values the hook will not re-render'),
469+
gray(
470+
'When given iterable yields consecutive identical values after the first, the hook will not re-render'
471+
),
470472
async () => {
471473
let timesRerendered = 0;
472474
const channel = new IteratorChannelTestHelper<string>();
@@ -490,6 +492,47 @@ describe('`useAsyncIter` hook', () => {
490492
}
491493
);
492494

495+
it(
496+
gray(
497+
"When given iterable's first yield is identical to the previous value, the hook does re-render"
498+
),
499+
async () => {
500+
let timesRerendered = 0;
501+
const channel1 = new IteratorChannelTestHelper<string>();
502+
503+
const renderedHook = await act(() =>
504+
renderHook(
505+
({ channel }) => {
506+
timesRerendered++;
507+
return useAsyncIter(channel);
508+
},
509+
{ initialProps: { channel: channel1 } }
510+
)
511+
);
512+
513+
await act(() => channel1.put('a'));
514+
515+
const channel2 = new IteratorChannelTestHelper<string>();
516+
await act(() => renderedHook.rerender({ channel: channel2 }));
517+
expect(timesRerendered).toStrictEqual(3);
518+
expect(renderedHook.result.current).toStrictEqual({
519+
value: 'a',
520+
pendingFirst: true,
521+
done: false,
522+
error: undefined,
523+
});
524+
525+
await act(() => channel2.put('a'));
526+
expect(timesRerendered).toStrictEqual(4);
527+
expect(renderedHook.result.current).toStrictEqual({
528+
value: 'a',
529+
pendingFirst: false,
530+
done: false,
531+
error: undefined,
532+
});
533+
}
534+
);
535+
493536
it(
494537
gray(
495538
'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'

spec/tests/useAsyncIterMulti.spec.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -597,7 +597,9 @@ describe('`useAsyncIterMulti` hook', () => {
597597
);
598598

599599
it(
600-
gray('If any given iterable yields consecutive identical values, the hook will not re-render'),
600+
gray(
601+
'If any given iterable yields consecutive identical values after the first, the hook will not re-render'
602+
),
601603
async () => {
602604
const channel1 = new IteratorChannelTestHelper<string>();
603605
const channel2 = new IteratorChannelTestHelper<string>();
@@ -624,6 +626,41 @@ describe('`useAsyncIterMulti` hook', () => {
624626
}
625627
);
626628

629+
it(
630+
gray(
631+
"When given iterable's first yield is identical to the previous value, the hook does re-render"
632+
),
633+
async () => {
634+
let timesRerendered = 0;
635+
const channel1 = new IteratorChannelTestHelper<string>();
636+
637+
const renderedHook = await act(() =>
638+
renderHook(
639+
({ channel }) => {
640+
timesRerendered++;
641+
return useAsyncIterMulti([channel]);
642+
},
643+
{ initialProps: { channel: channel1 } }
644+
)
645+
);
646+
647+
await act(() => channel1.put('a'));
648+
649+
const channel2 = new IteratorChannelTestHelper<string>();
650+
await act(() => renderedHook.rerender({ channel: channel2 }));
651+
expect(timesRerendered).toStrictEqual(3);
652+
expect(renderedHook.result.current).toStrictEqual([
653+
{ value: 'a', pendingFirst: true, done: false, error: undefined },
654+
]);
655+
656+
await act(() => channel2.put('a'));
657+
expect(timesRerendered).toStrictEqual(4);
658+
expect(renderedHook.result.current).toStrictEqual([
659+
{ value: 'a', pendingFirst: false, done: false, error: undefined },
660+
]);
661+
}
662+
);
663+
627664
it(
628665
gray(
629666
'When given rapid-yielding iterables, consecutive values are batched into a single render that takes only the most recent values'

src/common/iterateAsyncIterWithCallbacks.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,24 @@ function iterateAsyncIterWithCallbacks<T>(
2222

2323
(async () => {
2424
try {
25-
for await (const value of { [Symbol.asyncIterator]: () => iterator }) {
26-
if (!iteratorClosedByConsumer && !Object.is(value, lastValue)) {
27-
lastValue = value;
28-
changeCb({ value, done: false, error: undefined });
25+
const { done, value } = await iterator.next();
26+
27+
if (iteratorClosedByConsumer) {
28+
return;
29+
}
30+
31+
if (!done) {
32+
lastValue = value;
33+
changeCb({ value, done: false, error: undefined }); // Ensuring the first yield is exempt from the "different from previous value" check
34+
35+
for await (const value of { [Symbol.asyncIterator]: () => iterator }) {
36+
if (!iteratorClosedByConsumer && !Object.is(value, lastValue)) {
37+
lastValue = value;
38+
changeCb({ value, done: false, error: undefined });
39+
}
2940
}
3041
}
42+
3143
if (!iteratorClosedByConsumer) {
3244
changeCb({ value: lastValue, done: true, error: undefined });
3345
}

0 commit comments

Comments
 (0)