Watch, de Dmitry Nucky Thompson

Por fin llegamos a la última parte de este pequeño tutorial de introducción a React. Lógicamente he dejado muchísimas cosas en el tintero, por lo que no descarto escribir más entradas en el futuro en las que centrarnos en diferentes aspectos del stack de desarrollo JS, pero creo que con esta última entrada ya tendrás las nociones básicas necesarias para empezar a andar por tu propio pie.

La semana pasada dejamos nuestra aplicación de ejemplo en un punto muy interesante. El objetivo que nos pusimos era crear una pequeña aplicación con múltiples contadores, los cuales podríamos añadir y quitar a voluntad:

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

Lo último que hicimos fue dejar preparado un store que se encargaba de mantener el estado. Es decir, implementamos el esqueleto que sería capaz de sostener a nuestra aplicación. Ahora lo único que nos queda es dar cara y ojos a esta aplicación través de los componentes.

Hoy veremos cómo conectar los componentes que hemos creado en React con nuestro store de WordPress basado en Redux para que la interfaz muestre el estado que tenemos en el store y para que las interacciones con el usuario actualicen ese estado.

Solución a los deberes de la semana pasada

Antes de entrar en materia, vamos a ver la solución a los deberes que te puse la semana pasada. Si recuerdas, te dije que re-implementaras el store de tal forma que los datos de los contadores ya no se guardaran como un objeto con pares llave » valor, sino como un array de objetos, cada uno de ellos con su identificador y su valor:

const originalStore = {
  x: 1,
  y: 2,
};
const newStore = [
  { id: 'x', value: 1 },
  { id: 'y', value: 2 },
];

¿Qué cambios había que hacer para conseguir esta solución?

Cambios en las acciones

Las acciones son exactamente igual que las que teníamos anteriormente. Tal y como vimos la semana pasada, las acciones no son funciones que actualicen directamente el estado de tu aplicación, sino que sencillamente generan un objeto con la «petición de actualización» que quieres realizar en tu store. Así pues, actions.js sigue igual:

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,
  };
}

Cambios en el reducer

El reducer es la función que se encarga de actualizar el estado partiendo del estado anterior y aplicando los cambios pertinentes según qué acción reciba. En este caso, como queremos cambiar la estructura de datos que almacena el estado de nuestra aplicación, está claro que deberemos cambiar el reducer:

import { map, find, without } from 'lodash';
export default function reducer( state = [], action ) {
  switch ( action.type ) {
    case 'ADD_COUNTER':
      return [
        ...state,
        { id: action.counterId, value: 0 },
      ];
    case 'REMOVE_COUNTER':
      return without(
         state,
         find( state, { id: action.counterId } )
      );
    case 'SET_COUNTER_VALUE':
      return map(
        state,
        ( counter ) =>
          action.id === action.counterId
            ? { ...counter, value: action.value }
            : counter
      );
  }
  return state;
}

Tal y como puedes imaginar, lo único que teníamos que hacer aquí era cambiar el valor por defecto del estado del objeto vacío {} al array vacío []. Luego, las diferentes acciones consisten en añadir un nuevo objeto al array, quitarlo o sustituirlo cuando recibimos un nuevo valor.

Cambios en los selectores

Finalmente, tenemos el fichero con los selectores selectors.js. Como vimos la semana pasada, los selectores reciben como parámetro el estado que tenemos en nuestra aplicación (junto con cualquier otro parámetro necesario para realizar la consulta). Por lo tanto, como hemos cambiado la representación del estado, habrá que cambiar el cuerpo de los selectores:

import { map, find } from 'lodash';
export function getCounterIds( state ) {
  return map( state, 'id' );
}
export function getCounterValue( state, counterId ) {
  const counter = find( state, { id: counterId } ) || {};
  return counter.value;
}

En nuestro ejemplo sólo teníamos dos selectores, así que solo tenemos que reimplementar dos funciones. La primera se encarga de devolver los identificadores de todos nuestros contadores, lo cual podemos conseguir muy fácilmente con un map. La segunda devuelve el valor del contador especificado (o undefined si dicho contador no está), así que basta con buscar el contador cuyo id es counterId usando la función auxiliar de lodash find y devolver el atributo value del contador que hemos encontrado.

Un último apunte

La principal ventaja de usar un store es que nos permite cambiar totalmente la forma en la que organizamos los datos internamente; mientras mantengamos su interfaz (acciones y selectores), todo funcionará con normalidad. De esta forma, es posible implementar rápidamente una solución quizás no muy optimizada pero funcional y, más adelante, según crezcan nuestras necesidades, adaptarlo para mejorar su eficiencia, velocidad, uso de memoria, etc.

Re-escribiendo los componentes de nuestra interfaz

