WordPress.org

WordPress Developer Blog

How to use WordPress React components for plugin pages

How to use WordPress React components for plugin pages

If you’ve ever felt that most WordPress admin areas seem static and outdated compared to the modern and dynamic Post and Site Editor, you’re not alone.

But there’s good news: this is changing. With WordPress entering Phase 3: Collaboration, there’s an ongoing effort to improve the overall admin experience and integrate the new visual language throughout.

The WordPress ecosystem is changing, too. Numerous well-known plugins have had their settings pages updated. More and more developers choose React for their user interfaces to enhance user experience and speed up the introduction of new features.

What if you want to do the same?

This article will guide you through creating a React-based settings page for a plugin utilizing WordPress React components.

What are you going to build?

For this article, you’ll build a plugin that displays an announcement bar above the header on the front end.

It will include a settings page allowing you to customize the message, toggle the bar’s visibility, and adjust its size.

Here’s a sneak peek at the end result:

The power of modular architecture

Engaging with the Post and Site Editor gives the impression of a unified system. However, they are the product of the orchestration of independent components and packages.

From the beginning, the Gutenberg project embraced having loosely coupled or completely decoupled packages. With this modular architecture, it’s possible to create different interfaces or even applications.

You will use these packages to create a settings page, but given their reusability and versatility, you could use them even for non-WordPress-based projects.

Assumed knowledge and prerequisites

This article assumes you have some familiarity with React. It will be particularly accessible if you have ever developed a basic custom block, as the skills and concepts are highly transferable.

If you want to follow along with coding, create the following starter files:

/plugins/unadorned-announcement-bar/
├── index.php
├── package.json
└── src/
    ├── index.js
    └── index.scss

You’ll also need wp-scripts for the JavaScript build step. For a step-by-step walkthrough on setting up and using it, check out the Get started with wp-scripts.

If you are already comfortable with it, here’s what you need in the package.json to get started:

{
    "devDependencies": {
        "@wordpress/scripts": "^27.0.0"
    },
    "scripts": {
        "build": "wp-scripts build",
        "start": "wp-scripts start"
    }
}

Laying the groundwork

The first steps do not differ from creating a plugin with a “traditional” custom settings page.

This section is kept short and to the point. If you require more information on any of the steps, you can refer to the Plugin Basics pages:

Setting up the menu and settings page

First, to get started, add the minimally required plugin header comment to index.php file, then register a sub-menu under the Settings menu:

<?php
/**
 * Plugin Name: Unadorned Announcement Bar
 */

function unadorned_announcement_bar_settings_page() {
    add_options_page(
        __( 'Unadorned Announcement Bar', 'unadorned-announcement-bar' ),
        __( 'Unadorned Announcement Bar', 'unadorned-announcement-bar' ),
        'manage_options',
        'unadorned-announcement-bar',
        'unadorned_announcement_bar_settings_page_html'
    );
}

add_action( 'admin_menu', 'unadorned_announcement_bar_settings_page' );

Next, add the callback function to the same PHP file:

function unadorned_announcement_bar_settings_page_html() {
    printf(
        '<div class="wrap" id="unadorned-announcement-bar-settings">%s</div>',
        esc_html__( 'Loading…', 'unadorned-announcement-bar' )
    );
}

It’s enough to output an HTML element with a unique id. You’ll use this id to target the element in JavaScript. The “Loading…” message will be replaced once the JavaScript part is initialized.

To access your settings page, go to the Settings and then Unadorned Announcement Bar:

Enqueuing the JavaScript for React

Given that you are building a React-based settings page, you must enqueue the necessary JavaScript file. To do this, add the following code to the index.php file:

function unadorned_announcement_bar_settings_page_enqueue_style_script( $admin_page ) {
    if ( 'settings_page_unadorned-announcement-bar' !== $admin_page ) {
        return;
    }

    $asset_file = plugin_dir_path( __FILE__ ) . 'build/index.asset.php';

    if ( ! file_exists( $asset_file ) ) {
        return;
    }

    $asset = include $asset_file;

    wp_enqueue_script(
        'unadorned-announcement-bar-script',
        plugins_url( 'build/index.js', __FILE__ ),
        $asset['dependencies'],
        $asset['version'],
        array(
            'in_footer' => true,
        )
    );
}

add_action( 'admin_enqueue_scripts', 'unadorned_announcement_bar_settings_page_enqueue_style_script' );

You are following best practices by ensuring the JavaScript is only loaded on the settings page, and since you are setting the dependencies and the version dynamically, you’ll never have to update them.

The build folder and its contents are created when the JavaScript is compiled with wp-scripts.

Kick-starting the React settings page

With the previous steps out of the way, it’s finally time to write some React code.

To confirm everything, you have done works, start with something basic, like rendering a placeholder component.

Add this code to your src/index.js file:

import domReady from '@wordpress/dom-ready';
import { createRoot } from '@wordpress/element';

const SettingsPage = () => {
    return <div>Placeholder for settings page</div>;
};

domReady( () => {
    const root = createRoot(
        document.getElementById( 'unadorned-announcement-bar-settings' )
    );

    root.render( <SettingsPage /> );
} );

