How to register a custom store with @wordpress/data (JS) in WordPress

Contents

Introduction

This article explains, in exhaustive detail, how to register a custom store with @wordpress/data (JavaScript) and use it correctly inside a WordPress block, plugin or admin UI. It covers the conceptual background, exact APIs, synchronous and asynchronous patterns (controls and resolvers), React integration (useSelect/useDispatch and HOCs), createReduxStore convenience, TypeScript considerations, testing, debugging, advanced topics and common pitfalls. Example code is provided for each important step. All examples are ready to paste into a modern Gutenberg plugin environment that uses the WordPress packages.

Core concepts and terminology

  • Store namespace: Each store is registered under a unique namespaced string, for example my-plugin/store. The namespace must be unique to avoid collisions with core or other plugins.
  • Reducer: A pure function that accepts (state, action) and returns the next state. Must be immutable and deterministic.
  • Actions: Plain action creators—functions that typically return plain action objects. registerStore accepts an actions object so that wp.data.dispatch(namespace) can provide typed action creators.
  • Selectors: Functions that read state and return derived data for UI. Selectors are called through wp.data.select(namespace).selectorName( … ). They should be pure and can be memoized.
  • Controls: Small pluggable handlers used by resolvers to perform side effects (often async). Controls receive a payload describing a request and return a Promise or value. Commonly used to wrap apiFetch.
  • Resolvers: Generator functions attached to selectors. When a selector is called and required data is missing, the resolver runs and can yield control actions (which invoke controls). Resolvers usually dispatch the result into the store (via actions).
  • createReduxStore: A helper from @wordpress/data that assembles a settings object for registerStore from a reducer/actions/selectors/etc. It keeps the API consistent between stores built with plain Redux and stores built following WP patterns.
  • useSelect/useDispatch and withSelect/withDispatch: React bindings to read selectors and dispatch actions inside blocks or React components.

Registering a simple synchronous store

A minimal store requires a unique name, a reducer, an actions object, and selectors. The reducer must return an initial state when state is undefined.

import { registerStore } from @wordpress/data

const STORE_NAME = my-plugin/store

// 1) default state
const DEFAULT_STATE = {
  items: [],
}

// 2) reducer
function reducer( state = DEFAULT_STATE, action ) {
  switch ( action.type ) {
    case ADD_ITEM:
      return { ...state, items: [ ...state.items, action.item ] }
    case SET_ITEMS:
      return { ...state, items: action.items }
    default:
      return state
  }
}

// 3) action creators (plain)
const actions = {
  addItem( item ) {
    return {
      type: ADD_ITEM,
      item,
    }
  },
  setItems( items ) {
    return {
      type: SET_ITEMS,
      items,
    }
  },
}

// 4) selectors
const selectors = {
  getItems( state ) {
    return state.items
  },
  getItemCount( state ) {
    return state.items.length
  },
}

// 5) register
registerStore( STORE_NAME, {
  reducer,
  actions,
  selectors,
} )

After registration you can access the store through:

  • Imperatively in non-React code: wp.data.dispatch(my-plugin/store).addItem(…)
  • In React with hooks: const items = useSelect( select => select(my-plugin/store).getItems() )

React integration example (hooks)

import { useSelect, useDispatch } from @wordpress/data
import { Button } from @wordpress/components

function MyComponent() {
  const items = useSelect( ( select ) => select( my-plugin/store ).getItems(), [] )
  const { addItem } = useDispatch( my-plugin/store )

  return (
    
    { items.map( (item, i) =>
  • { item }
  • ) }
) }

Asynchronous data: controls and resolvers

Synchronous actions handle local state updates. For fetching remote data or other side effects, the recommended pattern in @wordpress/data is:

  1. Define a control that performs the side effect (commonly wrapping wp.apiFetch).
  2. Create selector(s) that expose the derived state to callers.
  3. Attach resolver generator functions to those selectors. When a selector is called and data is missing, the resolver yields a control action that triggers the control. The resolver then dispatches actions with the result to update the store.

Key points: resolvers run transparently when a selector is invoked resolvers can yield control actions like { type: API_FETCH, path: /wp/v2/posts } where API_FETCH is a control key defined in controls.

import apiFetch from @wordpress/api-fetch
import { registerStore } from @wordpress/data

