Tutorial de introducción a Redux

React

En este tutorial aprenderás a usar React con Redux creando una aplicación desde cero. Aprenderás qué es una store y qué son las acciones y los reducers, además de ver cómo se usan las funciones dispatch o connect. Aprenderás todo esto creando una aplicación real que servirá como base para crear muchas otras aplicaciones. La aplicación realizará peticiones asíncronas a una API.

Este tutorial está destinado a aquellas personas que ya tienen ciertos conocimientos de React pero que todavía no se han adentrado en el uso de Redux, una librería que se ha convertido casi en imprescindible cuando se desarrollan aplicaciones de mediano y gran tamaño.

Introducción

Vamos a crear una pequeña aplicación que nos permita obtener posts desde una API y mostrarlos por pantalla, creando así un blog que también incluya su sección de comentarios. Para ello usaremos Redux.

Además, en la segunda parte de este tutorial también usaremos la librería Redux Toolkit (RTK), que son una serie de herramientas oficiales de Redux. Primero crearemos la aplicación usando sencillamente Redux y luego la modificaremos usando Redux Toolkit.

Antes de continuar con este tutorial se recomienda que tengas conocimientos previos de desarrollo web y de JavaScript en su versión ES6, además de unos conocimientos mínimos de React. Si no los tienes, consulta las siguientes guías y tutoriales antes de continuar:

En el tutorial de introducción a React se explican muchos conceptos, como las propiedades o el estado de los componentes, aunque no se cubre todo. Por ello, también se recomienda que conozcas qué son las hooks y cómo crear rutas con React Router. Los siguientes tutoriales podrían serte de ayuda en caso de que nunca hayas usado estos componentes:

Obtendremos los datos desde una API REST usando Redux Thunk. En concreto, obtendremos los datos desde la API de demostración JSON Placeholder. Si nunca has usado una API, también puedes consultar el tutorial en donde explico cómo conectarte a una API con JavaScript o también otro el que explico qué es y cómo se usa la API Fetch de JavaScript.

A continuación puedes ver tanto la aplicación en funcionamiento como el código de la aplicación que vamos a crear usando Redux:

Si te atascas, no dudes en consultar el código en los enlaces anteriores, ya que puede que veas todo más claro.

Qué es Redux

Redux es una librería de JavaScript que se usa para gestionar el estado de las aplicaciones a través de un contenedor de estados. En React, habitualmente, gestionas el estado a nivel de componente e intercambias los valores de dicho estado con otros componentes a través de las propiedades. Cuando usas Redux, el estado de la aplicación es gestionado por un único objeto inmutable al que se conectan los componente. Con cada actualización del estado de Redux, se genera una copia del estado anterior junto con los cambios que se han realizado.

Mediante Redux podrás gestionar el estado global de tu aplicación, pudiendo obtener o modificar el estado de cualquier componente conectado con Redux. Además, cuando usas Redux, el desarrollo y la depuración de las aplicaciones es más sencillo cuando usas las DevTools de Redux, ya que siempre podrás consultar los estados anteriores de la aplicación.

Sin embargo, Redux también conlleva ciertos problemas, al igual que todo. Por ejemplo, el código de tu aplicación crecerá bastante, ya que necesitas configurar Redux por componente. Por ello, no se recomiendo usar Redux en aplicaciones pequeñas, siendo en este último caso más recomendable usar la API contextual de React. Si no sabes lo que es la API contextual, puedes consultar el siguiente tutorial, en el que explico qué es la Context API de React y cómo se utiliza, aunque no la necesitarás para seguir este tutorial.

Una buena opción consiste en usar la Context API en aplicaciones que no son demasiado grandes y migrar los componentes a Redux cuando la aplicación crezca. Sin embargo, el uso de Redux Toolkit soluciona en gran parte este problema de Redux.

Redux fue creado por Dan Abramov y por Andrew Clark. No es una librería específica de React, aunque suelen ir de la mano. También existen otras librerías de gestión de estado similares a Redux para otros frameworks como por ejemplo Vuex, que se integra con Vue.

Entorno de desarrollo

Antes de comenzar, se recomienda que tengas la extensión Redux DevTools instalada en tu navegador. Está disponible tanto para Chrome como para Firefox y puedes encontrarla en los siguientes enlaces:

Creación del proyecto

Para crear el proyecto vamos a usar la utilidad create-react-app. Para ello usa el siguiente comando:

npx create-react-app tutorial-redux

La aplicación se creará en el directorio /tutorial-redux, así que accede a este directorio mediante el comando cd:

cd redux-tutorial

Vamos a instalar los siguientes paquetes mediante el comando que verás más adelante:

  • Redux: Librería básica de Redux.
  • React Redux: Redux no ha sido creado para ser usado exclusivamente con React, de ahí que instalemos React Redux, que son los conectores que unirán React con Redux.
  • Redux Thunk: Middleware asíncrono para Redux.
  • Redux DevTools: Sirve para depurar aplicaciones que usan Redux.

Además, también instalaremos React Router mediante el paquete react-router-dom. Este paquete agrega diferentes componentes que permiten usar varias rutas del navegador en aplicaciones React. Si nunca has utilizado React Router, puedes consultar el tutorial de React Router.

Para instalar los paquetes, usa uno de los siguientes comandos, en función de si vas a usar npm o Yarn:

# Si usas npm
npm i redux react-redux redux-thunk redux-devtools-extension react-router-dom

