react-select: ForwardRef component errors out in custom WP block when Gutenberg plugin is active
Hey y’all,
I’m having a problem with a custom WordPress block that uses react-select and I have come to believe that this is a bug in react-select itself. I know that as a general rule, you shouldn’t assume bugs are in the libraries that you use, but I’ve been at this for weeks and this is the best lead I have right now.
The backstory? My organization contracted with a third party to build out their new website. They are a WP shop, so naturally, they built a WP site. During development, they activated a beta version of Gutenberg (the page editor) to develop against upcoming features, but they left the beta version of Gutenberg on for some reason. (I wasn’t there at the time.) Everything worked fine, until we updated WP to 5.8, then all of the sudden the custom blocks stopped working. I accidentally found the workaround of deactivating the beta of Gutenberg, and ever since then I’ve been working on figuring out what is happening.
Let’s get to some concrete details. I’ll start with screenshots:
Here is what my local environment looks like before the bug strikes:
The “CPT Archive” component in the center of the screen is the custom block I’m using in my test page, but there are several blocks that break due to this bug.
This next screenshot is production, where the workaround has been applied and everything works as expected:
If you look to the right, you’ll see a panel with several controls for the block. This is where react-select is used, under the “Choose up to three posts” text. I’ll post the source code for this component below these screenshots.
Here is the final screenshot, where things go wrong…
As soon as you click on the block, when the side panel with the react-select component tries to mount, it completely breaks the custom block. If you look at the console log, there’s a mention of the <ForwardRef> component erroring out.
I can’t post the entire codebase here, but I will post two React components. The first one is called PostTypePicker:
import AsyncSelect from 'react-select/async';
import ResourcePicker from './ResourcePicker';
const { wp } = window;
const { Component } = wp.element;
const { apiFetch } = wp;
/**
* PostPicker is the react component for building React Select based
* post pickers.
*
* @param post_types
*/
class PostTypePicker extends ResourcePicker {
constructor(props) {
console.log('PostTypePicker constructor');
super(props);
this.state = {
options: [],
};
this.loadOptions = this.loadOptions.bind(this);
}
componentDidMount() {
const this2 = this;
console.log('PostTypePicker componentDidMount');
console.log(this2);
}
async loadOptions() {
console.log('PostTypePicker loadOptions');
const { postTypeLimit } = this.props;
const self = this;
return apiFetch({ path: '/washu/v1/posttypes' }).then((options) => {
return options
.filter((opt) => {
if (!postTypeLimit) {
return true;
}
return postTypeLimit.includes(opt.name);
})
.map((opt) => {
const arr = [];
arr.value = opt.name;
arr.label = self.prettifyLabel(opt.label);
return arr;
});
});
}
/**
* Extract necessary values and store in attr (for multiselects).
*
* TODO: Keeping for legacy purposes, will need tweaks when re-implementing multiselects
*
* @param types
*/
handlePostTypeChange(types) {
const post_type = types.map((type) => {
return type.value;
});
wp.setAttributes({ post_type });
}
/**
* Take a snake/kebab-case slug and turn it into a nice pretty label
*
* @param label
* @returns string
*/
prettifyLabel(label) {
const strip_pre = lodash.replace(label, /washu_/g, '');
const spacify = lodash.replace(strip_pre, /[_|-]/g, ' ');
if (['post', 'posts'].includes(spacify.toLowerCase())) {
return 'News';
}
return lodash.startCase(spacify);
}
/**
* Regenerate value/label pairs from slug (for multiselects).
*
* @param post_types
* @returns object
*/
rehydratePostTypeSelection(post_types) {
console.log('PostTypePicker rehydratePostTypeSelection');
if (Array.isArray(post_types)) {
return post_types.map((type) => {
return {
value: type,
label: this.prettifyLabel(type),
};
});
}
return {
value: post_types,
label: this.prettifyLabel(post_types),
};
}
render() {
const multiple = this.props.isMulti;
const handleChange = this.props.onChange;
const { selected } = this.props;
console.log('PostTypePicker rendering now');
console.log(this);
return (
<AsyncSelect
isMulti={multiple}
value={this.rehydratePostTypeSelection(selected)}
loadOptions={this.loadOptions}
defaultOptions
onChange={handleChange}
/>
);
}
}
export default PostTypePicker;
The next one is called ResourcePicker:
import AsyncSelect from 'react-select/async';
const { wp } = window;
const { Component } = wp.element;
const { Spinner } = wp.components;
/**
* ResourcePicker is the base react component for building React Select based
* pickers. It uses a corresponding Resource object as the source of the data.
*/
class ResourcePicker extends Component {
/**
* Initializes the Resource Picker
*/
constructor() {
super(...arguments); // eslint-disable-line
this.state = {
loaded: false,
};
this.onChange = this.onChange.bind(this);
this.getResource = this.getResource.bind(this);
this.idMapper = this.idMapper.bind(this);
}
/**
* Fetch Selected Terms from the Resource
*/
componentDidMount() {
const self = this;
const { initialSelection } = this.props;
const resource = this.getResource(this.props);
if (!resource.getSelection()) {
resource.loadSelection(initialSelection).then((results) => {
self.setState({ loaded: true });
});
} else {
self.setState({ loaded: true });
}
console.log('ResourcePicker componentDidMount');
console.log(self);
}
/**
* Renders the ResourceSelect component
*
* @returns {object}
*/
render() {
if (!this.state.loaded) {
return <Spinner />;
}
const { isMulti } = this.props;
const resource = this.getResource(this.props);
console.log('ResourcePicker render');
console.log(this);
return (
<AsyncSelect
menuPortalTarget={document.body}
styles={{ menuPortal: (base) => ({ ...base, zIndex: 99999 }) }}
isMulti={isMulti}
isClearable
defaultValue={resource.getSelection()}
loadOptions={resource.getFinder()}
defaultOptions
onChange={this.onChange}
getOptionLabel={resource.getOptionLabel()}
getOptionValue={resource.getOptionValue()}
/>
);
}
/**
* Updates the data sources connected to this Resource Select
*
* @param {object} selection The new selection
* @param {object} opts Optional opts
*/
onChange(selection, { action }) {
console.log('ResourcePicker onChange');
const { onChange } = this.props;
if (!onChange) {
return;
}
switch (action) {
case 'select-option':
case 'remove-value':
onChange(this.serializeSelection(selection));
break;
case 'clear':
onChange(this.serializeSelection(null));
break;
default:
break;
}
}
/**
* Saves the selection in the Control attributes
*
* @param {object} selection The new selection
* @returns {string}
*/
serializeSelection(selection) {
const { setAttributes } = this.props;
const multiple = this.props.isMulti;
const resource = this.getResource(this.props);
let picks;
if (multiple) {
const values = selection
? lodash.map(selection, this.props.idMapper || this.idMapper)
: [];
resource.selectedIDs = values;
resource.selection = selection || [];
picks = lodash.join(values, ',');
} else {
const value = selection ? selection.id : '';
resource.selectedIDs = selection ? [selection.id] : [];
resource.selection = selection ? [selection] : [];
picks = `${value}`;
}
return picks;
}
/**
* Lazy initializes the resource object.
*
* @param {object} props The component props.
* @returns {object}
*/
getResource(props) {
/* abstract */
return null;
}
/**
* Extracts item id from item.
*
* @param {object} item The item to map
* @returns {number}
*/
idMapper(item) {
return item.id || false;
}
}
export default ResourcePicker;
As you can see in the code, I’ve added calls to console.log. For the particular CPT Archive component, it uses PostTypePicker (the first one). The error happens after PostTypePicker.render() is called, but before PostTypePicker.componentDidMount(). So, this leads me to believe that the error about <ForwardRef> is happening within AsyncSelect.render() or similar.
An additional detail: when the stable version of Gutenberg is enabled, it uses React 16.13.x. When the beta version of Gutenberg is enabled (when the bug triggers) it uses React 17.0.x.
Sorry this turned into a book of a bug report. I’m pretty much stumped on this one, and I’m hoping that it’s just something obvious I missed because this particular area is new to me.
Thanks in advance, Sam
About this issue
- Original URL
- State: closed
- Created 3 years ago
- Reactions: 1
- Comments: 16
Commits related to this issue
- Add react and react-dom as external deps https://github.com/JedWatson/react-select/issues/4893 — committed to SayHelloGmbH/hello-roots by markhowellsmead 2 years ago
@mevanloon I’m also using this as you described. Actually, when we understand the cause of the issue the solution is simple. The error occurs when the react version used by react-select is different from the one in Gutenberg. By providing react and react-dom as externals to the build context, Webpack uses the same react version that was added to the window object by the Gutenberg. As long as Gutenberg and react-select use two different react versions, it will throw errors.
Thank you @montchr You saved my day.
I recently ran into the same issue in a custom block after updating
react-selectfrom v3 to v5. In my case, the fix turned out to be extremely simple though it took quite some time to arrive there 😅 …Because Gutenberg provides its own versions of React and ReactDOM, we have to tell third-party libraries about those versions by mapping the package names to the global variables available in the block editor.
In a Webpack setup, that’s as simple as adding the mappings to the Webpack configuration’s
externalsoption:In this case,
react/react-domare the package names (i.e.npm install <package-name>), andReact/ReactDOMcorrespond to thewindow.Reactandwindow.ReactDOMglobal variables.Credit goes to this article for pointing me in the right direction.
Does that help with your issue @sehqlr?
@weswil07 React-Select v4 was a focus on removing the deprecated lifecycle methods so that it would be compatible with React v17 as well as an update to Emotion v11.
One suspicion I have is that using Emotion to translate the JSX means that there might be some conflict involved there, but I’m too unexperienced with both Emotion and v17 to say so with any confidence.
Hey, just want to mention that I have noticed the same error in an application I am working on. This is a Python app with React on the front end, and is being installed by another Python/React app using pip. I was able to get rid of the error by downgrading react-select to version 3.2.0. React-select 4.0.0 is the earliest version that this problem appears for us.