Search code examples
reactjswordpress-gutenberggutenberg-blocks

Why is my block only saving the previous state and not the current one?


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

Solution

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