0

*Edit - this was flagged for being off topic, which doesn't make any sense to me. This question is all about block development and the answer came down to my misunderstanding of the save component.

Let me preface by saying, I'm a React newbie - so I can't tell if this issue is stemming from my lack of React knowledge or if it's specific to gutenberg blocks and the save function.

The goal here is to create a custom navigation block (with child/inner blocks) that is more robust and customizeable than the core/navigation block.

The issue is this: I've added a hamburger menu icon from https://hamburger-react.netlify.app/ which is working in the editor. However whenever I try adding it in the same way to the save function so that it will render on the frontend, I get an error:

Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons: 1. You might have mismatching versions of React and the renderer (such as React DOM) 2. You might be breaking the Rules of Hooks 3. You might have more than one copy of React in the same app See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.

Heres my code:

import { registerBlockType } from '@wordpress/blocks'
import { 
    useBlockProps,
    InspectorControls,
    PanelColorSettings,
    useInnerBlocksProps,
} from '@wordpress/block-editor'
import { __ } from '@wordpress/i18n'
import { useState } from '@wordpress/element'
import './main.css'
import { Divide as Hamburger } from 'hamburger-react'


registerBlockType('des-blocks/navigation', {
    edit({ attributes, setAttributes }) {
        const blockProps = useBlockProps();
        const innerBlocksProps = useInnerBlocksProps();
        const { bgColor, textColor } = attributes
        const [isOpen, setOpen] = useState(false);

        return (
            <>
            <InspectorControls>
                <PanelColorSettings
                title={__('Colors', 'des-blocks')}
                colorSettings={[
                    {
                        label: __('Background Color', 'des-blocks'),
                        value: bgColor,
                        onChange: newVal => setAttributes({ bgColor: newVal })
                    },
                    {
                        label: __('Text Color', 'des-blocks'),
                        value: textColor,
                        onChange: newVal => setAttributes({ textColor: newVal })
                    }
                ]}
                />
            </InspectorControls>
            <div {...blockProps}>
                <Hamburger toggled={isOpen} toggle={setOpen} color={textColor} />
                <ul 
                    {...innerBlocksProps} 
                    class="nav-menu"
                    style={{
                    'background-color': bgColor,
                    'color': textColor
                }} 
                />
            </div>
            </>
        );
    },

    save({ attributes }) {
        const { bgColor, textColor } = attributes
        const [isOpen, setOpen] = useState(false);
        const blockProps = useBlockProps.save();
        const innerBlocksProps = useInnerBlocksProps.save({
            style: {
                'background-color': bgColor,
                'color': textColor
            }
        });


        return (
            <>
            <div {...blockProps}>
                <Hamburger toggled={isOpen} toggle={setOpen} color={textColor} />
                <ul 
                    {...innerBlocksProps} 
                    class="nav-menu" 
                    style={{
                    'background-color': bgColor,
                    'color': textColor
                }} 
                />
            </div>
            </>
        )
    }
})

This is whats causing a problem in the save function.

<Hamburger toggled={isOpen} toggle={setOpen} color={textColor} />

1 Answer 1

2

This is because you've put interactive components inside your save component, which is forbidden.

A save components job is to generate raw HTML strings that get saved in the database. It does this in the editor in javascript, save components never run on the frontend, and there is no way to "save" an interactive or react component.

At the end of the day the block editor saves HTML and HTML comments, not React components.

To fix this, you will need to render save components that are static and not interactive, no state/reducers/stores/context/event hooks/etc. Then, enqueue JS for the frontend that either turns it into a hamburger menu or replaces it. You can use the hamburger component from netlify in this frontend JS file

Note that this is specifically for the save component. Frontend JS and your edit component etc can use state/etc just fine.

6
  • That makes sense, thanks! I still can't figure out how to get this component working on the frontend after days of hacking away at it. Would you advise against using react on the frontend unless going full headless WP?
    – Shelaine
    Apr 20, 2023 at 13:18
  • You mention not being able to use state. Is this true even for a view.js file? I was trying to use ReactDOM to insert the component into the nav div on the frontend. But keep getting the error Uncaught TypeError: Cannot read properties of null (reading 'useState') despite importing state in the file
    – Shelaine
    Apr 20, 2023 at 13:22
  • the name of the file is irrelevant, the save component can never use any state or interactive hooks. Given attributes A B C the save component must always produce the same output, and it can do so only by reading block attributes. It cannot read post meta, fetch data from an API, add click handlers, use react state, or call 3rd party APIs to fetch data or data stored on the window etc. The sole purpose of a save component is to generate static raw HTML either for block validation, or for saving to the database as a plaintext string
    – Tom J Nowell
    Apr 20, 2023 at 13:45
  • if you are having issues elsewhere with useState, e.g. in a frontend JS file, or your edit component, then that is unrelated to my answer or save components. Any react you use on the frontend is no different to if you enqueued a JS file for a shortcode that was built using react, it's very likely that you've written react code but forgotten to bundle or require react itself, but again that's a separate issue and totally unconnected to this question and my answer other than coincidence/context
    – Tom J Nowell
    Apr 20, 2023 at 13:46
  • To clarify I am enqueueing the frontend js file using "viewScript": "file:./view.js" in block.json per latest documentation. The file loads (I've tested it) but I cannot get the hamburger component to work which seems to stem from the useState error. It works within the edit component just fine. And I've stripped the save component of anything interactive which solved the invalid hook call error. Anyways you still earned the best answer to the original question. Thank you!
    – Shelaine
    Apr 20, 2023 at 14:57

Not the answer you're looking for? Browse other questions tagged or ask your own question.