Remember to install the mentioned packages with npm install {package}. As you encounter references to new packages, install those as well. If you haven’t done so, start the wp-scripts build process.

By using domReady, you ensure the DOM is ready for manipulation. Inside the callback, with the createRoot, you tell React to take over managing the DOM for the specified element. Then, it’s a matter of rendering the SettingsPage component.

These steps are common to all React applications. Something very similar occurs when the Post and Site Editor are initialized.

If you refresh your settings page, you should see the placeholder message displayed:

Reusable UI elements for WordPress and elsewhere

@wordpress/components is one of the cornerstone packages. It’s often used, as it contains common UI elements used for the Block and Site Editor, but they are generic enough to be used elsewhere.

See the WordPress Storybook for an overview of the available components. The Storybook allows you to browse individual components, their controls, options, and settings in isolation. You can modify their controls and arguments and see the changes right away.

There is a wide range of components available, and you can expect this list to expand as more areas of WordPress undergo overhauls

In the Storybook, you interact with React code components, which also exist as visual components. These mirror exactly what’s available and implemented in the code.

Visual components

Visual components are reusable bits of UI that can be used in Figma for creating mockups and to explore ideas early on before writing any code.

You can create a mockup for the plugin just by selecting from the available visual components:

If you want to remain consistent with the design of WordPress, it’s a matter of combining and arranging a few controls, along with adding a button and a title:

Building the UI of the settings page

The Panel

Based on the mockup, your settings are grouped into two sections, with the Appearance section being hidden by default.

The Panel component is a perfect fit for this use case, as it creates a container with an optional header and accepts a PanelBody component that you can collapse.

To integrate it, you first need to import the dependencies at the top of the src/index.js file:

import { __ } from '@wordpress/i18n';
import { Panel, PanelBody, PanelRow } from '@wordpress/components';

Remember that the import statements always go at the top of the file. This detail won’t be mentioned explicitly from now on to avoid repetition.

Then, modify your SettingsPage component. For now, use a placeholder message where the controls will eventually be:

const SettingsPage = () => {
    return (
        <Panel>
            <PanelBody>
                <PanelRow>
                    <div>Placeholder for message control</div>
                </PanelRow>
                <PanelRow>
                    <div>Placeholder for display control</div>
                </PanelRow>
            </PanelBody>
            <PanelBody
                title={ __( 'Appearance', 'unadorned-announcement-bar' ) }
                initialOpen={ false }
            >
                <PanelRow>
                    <div>Placeholder for size control</div>
                </PanelRow>
            </PanelBody>
        </Panel>
   );
};

The Panel, PanelBody, and PanelRow components are always used together. These and subsequent components offer various options, not all of which are required for this project. You can discover additional possibilities by exploring the Storybook.

At this point, if you refresh your settings page, you should see the components rendered:

Including the styles of the components

Right now, things do not look quite right. This is because, by default, the components are unstyled, and you still need to include their CSS.

You can solve this by enqueueing the wp-components stylesheet. Amend your current function in the index.php file:

function unadorned_announcement_bar_settings_page_enqueue_style_script( $admin_page ) {
    // ...

    wp_enqueue_style( 'wp-components' );
}

Now, your settings page should look WordPressy:

Managing the state of the controls

You don’t need any complicated state management for this plugin. With only three controls and three values to keep track of, you can start with three instances of useState and some default values.

It’s best to encapsulate it in a custom hook to separate the state from the presentation layer. From here, until it’s mentioned otherwise, add all code to the src/index.js file:

import { useState } from '@wordpress/element';

const useSettings = () => {
    const [ message, setMessage ] = useState('Hello, World!');
    const [ display, setDisplay ] = useState(true);
    const [ size, setSize ] = useState('medium');
    
    return {
        message,
        setMessage,
        display,
        setDisplay,
        size,
        setSize,
    };
};

While your code is currently just a few lines, it will inevitably grow in complexity once you attempt to save or load data from the database. The benefit of the custom hook and having a central place for state management will become more apparent very soon. Just stay tuned.

To access the state variables and setter functions in the SettingsPage component, call the useSettings and destructure the returned object:

const SettingsPage = () => {
    const {
        message,
        setMessage,
        display,
        setDisplay,
        size,
        setSize,
    } = useSettings();

    // ...
};

The message control

To allow for a longer text for the announcement bar message, you can’t go wrong with the TextareaControl component.

To keep things tidy, wrap it in a separate component:

import { TextareaControl } from '@wordpress/components';

const MessageControl = ( { value, onChange } ) => {
    return (
        <TextareaControl
            label={ __( 'Message', 'unadorned-announcement-bar' ) }
            value={ value }
            onChange={ onChange }
            __nextHasNoMarginBottom
        />
    );
};

As WordPress evolves, sometimes it’s necessary to adjust the style of existing components. When new styles are introduced, they are put behind a feature flag prop prefixed by __next. This offers a grace period for third parties to make any necessary adjustments.

After creating it, the next step is to substitute the placeholder with the MessageControl component. Use the message constant as the value and its setter function in your onChange handler:

