question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

ForwardRef component errors out in custom WP block when Gutenberg plugin is active

See original GitHub issue

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: Screenshot 2021-11-02 121722 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: Screenshot 2021-11-02 122010 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… Screenshot 2021-11-02 121818 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:closed
  • Created 2 years ago
  • Reactions:1
  • Comments:16

github_iconTop GitHub Comments

1reaction
RavishaHeshanE25commented, Apr 14, 2022

I would request this be reopened. I have the same issue, but a different subset: I use react-select in a component that I use within a Gutenberg, but also on the front-end with a ‘regular’ React.

Previous versions have worked just fine in tandem with Gutenberg, but trying the latest major updates (4.x, 5.x) gives me tens of ‘ForwardRef’ / Invalid hook call errors when using the same component in gutenberg. Sidenote: 4.x gives me tens of EmotionCSS errors, rather than forwardref.

@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.

1reaction
RavishaHeshanE25commented, Mar 10, 2022

I recently ran into the same issue in a custom block after updating react-select from v3 to v5. In my case, the fix turned out to be extremely simple though it took quite some time to arrive there sweat_smile …

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 externals option:

  externals: {
    'react': 'React',
    'react-dom': 'ReactDOM',
  },

In this case, react/react-dom are the package names (i.e. npm install <package-name>), and React/ReactDOM correspond to the window.React and window.ReactDOM global variables.

Credit goes to this article for pointing me in the right direction.

Does that help with your issue @sehqlr?

Thank you @montchr You saved my day.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Throws Warning while adding Gutenberg Block | WordPress.org
While adding a new Envira Gallery Gutenberg block in block editor, the below warning throws. Warning: Received false for a non-boolean attribute accept....
Read more >
wp.components.ServerSideRender throws an error ... - GitHub
I've setup the Gutenberg repo in the WP core dev area (src folder) and activated it. Then i changed window.wp.components.ServerSideRender to ...
Read more >
Block Error: Unexpected or Invalid Content - WordPress.com
This can happen for a number of reasons. Commonly, it will appear if you modify the HTML of the block in the Code...
Read more >
wordpress - gutenberg block stopped working - Stack Overflow
We had a working custom gutenberg block written as a plugin for over a year. Out of nowhere it stopped working tonight and...
Read more >
the above error occurred in the <forwardref> component:
Referring to this github issue, I found out that we can fix the error with following code put in test set up file....
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found