How to create a global settings panel with the Data API (JS) in WordPress

Contents

Introduction

This tutorial explains, step by step, how to create a global settings panel for a WordPress plugin using the JavaScript Data API (@wordpress/data). The goal is a modern, reactive settings panel: data is fetched via a REST endpoint, kept in a client-side store, editable via a React-style component, and saved back to the server. The Data API provides a single source of truth and convenient selectors, actions, and resolvers for loading and mutating remote data with caching and subscription support.

Overview and architecture

High-level architecture:

  • PHP: register REST endpoints and the setting(s) that live in the options table create an admin page where the React app mounts enqueue scripts and pass REST parameters (root URL and nonce).
  • JS: create a custom store with wp.data.registerStore (or using @wordpress/data helpers). The store exposes selectors and actions to read and write global settings. Resolvers will use @wordpress/api-fetch to retrieve data on first request.
  • UI: a component (React / wp.element) uses useSelect and useDispatch (or wp.data.select/dispatch) to display and save the settings.

Why use the Data API?

  • Centralized state: many components can read from the same store without prop drilling.
  • Automatic caching and resolving: resolvers make it easy to load data on-demand only once.
  • Consistent WP approach: integrates with Gutenberg and other WP admin packages using the same primitives.

Step-by-step implementation

1) Create the plugin scaffold (single-file example)

Create a PHP file in wp-content/plugins/my-global-settings/my-global-settings.php containing plugin header, option registration, REST routes, admin page and script enqueues.

lt?php
/
  Plugin Name: My Global Settings
  Description: Global settings panel using the Data API (JS).
  Version: 1.0
  Author: Example
 /

if ( ! defined( ABSPATH ) ) {
    exit
}

define( MGS_PLUGIN_VERSION, 1.0 )
define( MGS_PLUGIN_DIR, plugin_dir_path( __FILE__ ) )

/
  Register an option so WP validates/sanitizes if needed.
 /
function mgs_register_settings() {
    register_setting( mgs_options_group, mgs_global_settings, array(
        type => array,
        default => array(
            text => ,
            number => 10,
            enabled => false,
        ),
        sanitize_callback => mgs_sanitize_settings,
    ) )
}
add_action( admin_init, mgs_register_settings )

function mgs_sanitize_settings( value ) {
    value = (array) value
    value[text] = sanitize_text_field( value[text] ??  )
    value[number] = intval( value[number] ?? 0 )
    value[enabled] = (bool) ( value[enabled] ?? false )
    return value
}

/
  Register REST endpoints for reading and writing the settings.
 /
add_action( rest_api_init, function() {
    register_rest_route( mgs/v1, /settings, array(
        array(
            methods  => GET,
            callback => mgs_rest_get_settings,
            permission_callback => function() {
                return current_user_can( manage_options )
            },
        ),
        array(
            methods  => POST,
            callback => mgs_rest_update_settings,
            permission_callback => function() {
                return current_user_can( manage_options )
            },
            args => array(
                settings => array(
                    required => true,
                ),
            ),
        ),
    ) )
} )

function mgs_rest_get_settings( request ) {
    settings = get_option( mgs_global_settings, array() )
    return rest_ensure_response( settings )
}

function mgs_rest_update_settings( request ) {
    params = request->get_json_params()
    if ( ! is_array( params )  ! isset( params[settings] ) ) {
        return new WP_Error( invalid_data, Invalid settings data, array( status => 400 ) )
    }

    settings = params[settings]
    settings = mgs_sanitize_settings( settings )
    update_option( mgs_global_settings, settings )
    return rest_ensure_response( settings )
}

/
  Add admin menu page and enqueue scripts/styles.
 /
function mgs_admin_menu() {
    add_menu_page(
        Global Settings,
        Global Settings,
        manage_options,
        mgs-global-settings,
        mgs_render_admin_page,
        dashicons-admin-generic,
        80
    )
}
add_action( admin_menu, mgs_admin_menu )

function mgs_render_admin_page() {
    // The app will mount into this div.
    echo ltdiv id=mgs-rootgtlt/divgt
}