const STORE_NAME = my-plugin/remote

const DEFAULT_STATE = {
  posts: [],
  _resolved: {}, // common pattern to track resolution per key
}

function reducer( state = DEFAULT_STATE, action ) {
  switch ( action.type ) {
    case RECEIVE_POSTS:
      return {
        ...state,
        posts: action.posts,
        _resolved: { ...state._resolved, posts: true },
      }
    default:
      return state
  }
}

const actions = {
  receivePosts( posts ) {
    return { type: RECEIVE_POSTS, posts }
  },
}

const controls = {
  // control key API_FETCH will handle payload such as { path: /wp/v2/posts }
  API_FETCH( request ) {
    // return a promise here using the official wp.apiFetch wrapper
    return apiFetch( request )
  },
}

const resolvers = {
  getPosts() {
    // If already resolved, do nothing
    // The resolver receives the same arguments passed to the selector here no args.
    // Yielding an object with a type property matches the control key:
    const posts = yield { type: API_FETCH, path: /wp/v2/posts }
    // After receiving, dispatch action to store posts
    yield actions.receivePosts( posts )
  },
}

const selectors = {
  getPosts( state ) {
    return state.posts
  },
}

registerStore( STORE_NAME, {
  reducer,
  actions,
  selectors,
  controls,
  resolvers,
} )

Usage from React:

import { useSelect } from @wordpress/data

function PostsList() {
  const posts = useSelect( ( select ) => select( my-plugin/remote ).getPosts(), [] )
  return (
    
{ posts.length ? posts.map( p =>
{ p.title.rendered }
) : Loading… }
) }

Resolver details and guarantees

  • Resolvers are generator functions. When they yield values with a type matching a control key, the corresponding control is invoked with the yielded object (minus the type?) or the full yielded value—controls should be prepared to receive the yielded payload.
  • Resolvers should update the store by yielding/returning actions, or by dispatching inside the resolver using available action creators. In practice, yielding an action creator (like yield actions.receivePosts(posts)) will cause the action to be dispatched.
  • Resolvers are invoked only when the selector is first called and state indicates missing data. Track resolution with a simple _resolved map in the state.

createReduxStore helper

createReduxStore simplifies the registration process when you want to use a plain reducer/actions/selectors setup. It constructs a store settings object compatible with registerStore.

import { registerStore, createReduxStore } from @wordpress/data

const STORE_NAME = my-plugin/store

const DEFAULT_STATE = { count: 0 }

function reducer( state = DEFAULT_STATE, action ) {
  switch ( action.type ) {
    case INCREMENT:
      return { ...state, count: state.count   1 }
    default:
      return state
  }
}

const actions = {
  increment() {
    return { type: INCREMENT }
  },
}

const selectors = {
  getCount( state ) {
    return state.count
  },
}

const store = createReduxStore( STORE_NAME, {
  reducer,
  actions,
  selectors,
} )

registerStore( STORE_NAME, store )

createReduxStore wires action creators and selectors to behave consistently with other @wordpress/data stores. Use it when you want to follow the Redux-style reducer/actions/selectors pattern with less boilerplate.

Using HOCs: withSelect and withDispatch

If you are not using hooks or you need to connect class components, you can use higher-order components.

import { withSelect, withDispatch } from @wordpress/data
import { compose } from @wordpress/compose

function MyComponent( { items, addItem } ) {
  return (
    
    { items.map( (i, idx) =>
  • { i }
  • ) }
) } export default compose( withSelect( ( select ) => { return { items: select( my-plugin/store ).getItems() } } ), withDispatch( ( dispatch ) => { return { addItem: dispatch( my-plugin/store ).addItem } } ) )( MyComponent )

Namespacing strategy and collisions

  • Always use a reverse-domain or plugin-based prefix name, e.g. my-company/my-plugin/store.
  • Do not register multiple stores under the same name.
  • Core stores use core and other namespaces. Avoid those names.

Combining reducers and multiple stores

You can register multiple stores for different concerns. Alternatively, keep related data in the same namespace and use combineReducers (from redux) to compose sub-reducers.

import { combineReducers } from redux
import { registerStore } from @wordpress/data

const postsReducer = ( state = { byId: {}, ids: [] }, action ) => {
  // ...
  return state
}
const uiReducer = ( state = { isLoading: false }, action ) => {
  // ...
  return state
}

