Skip to content

Commit d438400

Browse files
author
Brian Kim
committed
warn when components yield multiple times in for...of loop
1 parent 7cd4e26 commit d438400

File tree

2 files changed

+61
-7
lines changed

2 files changed

+61
-7
lines changed

src/crank.ts

+28-7
Original file line numberDiff line numberDiff line change
@@ -1438,6 +1438,12 @@ const IsScheduling = 1 << 10;
14381438
*/
14391439
const IsSchedulingRefresh = 1 << 11;
14401440

1441+
/**
1442+
* A flag which is set when a component has yielded and needs new props. This
1443+
* flag is used to determine if a component is yielding with stale props.
1444+
*/
1445+
const NeedsNewProps = 1 << 12;
1446+
14411447
export interface Context extends Crank.Context {}
14421448

14431449
/**
@@ -1612,10 +1618,12 @@ export class Context<T = any, TResult = any> implements EventTarget {
16121618
ctx.f |= NeedsToYield;
16131619
}
16141620

1621+
ctx.f &= ~NeedsNewProps;
16151622
yield ctx.ret.el.props as ComponentProps<T>;
16161623
}
16171624
} finally {
16181625
ctx.f &= ~IsInForOfLoop;
1626+
ctx.f &= ~NeedsNewProps;
16191627
}
16201628
}
16211629

@@ -2246,7 +2254,7 @@ function enqueueComponentRun<TNode, TResult>(
22462254
// the bottom of the loop to be reached before returning the inflight
22472255
// value.
22482256
const isAtLoopbottom = ctx.f & IsInForAwaitOfLoop && !ctx.onProps;
2249-
resumePropsIterator(ctx);
2257+
resumePropsAsyncIterator(ctx);
22502258
if (isAtLoopbottom) {
22512259
if (ctx.inflightBlock == null) {
22522260
ctx.inflightBlock = new Promise(
@@ -2361,7 +2369,7 @@ function runComponent<TNode, TResult>(
23612369
const ret = ctx.ret;
23622370
const initial = !ctx.iterator;
23632371
if (initial) {
2364-
resumePropsIterator(ctx);
2372+
resumePropsAsyncIterator(ctx);
23652373
ctx.f |= IsSyncExecuting;
23662374
clearEventListeners(ctx);
23672375
let result: ReturnType<Component>;
@@ -2401,6 +2409,7 @@ function runComponent<TNode, TResult>(
24012409
];
24022410
}
24032411
} else if (hydrationData !== undefined) {
2412+
// hydration data should only be passed on the initial render
24042413
throw new Error("Hydration error");
24052414
}
24062415

@@ -2438,6 +2447,12 @@ function runComponent<TNode, TResult>(
24382447
}
24392448
}
24402449

2450+
if (ctx.f & NeedsNewProps && !(ctx.f & IsUnmounted)) {
2451+
console.error("Component yielded twice without accessing new props");
2452+
} else if (ctx.f & IsInForOfLoop) {
2453+
ctx.f |= NeedsNewProps;
2454+
}
2455+
24412456
if (isPromiseLike(iteration)) {
24422457
throw new Error("Mixed generator component");
24432458
}
@@ -2466,7 +2481,7 @@ function runComponent<TNode, TResult>(
24662481
const block = isPromiseLike(value) ? value.catch(NOOP) : undefined;
24672482
return [block, value];
24682483
} else if (ctx.f & IsInForOfLoop) {
2469-
// TODO: does this need to be done async?
2484+
// Async generator component using for...of loop
24702485
ctx.f &= ~NeedsToYield;
24712486
// we are in a for...of loop for async generator
24722487
if (!initial) {
@@ -2488,6 +2503,12 @@ function runComponent<TNode, TResult>(
24882503
const block = iteration.catch(NOOP);
24892504
const value = iteration.then(
24902505
(iteration) => {
2506+
if (ctx.f & NeedsNewProps && !(ctx.f & IsUnmounted)) {
2507+
console.error("Component yielded twice without accessing new props");
2508+
} else if (ctx.f & IsInForOfLoop) {
2509+
ctx.f |= NeedsNewProps;
2510+
}
2511+
24912512
let value: Promise<ElementValue<TNode>> | ElementValue<TNode>;
24922513
if (!(ctx.f & IsInForOfLoop)) {
24932514
runAsyncGenComponent(ctx, Promise.resolve(iteration), hydrationData);
@@ -2648,7 +2669,7 @@ async function runAsyncGenComponent<TNode, TResult>(
26482669
/**
26492670
* Called to resume the props async iterator for async generator components.
26502671
*/
2651-
function resumePropsIterator(ctx: ContextImpl): void {
2672+
function resumePropsAsyncIterator(ctx: ContextImpl): void {
26522673
if (ctx.onProps) {
26532674
ctx.onProps(ctx.ret.el.props);
26542675
ctx.onProps = undefined;
@@ -2728,14 +2749,14 @@ function unmountComponent(ctx: ContextImpl): void {
27282749
} else {
27292750
// The logic for unmounting async generator components is in the
27302751
// runAsyncGenComponent function.
2731-
resumePropsIterator(ctx);
2752+
resumePropsAsyncIterator(ctx);
27322753
}
27332754
}
27342755
}
27352756
}
27362757