function mgs_enqueue_assets( hook ) {
    if ( hook !== toplevel_page_mgs-global-settings ) {
        return
    }

    // Register scripts. In production, compile/produce a single build file.
    wp_register_script(
        mgs-app,
        plugins_url( build/index.js, __FILE__ ),
        array( wp-element, wp-components, wp-data, wp-api-fetch ),
        MGS_PLUGIN_VERSION,
        true
    )

    // Pass REST info to the script.
    wp_localize_script( mgs-app, mgsEnv, array(
        root  => esc_url_raw( rest_url() ),
        nonce => wp_create_nonce( wp_rest ),
    ) )

    wp_enqueue_script( mgs-app )

    // Optional: small style
    wp_register_style( mgs-style, plugins_url( build/styles.css, __FILE__ ), array(), MGS_PLUGIN_VERSION )
    wp_enqueue_style( mgs-style )
}
add_action( admin_enqueue_scripts, mgs_enqueue_assets )

Notes about the PHP code

  • We register a setting to centralize sanitization. This is optional but recommended.
  • REST endpoints use capability checks. Always ensure only authorized users can read/update settings.
  • We output a simple container with ID mgs-root for mounting the React app.
  • wp_localize_script is used to pass the REST root and nonce to the JS app.

2) Build the JavaScript app and the Data store

We will create a custom store named mgs/global-settings. The store includes:

  • Selectors: getSettings(), isResolvingGetSettings(), getLastSaved()
  • Actions: receiveSettings(), saveSettings()
  • Resolver: when getSettings selector is called and state is empty, it will perform a REST GET to load settings
  • Thunks: saving will POST to the REST endpoint and update the store on success.

Use the modern @wordpress/scripts tool for building. Example package.json and source will be condensed here adjust if you already have a build setup.

Example: store and API interactions (ESNext)

/
  src/store/index.js
 
  NOTE: This file uses wp global references for packages:
  const { registerStore } = wp.data
  const apiFetch = wp.apiFetch
  const { createReduxStore } = wp.data
 
  If you use a build system, import from @wordpress/data and @wordpress/api-fetch.
 /

( function( wp ) {
    const { registerStore } = wp.data
    const apiFetch = wp.apiFetch

    const STORE_NAME = mgs/global-settings

    // Action types
    const RECEIVE_SETTINGS = RECEIVE_SETTINGS
    const REQUEST_SETTINGS = REQUEST_SETTINGS
    const SAVE_SETTINGS_SUCCESS = SAVE_SETTINGS_SUCCESS
    const SAVE_SETTINGS_FAILURE = SAVE_SETTINGS_FAILURE

    // Actions
    function receiveSettings( settings ) {
        return { type: RECEIVE_SETTINGS, settings }
    }
    function requestSettings() {
        return { type: REQUEST_SETTINGS }
    }
    function saveSettingsSuccess( settings ) {
        return { type: SAVE_SETTINGS_SUCCESS, settings }
    }
    function saveSettingsFailure( error ) {
        return { type: SAVE_SETTINGS_FAILURE, error }
    }

    // Reducer
    const DEFAULT_STATE = {
        settings: undefined, // undefined means not requested yet
        isLoading: false,
        lastSaved: null,
        error: null,
    }

    function reducer( state = DEFAULT_STATE, action ) {
        switch ( action.type ) {
            case REQUEST_SETTINGS:
                return Object.assign( {}, state, { isLoading: true } )
            case RECEIVE_SETTINGS:
                return Object.assign( {}, state, { settings: action.settings, isLoading: false, error: null } )
            case SAVE_SETTINGS_SUCCESS:
                return Object.assign( {}, state, { settings: action.settings, lastSaved: Date.now(), error: null } )
            case SAVE_SETTINGS_FAILURE:
                return Object.assign( {}, state, { error: action.error } )
            default:
                return state
        }
    }

    // Selectors
    const selectors = {
        getSettings( state ) {
            return state.settings
        },
        isResolvingGetSettings( state ) {
            return state.isLoading
        },
        getLastSaved( state ) {
            return state.lastSaved
        },
    }

    // Resolvers
    const resolvers = {
        getSettings() {
            // If settings already present, do nothing.
            const storeState = wp.data.select( STORE_NAME )
            if ( storeState.getSettings() !== undefined ) {
                return
            }

            // Dispatch request action
            wp.data.dispatch( STORE_NAME ).__requestSettings()

            try {
                const settings = yield apiFetch( { path: /mgs/v1/settings } )
                wp.data.dispatch( STORE_NAME ).__receiveSettings( settings )
            } catch ( err ) {
                wp.data.dispatch( STORE_NAME ).__saveSettingsFailure( err )
            }
        }
    }

    // Controls: none needed here. Using direct apiFetch inside resolvers/actions is fine.
    // Actions exposed to components (thunks)
    const actions = {
        __requestSettings() {
            return { type: REQUEST_SETTINGS }
        },
        __receiveSettings( settings ) {
            return { type: RECEIVE_SETTINGS, settings }
        },
        __saveSettingsFailure( error ) {
            return { type: SAVE_SETTINGS_FAILURE, error }
        },

        saveSettings( settings ) {
            return async ( dispatch ) => {
                try {
                    const result = await apiFetch( {
                        path: /mgs/v1/settings,
                        method: POST,
                        data: { settings },
                    } )
                    dispatch( saveSettingsSuccess( result ) )
                    return result
                } catch ( error ) {
                    dispatch( saveSettingsFailure( error ) )
                    throw error
                }
            }
        },
    }

    // Register the store
    registerStore( STORE_NAME, {
        reducer,
        actions,
        selectors,
        resolvers,
    } )

} )( window.wp )