const rootReducer = combineReducers( {
  posts: postsReducer,
  ui: uiReducer,
} )

registerStore( my-plugin/complex, {
  reducer: rootReducer,
  actions: { / ... / },
  selectors: { / ... / },
} )

Unregistering and replaceReducer

In dev workflows you may want to unload or replace a store. The @wordpress/data package exposes registerStore but not commonly a public API to unregister. In general:

  • Avoid re-registering a store at runtime in production—register store once during bootstrapping.
  • For hot module replacement (HMR) during development, you can replace the reducer via redux replaceReducer on the created Redux store if you stored the created store reference. If using createReduxStore/registerStore directly, you may need a custom dev-only pattern to keep a reference to the created Redux store and call replaceReducer.

TypeScript integration

When using TypeScript, strongly type your action creators, selectors and state. The WordPress packages have TypeScript definitions for many features sometimes you will need to add your own type augmentation.

import { registerStore } from @wordpress/data

type State = {
  items: string[]
}

const DEFAULT_STATE: State = { items: [] }

type AddItemAction = { type: ADD_ITEM item: string }
type SetItemsAction = { type: SET_ITEMS items: string[] }
type Action = AddItemAction  SetItemsAction

function reducer( state = DEFAULT_STATE, action: Action ): State {
  switch ( action.type ) {
    case ADD_ITEM:
      return { ...state, items: [ ...state.items, action.item ] }
    case SET_ITEMS:
      return { ...state, items: action.items }
    default:
      return state
  }
}

const actions = {
  addItem( item: string ) {
    return { type: ADD_ITEM as const, item }
  },
  setItems( items: string[] ) {
    return { type: SET_ITEMS as const, items }
  },
}

const selectors = {
  getItems( state: State ) {
    return state.items
  },
}

registerStore( my-plugin/ts, {
  reducer,
  actions,
  selectors,
} )

Note: Type inference across registerStore to wp.data.dispatch/select is not perfectly typed in all WP package versions. Use local typings where needed or cast to any for dispatch/select in complex cases.

Testing stores

Unit-test your reducer, selectors and action creators separately. For integration testing involving the data registry:

  • Use wp.data.dispatch to trigger actions and wp.data.select to inspect state.
  • For async tests, ensure resolvers/controls are awaited appropriately by wrapping with utilities that wait for the selector to have a resolved value.
  • Mock apiFetch or provide a fake control implementation to simulate remote responses.
// Example jest test pattern (pseudo-code)
import { registerStore } from @wordpress/data
import apiFetch from @wordpress/api-fetch

// mock apiFetch
jest.mock( @wordpress/api-fetch, () => jest.fn() )

test( resolves posts via resolver, async () => {
  apiFetch.mockResolvedValueOnce( [ { id: 1, title: x } ] )
  registerStore( / store with API_FETCH control that calls apiFetch / )
  // trigger selector
  const posts = wp.data.select( my-plugin/remote ).getPosts()
  // wait for promise microtasks - resolve resolver - often you need flushPromises()
  await flushPromises()
  expect( wp.data.select( my-plugin/remote ).getPosts().length ).toBe(1)
} )

Performance and memoization

  • Selectors should be cheap. Use memoization (for example reselect createSelector) for derived or expensive computations to avoid re-computation on every render.
  • useSelect accepts a second parameter: an array of dependencies. Provide stable selectors and dependency arrays to prevent unnecessary re-renders.
  • Normalize data in state (store entities by id) to keep selectors simple and efficient.

Debugging tips

  • Use console.log inside reducers, controls and resolvers during development to trace flows.
  • Inspect store contents: console.log( wp.data.select( my-plugin/store ) ) or inside components log selected values.
  • When a selector never triggers a resolver, check state._resolved flags (if using the pattern) or ensure your resolver is attached and yields a control action whose type matches a control key.
  • If actions are not dispatched as expected from resolvers, verify you yield an action creator or call the action creator and yield its result so the data layer dispatches it.

