Como crear middlewares/controls para efectos en data stores (JS) en WordPress

Contents

Introducción

Este artículo explica con todo lujo de detalles cómo diseñar e implementar middlewares y controls para gestionar efectos (side effects) en data stores con JavaScript, orientado a su uso en entornos WordPress (plugins, bloques, o integración con wp.data). Se describen conceptos, patrones, ejemplos de código reutilizables y buenas prácticas: logging, acciones asíncronas, controls para efectos como llamadas a la API, cache, debounce y actualizaciones optimistas.

Qué es un middleware / control en un data store

Un middleware es una capa intermedia que intercepta acciones antes o después de que lleguen al reducer (o al núcleo del store). Permite ejecutar lógica adicional: logging, transformación de la acción, realizar side effects (peticiones HTTP, timers), o despachar acciones adicionales.

Un control es una forma organizada de declarar efectos: la acción describe qué efecto se desea y el control contiene la lógica que realiza ese efecto. Esto separa intención (la acción) de implementación (el controlador del efecto).

Por qué usar middlewares/controls en WordPress

  • Permiten centralizar llamadas a la API REST o a otras fuentes (apiFetch, fetch, caches).
  • Facilitan testing al desacoplar side effects.
  • En Gutenberg y wp.data existe una integración natural para registrar stores con controls, lo que armoniza patrones en bloques y plugins.
  • Permiten añadir capacidades transversales (logging, debounce, reintentos) sin contaminar reducers ni componentes.

Patrón general: store applyMiddleware

A continuación se muestra un store mínimo con soporte para middlewares en estilo Redux. Este ejemplo sirve como base para extender con middlewares concretos (logger, thunk, controls).

// store-simple.js
function createStore(reducer, initialState, enhancer) {
  if (typeof enhancer === function) {
    return enhancer(createStore)(reducer, initialState)
  }
  let state = initialState
  const listeners = new Set()
  function getState() { return state }
  function dispatch(action) {
    state = reducer(state, action)
    listeners.forEach(l => l())
    return action
  }
  function subscribe(listener) {
    listeners.add(listener)
    return () =gt listeners.delete(listener)
  }
  // dispatch an init action
  dispatch({ type: @@INIT })
  return { getState, dispatch, subscribe }
}

function applyMiddleware(...middlewares) {
  return createStore => (reducer, initialState) => {
    const store = createStore(reducer, initialState)
    let dispatch = (action) =gt store.dispatch(action)
    const middlewareAPI = {
      getState: store.getState,
      dispatch: (action) =gt dispatch(action)
    }
    const chain = middlewares.map(mw =gt mw(middlewareAPI))
    dispatch = chain.reduceRight((next, mw) =gt mw(next), store.dispatch)
    return { ...store, dispatch }
  }
}

Explicación

  • createStore: función ligera que gestiona estado, dispatch y suscriptores.
  • applyMiddleware: permite componer middlewares que reciban {getState, dispatch} y devuelvan una función que envuelve dispatch.

Ejemplos de middlewares básicos

Logger middleware

function loggerMiddleware({ getState }) {
  return next => action => {
    console.group  console.group(action: {action.type  unknown})
    console.log(prev state, getState())
    console.log(action, action)
    const result = next(action)
    console.log(next state, getState())
    console.groupEnd  console.groupEnd()
    return result
  }
}

Thunk-style (para acciones que son funciones)

Permite que las acciones sean funciones que reciben (dispatch, getState) y realicen lógica asíncrona.

function thunkMiddleware({ dispatch, getState }) {
  return next => action => {
    if (typeof action === function) {
      return action(dispatch, getState)
    }
    return next(action)
  }
}

Sistema de controls para efectos declarativos

Este patrón declara efectos en la acción con una clave control. Un middleware inspecciona esa clave y ejecuta el handler registrado para ese control, devolviendo su resultado (posiblemente una promesa) y/o despachando nuevas acciones según convenga.

Implementación del middleware de controls

