Introducción a React (parte 3)

Publicada en WordPress.

Mira nuestro vídeo

Existe una versión mejor de tu web

Comparte este artículo

La semana pasada vimos cómo en realidad un componente React no es más que la representación gráfica de (parte de) los datos de nuestra aplicación. En el ejemplo que implementamos, un componente mostraba el valor de un contador junto con dos botones que permitían incrementarlo o decrementarlo. El «secreto» estaba en descubrir que las props que recibía ese componente no tenían por qué ser únicamente datos; también podían ser funciones.

Hasta el momento, pues, las dos ideas principales que hemos aprendido con la parte 1 y la parte 2 de este tutorial son:

  • Un componente React es una función pura que recibe un conjunto de propiedades y genera el código HTML para «pintarlo» en nuestra página web.
  • La propiedades que recibe pueden ser (a) los datos que debemos mostrar en la interfaz o (b) las funciones que, conectándolas como callbacks a los eventos del DOM, nos permitirán modificar esos datos.

Pues bien, en esta tercera parte vamos a expandir un poco más nuestros conocimientos sobre esta arquitectura y vamos a aprender a usar los stores de WordPress. Con ellos, podremos definir el estado de nuestra aplicación de una forma limpia, ordenada, testeable e independiente de los componentes gráficos que usaremos para visualizarlo.

Expandiendo nuestra aplicación de ejemplo

En la segunda parte de este tutorial creamos un contador con un par de botones para incrementar o decrementar su valor. Para ello, implementamos una pequeña chapuza en la que el valor del contador era una variable JavaScript que teníamos tirada en el fichero principal index.js de nuestro plugin. El componente Counter era algo tal que así:

export const Counter = ( { value, onIncrease, onDecrease } ) => (
  <div>
    <div>Counter: <strong>{ value }</strong></div>
    <button onClick={ onIncrease }>+</button>
    <button onClick={ onDecrease }>-</button>
  </div>
);

y el fichero principal, donde guardábamos el valor del contador y donde renderizábamos el componente, era así:

// Import dependencies
import { render } from '@wordpress/element';
import { Counter } from './components/counter';

// Store value
let value = 0;
function setValue( newValue ) {
  value = newValue;
}

// Render component in DOM
const wrapper = document.getElementById( 'react-example-wrapper' );
render(
  <Counter
    value={ value }
    onIncrease={ () => setValue( value + 1 ) }
    onDecrease={ () => setValue( value - 1 ) }
  />,
  wrapper
);

Pues bien, hoy vamos a retomar ese ejemplo y lo vamos a complicar un poquito más. En concreto, hoy implementaremos el store WordPress de una aplicación que nos permita añadir (y quitar) tantos contadores como queramos, cada uno de ellos con sus propios controles para incrementar y decrementar sus respectivos valores. El resultado final (que conseguiremos en la próxima entrada) será algo parecido a esto:

Múltiples contadores con React y Redux
Múltiples contadores implementados con componentes React y un store de WordPress basado en Redux.

Definiendo el estado de nuestra aplicación

Cuando nos enfrentamos a un problema como el que te estoy planteando hoy, lo primero que debes pensar son las funciones que te permiten consultar y modificar el estado de tu aplicación. En nuestro caso, teniendo en cuenta el tipo de aplicación que te he descrito, las operaciones que necesitamos son:

  • Añadir un nuevo contador
  • Eliminar un contador x
  • Establecer el valor de un contador x
  • Ver qué contadores tengo
  • Ver el valor de un contador x

Una vez tienes definida la interfaz, debes pensar en la información necesaria para mantener el estado de tu aplicación. En nuestro caso, esto es bastante fácil: necesitamos una estructura que nos permita saber cuál es valor del contador x.

¿Qué estructuras de datos me permiten responder una pregunta como esta? Pues, en realidad, depende de qué sea esa x. Si la x es el índice del contador (esto es, «quiero saber el valor del «primer» contador», «quiero modificar el valor del «tercer» contador», etc), un array de números me sirve: el valor del primer contador está en la posición 0, el valor del segundo en la posición 1, etc.

Pero si x es un identificador único para cada contador, lo mismo tengo que pensar en soluciones alternativas. Por ejemplo, un objeto en el que cada clave sea el identificador del contador y su valor es el número con su valor me puede servir:

const counters = {
  ae13a: 0,
  f18bb: 3,
  e889a: 1,
  8b1d3: -5,
};

pero ojo, porque también podría usar un array de objetos como el siguiente:

const counters = [
  { id: 'ae13a', value: 0 },
  { id: 'f18bb', value: 3 },
  { id: 'e889a', value: 1 },
  { id: '8b1d3', value: -5 },
];