# Si usas Yarn
yarn add redux react-redux redux-thunk redux-devtools-extension react-router-dom

Seguidamente debes eliminar los archivos de demostración que se incluyen por defecto con React. Para completar esta tarea desplázate al directorio /src:

cd src

Luego borra todos los archivos del directorio:

rm *

Seguidamente crea los directorios /acciones, /reducers, /paginas y /componentes en el interior del directorio /src:

mkdir acciones reducers paginas componentes

Luego crea los archivos index.js, App.js e index.css, que inicialmente estarán vacíos:

touch index.js index.css App.js

Dado que no es el objetivo de este tutorial el de aprender CSS, nos limitaremos a usar Bootstrap, así que edita el archivo index.css, copia el contenido del framework desde aquí, pégalo en el interior del archivo index.css y guárdalo. Si nunca has usado Boostrap, puedes consultar el tutorial de introducción a Bootstrap, aunque no es imprescindible, ya que solamente usaremos unas cuantas clases CSS del framework.

Cómo usar Redux

Antes de comenzar con el tutorial, vamos a explicar brevemente usa serie de conceptos de Redux. No te preocupes si no los entiendes, ya que luego veremos todo con más detalle. La idea es que tengas al menos una idea del funcionamiento de Redux y de sus componentes.

Stores en Redux

El estado de las aplicaciones Redux se guarda en una store que tendrás que crear. Las stores se inicializan con un reducer, que también tendremos que crear. Tras crear una store tendrás que pasársela a un <Provider> si quieres usarla con React. El Provider es un componente que incluirá en su interior toda la aplicación, de modo que cualquier componente pueda tener acceso a Redux.

Este podría se un ejemplo de creación de una store, en donde importamos la función createStore, el reducer que pasamos a dicha función y el Provider en el que incluiremos la aplicación:

import {createStore} from 'redux';
import {Provider} from 'react-redux';
import reducer from './reducers';

const store = createStore(reducer);

render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root'),
);

Acciones de Redux

Las acciones envían datos desde tu aplicación al almacén o store de Redux. A nivel de estructura, una acción es un objeto con dos propiedades:

  • type: Una cadena que suele estar en mayúsculas, asignada a una constante, que describirá la acción a realizar.
  • payload: Los datos que se se pasan a la acción, que por ejemplo, puede ser desde un id hasta los datos de un objeto que queremos crear o editar.

A continuación puedes ver un ejemplo en el que definimos una acción:

const BORRAR_TAREA = 'BORRAR_TAREA';

{
  type: BORRAR_TAREA,
  payload: id,
}

Además, en Redux también existen los action creators. Un creador de acción o action creator es sencillamente una función que devuelve una acción:

const borrarTarea = (id) => ({type: BORRAR_TAREA, payload: id});

Reducers en Redux

Un reducer es una función que acepta dos parámetros, que son un estado y una acción. Un reducer es inmutable y siempre devuelve una copia del estado. Suele incluir una sentencia switch que abarca todos los tipos de acciones:

const estadoInicial = {
  tareas: [
    {id: 1, text: 'Programar'},
    {id: 2, text: 'Comer'},
    {id: 3, text: 'Dormir'},
  ],
  errores: false,
  cargando: false
}

function tareaReducer(estado = estadoInicial, accion) {
  switch (accion.type) {
    case BORRAR_TAREA:
      return {
        ...estado,
        tareas: estado.tareas.filter((tarea) => tarea.id !== accion.payload),
      }
    default:
      return estado;
  }
}

Dicho de otro modo, los componentes de tu aplicación iniciarán acciones que realizarán ciertas tareas, mientras que los reducers se encargarán de modificar el estado de Redux. Sería entonces interesante que las propiedades de los componentes de React estuviesen conectadas al estado de Redux, actualizando su valor automáticamente, que es lo que veremos a continuación.

Componentes en Redux

Antes de usar un componente con Redux tendremos que conectarlo mediante la función connect() de React Redux, que permite conectar componentes React con Redux. Esta función permite mapear las propiedades de un componente con el estado de Redux. A los componentes de React que están conectados con Redux se les suele denominar containers.

Habitualmente, cuando quieras actualizar el estado de Redux en un componente usarás el método dispatch(), disponible en la store de Redux. Este método acepta un objeto como parámetro, que suele ser devuelto por un action creator:

const Componente = ({dispatch}) => {
  useEffect(() => {
    dispatch(borrarTarea())
  }, [dispatch]);
}

Crea la Store de Redux

Es hora de comenzar a a desarrollar el proyecto. Vamos a editar el archivo index.js, al que agregaremos todo lo que necesitarás en un proyecto real que use Redux. Vamos a importar los siguientes elementos:

  • createStore: Una función que nos permitirá crear la store de Redux, que es el componente encargado de mantener el estado de Redux en su interior.
  • applyMiddleware: Una función que nos permitirá usar middleware con Redux.
  • Provider: Nos permitirá incluir el componente App en su interior, de modo que podamos usar Redux con cualquier componente de la aplicación.
  • thunk: Un middleware que nos permitirá crear acciones asíncronas en Redux. Se lo pasaremos como parámetro a la función applyMiddleware.

Además también usaremos la función composeWithDevTools para poder inspeccionar los estados de Redux, conectando la aplicación con la extensión Redux DevTools.