// controls-middleware.js
function createControlsMiddleware() {
  const handlers = new Map()
  return {
    middleware() {
      return ({ dispatch, getState }) => next => action => {
        // Si la acción no tiene control, delega normalmente
        const control = action  action.control
        if (!control) {
          return next(action)
        }
        // control puede ser { type: FETCH_POSTS, args: [...] } o similar
        const handler = handlers.get(control.type)
        if (!handler) {
          // No hay handler: dejar pasar o lanzar error
          console.warn(No control handler registered for: {control.type})
          return next(action)
        }
        // Ejecutar handler con contexto y argumentos
        const result = handler({
          args: control.args  [],
          action,
          dispatch,
          getState,
        })
        // El handler puede devolver una promesa o valor. Podemos encadenar resultado.
        return result
      }
    },
    register(type, fn) {
      handlers.set(type, fn)
    }
  }
}

Ejemplo de registro y uso

// usage.js
const controls = createControlsMiddleware()
const store = createStore(
  reducer,
  initialState,
  applyMiddleware(thunkMiddleware, controls.middleware())
)

// registrar un handler que realiza fetch a la API
controls.register(API_FETCH, async ({ args }) =gt {
  const [ path, options ] = args
  const res = await fetch(path, options)
  if (!res.ok) throw new Error(Fetch failed)
  return res.json()
})

// acción que declara el control
function fetchPostsAction() {
  return {
    type: FETCH_POSTS,
    control: {
      type: API_FETCH,
      args: [/wp-json/wp/v2/posts, { method: GET }]
    }
  }
}

// uso
store.dispatch(fetchPostsAction()).then(posts =gt {
  console.log(posts fetched, posts)
}).catch(err =gt {
  console.error(err)
})

Notas sobre el patrón

  • La acción describe la intención (FETCH_POSTS) y delega la implementación del efecto a los controls.
  • Los handlers pueden devolver promesas, valores o incluso manejar errores y despachar acciones de éxito/error.
  • Este patrón favorece el testeo: los handlers se pueden mockear.

Patrones avanzados y ejemplos prácticos

1) Cache en controls

// simple-cache-control.js
function createCacheControl() {
  const cache = new Map()
  return async function ({ args }) {
    const key = JSON.stringify(args)
    if (cache.has(key)) {
      return cache.get(key)
    }
    const [ path ] = args
    const res = await fetch(path)
    const data = await res.json()
    cache.set(key, data)
    return data
  }
}

// registro
controls.register(API_FETCH_WITH_CACHE, createCacheControl())

2) Debounce control

// debounce-control.js
function createDebounceControl(delay = 300) {
  const timers = new Map()
  return ({ args }) =gt {
    const key = JSON.stringify(args[0]  default)
    return new Promise(resolve =gt {
      if (timers.has(key)) clearTimeout(timers.get(key))
      timers.set(key, setTimeout(async () =gt {
        timers.delete(key)
        // realizar la llamada real
        const [ path ] = args
        const res = await fetch(path)
        resolve(res.json())
      }, delay))
    })
  }
}
controls.register(API_FETCH_DEBOUNCE, createDebounceControl(250))

3) Optimistic updates

Se puede combinar un middleware (o logic en el action creator) para aplicar un cambio optimista, ejecutar el control para persistir en backend, y revertir si falla.

// optimistic example
function updateItemOptimistic(itemId, patch) {
  return (dispatch, getState) =gt {
    // aplicar cambio optimista
    dispatch({ type: UPDATE_ITEM_OPTIMISTIC, itemId, patch })
    // lanzar control para persistir
    return dispatch({
      type: UPDATE_ITEM_PERSIST,
      control: { type: API_PATCH, args: [/items/{itemId}, { method: PATCH, body: JSON.stringify(patch) }] }
    }).then(result =gt {
      // persistencia OK -> confirmar
      dispatch({ type: UPDATE_ITEM_SUCCESS, itemId, payload: result })
    }).catch(err =gt {
      // revertir
      dispatch({ type: UPDATE_ITEM_REVERT, itemId })
      throw err
    })
  }
}

