Between v10.3.4 and v10.4.8, JSX.Element became incompatible with immutable.js
See original GitHub issueSummary
As of v10.4.8, storing a Preact JSX.Element in one of the immutable.js container classes will result in a crash when you extract it with .toJS()
. This did not occur with v10.1.0.
I believe immutable.js is mistaking the JSX.Element for an immutable.js class and attempting to “unpack” it infinitely.
Repro
Here (branch preact-immutable-crash) is a small typescript+webpack+preact app. Test it with
npm install && npx webpack && (cd site && http-server -c-1)
(Assuming http-server previously installed with npm install -g http-server
.)
If you build and load commit eee4, it works just fine. If you build and load commit 2c20, it crashes. The only difference between the two commits is instead of Preact v10.1.0, it uses 10.4.8.
The crash is an uncaught “RangeError: Maximum call stack size exceeded” exception somewhere deep in the guts of immutable.js.
Where the problem occurs
I don’t think webpack or typescript are relevant to the problem; the code that triggers the problem seems to be merely
let demoSet = OrderedSet<string>().add("one").add("two")
const divArray = demoSet.map(
s => <div className="Id">{s}</div>
).toJS()
render(<div>Hello<br />{divArray}</div>, domNodeHere)
The problem occurs on the “.toJS” call. “map” is a lazy version of JS map(); my understanding(?) is the mapped function shouldn’t be called until you try to extract the data. Saying .toJS().map(
would avoid the crash, but I thought(?) I was avoiding an allocation by doing it in the other order.
Variants
While writing this bug I realized I could make the crash happen with an even simpler repro:
const simplerTest = List<h.JSX.Element>().push(<div>One</div>)
const divArray = simplerTest.toJS() // Crash on this line
I have also tested, and seen the same crash, with OrderedSet instead of List (though in retrospect it doesn’t make a lot of sense to keep a JSX.Element in a set).
If instead of saying .toJS()
I say .toArray()
, the crash does not occur. The difference is that toList is a “shallow” operation and toArray is recursive. If you have an immutable List of immutable Lists of strings, toArray will produce a JS array of immutable Lists of strings and toJS will produce a JS array of JS arrays of strings.
In other words it is safe to store a JSX.Element in a immutable.js structure and extract it, the only unsafe thing is an operation where immutable.js could potentially interpret a JSX.Element as a immutable.js container.
Impact
Immutable.js is obviously well fit to React/Preact; in my experience, contexts are virtually unusable without immutable data structures. So any incompatibility between Preact and Immutable.js is pretty bad.
The problem is less bad than it could be because it only occurs with .toJS()
. (Checking Google I find people recommending always using .toArray()
over .toJS()
anyway, for performance reasons.)
I think the problem is still pretty bad because (1) storing a List of Lists and then unpacking it with .toJS is a reasonable thing to do (2) the error message you get is extremely misleading, and Chrome prints out a completely broken stack when it occurs unless you specifically run in the debugger. I encountered this error on a project and it took me a long time to figure out that .toJS()
was the cause or even that the reason the error had occurred was because I upgraded Preact.
Template questions
“Expected behavior”
I should be able to store and extract JSX.Elements from immutable.js the same way I could a string using any method, including .toJS()
.
“For npm, copy & paste the output of this terminal command: npm ls --depth 0 | grep preact”
├── preact@10.4.8
Issue Analytics
- State:
- Created 3 years ago
- Comments:6 (2 by maintainers)
Top GitHub Comments
Yeah, based on your description it sounds like this is more appropriate to fix in immutable than in preact. Thanks for the help
Good point, you should forward that to the immutable.js maintainers 👍 It would set a bad example if we start adding workarounds in Preact for issues in other libraries. It certainly isn’t doable even if we wanted to, given our own size restrictions.