Este sería el contenido del archivo index.js, en donde importamos los elementos anteriores, el componente App y el conjunto de reducers, que todavía no hemos creado. Luego usamos la función createStore, a la que pasamos el conjunto de reducers que hemos importado con el objetivo de crear la store, importados como rootReducer. Seguidamente usamos la función applyMiddleware para configurar el middleware thunk y pasamos el resultado a la función composeWithDevTools.

Finalmente, agregamos el Provider, que aceptará la store que hemos creado como propiedad, incluyendo el componente App en su interior:

import React from 'react';
import {render} from 'react-dom';
import {createStore, applyMiddleware} from 'redux';
import {Provider} from 'react-redux';
import thunk from 'redux-thunk';
import {composeWithDevTools} from 'redux-devtools-extension';

import App from './App';
import rootReducer from './reducers';

import './index.css';

const store = createStore(rootReducer, composeWithDevTools(applyMiddleware(thunk)));

render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root'),
);

Luego edita el archivo App.js y agrega el siguiente código en su interior, en donde nos limitamos a devolver un sencillo Me gusta el olor a napalm por la mañana! (enlace motivacional):

import React from 'react';

const App = () => {
  return <div>Me gusta el olor a napalm por la mañana!</div>;
}

export default App;

Crea los Reducers de Redux

Ahora vamos a crear los reducers de la aplicación, que son funciones puras que determinarán los cambios a realizar en el estado de Redux. Esta función devolverá una copia del estado que incluirá el cambio realizado.

Podemos agregar todos los reducers que queramos y combinarlos en un único reducer que podrá usar la store, al que se le suele denominar rootReducer. Para combinar los reducers se usa la función combineReducers. Al combinarlos se facilita la organización del código.

Dado que vamos a crear un blog, necesitamos un reducer que gestione los posts, al que llamaremos postsReducer. Necesitaremos otro que gestione los datos de un post individual, al que llamaremos postReducer. Finalmente, necesitamos otro que gestione los comentarios de un post, al que llamaremos comentariosReducer. Luego podríamos combinarlos con otros reducers como por ejemplo uno que se encargue de los usuarios, al que podríamos llamar usersReducer, aunque en este caso no habrá usuarios en el blog.

Puedes escoger el nombre que quieras para los reducers aunque, dado que cada uno gestiona las peticiones a un endpoint de la API que usaremos, tiene sentido usar un nombre similar al del endpoint.

Agregaremos los reducers en la carpeta /reduders, que debes crear en el directorio /src si todavía no lo has hecho. Luego crea el archivo index.js en la carpeta /reducers e incluye este código en su interior:

import {combineReducers} from 'redux';

import postsReducer from './postsReducer';
import postReducer from './postReducer';
import comentariosReducer from './comentariosReducer';

const rootReducer = combineReducers({
  posts: postsReducer,
  post: postReducer,
  comentarios: comentariosReducer,
});

export default rootReducer;

Como ves, hemos importado los reducers postsReducer, postReducer y comentariosReducer, pero todavía no los hemos creado. Vamos a ello.

Reducer postsReducer

Vamos a crear el reducer postsReducer en el archivo postsReducer.js, en el directorio /reducers. El reducer tendrá un objeto de estado inicial estadoInicial, que constará del estado posts, usado para almacenar los posts del blog. También constará de los estados cargando y errores. Dado que todavía no hemos agregado acciones, nos limitaremos a devolver el objeto de estado sin cambio alguno:

export const estadoInicial = {
    posts: [],
    errores: false,
    cargando: false,
  }
  
  export default function postsReducer(estado = estadoInicial, accion) {
    switch (accion.type) {
      default:
        return estado;
    }
  }

Reducer postReducer

Crea el archivo postReducer.js, en el directorio /reducers. El estado inicial de este recuder contendrá un estado post con el post que se seleccionemos de la lista de posts, aunque inicialmente contendrá un objeto vacío. También contendrá una variable de estado cargando para indicarnos si se están cargando los datos y otra para indicarnos si se ha producido algún error. Por ahora todavía no agregaremos acciones:

export const estadoInicial = {
  post: {},
  errores: false,
  cargando: true,
}
  
export default function postReducer(estado = estadoInicial, accion) {
  switch (accion.type) {
    default:
      return estado;
  }
}

Reducer comentariosReducer

Finalmente, tendremos otro reducer para los comentarios, por lo que también debes crear el archivo comentariosReducer.js en el directorio /reducers. Este reducer contendrá la lista de comentarios y también un varaible de estado que indicará si se están cargando los comentarios o si se ha producido algún error. Por ahora tampoco agregaremos acciones:

export const estadoInicial = {
  comentarios: [],
  cargando: false,
  errores: false,
}

export default function comentariosReducer(estado = estadoInicial, accion) {
  switch (accion.type) {
    default:
      return estado;
  }
}

Usa las Redux DevTools

Si ahora accedes a la aplicación desde tu navegador, debería cargar correctamente, mostrándose Me gusta el olor a napalm por la mañana! por pantalla.

Una vez estés observando la aplicación, podrás acceder a las DevTools de Redux desde las herramientas para desarrolladores de Chrome o de Firefox. Podrás encontrarlas e una pestaña llamada Redux. Si haces clic en la sección State, podrás ver todo el estado de la aplicación:

Gracias a estas herramientas podrás depurar el código con más facilidad, especialmente cuando dispones de varios reducers o de muchas acciones.

Crea las acciones de Redux

Hemos creado tres reducers, pero por ahora, nuestros reducers devuelve el estado original sin modificar, ya que todavía no hemos agregado acciones. En cuanto las agreguemos entenderás mejor la utilidad de Redux. Agregaremos las acciones en un directorio llamado /acciones que crearemos en el directorio /src.