Nuestra nueva aplicación, comparada con la versión que implementamos en la parte 2, permite añadir y eliminar múltiples contadores. Teniendo en cuenta que queremos conseguir algo tal que así:

Múltiples contadores con React y Redux
La aplicación que queremos construir hoy.

Está claro que (1) deberemos modificar cada contador para que incluya un botón de Delete y (2) la aplicación en sí deberá tener un botón para añadir nuevos contadores.

Para el primer paso, abrimos el fichero src/components/counter.js y añadimos el nuevo botón Delete y una nueva prop onDelete para que, cuando pulsemos en el botón, se borre el contador:

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

Para lo segundo, podemos crear un nuevo componente que se encargue de pintar la lista de contadores y que incluya un botón para añadir nuevos. Por ejemplo, yo te propongo crear un componente src/components/counter-list.js tal que así:

import Counter from './counter';
const CounterList = ( { addCounter, counterIds } ) => (
  <div>
    { counterIds.map( ( id ) => (
      <Counter key={ id } counterId={ id } />
    ) ) }
    <button onClick={ addCounter }>Add Counter</button>
  </div>
);
export default CounterList;

Este nuevo componente tiene varios puntos que merece la pena comentar:

  • Se trata de un componente que usa otro componente. Aún no habíamos visto ningún ejemplo de ello, así que aquí tienes el primero. Lo único que necesitamos para poder usar un componente en otro componente es importarlo con import.
  • El import que hemos hecho no tiene llaves (import Counter vs import { Counter }) porque el export que hemos hecho en counter.js es un default. Cuando un export es default, el import es sin llaves.
  • El componente CounterList recibe dos propiedades: addCounter es la acción que nos permite añadir nuevos contadores y counterIds es un array con los contadores que tenemos en este momento en nuestra aplicación.
  • Para pintar cada contador, usamos el método map de un array. De esta forma, somos capaces de convertir cada «identificador» en un componente Counter.
  • En el componente Counter usamos dos props nuevas de las que aún no hemos hablado: key y counterId. key es simplemente una prop que React nos pide que usemos por cuestiones de optimización cuando hacemos el map de un array. counterId es una prop que te explicaré un poco más adelante.
  • El componente Counter que estamos pintando no incluye ninguna de las props que se supone que espera: no hay value ni tampoco las acciones onIncrease, onDecrease u onDelete. Esto es algo que está relacionado con el hecho de haber usado counterId y en seguida verás por qué lo hemos hecho así.
  • Al igual que Counter, CounterList es un export default.

Re-escribiendo el fichero principal de nuestro plugin

Nuestra aplicación ya no va a mostrar un único contador, sino que tiene que mostrar una lista de contadores. Por lo tanto, debemos modificar el fichero principal index.js que creamos en la parte 2 para que, en lugar de hacer render de Counter, haga el render de CounterList. Además, también podemos limpiar la «chapucilla» que hicimos creando una variable local para mantener el estado de nuestro plugin, porque ahora estamos usando los stores de WordPress.

Teniendo en cuenta todo esto, el nuevo fichero principal es tan sencillo como:

// Import dependencies
import { render } from '@wordpress/element';
import './store';
import CounterList from './components/counter-list';
// Render component in DOM
const wrapper = document.getElementById( 'react-example-wrapper' );
render( <CounterList />, wrapper );

Como ves, simplemente cargamos el store y el componente CounterList y renderizamos este último (usando la función render del paquete @wordpress/element) en un nodo que tenemos en preparado en el DOM. ¿El resultado? Un único botón para añadir nuevos contadores y ningún contador a la vista.

Por desgracia, el botón aún no hace nada… y esto se debe a que no hemos conectado el componente con el store.

Cómo conectar componentes React a un store Redux

Si has conseguido llegar hasta aquí, felicidades, porque estás a punto de hacer historia. Ahora mismo tenemos todos los ingredientes preparados y lo único que nos queda es juntarlos para preparar un plato delicioso. Por un lado, tenemos un store con selectores para consultar el estado de nuestra aplicación y acciones para actualizarlo. Por otro, tenemos los componentes necesarios para visualizar dicho estado. Lo único que nos falta es hacer que los componentes se nutran de la información que hay en el store

Conectando CounterList con el store

CounterList es un sencillo componente que requiere dos propiedades. Por un lado, espera una lista con los identificadores de todos los componentes que tenemos activos en nuestra aplicación: counterIds. Por otro lado, espera también un callback con el que añadir nuevos contadores: addCounter.

Si echas un vistazo a los selectores y las acciones que definimos en nuestro store en la parte 3 del tutorial, verás que tenemos todos los ingredientes que necesitamos. Por un lado, tenemos el selector getCounterIds, el cual nos devuelve «una lista con los identificadores de todos los componentes que tenemos». Por otro lado, tenemos la acción addCounter que, dado un identificador, nos permite «añadir un nuevo contador».