Si primero defines la interfaz para manipular y acceder a los datos, la implementación que uses luego por detrás es indiferente. De hecho, podrás cambiarla en cualquier momento según te convenga, lo cual es bastante cómodo.

Creando un store basado en Redux con WordPress

El paquete @wordpress/data de WordPress es un módulo que nos permite definir un almacén en el que almacenar (valga la redundancia) el estado de nuestra aplicación. Para crear un nuevo store usando este paquete, tenemos disponible la función registerStore, a la que deberemos pasarle la siguiente información:

  1. Un nombre que identifique nuestro store
  2. Un objeto selectors con las funciones que nos permiten consultar el estado de nuestro store
  3. Otro objeto actions con las funciones que nos permiten modificar el estado de nuestro store
  4. Un último objeto reducer que se encarga de actualizar el store cuando éste recibe una acción concreta

Como esto suena un poco abstracto, vamos a crear el store en nuestro ejemplo. Abre el ejemplo que empezamos la semana pasada y crea una nueva carpeta store en src. Luego, crea un fichero index.js con el siguiente código:

// Load dependencies
import { registerStore } from '@wordpress/data';

import reducer from './reducer';
import * as actions from './actions';
import * as selectors from './selectors';

registerStore( 'react-example/counters', {
  actions,
  reducer,
  selectors,
} );

en el que le pasamos las cuatros cosas que acabo de comentarte. Puedes darle el nombre que quieras a tu store, pero te recomiendo que sea una combinación del nombre de tu plugin (react-example) y una palabra que defina lo que almacenas en él (counters).

Si te fijas, el fichero tiene tres import de cosas que aún no hemos creado: reducer, actions y selectors. Vamos a crearlos uno a uno y ver qué tienen de especial cada uno de ellos.

Acciones en un store

Una de las cosas que (en principio) todo store necesita son un conjunto de funciones que nos permitan modificar los datos que tenemos almacenados. En nuestro ejemplo, esto lo vamos a meter en el fichero src/store/actions.js y tendrá la siguiente pinta:

export function addCounter( counterId ) {
  return {
    type: 'ADD_COUNTER',
    counterId,
  };
}

export function removeCounter( counterId ) {
  return {
    type: 'REMOVE_COUNTER',
    counterId,
  };
}

export function setCounterValue( counterId, value ) {
  return {
    type: 'SET_COUNTER_VALUE',
    counterId,
    value,
  };
}

Como puedes ver, definimos (y exportamos) tres funciones para manipular el store:

  • addCounter: esta función nos permitirá añadir un nuevo contador en nuestra aplicación. Como parámetro, esperamos recibir el identificador único que tendrá el nuevo contador.
  • removeCounter: nos permite eliminar un contador de nuestra aplicación. Lógicamente, también recibe como parámetro el identificador del contador a eliminar.
  • setCounterValue: permite establecer el valor concreto de un contador. En este caso, la signatura de la operación requiere que le pasemos el identificador del contador a actualizar junto al nuevo valor que debe tener.

Lo único raro que tienen estas acciones es el cuerpo de las mismas. Uno esperaría que estas funciones modificaran el estado de nuestra aplicación y, sin embargo, lo único que están haciendo es devolver un objeto. ¿Por qué?

En la arquitectura Redux (de la cual bebe WordPress), las acciones para modificar un store no modifican directamente el store, sino que lanzan (devuelven) un objeto con una «petición de modificación». Estas peticiones siempre siguen el mismo patrón: son un objeto con un atributo type que identifica la petición de forma única y tantas propiedades adicionales como sean necesarias para pasar los datos asociados a la modificación.

Actualizando el estado de un store usando su reducer

Si las acciones de un store solo representan una petición de actualización, necesitamos alguien o algo que actualice el estado cuando llega una de estas peticiones. Esta es precisamente la tarea del reducer de un store.

Un reducer es una función que toma como parámetros el estado actual de nuestra aplicación junto con una acción para actualizarlo y devuelve como resultado un nuevo estado en el que (si procede) se le ha aplicado la actualización.

En el apartado anterior hemos visto que nuestro store tiene tres acciones, así que nuestro reducer deberá ser capaz de reaccionar a cada una de ellas. Este podría ser el código de nuestro reducer, el cual meteríamos en el fichero reducer.js:

import { omit } from 'lodash';

export default function reducer( state = {}, action ) {
  switch ( action.type ) {
    case 'ADD_COUNTER':
      return {
        ...state,
        [ action.counterId ]: 0,
      };

    case 'REMOVE_COUNTER':
      return omit( state, action.counterId );

    case 'SET_COUNTER_VALUE':
      return {
        ...state,
        [ action.counterId ]: action.value,
      };
  }

  return state;
}

