0

I keep getting this error with React on the frontend

Uncaught TypeError: Cannot read properties of null (reading 'useState')

I'm trying to use this hamburger component (https://hamburger-react.netlify.app/) in my custom navigation block. It works in the editor but not in the frontend unless I remove state altogether. I would like to use state tho as it tracks whether to open or close the menu.

The code looks like this in my edit.js

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

    edit() {
        const [isOpen, setOpen] = useState(false);

        return (
            <div {...blockProps} id="nav-block">
                <Hamburger toggled={isOpen} toggle={setOpen} />
                <ul {...innerBlocksProps} class="nav-menu" />
            </div>
        );
    },

And my save component looks like this with no dynamic code (learned my lesson about that already).

    save() {
        const blockProps = useBlockProps.save();
        const innerBlocksProps = useInnerBlocksProps.save({});

        return(
            <div {...blockProps} id="nav-block">
                <ul {...innerBlocksProps} class="nav-menu" />
            </div>
        );
    },

I've added a frontend JS file via "viewScript": "file:./view.js" to my block.json - it works, I've tested it.

And this code is in my view.js file.

import { render, useState } from '@wordpress/element'
import { Divide as Hamburger } from 'hamburger-react'
import './main.css'

window.addEventListener( 'DOMContentLoaded', ( event ) => {

    const wrapper = document.getElementById( `nav-block` );
    const [isOpen, setOpen] = useState(false);

    render( <Hamburger toggled={isOpen} toggle={setOpen} />, wrapper );

} );

Notice that I added useState to the import statement here, while I didn't do so in the edit.js file. This changes nothing, I still get the same error which tells me Wordpress already loads useState - correct me if I'm wrong.

What am I missing here? It seems unique to Wordpress as I've tried researching React useState and it looks like I'm doing everything right.

6
  • Are you using wp-scripts to build the script? Apr 21 at 14:12
  • I'm not sure, how does one check this? Forgive me but I'm new to React/block dev and have mostly been following tutorials. I know I installed wordpress scripts via npm install @wordpress/scripts --save-dev and use npm run start while in development.
    – Shelaine
    Apr 21 at 14:56
  • useState is not defined in your edit function, so - "I added useState to the import statement here, while I didn't do so in the edit.js file" - why? Even if that wasn't the problem, that variable should still be defined there.
    – Sally CJ
    Apr 21 at 15:25
  • @SallyCJ Good question and to answer, simply becuase I didn't know any better. I've since added it back. Interesting that it still works without it tho.
    – Shelaine
    Apr 21 at 16:01
  • In that case, that variable might have actually been defined (in scope), but anyway, can we see your full code, perhaps on GitHub? Or if it's not too much of code, perhaps you can just add them to your post. But, are you absolutely sure your view script is the one throwing that error and on the front-end only? Is that the full error message and is it the only error thrown?
    – Sally CJ
    Apr 21 at 16:39

2 Answers 2

0

I tried your view script, i.e. the code in your view.js file, and it's true that I also got the same error in question. However, I spotted another error which appeared before the other error, and which explained why that other error appeared:

Warning: Invalid hook call. Hooks can only be called inside of the body of a function component.

  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.

So in your case, the problem is that you're breaking the Rules of Hooks, i.e. you're calling useState (which is a React hook) from within an event handler (which is the function called once the DOM is ready).

Therefore, instead of doing this:

window.addEventListener( 'DOMContentLoaded', ( event ) => {

    const wrapper = document.getElementById( `nav-block` );
    // You can not call useState here.
    const [isOpen, setOpen] = useState(false);

    render( <Hamburger toggled={isOpen} toggle={setOpen} />, wrapper );

} );

You can try something like this:

const MyHamburger = () => {
    // This is within a *function* component, so useState can be used.
    const [isOpen, setOpen] = useState(false);

    return <Hamburger toggled={ isOpen } toggle={ setOpen } />;
};

window.addEventListener( 'DOMContentLoaded', ( event ) => {

    const wrapper = document.getElementById( `nav-block` );

    render( <MyHamburger />, wrapper );

} );

Additional Notes

  1. I had script debugging enabled, i.e. SCRIPT_DEBUG is true, hence you should do the same while developing/testing your block, to ensure your code is free from errors/warnings/etc. :)

  2. I used WordPress v6.2, hence I also got this warning when I tested your code: (see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-element/#usage)

    Warning: ReactDOM.render is no longer supported in React 18. Use createRoot instead. Until you switch to the new API, your app will behave as if it's running React 17. Learn more: https://reactjs.org/link/switch-to-createroot

    So this is how I got rid of that warning:

    // At the top in the file, I imported createRoot instead:
    import { createRoot, useState } from '@wordpress/element';
    
    // ... then called it instead of wp.element.render():
    window.addEventListener( 'DOMContentLoaded', ( event ) => {
    
        const wrapper = document.getElementById( 'nav-block' );
    
        createRoot( wrapper ).render( <MyHamburger /> );
    
    } );
    

    However, you could also import both createRoot and render, and then replace the createRoot line above with:

    if ( createRoot ) {
        createRoot( wrapper ).render( <MyHamburger /> );
    } else {
        render( <MyHamburger />, wrapper );
    }
    
1
  • 1
    Thank you for such a thorough response, this completely answered my question and solved the problem!
    – Shelaine
    Apr 24 at 21:21
0

The problem is that the wp-element script is not being enqueued as a dependency. In the editor this script is always loaded because it's used by the editor, but it's not loaded on the front end. You will need to declare it as one of your view scripts for it to be loaded on the front end, and for useState to be available:

viewScript: ["wp-element", "file:./view.js"],

The reason this is necessary is because wp-scripts will see imports from @wordpress/element but not actually import the script. If it did, every block script would be bundled with its own copy of React. Instead it will change this import to a reference to the existing global instance of wp.element, but that only exists if wp-element is enqueued.

2
  • I was really excited for this to work, but I'm still getting the same error. I've also tried using import React, { useState } from 'react'; instead of @wordpress/element
    – Shelaine
    Apr 21 at 16:10
  • @Shelaine Actually, if you've correctly built your script, e.g. using the default config where the source code directory is src (and it contains the entry points, e.g. block.json and .js files), then they'll be automatically added to the asset file, e.g. view.asset.php, as long as your script has properly imported the dependencies (e.g. import { foo, barBaz } from 'dependency').
    – Sally CJ
    Apr 22 at 7:55

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

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