El paquete @wordpress/data, que ya usamos para registrar un store, ofrece un par de «componentes de alto orden» con los que vitaminar un componente existente extrayendo props de un store: withSelect y withDispatch. En otras palabras, aplicando withSelect y/o withDispatch sobre nuestro componente, podemos pasarle las props que queramos con valores que extraemos directamente de un store.

Como las cosas se entienden mejor con un ejemplo, veamos cómo quedaría el código en el caso de CounterList:

import { withSelect, withDispatch } from '@wordpress/data';
import { v4 as uuid } from 'uuid';
import Counter from './counter';
const CounterList = ( { addCounter, counterIds } ) => (
  <div>
    { counterIds.map( ( id ) => (
      <Counter key={ id } counterId={ id } />
    ) ) }
    <button onClick={ addCounter }>Add Counter</button>
  </div>
);
const withCounterIds = withSelect( ( select ) => {
  const { getCounterIds } = select( 'react-example/counters' );
  return {
    counterIds: getCounterIds(),
  };
} );
const withCounterAdder = withDispatch( ( dispatch ) => {
  const { addCounter } = dispatch( 'react-example/counters' );
  return {
    addCounter: () => addCounter( uuid() ),
  };
} );
export default withCounterAdder( withCounterIds( CounterList ) );

Vamos a destripar este código punto por punto:

  • Lo primero que hacemos en la versión extendida de CounterList es importar las nuevas dependencias que necesitaremos. En concreto, ya te he dicho que vamos a necesitar withSelect y withDispatch para conectar el store con el componente, así que ahí que van con el import. Luego tenemos otra dependencia: el paquete uuid. Se trata de un paquete que nos permite generar identificadores únicos.
  • El componente en sí (CounterList) no ha cambiado: sigue asumiendo que le van a pasar las dos props que necesita para funcionar y genera el HTML que ya hemos visto antes.
  • A continuación, usamos la función withSelect. Explicado someramente, withSelect es una función que devuelve otra función que podemos aplicar a un componente para que le añada propiedades extra.
    • Para poder usar withSelect, debes pasarle una función como parámetro. En nuestro caso, hemos creado una función anónima.
    • La función anónima te da un parámetro, select, el cual te permite acceder a los selectores de los stores que tengas registrados.
    • Lo primero que hacemos es acceder al store que hemos creado: react-example/counters y, en concreto, nos quedamos con el selector getCounterIds.
    • El resultado de esta función anónima son las props con datos que queremos pasarle a CounterList. Como nos interesa pasarle una lista de identificadores llamada counterIds, devolemos un objeto con esa prop y cuyo valor obtenemos al invocar getCounterIds.
    • El resultado de withSelect lo guardamos en una variable llamada withCounterIds (aunque podríamos haberla llamado como quisiéramos).
    • withCounterIds es una función que podemos aplicar a un componente cualquiera así: withCounterIds(MiComponente). El resultado de hacer esto es que MiComponente pasará a tener una prop llamada counterIds cuyo valor se ha extraído del store react-example/counters.
  • Luego usamos la función withDispatch. Su funcionamiento es exactamente el mismo que withSelect, pero en lugar de darnos acceso a los selectores de un store, nos da acceso a las acciones.
    • withDispatch, igual que withSelect, espera una función como parámetro.
    • Esta función tiene como primer parámetro dispatch, quien nos da acceso a las acciones de los stores que tengamos disponibles.
    • Usando dispatch, obtenemos la acción addCounter de nuestro store: react-example/counters.
    • La función addCounter que espera nuestro componente es una función sin parámetros, pero la acción addCounter que tenemos en nuestro store sí espera un identificador único como parámetro. Por lo tanto, está claro que no podemos usar directamente la acción del store en nuestro componente… La solución es sencilla: basta con que el addCounter que pasaremos a nuestro componente sea una función anónima que internamente use la acción addCounter del store pasándole ese identificador único, el cual podemos generar al momento usando el paquete uuid.
    • withCounterAdder es otra función que podemos aplicar a un componente cualquiera (de forma análoga a withCounterIds) cuyo efecto es añadir a dicho componente la prop addCounter que acabamos de describir.
  • Finalmente, extendemos nuestro componente CounterList aplicándole, primero, la función withCounterIds para que CounterList tenga la prop counterIds con los valores que hay en el store y, al resultado de esto, le aplicamos withCounterAdder para que también incluya la prop addCounter.

Y, con esto, ¡ya puedes añadir nuevos contadores! Ahora solo nos queda conectar un contador cualquiera con el store

Conectando cada Counter con el store

