Update block once an API request returns with a value

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
);