Acciones postsActions

Necesitamos obtener los posts desde una API y luego almacenarlos en el estado de Redux. La obtención de los posts implica una acción asíncrona, por lo que tendremos que usar Redux Thunk, que ya hemos configurado en la store, en el archivo index.js. Crea el archivo postsActions.js, en donde agregaremos las acciones que hagan referencia a los posts.

En general, necesitamos una acción que le diga a Redux que queremos obtener los posts desde la API, otra que, en caso de éxito, pase las acciones al estado de Redux y finalmente otra que informe a Redux de algún posible de error en caso de que ocurra.

En al parte superior del archivo agregaremos los tipos de acción como una serie de constantes que contendrán el nombre de la acción como una cadena de texto. En realidad podrías ahorrarte la declaración de las constantes, pero se ha convertido en un estándar, ya que de este modo podrás exportar las acciones y evitar errores tipográficos cuando hagas referencia a ellas. Estas constantes suelen declararse con mayúsculas y con las palabras separadas por guiones bajos, en Snake Case:

export const GET_POSTS = 'GET_POSTS';
export const GET_POSTS_EXITO = 'GET_POSTS_EXITO';
export const GET_POSTS_ERROR = 'GET_POSTS_ERROR';

Seguidamente, debemos agregar los action creators, que devolverán una acción compuesta por un tipo y, opcionalmente, un payload:

// Acción que pasa los posts obtenidos a Redux en caso de éxito
export const accionGetPostsExito = (posts) => ({
  type: GET_POSTS_EXITO,
  payload: posts,
});
  
// Acción que indica a Redux que ha ocurrido un error al obtener los posts
export const accionGetPostsError = () => ({
  type: GET_POSTS_ERROR,
});

Finalmente debemos crear una funcion o acción que englobe a las acciones anteriores. Cuando la llamemos, se invocará a la acción GET_POSTS mediante el action creator gestPosts() para informar a Redux de que debe realizar una petición a la API. Luego, en el interior de un try/catch obtendremos los datos y lanzaremos o bien la acción de éxito GET_POSTS_EXITO mediante el action creator getPostsExito() o bien la acción de error GET_POSTS_ERROR mediante el action creator getPostsError().

Para invocar a los action creators usaremos el método dispatch(), que es un método disponible en la store que acepta un objeto como parámetro. Este método se usa para actualizar el estado de Redux y suele ser el resultado de invocar a un action creator, como en el siguiente ejemplo:

const Component = ({dispatch}) => {
  useEffect(() => {
    dispatch(borrarTarea())
  }, [dispatch]);
}

Volviendo a la aplicación, combina los actions creators mediante la siguiente función asíncrona, que usa async/await para obtener los datos de la API:

// Combina los actions creators
export function getPosts() {
  return async (dispatch) => {

    dispatch(accionGetPosts());
  
    try {
      const response = await fetch('https://jsonplaceholder.typicode.com/posts');
      const data = await response.json();
  
      dispatch(accionGetPostsExito(data));
    } catch (error) {
      console.log(error);
      dispatch(accionGetPostsError());
    }
  }
}

Si quieres saber más cosas acerca del uso de async/await, consulta el siguiente tutorial, en donde explico cómo usar async/await en JavaScript.

Acciones postActions

Vamos a obtener un post individual desde la API y lo almacenaremos en el estado de Redux. Crea el archivo postActions.js y agrega el siguiente código:

export const GET_POST = 'GET_POST';
export const GET_POST_EXITO = 'GET_POST_EXITO';
export const GET_POST_ERROR = 'GET_POST_ERROR';

// Acción que indica a Redux que obtenga un post
export const accionGetPost = () => ({
  type: GET_POST,
});
    
// Acción que pasa el post obtenido a Redux en caso de éxito
export const accionGetPostExito = (post) => ({
  type: GET_POST_EXITO,
  payload: post,
});

// Acción que indica a Redux que ha ocurrido un error al obtener el post
export const accionGetPostError = () => ({
  type: GET_POST_ERROR,
});

export function getPost(postId) {
  return async dispatch => {

    dispatch(accionGetPost());

    try {
      const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${postId}`);
      const data = await response.json();

      dispatch(accionGetPostExito(data));
    } catch (error) {
      console.log(error);
      dispatch(accionGetPostError());
    }
  }
}

El código es muy simular al de las acciones que hemos creado para la lista de posts. La única diferencia es que la función que se encarga de obtener el post desde la API ahora recibe el postId del post como argumento. Además, almacenaremos un único post, que es el que obtenemos.

Acciones commentsActions

Vamos a obtener los comentarios de un post y a almacenarlos en el estado de Redux. Para ello, crea el archivo comentariosActions.js y agrega el siguiente código, que no difiere demasiado del que hemos usado en otros grupos de acciones:

export const GET_COMENTARIOS = 'GET_COMENTARIOS ';
export const GET_COMENTARIOS_EXITO = 'GET_COMENTARIOS_EXITO';
export const GET_COMENTARIOS_ERROR = 'GET_COMENTARIOS_ERROR';

// Acción que indica a Redux que obtenga los comentarios
export const accionGetComentarios = () => ({
  type: GET_COMENTARIOS,
});
    
// Acción que pasa los comentarios obtenidos a Redux en caso de éxito
export const accionGetComentariosExito = (comentarios) => ({
  type: GET_COMENTARIOS_EXITO,
  payload: comentarios,
});
    
// Acción que indica a Redux que ha ocurrido un error al obtener los comentarios
export const accionGetComentariosError = () => ({
  type: GET_COMENTARIOS_ERROR,
});

export function getComentarios(postId) {
  return async dispatch => {

    dispatch(accionGetComentarios());

    try {
      const response = await fetch(`https://jsonplaceholder.typicode.com/comments?postId=${postId}`);
      const data = await response.json();

      dispatch(accionGetComentariosExito(data));
    } catch (error) {
      console.log(error);
      dispatch(accionGetComentariosError());
    }
  }
}

