Skip to content

Commit 03ec690

Browse files
erikrasjoshua-burbidgeErik Rasmussen
authored
Fix #1055: Prevent overwriting getter-only properties in renderComponent (#1056)
* replace bundlesize with size-limit to remove iltorb * use mjs for config files * add eslint config for mjs * Fix #1055: Prevent overwriting getter-only properties in renderComponent The component branch of renderComponent was using Object.assign(lazyProps, rest) which attempts to overwrite getter-only properties (like 'active'). This causes 'Cannot set property active' errors, especially with React 19. Fix: Use the same safe property descriptor merging pattern already used in the render and children branches - only add properties that don't conflict with existing getters. Tests: Added comprehensive coverage for getter-only property scenarios across all render paths (component, render, children). Tests fail with buggy code, pass with fix. Fixes: #1055 --------- Co-authored-by: joshua-burbidge <joshdburbidge@gmail.com> Co-authored-by: Erik Rasmussen <erik@mini.local>
1 parent 36ee99a commit 03ec690

7 files changed

Lines changed: 390 additions & 371 deletions

File tree

eslint.config.js renamed to eslint.config.mjs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,21 @@ export default [
161161
},
162162
},
163163

164+
// Configuration for .mjs files (ES modules in Node.js environment)
165+
{
166+
files: ["**/*.mjs"],
167+
languageOptions: {
168+
ecmaVersion: "latest",
169+
sourceType: "module",
170+
globals: {
171+
...nodeGlobals,
172+
},
173+
},
174+
rules: {
175+
"no-undef": "error",
176+
},
177+
},
178+
164179
// Specific overrides for ALL test files (JS and TS)
165180
{
166181
files: ["**/*.test.js", "**/*.test.jsx", "**/*.test.ts", "**/*.test.tsx"],

package-scripts.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ module.exports = {
1616
),
1717
size: {
1818
description: "check the size of the bundle",
19-
script: "bundlesize",
19+
script: "size-limit",
2020
},
2121
},
2222
build: {

package.json

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@
5353
"babel-core": "^7.0.0-bridge.0",
5454
"babel-eslint": "^10.1.0",
5555
"babel-jest": "^29.7.0",
56-
"bundlesize": "^0.18.2",
56+
"@size-limit/preset-small-lib": "^11.1.6",
57+
"size-limit": "^11.1.6",
5758
"doctoc": "^2.2.1",
5859
"dtslint": "^4.2.1",
5960
"eslint": "^9.27.0",
@@ -100,18 +101,18 @@
100101
"/typescript/"
101102
]
102103
},
103-
"bundlesize": [
104+
"size-limit": [
104105
{
105106
"path": "dist/react-final-form.umd.min.js",
106-
"threshold": "2kB"
107+
"limit": "4kB"
107108
},
108109
{
109110
"path": "dist/react-final-form.es.js",
110-
"threshold": "3kB"
111+
"limit": "4kB"
111112
},
112113
{
113114
"path": "dist/react-final-form.cjs.js",
114-
"threshold": "3kB"
115+
"limit": "4kB"
115116
}
116117
],
117118
"collective": {
File renamed without changes.

src/renderComponent.test.js

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,4 +69,76 @@ describe("renderComponent", () => {
6969
expect(getA).not.toHaveBeenCalled();
7070
expect(getB).not.toHaveBeenCalled();
7171
});
72+
73+
it("should not overwrite getter-only properties when using component prop", () => {
74+
// This test reproduces issue #1055
75+
// When lazyProps has getter-only properties (like 'active'), and props contains
76+
// a property with the same name, it should not attempt to overwrite the getter
77+
const Component = () => null;
78+
const props = {
79+
component: Component,
80+
active: "value-from-props", // This would cause "Cannot set property active" error
81+
customProp: "custom",
82+
};
83+
const lazyProps = {};
84+
Object.defineProperty(lazyProps, "active", {
85+
get: () => "value-from-getter",
86+
enumerable: true,
87+
// Note: no setter - this is getter-only
88+
});
89+
const name = "TestComponent";
90+
91+
// Should not throw "Cannot set property active"
92+
let result;
93+
expect(() => {
94+
result = renderComponent(props, lazyProps, name);
95+
}).not.toThrow();
96+
97+
// Check the React element was created with correct props
98+
expect(result.type).toBe(Component);
99+
100+
// The getter-only property should remain and use the getter value
101+
expect(result.props.active).toBe("value-from-getter");
102+
103+
// Custom props should still be passed through
104+
expect(result.props.customProp).toBe("custom");
105+
});
106+
107+
it("should handle getter-only properties in all render paths", () => {
108+
const lazyProps = {};
109+
Object.defineProperty(lazyProps, "active", {
110+
get: () => "getter-value",
111+
enumerable: true,
112+
});
113+
114+
// Test with render prop
115+
const render = jest.fn();
116+
renderComponent(
117+
{ render, active: "prop-value" },
118+
lazyProps,
119+
"TestComponent",
120+
);
121+
expect(render).toHaveBeenCalled();
122+
expect(render.mock.calls[0][0].active).toBe("getter-value");
123+
124+
// Test with children function
125+
const children = jest.fn();
126+
renderComponent(
127+
{ children, active: "prop-value" },
128+
lazyProps,
129+
"TestComponent",
130+
);
131+
expect(children).toHaveBeenCalled();
132+
expect(children.mock.calls[0][0].active).toBe("getter-value");
133+
134+
// Test with component prop
135+
const Component = () => null;
136+
const result = renderComponent(
137+
{ component: Component, active: "prop-value" },
138+
lazyProps,
139+
"TestComponent",
140+
);
141+
expect(result.type).toBe(Component);
142+
expect(result.props.active).toBe("getter-value");
143+
});
72144
});

src/renderComponent.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,20 @@ export default function renderComponent<T>(
1010
): React.ReactNode {
1111
const { render, children, component, ...rest } = props;
1212
if (component) {
13-
return React.createElement(
14-
component,
15-
Object.assign(lazyProps, rest, {
16-
children,
17-
render,
18-
}),
19-
);
13+
// FIX: Don't use Object.assign which tries to overwrite getters
14+
// Instead, create a new object with lazyProps descriptors first,
15+
// then add non-conflicting properties from rest
16+
const result = {} as any;
17+
Object.defineProperties(result, Object.getOwnPropertyDescriptors(lazyProps));
18+
const restDescriptors = Object.getOwnPropertyDescriptors(rest);
19+
for (const key in restDescriptors) {
20+
if (!(key in result)) {
21+
Object.defineProperty(result, key, restDescriptors[key]);
22+
}
23+
}
24+
result.children = children;
25+
result.render = render;
26+
return React.createElement(component, result);
2027
}
2128
if (render) {
2229
const result = {} as T;

0 commit comments

Comments
 (0)