Common pitfalls and troubleshooting

  1. No initial state — Ensure your reducer returns DEFAULT_STATE when state is undefined.
  2. Wrong store name — Dispatch/select use the exact same namespace string used in registerStore.
  3. Mutating state — Reducers must be pure and should return new state objects rather than mutating existing ones.
  4. Resolver never runs — Confirm the selector is being called and that your state indicates unresolved status so the data layer calls the resolver.
  5. Control type mismatch — The yielded object in a resolver must have a type key corresponding to a controls entry (e.g., yield { type: API_FETCH, path: /wp/v2/posts }).
  6. Using legacy globals — Prefer imports from @wordpress packages instead of relying on global wp. where possible however controls often use wp.apiFetch in environments without bundler configuration.

Compatibility and version notes

  • @wordpress/data APIs have been stable since Gutenberg matured, but always check your installed version if you rely on newer helper functions.
  • TypeScript support and type definitions improved across releases—if types arent available, add local type definitions or upgrade packages.

Reference API cheatsheet

registerStore( name, settings ) Registers a new store. settings typically include reducer, actions, selectors, controls, resolvers, persist.
createReduxStore( name, config ) Helper to produce a settings object compatible with registerStore for Redux-style stores (with actions, reducers, selectors).
wp.data.select( name ) Return selectors for store name. In code imports, use useSelect or select() wrapper.
wp.data.dispatch( name ) Return dispatch-bound action creators for store name. In React, prefer useDispatch(namespace).
useSelect / useDispatch / withSelect / withDispatch React bindings to read selectors and dispatch actions inside components.

Complete working example: synchronous async React component

This final example shows a combined approach: a store that has both local actions and an async resolver using a control. The component uses hooks to read and trigger actions.

/
  Combined example. Add to a JS module in your plugin build.
 /
import apiFetch from @wordpress/api-fetch
import { registerStore } from @wordpress/data
import { useSelect, useDispatch } from @wordpress/data
import { useEffect } from react

/ STORE SETUP /
const STORE = my-plugin/full

const DEFAULT_STATE = { items: [], _resolved: false }

function reducer( state = DEFAULT_STATE, action ) {
  switch ( action.type ) {
    case RECEIVE_ITEMS:
      return { ...state, items: action.items, _resolved: true }
    case ADD_ITEM:
      return { ...state, items: [ ...state.items, action.item ] }
    default:
      return state
  }
}

const actions = {
  receiveItems( items ) {
    return { type: RECEIVE_ITEMS, items }
  },
  addItem( item ) {
    return { type: ADD_ITEM, item }
  },
}

const controls = {
  FETCH( request ) {
    return apiFetch( request )
  },
}

const resolvers = {
  getItems() {
    // if already resolved do nothing
    const state = wp.data.select( STORE )
    if ( state  state._resolved ) {
      return
    }
    const items = yield { type: FETCH, path: /wp/v2/posts } // will call controls.FETCH
    yield actions.receiveItems( items )
  },
}

const selectors = {
  getItems( state ) {
    return state.items
  },
}

registerStore( STORE, { reducer, actions, selectors, controls, resolvers } )

/ REACT CONSUMER /
export function PostsViewer() {
  // When calling the selector, resolvers (if any) will run automatically
  const items = useSelect( ( select ) => select( STORE ).getItems(), [] )
  const { addItem } = useDispatch( STORE )

  useEffect( () => {
    // Example: add a synthetic item after mount
    addItem( synthetic )
  }, [] )

  if ( ! items  items.length === 0 ) {
    return 
Loading or no items
} return (
{ items.map( p =>
{ p.title?.rendered p.id }
) }
) }

Where to go next

  • Read the official package docs for @wordpress/data: https://developer.wordpress.org/block-editor/packages/packages-data/
  • Study examples in Gutenberg core to see how core stores are implemented (e.g., core/editor, core/block-editor).
  • Implement small stores in isolation and test with jest and mocked apiFetch before integrating in large plugins.

Conclusion (summary)

Registering a custom store with @wordpress/data involves naming your store, providing a reducer, adding actions and selectors, and optionally wiring controls and resolvers for async behavior. createReduxStore speeds up boilerplate for Redux-style stores. Use useSelect/useDispatch for React integration memoize selectors for performance write pure reducers and carefully track resolved state when using resolvers. Follow the patterns shown in examples to build maintainable, testable stores that integrate cleanly with WordPress and Gutenberg.



Acepto donaciones de BAT's mediante el navegador Brave 🙂



Leave a Reply

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