const SettingsPage = () => {
    const {
        message,
        setMessage,
        // ...
    } = useSettings();
    
    return (
        <Panel>
            <PanelBody>
                <PanelRow>
                    <MessageControl
                        value={ message }
                        onChange={ ( value ) => setMessage( value ) }
                    />
                </PanelRow>
                <PanelRow></PanelRow>
            </PanelBody>
            <PanelBody></PanelBody>
        </Panel>
    );
};

With all this work, your first control is working, allowing you to update the message:

The display control

To toggle the visibility of the announcement bar, you need an on/off switch. Opt for the ToggleControl component.

To maintain consistency, follow the same approach as shown in the previous section. First, create a separate component for the control:

import { ToggleControl } from '@wordpress/components';

const DisplayControl = ( { value, onChange } ) => {
    return (
        <ToggleControl
            label={ __( 'Display', 'unadorned-announcement-bar' ) }
            checked={ value }
            onChange={ onChange }
            __nextHasNoMarginBottom
        />
    );
};

Then, replace the placeholder you had with the DisplayControl component, and use the corresponding constant and setter function for the onChange handler:

const SettingsPage = () => {
    const {
        // ...
        display,
        setDisplay,
        // ...
    } = useSettings();

    return (
        <Panel>
            <PanelBody>
                <PanelRow></PanelRow>
                <PanelRow>
                    <DisplayControl
                        value={ display }
                        onChange={ ( value ) => setDisplay( value ) }
                    />
                </PanelRow>
            </PanelBody>
            <PanelBody></PanelBody>
        </Panel>
    );
};

The size control

For the size control, you have a couple of different components to choose from, but the best fit is the versatile FontSizePicker component.

Similar to the previous controls, create a separate component for it. The only difference here is that you must define a list of possible options:

import { FontSizePicker } from '@wordpress/components';

const SizeControl = ( { value, onChange } ) => {
    return (
        <FontSizePicker
            fontSizes={ [
                {
                    name: __( 'Small', 'unadorned-announcement-bar' ),
                    size: 'small',
                    slug: 'small',
                },
                {
                    name: __( 'Medium', 'unadorned-announcement-bar' ),
                    size: 'medium',
                    slug: 'medium',
                },
                {
                    name: __( 'Large', 'unadorned-announcement-bar' ),
                    size: 'large',
                    slug: 'large',
                },
                {
                    name: __( 'Extra Large', 'unadorned-announcement-bar' ),
                    size: 'x-large',
                    slug: 'x-large',
                },
            ] }
            value={ value }
            onChange={ onChange }
            disableCustomFontSizes={ true }
            __nextHasNoMarginBottom
        />
    );
};

By default, you can choose any size outside the list of predefined values, but to keep things “unadorned”, disable this possibility with the disableCustomFontSizes prop.

For the last time, replace the placeholder text with the actual component and configure the value and onChange prop:

const SettingsPage = () => {
    const {
        // ...
        size,
        setSize,
    } = useSettings();

    return (
        <Panel>
            <PanelBody></PanelBody>
            <PanelBody
                title={ __( 'Appearance', 'unadorned-announcement-bar' ) }
                initialOpen={ false }
>        
                <PanelRow>
                    <SizeControl
                        value={ size }
                        onChange={ ( value ) => setSize( value ) }
                    />
                </PanelRow>
            </PanelBody>
        </Panel>
    );
};

At this point, you have all three controls working, which is half of the job. Your settings page should now look like this:

The save button

To complete the basics of your settings page, you need to add two more components.

First and foremost, you need the Button component for saving. As you did before, create a wrapper component for it:

import { Button } from '@wordpress/components';

const SaveButton = ( { onClick } ) => {
    return (
        <Button variant="primary" onClick={ onClick } __next40pxDefaultSize>
            { __( 'Save', 'unadorned-announcement-bar' ) }
        </Button>
    );
};

Next, add the SaveButton component right after the Panel component:

const SettingsPage = () => {
    // ...

    return (
        <>
            <Panel></Panel>
            <SaveButton onClick={ () => {} } />
        </>
    );
};

When clicked, it’s okay if it doesn’t do anything specific for now. You’ll add the saving functionality in a moment.

Don’t forget to wrap the two components in a fragment (between the <></>) because you can only return one component in React.

The experimental heading

You might have noticed in the Storybook that some components are marked as experimental.

They are called experimental because WordPress’ backward compatibility guarantee doesn’t apply to them. They can change from one version to another, possibly breaking the code.

Use them with caution, but to try them out, you can use the __experimentalHeading component for the title instead of a plain h1 tag:

import {
    // eslint-disable-next-line @wordpress/no-unsafe-wp-apis
    __experimentalHeading as Heading,
} from '@wordpress/components';

const SettingsTitle = () => {
    return (
        <Heading level={ 1 }>
            { __( 'Unadorned Announcement Bar', 'unadorned-announcement-bar' ) }
        </Heading>
    );
};

If you lint your code with wp-scripts, that triggers a warning if experimental components are used. You can indicate that you know what you are doing with the eslint-disable-next-line.