Integración con WordPress (wp.data)

En WordPress, el paquete de datos expone wp.data.registerStore para crear stores integrados en la infraestructura de Gutenberg. Ese sistema admite la definición de controls que se ejecutan a partir de la descripción de la acción. El patrón mostrado anteriormente se adapta muy bien: registrar handlers para controls y devolver promesas desde ellos.

// Example: registerStore en un contexto WP (archivo JS de plugin/bloque)
const { registerStore } = wp.data

const storeConfig = {
  reducer(state = { posts: [], loading: false, error: null }, action) {
    switch (action.type) {
      case FETCH_POSTS_START:
        return { ...state, loading: true, error: null }
      case FETCH_POSTS_SUCCESS:
        return { ...state, loading: false, posts: action.payload }
      case FETCH_POSTS_ERROR:
        return { ...state, loading: false, error: action.payload }
      default:
        return state
    }
  },
  actions: {
    fetchPosts() {
      return {
        type: FETCH_POSTS,
        control: { type: API_FETCH, args: [/wp-json/wp/v2/posts] },
      }
    }
  },
  controls: {
    // WP data espera handlers que devuelvan valor o promesa
    API_FETCH({ args }) {
      const [ path ] = args
      return fetch(path).then(res =gt {
        if (!res.ok) throw new Error(Network error)
        return res.json()
      })
    }
  },
  selectors: {
    getPosts(state) {
      return state.posts
    },
    isLoading(state) {
      return state.loading
    }
  }
}

registerStore(my-plugin/posts, storeConfig)

Con esto, desde cualquier parte del código en el cliente se puede llamar:

wp.data.dispatch(my-plugin/posts).fetchPosts()
  .then(data =gt {
    // Aquí el handler devolvió los posts además puedes querer despachar acciones de éxito
    wp.data.dispatch(my-plugin/posts).receivePostsSuccess(data)
  })
  .catch(err =gt {
    // manejar error
    console.error(err)
  })

Notas sobre wp.data

  • registerStore acepta keys: reducer, actions, selectors, controls y resolvers (para lógica de resolución automática).
  • Los controls registrados en storeConfig interaccionan con el middleware de la librería y pueden devolver promesas.
  • En muchas implementaciones reales conviene que los handlers utilicen apiFetch (wp.apiFetch) para aprovechar cabeceras y nonce de WP.

Buenas prácticas y consideraciones

  1. Separa intención e implementación: las acciones declaran lo que quieres los controls lo ejecutan.
  2. Evita efectos en reducers: los reducers deben ser puros. Los efectos van a middlewares o controls.
  3. Maneja errores y estados de carga: despacha acciones de inicio/éxito/error o usa resolvers que gestionen cache y estado.
  4. Controla la concurrencia: usa debounce, cancel tokens o identificadores para evitar condiciones de carrera.
  5. Evita side effects ocultos: documenta los controls y su API así los desarrolladores saben qué espera cada control.
  6. Facilita testing: permite inyectar handlers mock o sustituir controles durante tests.
  7. Performance: memoiza respuestas frecuentes, limita peticiones en ráfagas y utiliza ttl en caches.
  8. Seguridad en WordPress: usa wp.apiFetch o añade nonces y cabeceras CSRF cuando llames a endpoints protegidos.

Conclusión técnica

Los middlewares y controls aportan una capa poderosa y flexible para gestionar efectos en data stores JavaScript. En WordPress se integran de forma natural con wp.data. El patrón que separa intención (acciones) de implementación (handlers/controls) mejora la mantenibilidad, permite testing más sencillo y facilita añadir comportamientos transversales (logging, cache, debounce, reintentos). Implementando un sistema de controls ligero —o aprovechando la infraestructura de wp.data— obtendrás un almacenamiento de estado robusto y preparado para interacciones complejas con APIs y la interfaz.



Acepto donaciones de BAT's mediante el navegador Brave 🙂



Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *