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
- Register setting(s) and REST routes in PHP ensure permissions and sanitization.
- Create an admin page that outputs a mount point and enqueues the compiled JS/CSS.
- In JavaScript, register a data store with selectors, actions and resolvers that call the REST endpoints.
- Build a UI component that uses useSelect/useDispatch (or wp.data.select/dispatch) to read and mutate settings through the store.
- 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 🙂 |