Tal y como ya te he avanzado, el reducer recibe el estado anterior del almacén (state, que, por cierto, por defecto es el objeto vacío {}) y una acción que pide actualizarlo. El cuerpo del reducer es bastante sencillo:

  • Empieza con una instrucción switch para discernir el tipo de acción (action.type) que hemos recibido y actuar en consecuencia:
    • Si se trata de ADD_COUNTER, genera un nuevo objeto que es una copia del anterior al que le añade una nueva clave usando el identificador que viene en la propia acción (action.counterId).
    • Si se trata de REMOVE_COUNTER, le quita al estado state la clave action.counterId.
    • Si es SET_COUNTER_VALUE, pone el nuevo valor action.value al contador con identificador action.counterId.

Lo más importante aquí es darte cuenta de que un reducer debe ser una función pura. Esto quiere decir que toda modificación que hagamos en el estado actual pasa por construir un nuevo estado que incluya los cambios pertinentes. Bajo ningún concepto, pues, debes mutar el estado anterior.

Selectores de un store

Ahora que ya sabes cómo actualizar los datos que tienes en un store, lo único que te queda por aprender es cómo consultarlos. Para ello, debemos definir un fichero selectors.js con el siguiente contenido:

export function getCounterIds( state ) {
  return Object.keys( state );
}

export function getCounterValue( state, counterId ) {
  return state[ counterId ];
}

Ninguna sorpresa, ¿verdad? Los selectores de un store tienen siempre, como mínimo, el parámetro state con toda la información del estado actual del store, junto con cualquier otro parámetro que pueda ser necesario para realizar la consulta que te interese.

En nuestro caso, por ejemplo, hemos creado dos selectores:

  • getCounterIds devuelve un array con los identificadores de todos los selectores que tenemos en nuestro store. Como el store es un objeto en el que las claves son los identificadores de los contadores, un Object.keys nos da la solución que queremos.
  • getCounterValue nos da el valor exacto del contador especificado. En este caso, el selector tiene dos parámetros: el estado state de nuestra aplicación y el identificador counterId del contador que nos interesa. Usando ambos parámetros, podemos ir a buscar el valor que nos interesa usando como clave counterId en state.

Cómo probar nuestro store

Para probar que el store funciona, abre el fichero src/index.js y añade un pequeño import al principio:

// Import dependencies
import { render } from '@wordpress/element';

import './store';
import { Counter } from './components/counter';

...

y luego, lógicamente, transpila el resultado con un npm run build. A continuación, accede a la página del plugin en tu WordPress, abre la consola JavaScript de tu navegador y prueba lo siguiente:

dispatch = wp.data.dispatch( 'react-example/counters' );
select = wp.data.select( 'react-example/counters' );

dispatch.addCounter( 'a' );
dispatch.addCounter( 'b' );
dispatch.addCounter( 'c' );
dispatch.setCounterValue( 'a', 3 );

select.getCounterIds();
// Array(3) [ "a", "b", "c" ]

select.getCounterValue( 'a' );
// 3

y podrás ver cómo tu store funciona correctamente. De hecho, existe una extensión tanto para Firefox como para Chrome llamada Redux DevTools que te permite consultar el estado de tu store, ver qué acciones va recibiendo y cómo las mismas van modificando el estado del store:

Redux DevTools en Firefox
La extensión Redux DevTools de Firefox y Chrome te permite consultar el estado de tus stores cómodamente.

¡Una auténtica pasada!

Próximos pasos

La verdad es que la entrada de hoy me ha salido un poco más extensa de lo que esperaba. Pero espero que la explicación te ayude a entender cómo funcionan los stores de WordPress y cómo puedes usarlos para almacenar los datos de tu aplicación.

La semana que viene avanzaremos con el ejemplo de hoy y conectaremos nuestros componentes React al store para que (a) lo que vemos en nuestra interfaz de usuario sea un reflejo de lo que hay en el store y para que (b) las interacciones del usuario con la UI sirvan para actualizar el store.

Mientras esperas, te voy a dejar con un pequeño ejercicio, cuya solución veremos también la semana que viene. Modifica el reducer para que los datos se almacenen así:

const counters = [
  { id: 'ae13a', value: 0 },
  { id: 'f18bb', value: 3 },
  { id: 'e889a', value: 1 },
  { id: '8b1d3', value: -5 },
];

y actualiza los selectores, el reducer y las acciones para que todo siga funcionando como en el ejemplo que hemos hoy. ¡Suerte!

Imagen destacada de Annie Theby en Unsplash.

Deja una respuesta

No publicaremos tu correo electrónico. Los campos obligatorios están marcados con: •

He leído y acepto la Política de privacidad de Nelio Software

Al marcar la casilla de aceptación estás dando tu legítimo consentimiento para que tu información personal se almacene en SiteGround y sea usada por Nelio Software con el propósito único de publicar aquí este comentario. Contáctanos para corregir, limitar, eliminar o acceder a tu información.