Usa las acciones de Redux

Ahora que que hemos creado las acciones, vamos a editar de nuevo los reducers que hemos creado anteriormente para agregarlas.

Reducer postsReducer

Edita el archivo postsReducer.js que habíamos dejado pendiente, en donde habíamos definido la siguiente función que incluía un switch:

export default function postsReducer(estado = estadoInicial, accion) {
    switch (accion.type) {
      default:
        return estado;
    }
  }

Ahora ya disponemos de acciones, por lo que las vamos a importar en la parte superior del archivo postsReducer.js:

import * as acciones from '../acciones/postsActions';

Ahora agregaremos una sentencia case por cada acción, devolviendo el estado al completo junto con los cambios que se hayan realizado. Para la acciónn GET_POSTS sencillamente estableceremos el valor de cargando como true, indicando que se iniciará una llamada a la API, evitando así saturar el servidor con llamadas adicionales mientras una está en proceso:

export default function postsReducer(estado = estadoInicial, accion) {
  switch (accion.type) {
    case acciones.GET_POSTS:
      return { ...estado, cargando: true };
    default:
      return estado;
  }
}

Del mismo modo, mediante la acción GET_POSTS_EXITO estableceremos el estado de cargando como false, ya que hemos obtenidos los posts con éxito, que tendremos que devolver en la variable de estado posts. Mediante la acción GET_POSTS_ERROR estableceremos el estado de cargando como false y el valor de errores como true.

Este sería el código completo del archivo postsReducer.js con lo que hemos hecho hasta ahora:

import * as acciones from '../acciones/postsActions';

export const estadoInicial = {
  posts: [],
  errores: false,
  cargando: false,
}
  
export default function postsReducer(estado = estadoInicial, accion) {
  switch (accion.type) {
    case acciones.GET_POSTS:
      return { ...estado, cargando: true };
    case acciones.GET_POSTS_EXITO:
      return {posts: accion.payload, cargando: false, errores: false}
    case acciones.GET_POSTS_ERROR:
      return {...estado, cargando: false, errores: true}
    default:
      return estado;
  }
}

Y con esto ya hemos finalizado el reducer, por lo que ya solo nos faltaría conectar los componentes que vayan a usar las acciones correspondientes de Redux, que en este caso es el componente PaginaPosts.

Reducer postReducer

Edita el archivo postReducer.js que habíamos dejado pendiente, importa las acciones del archivo /acciones/postActions.js en la parte superior del archivo y agrega las acciones usadas para obtener un post a la sentencia switch:

import * as acciones from '../acciones/postActions';

export const estadoInicial = {
  post: {},
  errores: false,
  cargando: true,
}
  
export default function postReducer(estado = estadoInicial, accion) {
  switch (accion.type) {
    case acciones.GET_POST:
      return { ...estado, cargando: true };
    case acciones.GET_POST_EXITO:
      return { post: accion.payload, cargando: false, errores: false };
    case acciones.GET_POST_ERROR:
      return { ...estado, cargando: false, errores: true };
    default:
      return estado;
  }
}

Usamos la acción GET_POST para indicar que se va a iniciar la obtención de un post desde la API, estabelciendo el valor de cargando a true. Mediante la acción GET_POST_EXITO establecemos el estado de cargando como false, ya que hemos obtenidos el posts con éxito, que devolvemos en la variable de estado post. Mediante la acción GET_POST_ERROR establecemos el estado de cargando como false y el valor de errores como true.

Ya no solo nos faltaría conectar el componente PaginaPost, pero todavía tenemos que terminar otro reducer.

Reducer comentariosReducer

Edita el archivo comentariosReducer.js que habíamos creado anteriormente e importa las acciones del archivo /acciones/comentariosActions.js en la parte superior del archivo. Luego agrega las acciones usadas para obtener los comentarios de un post a la sentencia switch:

import * as acciones from '../acciones/comentariosActions';

export const estadoInicial = {
  errores: false,
  cargando: true,
  comentarios: [],
}

export default function comentariosReducer(estado = estadoInicial, accion) {
  switch (accion.type) {
    case acciones.GET_COMENTARIOS:
      return { ...estado, cargando: true };
    case acciones.GET_COMENTARIOS_EXITO:
      return { ...estado, comentarios: accion.payload, cargando: false, errores: false };
    case acciones.GET_COMENTARIOS_ERROR:
      return { ...estado, cargando: false, errores: true };
    default:
      return estado;
  }
}

De un modo similar a los otros casos, hemos usado la acción GET_COMENTARIOS para iniciar la obtención de un post desde la API, estabelciendo el valor de cargando a true. Mediante la acción GET_COMENTARIOS_EXITO establecemos el estado de cargando como false, una vez obtenidos comentarios con éxito, que devolvemos en la variable de estado comentarios. Finalmente, usamos la acción GET_COMENTARIOS_ERROR, estableciendo el estado de cargando como false y el valor de errores como true, ya que algo no ha ido como se esperaba.

En este caso, el componente que conectaremos con Redux será PaginaPost, que será en donde se mostrarán los comentarios.

Configuración de las rutas

La aplicación que estamos creando utiliza React Router, de modo que podamos tener varias rutas diferenciadas en el navegador. Necesitamos una ruta que usaremos a modo página de inicio, otra que liste los posts del blog y otra que muestre cada post individual.

En caso de que todavía no lo hayas hecho, comienza creando el directorio /paginas en el interior del directorio /src. En su interior crea los archivos PaginaInicio.js, PaginaPosts.js y PaginaPost.js, que por ahora estarán vacíos.

Edita el archivo App.js e importa los componentes Router, Switch, Route, Redirect de react-router-dom. Luego importa las páginas de los archivos PaginaInicio.js, PaginaPosts.js y PaginaPost.js. Finalmente agrega un bloque Switch en el interior del Router con las rutas, tal y como ves a continuación:

import React from 'react';
import {BrowserRouter as Router, Switch, Route, Redirect} from 'react-router-dom';

import PaginaInicio from './paginas/PaginaInicio';
import PaginaPosts from './paginas/PaginaPosts';
import PaginaPost from './paginas/PaginaPost';

const App = () => {
  return (
    <Router>
      <Switch>
        <Route exact path="/" component={PaginaInicio} />
        <Route exact path="/posts" component={PaginaPosts} />
        <Route exact path="/posts/:postId" component={PaginaPost} />
        <Redirect to="/" />
      </Switch>
    </Router>
  )
}

export default App;

Y con esto ya tendremos las rutas creadas, aunque todavía debemos crear los componentes a los que hemos hecho referencia.

Creación de los componentes

Vamos a crear algunos componentes que usaremos en las páginas de la aplicación. Necesitamos un menú para la aplicación y también un componente que muestre cada uno de los posts de obtenemos desde la API.

Componente Menu

Vamos a agregar un pequeño menú a la aplicación. Para ello, crea el directorio /componentes en la carpeta /src. En su interior crea el archivo Menu.js y agrega el siguiente contenido, que no es otra cosa que un componente que muestra un sencillo menú usando los estilos de Bootstrap:

import React from 'react';
import { Link } from 'react-router-dom';

export const Menu = () => (
  <nav className="navbar navbar-expand-lg navbar-dark bg-dark mb-5">
    <div className="container">
      <ul className="navbar-nav">
        <li className="nav-item ">
          <Link className="nav-link" to="/">Inicio</Link>
        </li>
        <li className="nav-item">
          <Link className="nav-link" to="/posts">Posts</Link>
        </li>
      </ul>
    </div>
  </nav>
);

Luego importa el componente Menu en el archivo App.js y muéstralo justo antres de la etiqueta Switch, de modo que el menú se muestre en todas las rutas:

import React from 'react';
import {BrowserRouter as Router, Switch, Route, Redirect} from 'react-router-dom';

import { Menu } from './componentes/Menu';

import PaginaInicio from './paginas/PaginaInicio';
import PaginaPosts from './paginas/PaginaPosts';
import PaginaPost from './paginas/PaginaPost';

const App = () => {
  return (
    <Router>
      <Menu />
      <Switch>
        <Route exact path="/" component={PaginaInicio} />
        <Route exact path="/posts" component={PaginaPosts} />
        <Route exact path="/posts/:postId" component={PaginaPost} />
        <Redirect to="/" />
      </Switch>
    </Router>
  )
}

export default App;

Componente Post

Necesitamos un componente que muestre cada uno de los posts en la ruta /posts/:postId. Ubicaremos este archivo en el directorio /componentes. Este componente contendrá el título y el contenido de cada post.

Además, este componente también se encargará de mostrar los posts en la lista de posts del blog, usando para ello una propiedad a la que llamaremos resumen. Si el valor de resumen es true, mostraremos un resumen del post o, de lo contrario, mostraremos el post completo:

import React from 'react';
import {Link} from 'react-router-dom';

export const Post = ({post, resumen = false}) => (
  <article>
    {resumen ? <h2>{post.title}</h2> : <h1>{post.title}</h1>}
    <hr/>
    <p>{resumen ? post.body.substring(0, 100) : post.body}</p>

    {resumen && (
      <Link to={`/posts/${post.id}`} className="btn btn-primary">
        View Post
      </Link>
    )}
    <hr/>
  </article>
);

Componente Comentario

Vamos a crear también un componente que muestre los comentarios de cada post. Los comentarios que obtendremos de la API se componen de un título, una dirección de email y del cuerpo del mensaje. Crea el archivo Comentario.js en el directorio /componentes y agrega este código en su interior:

import React from 'react';

export const Comentario = ({ comentario }) => (
  <div className="d-flex text-muted pt-3">
    <p className="pb-3 mb-0 small lh-sm border-bottom">
        <strong className="d-block text-gray-dark">{comentario.title}</strong>
        <strong className="d-block text-gray-dark">{comentario.email}</strong>
        {comentario.body}
    </p>
  </div>
);

Creación de las páginas

Vamos a crear cada uno de los componentes que hemos usado en el router del archivo App.js. Dado que estos componentes representan las páginas de la aplicación, los ubicaremos en el directorio /paginas.

Página PaginaInicio

