Skip to content

Commit 1dea32a

Browse files
committed
fix: add React 19 compatibility for getWidget (defaultOptions + remove isForwardRef)
fixes #4907 - Add support for new static defaultOptions property on widgets (React 19 compatible) - Maintain backwards compatibility with legacy defaultProps.options pattern - Replace deprecated react-is.isForwardRef() with general component detection - Update documentation and add migration guide
1 parent 9db4b91 commit 1dea32a

5 files changed

Lines changed: 101 additions & 23 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ should change the heading of the (upcoming) version to include a major version b
7171
- Fixed issue with default value not being prefilled when object with if/then is nested inside another object, fixing [#4222](https://github.com/rjsf-team/react-jsonschema-form/issues/4222)
7272
- Fixed issue with schema array with nested dependent fixed-length, fixing [#3754](https://github.com/rjsf-team/react-jsonschema-form/issues/3754)
7373
- Updated `CustomValidator` type to accept `errorSchema`, so its implementation can be based on result of ajv validation ([#4898](https://github.com/rjsf-team/react-jsonschema-form/pull/4899))
74+
- Updated `getWidget.tsx` to support a new static `defaultOptions` property for widget default options (recommended for React 19 compatibility), while maintaining backwards compatibility with the legacy `defaultProps.options` pattern, fixing [#4907](https://github.com/rjsf-team/react-jsonschema-form/issues/4907). Also replaced `react-is.isForwardRef()` with a more general component detection check.
7475

7576
## @rjsf/validator-ajv8
7677

packages/docs/docs/advanced-customization/custom-widgets-fields.md

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,7 @@ This is useful if you expose the `uiSchema` as pure JSON, which can't carry func
250250

251251
### Custom widget options
252252

253-
If you need to pass options to your custom widget, you can add a `ui:options` object containing those properties. If the widget has `defaultProps`, the options will be merged with the (optional) options object from `defaultProps`:
253+
If you need to pass options to your custom widget, you can add a `ui:options` object containing those properties. If the widget has a static `defaultOptions` property, the options will be merged with the options object from `defaultOptions`:
254254

255255
```tsx
256256
import { RJSFSchema, UiSchema, WidgetProps } from '@rjsf/utils';
@@ -266,10 +266,9 @@ function MyCustomWidget(props: WidgetProps) {
266266
return <input style={{ color, backgroundColor }} />;
267267
}
268268

269-
MyCustomWidget.defaultProps = {
270-
options: {
271-
color: 'red',
272-
},
269+
// React 19+: Use defaultOptions instead of defaultProps.options
270+
(MyCustomWidget as any).defaultOptions = {
271+
color: 'red',
273272
};
274273

275274
const uiSchema: UiSchema = {
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# 7.x Upgrade Guide
2+
3+
## Breaking changes
4+
5+
### Widget defaultProps.options deprecated in favor of defaultOptions
6+
7+
In order to support React 19 properly, the `mergeWidgetOptions()` function in `@rjsf/utils` `getWidget.tsx` was updated to support a new `defaultOptions` static property.
8+
9+
React 19 [deprecated `defaultProps`](https://react.dev/blog/2024/04/25/react-19#removed-proptypes-and-defaultprops) for function components. To maintain React 19 compatibility, widgets should now use a static `defaultOptions` property instead of `defaultProps.options`.
10+
11+
**Note:** For backwards compatibility, `defaultProps.options` is still supported but **deprecated**. It will be removed in a future major version. You should migrate to the new pattern.
12+
13+
#### Migration
14+
15+
**Before (React 18 and earlier):**
16+
17+
```tsx
18+
import { WidgetProps } from '@rjsf/utils';
19+
20+
function MyCustomWidget(props: WidgetProps) {
21+
const { options } = props;
22+
const { color, backgroundColor } = options;
23+
return <input style={{ color, backgroundColor }} />;
24+
}
25+
26+
// Old pattern - deprecated in React 19
27+
MyCustomWidget.defaultProps = {
28+
options: {
29+
color: 'red',
30+
},
31+
};
32+
```
33+
34+
**After (React 19 compatible):**
35+
36+
```tsx
37+
import { WidgetProps } from '@rjsf/utils';
38+
39+
function MyCustomWidget(props: WidgetProps) {
40+
const { options } = props;
41+
const { color, backgroundColor } = options;
42+
return <input style={{ color, backgroundColor }} />;
43+
}
44+
45+
// New pattern - React 19 compatible
46+
(MyCustomWidget as any).defaultOptions = {
47+
color: 'red',
48+
};
49+
```
50+
51+
Note the key differences:
52+
53+
1. Use `defaultOptions` instead of `defaultProps.options`
54+
2. The options are now a flat object directly on the widget, not nested inside `defaultProps`
55+
56+
### react-is.isForwardRef() removed
57+
58+
The usage of `react-is.isForwardRef()` was removed from `getWidget.tsx` because React 19 no longer supports this function. ForwardRef components are now detected using `ReactIs.isValidElementType()`, which provides a robust check for all valid React component types.
59+
60+
This change should be transparent to most users, as the widget detection logic now properly handles:
61+
62+
- Regular function components
63+
- Memoized components (`React.memo`)
64+
- ForwardRef components (`React.forwardRef`)
65+
- Other exotic React components
66+
67+
## New deprecations
68+
69+
None in this release.
70+
71+
## Removed deprecations
72+
73+
None in this release.

packages/utils/src/getWidget.tsx

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { createElement } from 'react';
1+
// Note: createElement was removed as ReactIs.isForwardRef() is deprecated in React 19
22
import ReactIs from 'react-is';
33
import get from 'lodash/get';
44
import set from 'lodash/set';
55

6-
import { FormContextType, RJSFSchema, Widget, RegistryWidgetsType, StrictRJSFSchema } from './types';
6+
import { FormContextType, GenericObjectType, RJSFSchema, Widget, RegistryWidgetsType, StrictRJSFSchema } from './types';
77
import getSchemaType from './getSchemaType';
88

99
/** The map of schema types to widget type to widget name
@@ -61,10 +61,13 @@ const widgetMap: { [k: string]: { [j: string]: string } } = {
6161
},
6262
};
6363

64-
/** Wraps the given widget with stateless functional component that will merge any `defaultProps.options` with the
64+
/** Wraps the given widget with stateless functional component that will merge any `defaultOptions` with the
6565
* `options` that are provided in the props. It will add the wrapper component as a `MergedWidget` property onto the
6666
* `Widget` so that future attempts to wrap `AWidget` will return the already existing wrapper.
6767
*
68+
* NOTE: In React 19, `defaultProps` is deprecated for function components. Widgets should now define default options
69+
* using a static `defaultOptions` property instead of `defaultProps.options`.
70+
*
6871
* @param AWidget - A widget that will be wrapped or one that is already wrapped
6972
* @returns - The wrapper widget
7073
*/
@@ -74,7 +77,15 @@ function mergeWidgetOptions<T = any, S extends StrictRJSFSchema = RJSFSchema, F
7477
let MergedWidget: Widget<T, S, F> | undefined = get(AWidget, 'MergedWidget');
7578
// cache return value as property of widget for proper react reconciliation
7679
if (!MergedWidget) {
77-
const defaultOptions = (AWidget.defaultProps && AWidget.defaultProps.options) || {};
80+
// Support both new `defaultOptions` (React 19+) and legacy `defaultProps.options` for backwards compatibility
81+
const widgetWithDefaults = AWidget as Widget<T, S, F> & {
82+
defaultOptions?: GenericObjectType;
83+
defaultProps?: { options?: GenericObjectType };
84+
};
85+
const defaultOptions =
86+
widgetWithDefaults.defaultOptions ||
87+
(widgetWithDefaults.defaultProps && widgetWithDefaults.defaultProps.options) ||
88+
{};
7889
MergedWidget = ({ options, ...props }) => {
7990
return <AWidget options={{ ...defaultOptions, ...options }} {...props} />;
8091
};
@@ -101,11 +112,9 @@ export default function getWidget<T = any, S extends StrictRJSFSchema = RJSFSche
101112
): Widget<T, S, F> {
102113
const type = getSchemaType(schema);
103114

104-
if (
105-
typeof widget === 'function' ||
106-
(widget && ReactIs.isForwardRef(createElement(widget))) ||
107-
ReactIs.isMemo(widget)
108-
) {
115+
// Check if widget is a valid React component.
116+
// We exclude strings because in RJSF, string widgets are keys in the registry, not HTML elements.
117+
if (ReactIs.isValidElementType(widget) && typeof widget !== 'string') {
109118
return mergeWidgetOptions<T, S, F>(widget as Widget<T, S, F>);
110119
}
111120

packages/utils/test/getWidget.test.tsx

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -42,27 +42,23 @@ const TestRefWidget: Widget = forwardRef<HTMLSpanElement, Partial<WidgetProps>>(
4242
);
4343
});
4444

45-
TestRefWidget.defaultProps = {
46-
options: { id: 'test-id' },
47-
};
45+
// React 19+: Use defaultOptions instead of defaultProps.options
46+
(TestRefWidget as any).defaultOptions = { id: 'test-id' };
4847

4948
function TestWidget(props: WidgetProps) {
5049
const { options } = props;
5150
return <div {...options}>test</div>;
5251
}
5352

54-
TestWidget.defaultProps = {
55-
id: 'foo',
56-
};
53+
// Note: This widget has no defaultOptions, testing fallback behavior
5754

5855
function TestWidgetDefaults(props: WidgetProps) {
5956
const { options } = props;
6057
return <div {...options}>test</div>;
6158
}
6259

63-
TestWidgetDefaults.defaultProps = {
64-
options: { color: 'yellow' },
65-
};
60+
// React 19+: Use defaultOptions instead of defaultProps.options
61+
(TestWidgetDefaults as any).defaultOptions = { color: 'yellow' };
6662

6763
const widgetProps: WidgetProps = {
6864
id: '',

0 commit comments

Comments
 (0)