ForwardRef component errors out in custom WP block when Gutenberg plugin is active
See original GitHub issueHey 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
Issue Analytics
- State:
- Created 2 years ago
- Reactions:1
- Comments:16
Top GitHub Comments
@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.