Vamos a crear primero la página de inicio, que estará en el archivo /paginas/PaginaInicio.js. Constará de un sencillo enlace al blog de la página, que está en la ruta /posts, por lo que no es necesario dar demasiadas explicaciones:

import React from 'react';
import {Link} from 'react-router-dom';

const PaginaInicio = () => (
  <div className="container">
    <div className="row">
      <div className="col-md-12">
        <h1>Inicio</h1>
        <p>Esta es la página de inicio!</p>

        <Link to="/posts" className="btn btn-primary">
          Ver Posts
        </Link>
      </div>
    </div>
  </div>
);

export default PaginaInicio;

Página PaginaPosts

Vamos a crear el archivo /paginas/PaginaPosts.js, en donde agregaremos nuestro componente. Primero agregaremos su estructura básica y luego veremos cómo conectar el componente con Redux.

Edita el archivo /paginas/PaginaPosts.js, que por ahora solamente contendrá el siguiente código:

import React from 'react';

const PaginaPosts = () => {
  return (
    <div className="container">
      <h1 className="mb-4">Posts</h1>
    </div>
  );
}

export default PaginaPosts;

Hemos llegado a la parte más importante del tutorial, en la que conectaremos el componente con Redux. Para ello usaremos la función connect de react-redux. La función connect es un modo de conectar React a Redux. Suele hacerse referencia a los componentes conectados como containers.

La función connect conectará la store de Redux con un componente de React. Le pasaremos un parámetro llamado mapStateToProps, que usará cualquier estado de la store de Redux y lo pasará a las propiedades del componente React. Puedes ver todos los detalles de la función connect en este enlace.

En este caso usaremos las propiedades posts, errores y cargando, que mapearemos con el estado definido en el reducer postsReducer de Redux mediente la función mapStateToProps, que pasaremos a la función connect:

import React from 'react';
import {connect} from 'react-redux';

const PaginaPosts = ({posts, cargando, errores}) => {
  return (
    <div className="container">
      <h1 className="mb-4">Posts</h1>
    </div>
  );
}

// Mapeo del estado de Redux con las propieades del componente
const mapStateToProps = (state) => ({
  posts: state.posts.posts,
  errores: state.posts.errores,
  cargando: state.posts.cargando,
});

// Conexión de React con redux
export default connect(mapStateToProps)(PaginaPosts);

Seguidamente, incluiremos la función asíncrona getPosts desde el archivo postsActions.js, que incluye las acciones que nos permiten obtener los posts, devolverlos o devolver un error. Mediante la función useEffect, que importaremos desde react, ejecutaremos la función getPosts cuando se monte el componente. A su vez, la función getPosts llamará a las diferentes acciones disponibles. La función dispatch estará disponible en todos los componentes conectados con Redux:

import React, {useEffect} from 'react';
import {connect} from 'react-redux';

// Importamos la acción getPosts
import {getPosts} from '../acciones/postsActions';

const PaginaPosts = ({dispatch, posts, cargando, errores}) => {

  useEffect(() => {
    dispatch(getPosts());
  }, [dispatch]);

  return (
    <div className="container">
      <h1 className="mb-4">Posts</h1>
    </div>
  );
}

// Mapeo del estado de Redux con las propieades del componente
const mapStateToProps = (state) => ({
  posts: state.posts.posts,
  errores: state.posts.errores,
  cargando: state.posts.cargando,
});

// Conexión de React con redux
export default connect(mapStateToProps)(PaginaPosts);

Y finalmente vamos a importar el componente Post y a agregar la función mostrarPosts  que mostrará la lista posts o un mensaje de carga o de error según corresponda, en base a los estados de la página:

import React, {useEffect} from 'react';
import {connect} from 'react-redux';

// Importamos la acción getPosts
import {getPosts} from '../acciones/postsActions';
import {Post} from '../componentes/Post';


const PaginaPosts = ({dispatch, posts, cargando, errores}) => {

  useEffect(() => {
    dispatch(getPosts());
  }, [dispatch]);


  // Función que muestra la lista de posts
  const mostrarPosts = () => {

    if (cargando) return <p>Cargando posts...</p>
    if (errores) return <p>Ha ocurrido un error.</p>

    return posts.map((post) => <Post key={post.id} post={post} resumen={true} />);
  }

  return (
    <div className="container">
      <h1 className="mb-4">Posts</h1>
      <hr/>
      {mostrarPosts()}
    </div>
  );
}

// Mapeo del estado de Redux con las propieades del componente
const mapStateToProps = (state) => ({
  posts: state.posts.posts,
  errores: state.posts.errores,
  cargando: state.posts.cargando,
});

// Conexión de React con redux
export default connect(mapStateToProps)(PaginaPosts);

Con esto ya habremos conectado el componente con la store de Redux, pudiendo obtener así los datos desde la API y almacenarlos en la store de Redux.

Antes de continuar, vamos a ver lo que hemos hecho hasta ahora por pantalla. Para ello, elimina o comenta temporalmente la línea en la que importas el componente PaginaPost en el archivo App.js. Elimina o comenta también la línea en la que definimos la ruta /posts/:postId.

Si ahora accedes a tu navegador, podrás ver la aplicación en funcionamiento. mediante las DevTools de Redux podrás ver cómo se modifica el estado a medida que se van sucediendo las acciones:

Antes de continuar, edita el archivo App.js y agrega de nuevo la línea en la que importamos el componente PaginaPost y también la ruta /posts/:postId.

Página PaginaPost