Veamos, ahora, los cambios que tenemos que hacer en el componente Counter para conseguir conectarlo con nuestro store. Básicamente, lo único que tenemos que hacer es repetir el proceso que acabamos de ver para CounterList, pero usando los selectores y las acciones de react-example/counters que nos interesa.

De nuevo, vamos a empezar viendo el código y luego explicamos sus particularidades:

import { compose } from '@wordpress/compose';
import { withSelect, withDispatch } from '@wordpress/data';
const Counter = ( { value, onDelete, onIncrease, onDecrease } ) => (
  <div>
    <div>Counter: <strong>{ value }</strong></div>
    <button onClick={ onIncrease }>+</button>
    <button onClick={ onDecrease }>-</button>
    <button onClick={ onDelete }>Delete</button>
  </div>
);
const withValue = withSelect( ( select, { counterId } ) => {
  const { getCounterValue } = select( 'react-example/counters' );
  return {
    value: getCounterValue( counterId ),
  };
} );
const withActions = withDispatch( ( dispatch, { counterId, value } ) => {
  const {
    setCounterValue,
    removeCounter,
  } = dispatch( 'react-example/counters' );
  return {
    onIncrease: () => setCounterValue( counterId, value + 1 ),
    onDecrease: () => setCounterValue( counterId, value - 1 ),
    onDelete: () => removeCounter( counterId ),
  };
} );
export default compose( withValue, withActions )( Counter );
  • Como siempre, empezamos haciendo los import de rigor que vamos a usar en este componente.
  • El componente en sí, Counter, es el mismo que teníamos antes, con sus props: value, onDelete, onIncrease y onDecrease.
  • Lo primero que hacemos es usar withSelect para recuperar el valor del contador que tenemos en el store.
    • Nuestro store mantiene el estado de muchos contadores, así que tenemos que indicarle exactamente cuál es el contador que nos interesa usando su identificador. Por suerte, cuando CounterList pintaba cada uno de los Counter usando la función map, les pasaba una propiedad counterId, ¿recuerdas? Pues la función anónima que le pasamos a withSelect tiene como segundo parámetro las props del componente, con lo que tenemos acceso a counterId.
    • El cuerpo de esta función anónima es muy similar al ejemplo que hemos visto con CounterList. Simplemente accedemos al selector getCounterValue y devolvemos como resultado el valor concreto value que tiene el selector con identificador counterId.
  • Las acciones que necesita Counter las tenemos que obtener a través de withDispatch.
    • En nuestro store, sabemos que disponemos de las acciones setCounterValue y removeCounter.
    • Ambas acciones necesitan el identificador del contador a actualizar o borrar, el cual, tal y como acabamos de ver, tenemos disponible en las props de entrada que están en el segundo parámetro de la función anónima.
    • La acciónsetCounterValue necesita, además, conocer el nuevo valor a meterle al contador. Como las funciones de Counter son incrementar o decrementar en uno el valor actual, necesitamos conocer dicho valor. Por suerte, este lo acabamos de obtener con el withSelect y está en la prop llamada value.
    • El resultado de aplicar withDispatch son las tres funciones que espera nuestro componente: onIncrease es una función anónima que suma 1 al value actual de counterId; onDecrease es otra función anónima que hace lo mismo, pero restando 1; y onDelete es una función que usa la acción removeCounter con el identificador que toca.
  • Finalmente, aplicamos ambas funciones withValue y withActions a nuestro componente Counter. No obstante, esta vez, en lugar de primero aplicar una función y luego otra sobre el resultado de la primera, he decidido usar la función auxiliar compose del paquete @wordpress/compose, ya que permite escribir un código un poco más inteligible.

¡Y esto es todo! Ya deberías tener la aplicación funcionando correctamente 🙂

En resumen

A lo largo de esta pequeña introducción a React/Redux en WordPress hemos visto todos los ingredientes necesarios para crear buenas interfaces de usuario usando esta tecnología. En esencia, hemos visto que los componentes deben ser simples funciones que reciben propiedades y generan HTML, hemos visto cómo podemos usar los stores para mantener el estado de la aplicación de forma independiente a los componentes de la UI y hemos visto cómo podemos conectar ambas partes.

Conseguir que un componente obtenga los valores que necesita de un store o permitir que un componente pueda modificar el estado de un store es tan fácil como conectarlo al mismo usando las funciones withSelect y withDispatch del paquete @wordpress/data.

Imagen destacada de Dmitry Nucky Thompson en Unsplash.

Deja una respuesta

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

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

Tus datos personales se almacenarán en SiteGround y serán usados por Nelio Software con el único objetivo de publicar tu comentario aquí. Con el envío de este comentario, nos das el consentimiento expreso para ello. Escríbenos para acceder, rectificar, limitar o eliminar tus datos personales.