Desde la introducción de React Hooks, el abanico de opciones para gestionar el estado de nuestras aplicaciones se ha ampliado significativamente. Entre estas opciones, destaca Redux, uno de los gestores de estado global más conocidos y utilizados. Desde sus inicios, Redux ha evolucionado de manera muy positiva, proporcionando a todos los componentes de React una única fuente de verdad a la que pueden acceder y modificar según las necesidades del proyecto.

En este artículo, nos centraremos en la implementación de Redux utilizando Redux Toolkit. Esta biblioteca oficial simplifica aún más el flujo de Redux, dejando atrás los días de configuraciones complicadas y código repetitivo.

Sabemos que existen varias formas de gestionar el estado en React (estado local, contextos, useReducer…) y Redux es especialmente útil en proyectos de gran envergadura, donde la comunicación entre distintos nodos se vuelve crucial. Aunque, en este post, veremos un ejemplo práctico en una aplicación sencilla para que sea más entendible.

¡Comenzamos!

Instalación de las librerías

Antes de comenzar debemos instalar las librerías react-redux y redux-toolkit a través del terminal de la siguiente manera:

npm install @reduxjs/toolkit react-redux

Creando el store de datos

El store es el "almacén" del estado global, ahí guardaremos los datos que queremos que estén disponibles a nivel global. Este objeto contiene el estado actual de la aplicación y proporciona métodos para despachar acciones y suscribirse a cambios en el estado.

Crearemos el store Redux usando configureStore de Redux Toolkit. Este proceso centraliza el estado y la lógica de la aplicación en un único objeto de store. Debemos importar configureStore y pasarle un objeto de configuración con una propiedad 'reducer' que combine los reducers de los slices (porciones del estado) que crearemos más adelante

Otra de las ventajas que tiene configureStore es que está integrado con las DevTools mediante Redux DevTools Extension, facilitando la depuración y monitoreo del estado.

De momento vamos a crear el store vacío, que tendrá este aspecto:

// src/store/index.js

import { configureStore } from '@reduxjs/toolkit'

export const store = configureStore({
  reducer: {
    //aquí indicaremos más adelante los reducers de los slices
  },
})

Integración del store en la aplicación mediante Provider

Para que el store esté disponible en toda la aplicación, debemos ir al archivo principal de nuestra aplicación React, generalmente llamado index.js o index.tsx. Este es el archivo donde se configura la raíz de la aplicación y se renderiza el componente principal App. En este archivo, importamos el componente Provider de react-redux, envolveremos el componente App con él y le pasaremos como prop el store que acabamos de crear. Esto hace que el store esté disponible para todos los componentes descendientes de App.

Es importante que el Provider envuelva a la aplicación en el nivel más alto posible. De esta manera, todos los componentes dentro del pueden acceder al estado global del store.

// src/index.js

import React from 'react'
import ReactDOM from 'react-dom/client'
import { Provider } from 'react-redux'
import { store } from './store'
import App from './App'

const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>,
)

Inicializar el store con datos: creando los slices

Un slice es una función que representa una porción del estado (store) y la lógica relacionada (reducers y acciones). Para crearlo utilizaremos la función createSlice() de Redux Toolkit (RTK), en ella definimos un nombre para la slice, un estado inicial de esa porción del estado global, y un conjunto de reducers. Cada reducer define cómo cambia el estado en respuesta a una acción específica. Redux Toolkit genera automáticamente las acciones correspondientes a cada reducer.

Vamos a utilizar como ejemplo una aplicación que muestra un listado de notas, de momento, queremos guardar en nuestro slice el array de objetos que contiene las notas que vamos a mostrar (noteList). Creamos src/store/notes/notes-slice.js que tendrá el siguiente aspecto:

// src/store/notes/notes-slice.js

import { createSlice } from '@reduxjs/toolkit'

export const noteSlice = createSlice({
  name: 'noteSlice', // indicamos el nombre del slice
  initialState: { //aquí se guarda el estado inicial
    noteList: [],
  },
  reducers: { //objeto que contiene las acciones que modificarán el estado
    setNoteList: (state, action) => {
      state.noteList = action.payload
    },
    addNote: (state, action) => {
      state.noteList.push(action.payload)
    },
  },
})

export const { setNoteList, addNote } = noteSlice.actions
export const notesReducer = noteSlice.reducer

He añadido dos reducers que más adelante veremos en detalle:

Es importante exportar las acciones, generadas automáticamente, y el reducer para poder utilizarlo desde otras partes de la aplicación.

Referenciar el slice en el store

En el archivo de configuración del store, en la clave ‘reducer’ incluimos el reducer del slice que acabamos de crear (notesReducer) y lo asignamos a una clave identificativa del mismo, en este caso lo he llamado notesSliceStore, pero se podría utilizar cualquier otro nombre:

// src/store/index.js

import { configureStore } from '@reduxjs/toolkit'
import { notesReducer } from 'store/notes/notes-slice'

