fix(compat): avoid mutating user-provided style objects when appending px#5117
fix(compat): avoid mutating user-provided style objects when appending px#5117JSap0914 wants to merge 2 commits into
Conversation
…g px handleDomVNode() was directly mutating the style object passed by the user via value[key] += 'px'. This permanently modifies the caller's original object—an unexpected side effect. Fix by shallow-copying the style object before the per-key px loop. The existing px-append behaviour is preserved; the user's object is left unchanged.
📊 Tachometer Benchmark ResultsSummaryA summary of the benchmark results will show here once they finish. ResultsThe full results of your benchmarks will show here once they finish. |
| } | ||
|
|
||
| if (i === 'style' && typeof value === 'object') { | ||
| value = assign({}, value); |
There was a problem hiding this comment.
The PR description says:
(...) mutates the style object supplied by the caller when appending
pxto numeric values
which hints that it's only a problem when px is appended. The change here though is broader. It also applies when no px is appended too. That seems wasteful in terms of performance that we always pay the cost of cloning an object for that too.
| const style = { margin: 10, opacity: 0.5 }; | ||
| const originalMargin = style.margin; | ||
| const originalOpacity = style.opacity; | ||
|
|
||
| render(<div style={style} />, scratch); | ||
|
|
||
| expect(style.margin).to.equal( | ||
| originalMargin, | ||
| 'style.margin should not be mutated' | ||
| ); | ||
| expect(style.opacity).to.equal( | ||
| originalOpacity, | ||
| 'style.opacity should not be mutated' | ||
| ); |
There was a problem hiding this comment.
Let's simplify this:
| const style = { margin: 10, opacity: 0.5 }; | |
| const originalMargin = style.margin; | |
| const originalOpacity = style.opacity; | |
| render(<div style={style} />, scratch); | |
| expect(style.margin).to.equal( | |
| originalMargin, | |
| 'style.margin should not be mutated' | |
| ); | |
| expect(style.opacity).to.equal( | |
| originalOpacity, | |
| 'style.opacity should not be mutated' | |
| ); | |
| const style = { margin: 10, opacity: 0.5 }; | |
| render(<div style={style} />, scratch); | |
| expect(style).to.equal({ margin: 10, opacity: 0.5 }); |
| for (let key in value) { | ||
| if (typeof value[key] === 'number' && !IS_NON_DIMENSIONAL.test(key)) { | ||
| if (!cloned) { | ||
| value = assign({}, value); |
There was a problem hiding this comment.
The clone is now lazy — only runs the first time a numeric value actually needs appended, so style objects with no such properties pay zero clone cost.
Bug
handleDomVNodeincompat/src/render.jsmutates the style object supplied by the caller when appendingpxto numeric values:valueis a direct reference toprops[i](the original object passed by the user). After this loop the caller's object is permanently modified—margin: 10becomesmargin: "10px"—which is an unexpected side effect.Fix
Shallow-copy the style object before the loop so mutations stay local to
normalizedProps:The px-append behaviour and the rendered output are unchanged; only the caller's object is now left untouched.
Verification
New test: "should not mutate the original style object when appending px" — this test failed before the fix (
style.marginwas mutated to"10px") and passes after it.All 46 existing compat-render tests continue to pass; full suite: 1242 passed, 12 skipped across 101 test files.