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
- Separa intención e implementación: las acciones declaran lo que quieres los controls lo ejecutan.
- Evita efectos en reducers: los reducers deben ser puros. Los efectos van a middlewares o controls.
- Maneja errores y estados de carga: despacha acciones de inicio/éxito/error o usa resolvers que gestionen cache y estado.
- Controla la concurrencia: usa debounce, cancel tokens o identificadores para evitar condiciones de carrera.
- Evita side effects ocultos: documenta los controls y su API así los desarrolladores saben qué espera cada control.
- Facilita testing: permite inyectar handlers mock o sustituir controles durante tests.
- Performance: memoiza respuestas frecuentes, limita peticiones en ráfagas y utiliza ttl en caches.
- 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 🙂 |