27372758
function returnComponent(ctx: ContextImpl): void {
2738-
resumePropsIterator(ctx);
2759+
resumePropsAsyncIterator(ctx);
27392760
if (ctx.iterator && typeof ctx.iterator!.return === "function") {
27402761
try {
27412762
ctx.f |= IsSyncExecuting;
@@ -2883,7 +2904,7 @@ function handleChildError<TNode>(
28832904
throw err;
28842905
}
28852906

2886-
resumePropsIterator(ctx);
2907+
resumePropsAsyncIterator(ctx);
28872908
let iteration: ChildrenIteratorResult | Promise<ChildrenIteratorResult>;
28882909
try {
28892910
ctx.f |= IsSyncExecuting;

test/warnings.tsx

+33
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as Assert from "uvu/assert";
33
import * as Sinon from "sinon";
44

55
import {createElement} from "../src/crank.js";
6+
import type {Child, Context} from "../src/crank.js";
67
import {renderer} from "../src/dom.js";
78

89
const test = suite("warnings");
@@ -40,4 +41,36 @@ test("sync generator component warns on implicit return", async () => {
4041
Assert.is(mock.callCount, 1);
4142
});
4243

44+
test("for of with multiple yields", async () => {
45+
function* Component(this: Context): Generator<Child> {
46+
for ({} of this) {
47+
yield <div>Hello</div>;
48+
yield <div>Goodbye</div>;
49+
}
50+
}
51+
52+
renderer.render(<Component />, document.body);
53+
Assert.is(document.body.innerHTML, "<div>Hello</div>");
54+
renderer.render(<Component />, document.body);
55+
Assert.is(document.body.innerHTML, "<div>Goodbye</div>");
56+
Assert.is(mock.callCount, 1);
57+
});
58+
59+
test("for of with multiple yields in async generator component", async () => {
60+
async function* Component(this: Context): AsyncGenerator<Child> {
61+
for ({} of this) {
62+
yield <div>Hello</div>;
63+
yield <div>Goodbye</div>;
64+
}
65+
}
66+
67+
await renderer.render(<Component />, document.body);
68+
Assert.is(document.body.innerHTML, "<div>Hello</div>");
69+
await new Promise((resolve) => setTimeout(resolve, 100));
70+
Assert.is(document.body.innerHTML, "<div>Hello</div>");
71+
await renderer.render(<Component />, document.body);
72+
Assert.is(document.body.innerHTML, "<div>Goodbye</div>");
73+
Assert.is(mock.callCount, 1);
74+
});
75+
4376
test.run();

0 commit comments

Comments
 (0)