This time, add the component before the Panel component to render it at the top of the page:

const SettingsPage = () => {
    // ...

    return (
        <>
            <SettingsTitle />
            <Panel></Panel>
            <SaveButton />
        </>
    );
};

Persisting and loading the settings

On regular WordPress settings pages, when you make a change and save it, you submit a form, and the page is reloaded.

This approach doesn’t align with React’s dynamic interaction model. When something is loaded or saved in Block and Site Editor, that happens seamlessly while many REST API requests are performed in the background. Similarly, you’ll use the REST API to load and save your settings.

You could store the state of the controls as separate single values in the options table, but that would create a new record for each unnecessarily. For this plugin, you should choose the other option: storing them as an array of values under a single key.

Defining the default and schema

To always get back an expected value when you call get_option( 'unadorned_announcement_bar' ), you should define a default for it.

Also, if you also define the schema strictly, you don’t have to worry about the validation when using the REST API, that’s going to be taken care of by WordPress.

You achieve both using the register_setting function. Place the following code to your index.php file:

function unadorned_announcement_bar_settings() {
    $default = array(
        'message' => __( 'Hello, World!', 'unadorned-announcement-bar' ),
        'display' => true,
        'size'    => 'medium',
    );
    $schema  = array(
        'type'       => 'object',
        'properties' => array(
            'message' => array(
                'type' => 'string',
            ),
            'display' => array(
                'type' => 'boolean',
            ),
            'size'    => array(
                'type' => 'string',
                'enum' => array(
                    'small',
                    'medium',
                    'large',
                    'x-large',
                ),
            ),
        ),
    );

    register_setting(
        'options',
        'unadorned_announcement_bar',
        array(
            'type'         => 'object',
            'default'      => $default,
            'show_in_rest' => array(
                'schema' => $schema,
            ),
        )
    );
}

add_action( 'init', 'unadorned_announcement_bar_settings' );

Loading the settings

You want to load the settings once you arrive at the settings page.

Whenever you want to trigger an action once the component is mounted, you can use the useEffect hook. In this case, you want to make a request with apiFetch to the Site Settings endpoint to load and use the saved settings.

Since all this is related to state management, modify your useSettings custom hook located in the src/index.js file accordingly:

import apiFetch from '@wordpress/api-fetch';
import { useEffect } from '@wordpress/element';

const useSettings = () => {
    const [ message, setMessage ] = useState();
    const [ display, setDisplay ] = useState();
    const [ size, setSize ] = useState();

    useEffect( () => {
        apiFetch( { path: '/wp/v2/settings' } ).then( ( settings ) => {
            setMessage( settings.unadorned_announcement_bar.message );
            setDisplay( settings.unadorned_announcement_bar.display );
            setSize( settings.unadorned_announcement_bar.size );
        } );
    }, [] );
    
    // ...
};

The defaults defined in register_setting are made available by the endpoint. For this reason, you no longer need to have default values in your useState calls. Once the data is returned by the endpoint, you update the state with the setter functions. This process happens almost instantaneously.

Saving the settings

Just like how you’ve handled the data loading, you can use the Site Settings endpoint to persist the settings.

This time, the REST API call should not occur on page load, but when you click the SaveButton. Therefore, first encapsulate the apiFetch in a function inside the useSettings hook:

const useSettings = () => {
    // ...

    const saveSettings = () => {
        apiFetch( {
            path: '/wp/v2/settings',
            method: 'POST',
            data: {
                unadorned_announcement_bar: {
                    message,
                    display,
                    size,
                },
            },
        } );
    };

    return {
        // ...
        saveSettings,
    };
};

Then, you can use exported saveSettings function as the handler of the onClick in your SaveButton component:

const SettingsPage = () => {
    const {
        // ...
        saveSettings,
    } = useSettings();

    return (
        <>
            <SettingsTitle/>
            <Panel></Panel>
            <SaveButton onClick={ saveSettings } />
        </>
    );
};

Now, by pressing the Save button, you can persist your changes. If you refresh your settings page, the saved settings will be loaded.

There are only a few more things missing, one of which is the front-end functionality.

Displaying the announcement bar on the front end

The most straightforward way to display the announcement bar is to output it right after the body tag.

You can use the wp_body_open hook, which nowadays is expected to be present in any theme. Add the following code to your index.php file:

function unadorned_announcement_bar_front_page() {
    $options = get_option( 'unadorned_announcement_bar' );

    if ( ! $options['display'] ) {
        return;
    }

    printf(
        '<div>%s</div>',
        esc_html( $options['message'] )
    );
}

add_action( 'wp_body_open', 'unadorned_announcement_bar_front_page' );

Since there’s minimal styling required for the announcement bar, you can simply inline the CSS. Update the function you just created accordingly:

function unadorned_announcement_bar_front_page() {
    // ...

    printf(
        '<div style="%s">%s</div>',
        sprintf(
            'background: var(--wp--preset--color--vivid-purple, #9b51e0); color: var(--wp--preset--color--white, #ffffff); padding: var(--wp--preset--spacing--20, 1.5rem); font-size: %s;',
            esc_attr( $options['size'] )
        ),
        esc_html( $options['message'] )
    );
}

