0
votes

I've made a block for the new wordpress editor and I'm stucked. Everything works fine, except when I save my page. What get posted is the block but in a previous state.

Ex: if I focus on an editable zone add a word and press publish, the latest addition won't be in the payload. Although I can totally see it when I'm logging the components attributes. If I press publish a second time, it will work fine.

Seems to me theres some kind of race condition or some state management I'm doing wrong. I'm totally new to React and Gutenberg.

Here is my save function:

import { RichText } from '@wordpress/editor';
import classnames from 'classnames';

export default function save({ attributes }) {
    const { align, figures } = attributes;

    const className = classnames({ [ `has-text-align-${ align }` ]: align });

    return (
        <ul className={ className }>
            { figures.map( (figure, idx) => (
                <li key={ idx }>
                    <RichText.Content tagName="h6" value={ figure.number } />
                    <RichText.Content tagName="p" value={ figure.description } />
                </li>
            ) ) }
        </ul>
    );
}

and my edit:

import { Component } from '@wordpress/element';
import { AlignmentToolbar, BlockControls, RichText } from '@wordpress/editor';
import { Toolbar } from '@wordpress/components';

import classnames from 'classnames';

import { normalizeEmptyRichText } from '../../utils/StringUtils';
import removeIcon from './remove-icon';

const MIN_FIGURES = 1;
const MAX_FIGURES = 4;

export default class FigureEdit extends Component {

    constructor() {
        super(...arguments);

        this.state = {};
        for(let idx = 0; idx < MAX_FIGURES; idx++){
            this.state[`figures[${idx}].number`] = '';
            this.state[`figures[${idx}].description`] = '';
        }
    }

    onNumberChange(idx, number) {
        const { attributes: { figures: figures }, setAttributes } = this.props;
        figures[idx].number = normalizeEmptyRichText(number);
        this.setState({ [`figures[${idx}].number`]: normalizeEmptyRichText(number) });
        return setAttributes({ figures });
    }

    onDescriptionChange(idx, description) {
        const { attributes: { figures: figures }, setAttributes } = this.props;
        figures[idx].description = normalizeEmptyRichText(description);
        this.setState({ [`figures[${idx}].description`]: normalizeEmptyRichText(description) });
        return setAttributes({ figures });
    }

    addFigure() {
        let figures = [...this.props.attributes.figures, {number:'', description:''}];
        this.props.setAttributes({figures});
    }

    removeFigure() {
        let figures = [...this.props.attributes.figures];
        figures.pop();
        this.resetFigureState(figures.length);
        this.props.setAttributes({figures});
    }

    resetFigureState(idx) {
        this.setState({
            [`figures[${idx}].number`]: '',
            [`figures[${idx}].description`]: ''
        });
    }

    render() {
        const {attributes, setAttributes, className} = this.props;
        const { align, figures } = attributes;

        const toolbarControls = [
            {
                icon: 'insert',
                title: 'Add a figure',
                isDisabled: this.props.attributes.figures.length >= MAX_FIGURES,
                onClick: () => this.addFigure()
            },
            {
                icon: removeIcon,
                title: 'Remove a figure',
                isDisabled: this.props.attributes.figures.length <= MIN_FIGURES,
                onClick: () => this.removeFigure()
            }
        ];

        return (
            <>
                <BlockControls>
                    <Toolbar controls={ toolbarControls } />
                    <AlignmentToolbar
                        value={ align }
                        onChange={ align=>setAttributes({align}) }
                    />
                </BlockControls>
                <ul className={ classnames(className, { [ `has-text-align-${ align }` ]: align }) }>
                    { figures.map( (figure, idx) => (
                        <li key={ idx }>
                            <RichText
                                className="figure-number"
                                formattingControls={ [ 'link' ] }
                                value={ figure.number }
                                onChange={ number => this.onNumberChange(idx, number) }
                                placeholder={ !this.state[`figures[${idx}].number`].length ? '33%' : '' }
                            />
                            <RichText
                                className="figure-description"
                                formattingControls={ [ 'link' ] }
                                value={ figure.description }
                                onChange={ description => this.onDescriptionChange(idx, description) }
                                placeholder={ !this.state[`figures[${idx}].description`].length ? 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor' : '' }
                            />
                        </li>
                    ) ) }
                </ul>
            </>
        );
    }
}
1
Note there's also this dirty hack I've done on the placeholder which I think is related. My placeholders where overlapping my values until I blurred out of the component. - Nicolas Reynis
setState is asynchronous, so it's enqueuing an update, not writing to state immediately. You will have to either use the callback arg on setState or use a lifecycle method like componentDidUpdate() - DJ2
This does not really explain why my state isn't up to date when I publish. There's a gap of dozens of seconds between the input and the submission. SetState even if it's asynchronous should be done in a few ticks. - Nicolas Reynis
setState doesn't magically update your initialized values based off an elapsed time. You could let it sit there for days and submit, it still wouldn't have your "updated" state values. - DJ2
The issue is that setState is async, so that’s why you’d need to use the callback function arg to get the latest value set in state to begin with. My suggestion would be to find the appropriate lifecycle method to put that logic in, if the setState callback func arg doesn’t handle your use case - DJ2

1 Answers

1
votes

The problem was related to immutability, in my onChange method, I was mutating the figures attribute. It seems that you must not do that. You have to give a new object if you want the props to be correctly refreshed.

My change handler now looks something like this:

onChange(idx, field, value) {
    const figures = this.props.attributes.figures.slice();
    figures[idx][field] = normalizeEmptyRichText(value);
    return this.props.setAttributes({ figures });
}

As I suspected this also fixed the placeholder issue. With the placeholder now working as expected I can remove all the state and constructor code. Which confirm this had nothing to do with setState.