3) UI: Settings panel component

You can write the UI using JSX and build with @wordpress/scripts or write using wp.element.createElement to avoid a build step. Below is a JSX-style component in a real project compile with a bundler (e.g. @wordpress/scripts).

/
  src/app.js
 
  Example using wp.element and wp.data hooks.
 /
import { render, useState, useEffect } from @wordpress/element
import { useSelect, useDispatch } from @wordpress/data
import { TextControl, CheckboxControl, Button, Panel, PanelBody } from @wordpress/components

function GlobalSettingsPanel() {
    const settings = useSelect( ( select ) => select( mgs/global-settings ).getSettings(), [] )
    const isLoading = useSelect( ( select ) => select( mgs/global-settings ).isResolvingGetSettings(), [] )
    const lastSaved = useSelect( ( select ) => select( mgs/global-settings ).getLastSaved(), [] )

    const { saveSettings } = useDispatch( mgs/global-settings )

    const [local, setLocal] = useState( { text: , number: 0, enabled: false } )
    const [saving, setSaving] = useState( false )
    const [error, setError] = useState( null )

    // Populate local state when settings arrive
    useEffect( () => {
        if ( settings ) {
            setLocal( Object.assign( {}, { text: , number: 0, enabled: false }, settings ) )
        }
    }, [ settings ] )

    // Request settings if not present (useSelect resolvers will handle this automatically
    // if selectors are implemented as resolvers. If you used registerStore resolvers as above,
    // they run automatically.)
    // const settingsFromStore = useSelect( ( select ) => select( mgs/global-settings ).getSettings(), [] )

    function onSave() {
        setSaving( true )
        setError( null )
        saveSettings( local )
            .then( () => {
                setSaving( false )
            } )
            .catch( ( err ) => {
                setError( err )
                setSaving( false )
            } )
    }

    if ( isLoading  !settings ) {
        return ltpgtLoading...lt/pgt
    }

    return (
        ltdivgt
            ltPanelgt
                ltPanelBody title=Global Settings initialOpen={ true }gt
                    ltTextControl
                        label=Text
                        value={ local.text }
                        onChange={ ( val ) =gt setLocal( prev =gt ( { ...prev, text: val } ) ) }
                    /gt
                    ltTextControl
                        type=number
                        label=Number
                        value={ local.number }
                        onChange={ ( val ) =gt setLocal( prev =gt ( { ...prev, number: parseInt( val, 10 )  0 } ) ) }
                    /gt
                    ltCheckboxControl
                        label=Enabled
                        checked={ local.enabled }
                        onChange={ ( val ) =gt setLocal( prev =gt ( { ...prev, enabled: val } ) ) }
                    /gt
                    ltButton isPrimary onClick={ onSave } isBusy={ saving }gtSave Settingslt/Buttongt
                    { error  ltp style={{ color: red }}gtError saving settingslt/pgt }
                    { lastSaved  ltpgtLast saved: { new Date( lastSaved ).toLocaleString() }lt/pgt }
                lt/PanelBodygt
            lt/Panelgt
        lt/divgt
    )
}