export const store = configureStore({
  reducer: {
    notesSliceStore: notesReducer,
  },
})

Hasta aquí la configuración del Store, ya lo tenemos disponible para acceder a él, leerlo y modificar los datos que contiene. ¿Y cómo hacemos esto? Gracias a dos hooks proporcionados por react-redux:

Actualizando el store con useDispatch()

Este hook permite a los componentes funcionales de React despachar acciones al store de Redux. Despachar una acción significa enviar una acción al store de Redux para que pueda ser procesada por los reducers, los cuales actualizarán el estado en base a la información que lleve esa acción (action.payload).

El action.payload es la parte de la acción que lleva los datos específicos relacionados con la acción. Es decir, el payload contiene la información necesaria para llevar a cabo el cambio de estado.

Con dos ejemplos se va a ver más claro. Volviendo a los dos reducers que hemos creado con anterioridad setNoteList y addNote, vamos a hacer un dispatch de setNoteList para que cargue en el estado la respuesta de la consulta a la API, esta consulta la hacemos desde un hook personalizado, pero se podría hacer desde cualquier otro lugar de la aplicación:

// src/hooks/useFetchAllNotes.js

import { useState, useEffect } from 'react'
import { useDispatch } from 'react-redux' //importamos useDispatch

import { setNoteList } from 'store/notes/notes-slice'
import { fetchAllNotes } from 'api/note'

export const useFetchAllNotes = () => {
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState(null)
  const dispatch = useDispatch() //almacenamos useDispatch() en una constante para poder utilizarlo en el componente

  useEffect(() => {
    const getAllNotes = async () => {
      try {
        const response = await fetchAllNotes() // obtenemos la respuesta a la consulta de la API
        dispatch(setNoteList(response)) // enviamos 'response' (lo que será el action.payload) al reductor setNoteList que se encargará de cargarla en state.noteList
      } catch (err) {
        setError(err)
      } finally {
        setLoading(false)
      }
    }
    getAllNotes()
  }, [])

  return { loading, error }
}

Y también hacemos un dispatch de addNote, este añadirá una nota nueva (createNote) al array noteList del estado:

// src/pages/NoteCreate/NoteCreate.jsx

import { useDispatch } from 'react-redux' //importamos useDispatch
import NoteForm from 'components/NoteForm/NoteForm'
import { addNote } from 'store/notes/notes-slice'
import { createNote } from 'api/note'

const NoteCreate = () => {
  const dispatch = useDispatch()  //almacenamos useDispatch() en una constante para poder utilizarlo en el componente

  const handleSubmit = async formValues => {
    const createdNote = await createNote(formValues)//creamos una nota nueva
    dispatch(addNote(createdNote)) //enviamos la nueva nota (createNote, que será el action.payload) al reductor addNote que se encargará de añadirla al array state.noteList
  }

  return <NoteForm onSubmit={handleSubmit} />
}

export default NoteCreate

Recordemos que los dispatch (setNoteList y addNote) conectan con el slice del store y realizan la acción indicada:

Consultando el store con useSelector()

Este hook permite a los componentes de React leer datos del store de Redux. Proporciona una forma de suscribirse a cambios en el estado y actualizar dinámicamente el componente cuando esos cambios ocurren. Utiliza la comparación de referencias para evitar renderizaciones innecesarias, lo que mejora el rendimiento de la aplicación.

Siguiendo con el ejemplo anterior, si queremos recuperar el listado de notas del estado para posteriormente realizar un .map y renderizar cada nota, haremos lo siguiente:

// src/pages/NoteList/NoteList.jsx

import { useSelector } from 'react-redux' //importamos useSelector
import Note from 'components/Note/Note'

const NoteList = () => {
  const noteList = useSelector(store => store.notesSliceStore.noteList) //recuperamos de notesSliceStore (src/store/index.jsx) el reducer notesReducer y lo guardamos en la constante
  return (
    <ul>
      {noteList.map(note => (
        <li key={note.id}>
          <Note title={note.title} content={note.content} />
        </li>
      ))}
    </ul>
  )
}
export default NoteList

Conclusiones

Como hemos podido comprobar, la combinación de slices de Redux Toolkit junto con useSelector y useDispatch proporciona una forma eficiente y efectiva de gestionar el estado global en aplicaciones React.

Al organizar las porciones de estado y sus reducers en un solo lugar, los slices fomentan la modularidad del código, lo que simplifica su mantenimiento y mejora su legibilidad. El uso de useSelector y useDispatch también contribuye a reducir el boilerplate, eliminando la necesidad de configuraciones complejas para conectar componentes a Redux.

Esta práctica simplifica el código, mejora el rendimiento y facilita la comprensión del flujo de datos en la aplicación.

¡Te invito a que la pongas en práctica!

Referencias

Cuéntanos qué te parece.

Los comentarios serán moderados. Serán visibles si aportan un argumento constructivo. Si no estás de acuerdo con algún punto, por favor, muestra tus opiniones de manera educada.

Suscríbete