__TL;DR__: The crank.js example at https://codesandbox.io/s/crank-todomvc-k6s0xIt sometimes causes an exception around the re-rendering and dispatch of two "todo.destroy" events from the TodoItem, breaking the app. The immediate fix I found is instead of dispatching the "todo.destroy" event from the keydown handler, just call ev.target.blur().
While using the crank.js example at https://codesandbox.io/s/crank-todomvc-k6s0x when I edit a todo, delete the all the characters from the title, and the press the enter or escape key, the following exception is thrown in the console:
index.js:27 TypeError: Generator is already executing.
聽 聽 at step (index-4b79835a.js? [sm]:62)
聽 聽 at Object.eval [as next] (index-4b79835a.js? [sm]:58)
聽 聽 at HostNode.commit (index-4b79835a.js? [sm]:1374)
聽 聽 at ComponentNode.commit (index-4b79835a.js? [sm]:1644)
聽 聽 at ComponentNode.ParentNode.updateChildren (index-4b79835a.js? [sm]:1271)
聽 聽 at ComponentNode.updateChildren (index-4b79835a.js? [sm]:1458)
聽 聽 at ComponentNode.step (index-4b79835a.js? [sm]:1523)
聽 聽 at ComponentNode.run (index-4b79835a.js? [sm]:1614)
聽 聽 at ComponentNode.refresh (index-4b79835a.js? [sm]:1546)
聽 聽 at Context.refresh (index-4b79835a.js? [sm]:1747)
聽 聽 at Context.eval (index.js? [sm]:269)
聽 聽 at Context.dispatchEvent (index-4b79835a.js? [sm]:604)
聽 聽 at Context.CrankEventTarget.dispatchEvent (index-4b79835a.js? [sm]:833)
聽 聽 at Context.CrankEventTarget.dispatchEvent (index-4b79835a.js? [sm]:835)
聽 聽 at Context.CrankEventTarget.dispatchEvent (index-4b79835a.js? [sm]:835)
聽 聽 at HTMLLIElement.TodoItem.addEventListener.capture (index.js? [sm]:118)
聽 聽 at updateChildren (dom.js? [sm]:125)
聽 聽 at HostContext.eval (dom.js? [sm]:228)
聽 聽 at step (index-4b79835a.js? [sm]:109)
聽 聽 at Object.eval [as next] (index-4b79835a.js? [sm]:58)
聽 聽 at HostNode.commit (index-4b79835a.js? [sm]:1374)
聽 聽 at ComponentNode.commit (index-4b79835a.js? [sm]:1644)
聽 聽 at ComponentNode.ParentNode.updateChildren (index-4b79835a.js? [sm]:1271)
聽 聽 at ComponentNode.updateChildren (index-4b79835a.js? [sm]:1458)
聽 聽 at ComponentNode.step (index-4b79835a.js? [sm]:1523)
聽 聽 at ComponentNode.run (index-4b79835a.js? [sm]:1614)
聽 聽 at ComponentNode.refresh (index-4b79835a.js? [sm]:1546)
聽 聽 at Context.refresh (index-4b79835a.js? [sm]:1747)
聽 聽 at Context.eval (index.js? [sm]:269)
聽 聽 at Context.dispatchEvent (index-4b79835a.js? [sm]:604)
聽 聽 at Context.CrankEventTarget.dispatchEvent (index-4b79835a.js? [sm]:833)
聽 聽 at Context.CrankEventTarget.dispatchEvent (index-4b79835a.js? [sm]:835)
聽 聽 at Context.CrankEventTarget.dispatchEvent (index-4b79835a.js? [sm]:835)
聽 聽 at HTMLLIElement.eval (index.js? [sm]:95)
Browser info:
Google Chrome 83.0.4103.14聽(Official Build)聽beta聽(64-bit)
Revision be04594a2b8411758b860104bc0a1033417178be-refs/branch-heads/4103@{#119}
JavaScript: V8聽8.3.110.4
OS: Linux
It looks like the code is dispatching two "todo.destroy" events, one when enter/escape is pressed (through the keydown handler) and also, when active is set to false and the TodoItem is re-rended (or something else) causes the blur event to be triggered on the input, thus dispatching a second "todo.destroy" event. Not sure why that blows up so spectacularly though.
The fix I found is instead of dispatching the "todo.destroy" event from the keydown handler, just call ev.target.blur(). There may be a better error to be showing or some way to catch it.
Oddly I can鈥檛 reproduce it in Safari, and scarily this seems to be an error caused somewhere between the typescript generator output and Chrome. Will investigate further. In the meantime, possible related issues:
https://github.com/microsoft/TypeScript/issues/9372
https://github.com/redux-saga/redux-saga/issues/703
Thank you for the report!
Edited to add V8 version:
JavaScript: V8聽8.3.110.4
Checking in my investigation into the bug before going to take a shower to think about solutions:
todo.destroy event. This causes the app to rerender and remove the TodoItem from the page, which in turn triggers the blur event in Chrome, which triggers a second todo.destroy event, all synchronously. Here鈥檚 a minimal reproduction of the problem:/** @jsx createElement */
import { createElement } from "@bikeshaving/crank";
import { renderer } from "@bikeshaving/crank/dom";
const ENTER_KEY = 13;
function* Input() {
this.addEventListener("keydown", ev => {
if (ev.keyCode === ENTER_KEY) {
console.log("KEYDOWN");
this.dispatchEvent(new Event("bug.destroy", { bubbles: true }));
}
});
this.addEventListener("blur", ev => {
console.log("BLUR");
this.dispatchEvent(new Event("bug.destroy", { bubbles: true }));
});
while (true) {
yield <input class="edit" value="Hello?" />;
}
}
function* App() {
let show = true;
this.addEventListener("bug.destroy", ev => {
show = false;
this.refresh();
});
while (true) {
yield <div>{show && <Input />}</div>;
}
}
renderer.render(<App />, document.body);
let a;
function *b() {
let i = 0;
while (true) {
if (a) {
a.next();
}
yield i++;
}
}
const c = b();
c.next(); // {value: 0, done: false}
c.next(); // {value: 1, done: false}
a = c;
c.next();
// TypeError: Generator is executing
It鈥檚 very seldom that a generator can reference or trigger its own iterator while executing, but this is something which can happen in Crank and which we should guard against.
Solutions: TK
This causes the app to rerender and remove the TodoItem from the page, which in turn triggers the blur event in Chrome, which triggers a second todo.destroy event, all synchronously.
Why is the removal of TodoItem not resulting in the event listeners being removed? Is it just an order-of-operations issue in Crank?
@kyeotic
Why is the removal of TodoItem not resulting in the event listeners being removed?
You鈥檙e right the event listener wasn鈥檛 being removed in time. However there is a more fundamental problem, which is that because event dispatch is synchronous, a developer might inadvertently cause a sync cascade of updates between parent and child:
function Child(this: Context) {
this.dispatchEvent(new Event("test", {bubbles: true}));
return <span>child</span>;
}
function* Parent(this: Context) {
this.addEventListener("test", () => {
try {
this.refresh();
done();
} catch (err) {
done(err);
}
});
while (true) {
yield (
<div>
<Child />
</div>
);
}
}
renderer.render(<Parent />, document.body); // Generator is already executing
The solutions were:
My preferences for solutions were 2, 1, 3, 4. I chose 2 and maybe we鈥檒l warn when people write code which attempts to synchronously step through a generator multiple times.
@brainkim That makes sense, I wasn't thinking about the sync loop. I agree that #2 is good as long as their is a warning, with the possibility of turning it into an error via configuration somehow. Errors have the advantages of stack traces, which can greatly simplify debugging in large component trees.
Fixed in 0.1.2. Thanks for the bug report it was very helpful!
@brainkim Hey, I would like to clarify. Is Crank removing event listeners before next refresh operation? You said that refreshes are synchronous. So the blur even should never be called, no matter what browser it is. Right?
And another thing about 4 options that you presented. Which one did you choose?
@lukejagodzinski I chose option 2, fail silently. We may end up adding warnings to the library indicating that you called refresh on a synchronously iterating component, but I think failing silently was the best choice because as we saw in the issue, the situation can differ according to subtle differences in the ways different browsers handle events, and it can happen if, for instance, the user synchronously dispatches an event from the child of a generator component.
I have to think a little about what sort of warnings Crank should emit, because I think the situation in React where the console yelled at you for every little thing like rendering an array whose members aren鈥檛 keyed was really annoying, and I don鈥檛 want to replicate that development experience. I鈥檇 much prefer it if things you did wrong were actually just discoverable as you write and test your app rather than having the console yell at you.
As far as event handlers go, Crank removes event handlers when nodes are unmounted in the case of using the this.addEventListener API, but not yet when you鈥檙e using the onevent props APIs. We may remove onevent props from removed nodes, I just want to make sure there aren鈥檛 any actual ramifications to nulling out onevent callbacks, and I鈥檓 not sure it鈥檚 completely necessary, given that most modern browsers don鈥檛 really care if you null out onevent props. If you think we should clear onevent props when nodes are removed from the DOM it鈥檒l probably be a couple extra lines of code. But again, most of the danger of having event callbacks fire one last time before nodes are removed has been eliminated with the fix above. Let me know what you think.
@brainkim ok it makes sense to choose option 2 as these event listeners won't be executed anyway but browser is just trying to do so. Thanks for clarification :). Also it would be good to check in specs of generators what should be the correct behavior. Maybe Chrome just does it wrong? From experience I would say that Safari is doing something wrong :P but maybe this time it's different story :)
Also about errors/warnings management. I actually don't have anything against those console warnings. But in the ideal world it could be just supported by the code editor (eslint plugin maybe?) that just tells you as you write that something is wrong. There could also be a bundler plugin that checks such stuff and informs you about problems. However, correct me if I'm wrong those React errors does not leak into production, there are only seen in dev env, right?
IMHO, the best option would be eslint plugin, like react-hooks plugin for eslint.
Ok now I understand the difference. Yep I don't think those onevent's should be removed/nullified. They will be removed anyway by the garbage collector.
Thanks!
Just as a remark: There are also subtle logical bugs in the TodoMVC demo. The problem is that the component TodoItem does not handle prop value changes properly.
It will not be a problem in this simple TododMVC demo, but yet the implementation of the component TodoItem is not really correct:
let title = todo.title - this mutable title variable is used e.g. directly in the blur event handler. For example in the following case this will not work as desired: Component TodoItem will be mounted with a certain todo prop => then the todo prop will be changed (without unmounting) => then the double-click happens => then the "blur" happens (without any key presses in between) => problem: That todo.edit custom event will be dispatch with the wrong title property.
As title is also used directly in the JSX output, again for example in case that the prop todo changes without any key presses in between, after a double-click the output may show the wrong title.
The ESC_KEY is not handled properly - should behave like a dismiss not like an apply.
Component Mainand Footer:
Main component function and the Footer component functions are effectful - shouldn't they be generator functions? Otherwise those event handlers will be registered on each refresh.let title = todo.title - this mutable title variable is used e.g. directly in the blur event handler. For example in the following case this will not work as desired: Component TodoItem will be mounted with a certain todo prop => then the todo prop will be changed (without unmounting) => then the double-click happens => then the "blur" happens (without any key presses in between) => problem: That todo.edit custom event will be dispatch with the wrong title property.
As title is also used directly in the JSX output, again for example in case that the prop todo changes without any key presses in between, after a double-click the output may show the wrong title.
What are the exact user actions which trigger this bug? I鈥檓 having trouble understanding. If you鈥檙e saying that the todo item related to the currently edited todo is being changed as the user is editing it, it鈥檚 not exactly clear what the correct behavior should be, and todomvc leaves it underspecified. My intuition is that even if you had some external events affecting the todos, the most pleasant ux would be to not change the currently edited todo, and let the user decide what to do.
Meanwhile, don鈥檛 you appreciate how you鈥檙e able to reason about local changes relative to parent updates 馃槂
The ESC_KEY is not handled properly - should behave like a dismiss not like an apply.
My bad I鈥檒l make that fix eventually if someone doesn鈥檛 get to it first.
Both the Main component function and the Footer component functions are effectful - shouldn't they be generator functions? Otherwise those event handlers will be registered on each refresh.
We clear event listeners when the component is a sync function component as a convenience. It doesn鈥檛 really matter for performance in todomvc but you may want to use a generator function if the component is frequently updated as an optimization.
We clear event listeners when the component is a sync function component as a convenience.
Ah okay, I see, thanks - stupid me. That may be the reason why you always talk about "basic"/"sync"/"simple" components and not about "stateless" components ;-) ... But frankly, I'm not really sure whether I see a big win in having access to this in those "basic"/"sync"/"simple" components as it allows to do confusing things and IMHO everything that you can do with "basic"/"sync"/"simple" components which is not pure/stateless/effectless feels like an anti-pattern to me (just to save two lines of code) - but that's just MHO, of course.
What are the exact user actions which trigger this bug?
Maybe that's more a matter of what we expect a really correct implemented component should look like. IMHO, a component should ALWAYS behave in reasonable and determinate way independent what happens in the outside world.
Regarding this mutable title variable: This will only be updated on input and keydown and on keydown only a title = title.trim() will be performed.
Maybe it's easier to say that title = ev.target.value.trim() should be used in the keydown and blur event listeners. And in that for loop, the variable title should be updated in case that we are not in "edit mode" and title differs from todo.title.
PS: I personally would leave the edit mode automatically in case the values of todo.id or todo.title change, this may confuse the user a bit in that moment but at least no data would be changed in a undesired manner - but I guess nobody's doing it that way in those dozens of TodoMVC implementations ;-), so anyway...