document.addEventListener( DOMContentLoaded, function() {
    const el = document.getElementById( mgs-root )
    if ( el ) {
        render( ltGlobalSettingsPanel /gt, el )
    }
} )

4) Building and enqueuing the compiled bundle

Use a build tool to compile the ESNext/JSX code into a single script at build/index.js referenced in the PHP. For small demos you could avoid a build step and write the UI without JSX using wp.element.createElement for production, use @wordpress/scripts so you can import @wordpress/components and other packages.

{
  name: mgs-plugin,
  version: 1.0.0,
  devDependencies: {
    @wordpress/scripts: ^25.0.0
  },
  scripts: {
    build: wp-scripts build,
    start: wp-scripts start
  }
}

5) Minimal CSS (optional)

/ build/styles.css /
#mgs-root {
  padding: 20px
  max-width: 700px
}

Practical notes and advanced topics

State shape and store design

Keep the state shape predictable. For the example above:

settings object undefined — the actual settings object undefined indicates not requested yet
isLoading boolean — whether the store is resolving the GET request
lastSaved timestamp — when a save was last persisted successfully
error objectnull — last error encountered

Selectors vs. direct API calls

  • Prefer using selectors (wp.data.select) everywhere in your UI logic because they can be combined with resolvers to automatically fetch missing data.
  • Avoid directly calling apiFetch in multiple components centralize data access through the store.

Resolvers and request deduplication

Resolvers run the first time a selector is requested and the store value is not present. If multiple components simultaneously select the data, resolvers help ensure only one network request is issued. In the example above we used a simple REQUEST_SETTINGS action to mark state.isLoading more advanced patterns can use promises to track ongoing fetches and to prevent race conditions.

Optimistic updates and error handling

  • To provide quicker UX, you can update local state before the server response (optimistic update). Then revert if the server returns an error.
  • Alternatively, wait for the server response and only then update the store. The example uses the latter approach to keep things simple.

Using the store from other parts of your plugin

Any script running in the admin that depends on wp.data can access the store once it is registered. Example usage from another script:

// Read settings
const settings = wp.data.select( mgs/global-settings ).getSettings()

// Subscribe to changes
wp.data.subscribe( () =gt {
    const current = wp.data.select( mgs/global-settings ).getSettings()
    // do something when current changes
} )

// Save new settings
wp.data.dispatch( mgs/global-settings ).saveSettings( { text: New, number: 5, enabled: true } )

Security considerations

  • Always validate capabilities in the REST endpoints (current_user_can).
  • Use nonces for REST emailless calls if you want additional safety wp.apiFetch will automatically include X-WP-Nonce when configured via wp_localize_script (or set it through headers).
  • Sanitize and validate all data saved to options or post meta on the server side.

Testing and debugging tips

  • Open the browser console and inspect network requests to /wp-json/mgs/v1/settings to ensure GET and POST behave as expected.
  • If selectors return undefined, ensure resolvers are registered and that the store is registered before components mount. Register stores early (e.g. in the main app bundle that is enqueued for the admin page).
  • Use wp.data.select(core/data).getEntityRecords or other core stores for different data types, but for plugin-global settings you usually create a custom store.

Complete workflow summary

  1. Register setting(s) and REST routes in PHP ensure permissions and sanitization.
  2. Create an admin page that outputs a mount point and enqueues the compiled JS/CSS.
  3. In JavaScript, register a data store with selectors, actions and resolvers that call the REST endpoints.
  4. Build a UI component that uses useSelect/useDispatch (or wp.data.select/dispatch) to read and mutate settings through the store.
  5. Test and secure your endpoints and make UX improvements such as optimistic updates or visual loaders.

Example: Quick reference of the REST endpoints

GET /wp-json/mgs/v1/settings
POST /wp-json/mgs/v1/settings nbsp with JSON body: { settings: { text: …, number: 1, enabled: true } }

Wrapping up

This tutorial demonstrated a robust way to create a global settings panel using the WordPress Data API. The pattern centralizes data access, benefits from resolver-based fetching and allows any admin script to access and react to settings changes via wp.data. Use the examples as a starting point and extend the store with more selectors, fine-grained actions, optimistic updates, and richer validation as your plugin grows.



Acepto donaciones de BAT's mediante el navegador Brave 🙂



Leave a Reply

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