Crea el archivo /paginas/PaginaPost.js, en donde agregaremos el componente que mostrará cada post al completo junto a sus comentarios. Ejecutaremos la función getPost en la función useEffect, siguiendo el mismo esquema que hemos usado a la hora de obtener la lista de posts. La única diferencia es que ahora obtenemos el parámetro postId de la ruta actual, localizado en el objeto match.params, que luego pasamos a la función getPost:

import React, {useEffect} from 'react';
import {connect} from 'react-redux';

import {getPost} from '../acciones/postActions';
import {Post} from '../componentes/Post';

const PaginaPost = ({ match, dispatch, post, errores, cargando }) => {

  useEffect(() => {
    const { postId } = match.params;
    dispatch(getPost(postId));
  }, [dispatch, match])

  // Función que muestra un post
  const mostrarPost = () => {
    if (cargando.post) return <p>Cargando post...</p>
    if (errores.post) return <p>Ha ocurrido un error.</p>

    return <Post post={post} resumen={false} />
  }

  return (
    <div className="container">
      {mostrarPost()}
    </div>
  )
}

const mapStateToProps = state => ({
  post: state.post.post,
  errores: { post: state.post.errores },
  cargando: { post: state.post.cargando },
});

export default connect(mapStateToProps)(PaginaPost);

Tal y como ves, en esa ocasión hemos agregado el componente Post con el valor de la propiedad resumen establecido como false, ya que nos interesa mostrar el post completo. Este es el resultado de lo que hemos hecho hasta ahora:

Comentarios

Ya solamente nos falta mostrar los comentarios de las entradas del blog. Para ello, edita de nuevo el archivo /paginas/PaginaPost.js. Primero debemos importar la acción getComentarios y el componente Comentario en la parte superior del archivo:

import { getComentarios } from '../acciones/comentariosActions';
import { Comentario } from '../componentes/Comentario';

Luego agregaremos también los comentarios en la función mapStateToProps:

const mapStateToProps = state => ({
  post: state.post.post,
  comentarios: state.comentarios.comentarios,
  errores: { post: state.post.errores },
  cargando: { post: state.post.cargando },
});

Debemos agregar también la función que nos permita mostrar la lista de comentarios para luego mostrarlos por pantalla debajo del contenido del post:

const mostrarComentarios = () => {
    if (cargando.comentarios) return <p>Cargando comentarios...</p>
    if (errores.comentarios) return <p>Ha ocurrido un error al obtener los comentarios.</p>

    return comentarios.map(comentario => (
      <Comentario key={comentario.id} comentario={comentario} />
    ));
  }

También de iniciar la acción que obtiene los getComentarios comentarios:

dispatch(getComentarios(postId));

Este sería el código completo del archivo PaginaPost.js, incluyendo los comentarios:

import React, {useEffect} from 'react';
import {connect} from 'react-redux';

// Importamos la acción getPosts y el componente Post
import {getPost} from '../acciones/postActions';
import {Post} from '../componentes/Post';

// Importamos la acción getComentarios y el componente Comentario
import { getComentarios } from '../acciones/comentariosActions';
import { Comentario } from '../componentes/Comentario';

const PaginaPost = ({ match, dispatch, post, comentarios, errores, cargando }) => {

  useEffect(() => {
    const { postId } = match.params;
    dispatch(getPost(postId));
    dispatch(getComentarios(postId));
  }, [dispatch, match]);

  // Función que muestra un post
  const mostrarPost = () => {
    if (cargando.post) return <p>Cargando post...</p>
    if (errores.post) return <p>Ha ocurrido un error.</p>

    return <Post post={post} resumen={false} />
  }

  const mostrarComentarios = () => {
    if (cargando.comentarios) return <p>Cargando comentarios...</p>
    if (errores.comentarios) return <p>Ha ocurrido un error al obtener los comentarios.</p>

    return comentarios.map(comentario => (
      <Comentario key={comentario.id} comentario={comentario} />
    ));
  }

  return (
    <div className="container">
      {mostrarPost()}
      <h2>Comentarios</h2>
      {mostrarComentarios()}
    </div>
  )
}

const mapStateToProps = state => ({
  post: state.post.post,
  comentarios: state.comentarios.comentarios,
  errores: { post: state.post.errores },
  cargando: { post: state.post.cargando },
});

export default connect(mapStateToProps)(PaginaPost);

Si accedes a ti navegador e inspeccionas la página con las DevTools de Redux deberías ver algo así:

Finalizando

Y con esto ya hemos terminado de crear la aplicación React usando Redux. El funcionamiento de Redux es complicado de entender, así que enhorabuena si has llegado hasta aquí. Has aprendido las funcionalidades de Redux que se utilizan la mayor parte de las veces. Puedes consultar el código de la aplicación en GitHub o también verla en funcionamiento aquí.


Avatar de Edu Lazaro

Edu Lázaro: Ingeniero técnico en informática, actualmente trabajo como desarrollador web y programador de videojuegos.

👋 Hola! Soy Edu, me encanta crear cosas y he redactado esta guía. Si te ha resultado útil, el mayor favor que me podrías hacer es el de compatirla en Twitter 😊

Si quieres conocer mis proyectos, sígueme en Twitter.

1 comentario en “Tutorial de introducción a Redux

Responder a Gonzalo Cancelar la respuesta

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

“- Hey, Doc. No tenemos suficiente carretera para ir a 140/h km. - ¿Carretera? A donde vamos, no necesitaremos carreteras.”