Or, if you want to avoid a lengthy inline CSS, you can use the compile_css method of WP_Style_Engine:

function unadorned_announcement_bar_front_page() {
    // ...

    $css = WP_Style_Engine::compile_css(
        array(
            'background' => 'var(--wp--preset--color--vivid-purple, #9b51e0)',
            'color'      => 'var(--wp--preset--color--white, #ffffff)',
            'padding'    => 'var(--wp--preset--spacing--20, 1.5rem)',
            'font-size'  => $options['size'],
        ),
        ''
    );

    printf(
        '<div style="%s">%s</div>',
        esc_attr( $css ),
        esc_html( $options['message'] )
    );
}

The result are the same:

The final touches

At this point, you can display the announcement bar and change its settings. Your plugin is functional, but with adding a few more details, you could make it more polished.

Adding notifications

When you save settings, there’s no visual indication that the process was successful. To improve this situation, adding a notification system would eliminate any guesswork.

It goes beyond the goals of this introductory article to explain some details of the notification’s implementation. But you’ll find a course mentioned at the end of the article that explains it in great detail. You would gain a lot by taking it.

Essentially, it comes down to two things. First, you have to trigger some type of notice.
You can make the following changes to your custom hook in the src/index.js file:

import { useDispatch } from '@wordpress/data';
import { store as noticesStore } from '@wordpress/notices';

const useSettings = () => {
    // ...

    const { createSuccessNotice } = useDispatch( noticesStore );

    // ...

    const saveSettings = () => {
        apiFetch( {
            // ...
        } ).then( () => {
            createSuccessNotice(
                __( 'Settings saved.', 'unadorned-announcement-bar' )
            );
        } );
    };

    // ...
};

For the next step, WordPress provides some components to abstract away most of the complexities.

With just a few lines, you can create a component that renders the notices, if there are any. You can use the following code and add it to the src/index.js file:

import { useDispatch, useSelect } from '@wordpress/data';
import { store as noticesStore } from '@wordpress/notices';
import { NoticeList } from '@wordpress/components';

const Notices = () => {
    const { removeNotice } = useDispatch( noticesStore );
    const notices = useSelect( ( select ) =>
        select( noticesStore ).getNotices()
    );

    if ( notices.length === 0 ) {
        return null;
    }

    return <NoticeList notices={ notices } onRemove={ removeNotice } />;
};

As the last step, include the Notices component in the existing SettingsPage. Based on the mockup, you should add it after the SettingsTitle:

const SettingsPage = () => {
    // ...

    return (
        <>
            <SettingsTitle/>
            <Notices />
            <Panel></Panel>
            <SaveButton />
        </>
    );
};

Now, you should see a notification displayed whenever you save your settings.

Adding custom styles

The current settings page looks alright without any custom CSS. However, some minor adjustments are needed to match the design closely.

First, you must import the index.scss to index.js to compile the Sass file to a regular CSS:

import './index.scss';
// ..

domReady( () => {
    // ...
} );

Then, you have to enqueue the CSS file. Add wp_enqueue_style to the existing unadorned_announcement_bar_settings_page_enqueue_style_script function located in the index.php file:

function unadorned_announcement_bar_settings_page_enqueue_style_script( $admin_page ) {
    // ...
    
    wp_enqueue_style(
        'unadorned-announcement-bar-style',
        plugins_url( 'build/index.css', __FILE__ ),
        array_filter(
            $asset['dependencies'],
            function ( $style ) {
                return wp_style_is( $style, 'registered' );
            }
        ),
        $asset['version'],
    );
}

You can safely remove the wp_enqueue_style( 'wp-components' ), as it’s already included in the dependencies list.

All you need to do is tweak some of the spacings and widths. Add the following styles to the src/index.scss file:

#unadorned-announcement-bar-settings {
    max-width: 800px;

    h1 {
        padding-block: 8px;
    }

    .components-base-control:has(textarea) {
        flex-basis: 400px;
    }

    .components-notice {
        &, &__content {
            margin: 0;
        }
    }

    .components-notice-list {
        display: flex;
        flex-direction: column;
        gap: 8px;
        margin-block-end: 16px;
    }

    .components-panel {
        margin-block-end: 8px;
    }
}

With the styles added, your settings page should look more arranged:

Next steps

The repository and the playground

You can check the repository on GitHub to see the entire code together.

If you followed along and created the plugin step-by-step, you know it’s entirely possible to have all JavaScript code in the src/index.js. In the repository, the JavaScript is split into multiple files to showcase how you could organize a more complex React application. However, essentially, it’s the same code.

If you want to interact with the plugin and see it in action without downloading and installing it, go to the prepared WordPress Playground.

Ideas for improvements

While the plugin is feature-complete, there’s always room for improvement.

So far, you’ve implemented successful notifications, but what about handling errors from the REST API?

The code could also account for cases where the wrong data type is returned since the value of the unadorned_announcement_bar option in the options table could be updated by other means than the REST API.

