Question

I've got a block that allows the user to enter a script when in edit mode, then sends this script to a REST endpoint which returns a WP_Post representing a media file which is then displayed to the user. The REST endpoint can take many seconds (even minutes) to send a reply, so I want to do this updating asynchronously and display a spinner while the user waits.

The problem is that the spinner shows indefinitely, even after the REST endpoint returns its value. I'm not sure what I'm doing wrong. Here is my (abbreviated) block code:

( function( wp ) {
    var registerBlockType = wp.blocks.registerBlockType;
    var withSelect = wp.data.withSelect;

    var el = wp.element.createElement;
    var Component = wp.element.Component;
    var Spinner = wp.components.Spinner;

    var __ = wp.i18n.__;

    function getEditComponent( blockName, blockTitle ) {
        return class extends Component {
            render() {
                const { mediaFile, isSelected, className, attributes, setAttributes } = this.props;
                const { script } = attributes;

                // Check if the mediaFile has been populated via REST yet.
                const hasMedia = Array.isArray( mediaFile ) && mediaFile.length;

                if ( isSelected ) {
                    // Show edit textarea.
                    return el(
                        'div',
                        { className },
                        el(
                            'label',
                            null,
                            __( 'Script:', 'my-textdomain' ),
                            el(
                                'textarea',
                                {
                                    onChange: ( event ) => {
                                        // Update script attribute as it is typed.
                                        setAttributes( { script: event.target.value } );
                                    },
                                    value: script,
                                    style: {
                                        width: '100%'
                                    },
                                    spellCheck: false,
                                    placeholder: __( 'Enter script', 'my-textdomain' ),
                                }
                            )
                        )
                    );
                } else {
                    if ( ! hasMedia ) {
                        return (
                            el(
                                Placeholder,
                                {
                                    label: __( 'label', 'my-textdomain' ),
                                },
                                // Display spinner until mediaFile has been returned from REST.
                                ! Array.isArray( mediaFile ) ? Spinner() : __( 'Media is being generated.', 'my-textdomain' ),
                            )
                        );
                    }

                    return el(
                        'img',
                        {
                            src: mediaFile.guid || '', // Display mediaFile's URL.
                        }
                    );
                }
            }
        };
    };

    const name = 'my-namespace/my-block';
    const title = __( 'Script', 'my-textdomain' );
    const edit = getEditComponent( name, title );

    registerBlockType( name, {
        // Various properties here, like title, description, etc...

        edit: withSelect( ( select, props ) => {
            const { attributes } = props;
            const { script } = attributes;

            // Get media file using script.
            const mediaFile = wp.apiFetch(
                {
                    path: '/my-api-namespace/v1/do-thing',
                    method: 'POST',
                    data: {
                        script: script,
                    },
                }
            );

            return {
                mediaFile,
            };
        } )( edit ),

        save: null, // not shown here
    } );
} )(
    window.wp
);

The edit function is where the API fetch is made, and it is written into the edit class's property mediaFile. Initially this is of type Promise returned by wp.apiFetch, but eventually (I think) it becomes the result of that fetch (in my case a WP_Post array).

The edit class's render function checks whether mediaFile is populated or not, and decides whether to show a "loading" spinner or the actual media file image.

Was it helpful?

Solution

I figured it out by reading up the React docs. Now I use the edit component's own state to store the media file retrieved from REST, and the function to make the REST request is now within the edit component. I don't need a withSelect any more.

This is a nicer approach to the one in the first post because it involves the component setting its own state. Setting component state from outside the component is not allowed, with the recommendation instead being that you should share a variable from the outside scope with the component and then update that. Furthermore, properties passed to components should be immutable. In the question, the mediaFile property was not immutable.

These points led me to implement the REST request in the edit component itself, using its own state. It now fetches the media file from REST and updates its state when it gets a response. The state is then read by the render function to check if the media file is available or not, and shows the spinner if not. The setState call made by the apiFetch.then function forces a re-render, so the image is shown when it is retrieved.

Here is the (abbreviated) working block:

( function( wp ) {
    var registerBlockType = wp.blocks.registerBlockType;
    var withSelect = wp.data.withSelect;

    var el = wp.element.createElement;
    var Component = wp.element.Component;
    var Spinner = wp.components.Spinner;

    var __ = wp.i18n.__;

    function getEditComponent( blockName, blockTitle ) {
        return class extends Component {
            constructor( props ) {
                super( props );

                this.state = {
                    mediaFile: {},
                };
            }

            getPlot() {
                const { attributes } = this.props;
                const { script } = attributes;

                if ( this.gettingPlot ) {
                    return;
                }

                this.gettingPlot = true;

                wp.apiFetch(
                    {
                        path: '/my-api-namespace/v1/do-thing',
                        method: 'POST',
                        data: {
                            script: script,
                        },
                    }
                ).then(
                    ( media ) => {
                        this.setState(
                            {
                                mediaFile: media,
                            }
                        );

                        this.gettingPlot = false;
                    }
                ).catch(
                    () => {
                        this.gettingPlot = false;
                    }
                );
            }

            render() {
                const { isSelected, className, attributes, setAttributes } = this.props;
                const { script } = attributes;

                let mediaFile = this.state.mediaFile;                
                const hasMedia = ( mediaFile != null && Object.keys( mediaFile ).length );

                if ( isSelected ) {
                    return el(
                        'div',
                        { className },
                        el(
                            'label',
                            null,
                            __( 'Script:', 'my-textdomain' ),
                            el(
                                'textarea',
                                {
                                    onChange: ( event ) => {
                                        // Update script attribute as it is typed.
                                        setAttributes( { script: event.target.value } );
                                    },
                                    value: script,
                                    style: {
                                        width: '100%'
                                    },
                                    spellCheck: false,
                                    placeholder: __( 'Enter script', 'my-textdomain' ),
                                }
                            )
                        )
                    );
                } else {
                    this.getPlot();

                    if ( ! hasMedia ) {
                        return (
                            el(
                                Placeholder,
                                {
                                    label: __( 'label', 'my-textdomain' ),
                                },
                                // Display spinner until media file can be read.
                                Spinner(),
                            )
                        );
                    }

                    return el(
                        'img',
                        {
                            src: mediaFile.guid || '',
                        }
                    );
                }
            }
        };
    };

    const name = 'my-namespace/my-block';
    const title = __( 'Script', 'my-textdomain' );
    const edit = getEditComponent( name, title );

    registerBlockType( name, {
        // Various properties here, like title, description, etc...

        edit,

        save: null, // not shown here
    } );
} )(
    window.wp
);
Licensed under: CC-BY-SA with attribution
Not affiliated with wordpress.stackexchange
scroll top