Or you can explore adding a few more settings. For example, a control can be added to change the colors of the announcement bar. You could use the ColorPalette component for that.

Diving deeper

To learn more about creating dynamic user interfaces with WordPress React components, you should continue with the Using the WordPress Data Layer course.

It provides a step-by-step and comprehensive introduction to the data layer and guides you through creating a more complex React app to manage WordPress pages. You can learn about other components and have an in-depth look at notifications.

Props to @greenshady, @bph for the editorial review, and to @carmen222, @strangerkir especially for their moral support.

Categories: ,

28 responses to “How to use WordPress React components for plugin pages”

  1. Pascal Birchler Avatar

    The tutorial is missing i18n best practices and requirements such as a `wp_set_script_translations()` call for example.

    1. Birgit Pauli-Haack Avatar

      Thank you, Pascal, that’s good flag. Not every tutorial needs to cover all aspects, all the time, though. This is an introductory post. Internationalization is covered (maybe not enough?) in the block editor handbook with a separate chapter: Internationalization.

  2. Mateus Machado Luna Avatar

    Thank you for this detailed tutorial! It will be very useful as a reference for future projects!

    I have two questions, one more direct and another more broad…

    1. I’ve seen usages of the wordpress/ like you did, by installing the dependencies in the package.json, but I’ve also seen cases where one would simply add wp- as a dependency for their script in the enqueue functions and then import it from the global wp.. Is any of this approaches more “correct” than the other? Can you elaborate about possible advantages/disadvantages?

    2. As a plugin author, I took the decision to build my plugin admin pages years ago using Vue. Sadly back then we didn’t have all the amazing ecosystem that is taking shape now. I have the desire to start using the new React components but I find it pretty hard to do it gradually, specially because we built a large SPA so even page-per-page migration would be hard to do. Have you seen any similar situation where a migration strategy could be defined? I’ve been considering trying this library out, but I’m not sure if it could imply any performance loss: https://github.com/devilwjp/veaury

    Thanks anyway for the content, hope to see more of this!

    1. Róbert Mészáros Avatar

      Thanks, Mateus!

      I’m not familiar with Veaury, but I can certainly relate to the situation you’ve described. Technical debt can be indeed costly in numerous aspects.

      Without specific details about your project, off the top of my head, it’s rather challenging to outline even a basic strategy to migrate from Vue.js. However, I’ll think about it, and maybe I can come back with something constructive.

      But the general issue you’ve raised is quite relevant and could potentially form a solid foundation for an article. From what I see, there are already ideas for further articles on WordPress React components and plugins. So, I’m also looking forward to this entire topic being discussed more deeply and broadly.

      1. Mateus Machado Luna Avatar

        Thank you for both answers, I’ll dive into `wp-scripts` a bit more.

        Regarding the migration, our project is hugeeee. Even being open source I don’t foresee enough energy in our team to do a complete refactor that soon. I’ve played a bit using the VanillaJS way of rendering React components inside our Vue screens… tried with WordPress components as well. It does work, but adds layers and layers of complexity once we start having to track state changes and re-render stuff. In the end I don’t think there is an easy solution, just was hopping to hear from more folks in the same situation to see if there is something that I could be missing.

        Anyways, we’ll catch up! Thank you for the educational content, it really helps!

  3. Mateus Machado Luna Avatar

    Just noticed that the first question from my previous comment ended up really confusing as I was writing tags which were probably stripped out for security reasons… I’ll rewrite it:

    1. I’ve seen usages of the “wordpress/something” like you did, by installing the dependencies in the package.json, but I’ve also seen cases where one would simply add “wp-something”” as a dependency for their script in the enqueue functions and then import it from the global “wp.something”. Is any of this approaches more “correct” than the other? Can you elaborate about possible advantages/disadvantages?

    Sorry for that!

    1. Róbert Mészáros Avatar

      All great, Mateus, and thanks for the question!

      There’s a great article by Ryan Welcher on How webpack and WordPress packages interact. I recommend checking it out, as it contains an in-depth answer to your question and more.

      In essence: wp-scripts includes DependencyExtractionWebpackPlugin, which transforms references to @wordpress/* packages into the wp global in the final bundle during the build process.

      In the article, Ryan actually poses the rhetorical question:

      You may be wondering, why would I need this? All it does is convert the code to something I can just as easily write myself.

      Then, it gives a shortlist of the benefits!

  4. Creative Andrew Avatar

    Hi there! Thanks for your great article.

    I have two questions:

    1) Why is it needed to use these two hooks here? Isn’t just one sufficient or should I always use both when registering a setting?

    add_action( ‘rest_api_init’, ‘unadorned_announcement_bar_settings’ );
    add_action( ‘after_setup_theme’, ‘unadorned_announcement_bar_settings’ );

    2) Instead of using apiFetch, is it possible to use the different react hooks (useSelect/useEntityRecords/ useDispatch how handle the reading and writing?

    Best regards,
    Andrew

    1. Róbert Mészáros Avatar

      Hi Andrew, let me address your first question initially.

      Throughout the WordPress request lifecycle, specific actions, like plugins_loaded, are triggered regardless of whether we are operating within the context of the REST API, Admin, WP CLI, or the front end.

      Next to these, there are context-specific actions, such as rest_api_init or admin_init, which typically fire late in the process.

      The after_setup_theme action is executed for both the REST API and the front end.

      Given that after_setup_theme precedes rest_api_init, it’s redundant to apply the unadorned_announcement_bar_settings function to both. Utilizing just the after_setup_theme suffices as long as we intend to register the setting “everywhere”, as is the case here.

      If we were to solely have add_action( 'rest_api_init', 'unadorned_announcement_bar_settings' ), then on the front end, until the option exists in the database, calling get_option( 'unadorned_announcement_bar' ) would return false as the default.

      Thanks for asking about this; I’ll remove the forgotten and redundant line.

      P.S.: I’ve updated the code to use the init action.

    2. Róbert Mészáros Avatar

      Regarding your second question, it is most certainly possible to use the custom hooks you mentioned. WordPress offers various ways and abstractions.

      For example, after importing useEntityRecord from the @wordpress/core-data, you would be able to load the settings by calling useEntityRecord( 'root', 'site' ).

      In the mentioned course, Using the WordPress Data Layer, rather than using the more low-level apiFetch, the useSelect and useDispatch hooks are used to retrieve the list of existing pages and to create new ones.

      I highly recommend checking that out, as that course goes deeper than this introductory article.

      Besides the course, there are some relevant readings published here. Specifically on the useEntityRecords, there’s the useEntityRecords: an easier way to fetch WordPress data article. And there’s also the recently published How to work effectively with the useSelect hook that covers some fine details.

      When using these packages outside WordPress, it’s worth keeping in mind that these hooks are not merely syntactic sugar over the apiFetch. Whenever we use them, we interact with the underlying Redux store. Sometimes a direct call to the REST API is a better option, for example, if we weigh it against the dependencies list.

      I hope this gives you some useful pointers!

  5. Adrian Avatar

    It needs to be used once in the apiFetch, otherwise we would have a “rest_forbidden” error

    Example:
    In index.php under “unadorned-announcement-bar-script” add:
    wp_localize_script (‘unadorned-announcement-bar-script’, ‘params’, [ ‘nonce’ => wp_create_nonce( ‘wp_rest’ )]);

    In use-settings.jsx file add:
    apiFetch.use( apiFetch.createNonceMiddleware( window.params.nonce) );

    1. Róbert Mészáros Avatar

      Hi Adrian,

      As you highlighted, interacting with the Settings endpoint requires authentication.

      If you check the demo and inspect the value of wp.apiFetch.nonceMiddleware.nonce in your browser’s developer tools console you will see there’s a nonce set.

      Because wp-api-fetch is part of our dependency list (see build/index.asset.php), when the wp_default_packages_inline_scripts is called, the heavy lifting is done for us, and the createRootURLMiddleware and createNonceMiddleware are applied.

      To access the settings page, we have to be logged in and have the manage_options capability. Because of this, the pre-set nonce is enough, as it’s set in “our name”. We can call apiFetch without worrying about the authentication.

      If wp-api-fetch were not our dependency, if @wordpress/api-fetch were part of our bundle, or if we were using fetch or jQuery.ajax, then we would have to take the steps you mentioned.

      Thanks for the comment; I hope this helps!

      1. nicmare Avatar

        Wow! This comment about the nonce handling in the background was really helpful. Thank you Róbert.

  6. saxpaolo Avatar

    Hello – thanks for this great how-to article. Great job!

    Testing this locally, everything is working fine, but I’ve noticed that the textarea loose the focus at each keystroke, maybe because the component re-render everytime (?)

    But I’m a newby in React, learning stuff, so probably I’ve missed something…

    1. Róbert Mészáros Avatar

      Thanks a lot!

      As you described, the component is re-rendered when the TextareaControl value changes, but it shouldn’t lose focus.

      I haven’t been able to replicate this behavior in the demo. It would be helpful to know if you or others can reproduce it there. If not, consider comparing your code with the repository one more time.

      If you encounter the same issue on your system using just the minimal example provided in the development guidelines, I recommend opening a bug report in the Gutenberg repository.

  7. codersantosh Avatar

    A few years back, I developed a similar starter plugin called [wp-react-plugin-boilerplate](https://github.com/codersantosh/wp-react-plugin-boilerplate) for local WordPress meetup and WordCamp. However, encountering an issue with the schema and default WP settings API (as highlighted in https://github.com/codersantosh/wp-react-plugin-boilerplate/issues/8), it seems necessary to consider crafting and utilizing a custom REST endpoint instead.

  8. bluewinds Avatar

    I have been in WP for over 10 years now and I have to say that I am saddened to see how you have complicated simple things. Ok, it looks prettier and has arguably more options, but at what cost? Maybe it’s time to find a replacement for WP.

    1. nicmare Avatar

      i totally agree but thats the way how frontend code works today. its not really a wordpress issue. but anyways i am no big fan of react and compiling code either

    2. Justin Tadlock Avatar

      I remember how many people talked about the complexity of settings pages when the Settings API was introduced back in the early days. It wasn’t really documented that well, and folks had to learn how to build on top of new tools. I recall similar conversations about many, many other features (for example, the Customize API) over the years. It’s just the name of the game when you build for the web. You must be a constant student and learn new methods of handling what are—quite often—old problems.

      And I’m one of the most hardcore, old-school, PHP-loving developers you’ll find in this space. So I get it, but at the end of the day, the actual code from this tutorial and technique is pretty minimal. It’s not necessarily more complicated; it’s just different.

      Instead of what cost these changes bring, we should be asking what benefits come from such a system. And one of the major benefits of the JS-based component system WordPress now has is that it is consolidating all of the different methods for interacting with the UI into a single, standardized system. It’s going to take a while before everything is fully transitioned, but this will make development easier rather than harder in the long run (learn one system instead of a bunch of disparate APIs).

      1. bluewinds Avatar

        I agree, it is not that I do not know new languages and tech stacks, it is that there is no need to force JS where it doesn’t belong. But I understand: servers are becoming more and more expensive as well as CPU time, so it is a logical move to migrate (some) load to users’ devices which is free. The tricky part is to make a balance.

      2. Lars Gersmann Avatar

        I agree with you : things should get done easier and with less code.

        Why should we manually/hard-code a Plugin Settings page if we actually only need JSON Schema Form Renderer able to render the JSON Schema to a Gutenberg UI based settings page (it’s already declared using register_setting, isn’t it) ?

        We hacked together something which can do this at the cloudFest Hackathon 2024 – a JSON Schema Form Renderer utilizing Gutenberg UI components. Using this component a JSON Schema can be rendered to true Gutenberg UI forms : https://www.youtube.com/watch?v=9AmJBNytIwg

        This video will demo using JSON Schema to declare WordPress plugin settings and render the settings page with zero lines of Code using Gutenberg UI.

        GitHub Repo here : https://github.com/lgersman/cfhack2024-wp-react-jsonschema-form

        This is a result of a cloudFest Hackathon 2024 project : https://hackathon.cloudfest.com/project/json-schema-field-form-renderer/

        From my personal opinion this is the way to go – we get rid of manually writing (not only) plugin settings pages.

        PS: Work on this project is in progress.

        1. Justin Tadlock Avatar

          Very cool. Have you proposed this system on the Gutenberg repo? I could definitely see something like this being the next step in the evolution of how we extend WordPress.

  9. Siddharth Thevaril Avatar

    Thanks for this brilliant article!

    I have a question though. When you edit the form fields and try to navigate without saving, WordPress shows a popup saying “Changes you made may not be saved”.

    With the React implementation, when you save the form, this popup still shows up. Is there a way to inform WordPress that the form fields are no longer dirty?

  10. Dan Zakirov Avatar

    Thank you to the author of this article for providing an excellent guide. The step-by-step instructions were well-detailed, and I discovered several new things that I found particularly helpful.

  11. Ahamed Arshad Avatar
    Ahamed Arshad

    Those who uses 6.5.X and try to use `@wordpress/scripts:28` this tutorial wont work. As version 28 contains `new JSX transform` it fails to load. I am not a Weback config expert. But if someone could provide a snippet for this issue would be great and appreciated.

    1. Róbert Mészáros Avatar

      Thanks for the report, Ahamed. From a quick search, it looks like this is a known issue and actions are going to be taken about it.

      This comment summarizes the current status quo the best:

      It’s clear that we overlooked a bit the impact of this change. […] That said, the solution here is to actually stick to the old version of wp-scripts in the build tooling.

      I think there’s a couple of things we should do here: […] Write a post on make/core that serves as a dev note quickly to explain the change and the potential impact on build tools.

      I’ll keep an eye on it, and based on the outcome, amend the article.

      Update: A post was published on Make WordPress Core explaining the compatibility of WP Scripts and WP Core.

  12. Anh Tran Avatar

    I’m using createRoot from @wordpress/element, but seeing this error in the console:

    Warning: You are importing createRoot from “react-dom” which is not supported. You should instead import it from “react-dom/client”.

    Looking at the @wordpress/element package, I don’t see any problems. It just export what’s in React and react-dom.

    Do you have any idea why this error is happening?

    1. Róbert Mészáros Avatar

      Are you seeing this warning while following this tutorial, or is it related to a different project?

      Could you specify the versions of WordPress and wp-scripts you’re using? You can check the version by running npm list @wordpress/scripts.

      I couldn’t reproduce the warning after a quick check, but with the requested information, I might be able to investigate further.

      Recent changes might affect the build files, as noted in the previous comment. More information can be found in the JSX in WordPress 6.6 post.

      If this is related to a different codebase, I found a similar issue reported that was resolved by updating the @wordpress/dependency-extraction-webpack-plugin.

      If you’re interested, there has been some discussion about using @wordpress/element versus React directly. You might want to check the discussion out.

Leave a Reply

Your email address will not be published. Required fields are marked *