Ir al contenido principal

Redux Essentials, Parte 1: Conceptos y Visión General de Redux

Traducción Beta No Oficial

Esta página fue traducida por PageTurner AI (beta). No está respaldada oficialmente por el proyecto. ¿Encontraste un error? Reportar problema →

Lo que aprenderás
  • Qué es Redux y por qué podrías querer usarlo
  • Términos y conceptos clave de Redux
  • Cómo fluyen los datos en una aplicación Redux

Introducción

¡Bienvenido al tutorial de Redux Essentials! Este tutorial te introducirá a Redux y te enseñará a usarlo correctamente, utilizando nuestras herramientas recomendadas y mejores prácticas más recientes. Cuando termines, deberías poder comenzar a construir tus propias aplicaciones Redux usando las herramientas y patrones aprendidos aquí.

En la Parte 1 de este tutorial, cubriremos los conceptos y términos clave que necesitas conocer para usar Redux, y en Parte 2: Estructura de Aplicaciones Redux examinaremos una aplicación típica React + Redux para ver cómo encajan las piezas.

Comenzando en Parte 3: Flujo Básico de Datos en Redux, usaremos ese conocimiento para construir una pequeña aplicación de feed de redes sociales con características reales, veremos cómo funcionan esas piezas en la práctica y hablaremos sobre patrones importantes y pautas para usar Redux.

Cómo leer este tutorial

Este tutorial se centra en mostrarte cómo usar Redux correctamente, explicando los conceptos sobre la marcha para que puedas entender cómo construir aplicaciones Redux de manera adecuada.

Hemos intentado que estas explicaciones sean accesibles para principiantes, pero necesitamos asumir algunos conocimientos previos:

Requisitos previos

Si aún no te sientes cómodo con esos temas, te animamos a dedicar tiempo a familiarizarte con ellos primero, y luego volver para aprender sobre Redux. ¡Estaremos aquí cuando estés listo!

También debes asegurarte de tener instaladas las extensiones React DevTools y Redux DevTools en tu navegador:

¿Qué es Redux?

Es útil entender primero qué es esta cosa llamada "Redux". ¿Qué hace? ¿Qué problemas me ayuda a resolver? ¿Por qué querría usarlo?

Redux es un patrón y biblioteca para gestionar y actualizar el estado global de la aplicación, donde la UI dispara eventos llamados "actions" que describen lo sucedido, y una lógica de actualización separada llamada "reducers" modifica el estado en respuesta. Sirve como almacén centralizado para el estado que debe usarse en toda tu aplicación, con reglas que garantizan que el estado solo se puede actualizar de manera predecible.

¿Por qué debería usar Redux?

Redux te ayuda a gestionar el estado "global" - estado necesario en muchas partes de tu aplicación.

Los patrones y herramientas proporcionados por Redux facilitan comprender cuándo, dónde, por qué y cómo se actualiza el estado en tu aplicación, y cómo se comportará tu lógica de aplicación cuando ocurran esos cambios. Redux te guía hacia la escritura de código predecible y testeable, lo que ayuda a darte confianza en que tu aplicación funcionará como se espera.

¿Cuándo debería usar Redux?

Redux te ayuda a manejar el estado compartido, pero como cualquier herramienta, tiene compensaciones. Implica aprender más conceptos y escribir más código. También añade cierta complejidad indirecta a tu código y requiere seguir ciertas restricciones. Es un equilibrio entre productividad a corto y largo plazo.

Redux es más útil cuando:

  • Tienes grandes cantidades de estado de aplicación necesarias en muchos lugares

  • El estado de la aplicación se actualiza frecuentemente con el tiempo

  • La lógica para actualizar ese estado puede ser compleja

  • La aplicación tiene una base de código mediana o grande con múltiples colaboradores

No todas las aplicaciones necesitan Redux. Dedica tiempo a considerar qué tipo de app estás construyendo y decide qué herramientas son mejores para resolver los problemas específicos que enfrentas.

¿Quieres saber más?

Si no estás seguro de si Redux es adecuado para tu aplicación, estos recursos ofrecen orientación adicional:

Bibliotecas y herramientas de Redux

Redux es fundamentalmente una pequeña biblioteca JS independiente, pero normalmente se usa con estos paquetes:

Redux Toolkit

Redux Toolkit es nuestro enfoque recomendado para escribir lógica Redux. Contiene paquetes y funciones esenciales para construir apps Redux, incorpora mejores prácticas recomendadas, simplifica tareas comunes, previene errores frecuentes y facilita el desarrollo de aplicaciones Redux.

React-Redux

Redux se integra con cualquier framework UI, siendo React el más común. React-Redux es nuestro paquete oficial que permite a tus componentes React interactuar con el store mediante lectura de estado y despacho de acciones.

Extensión Redux DevTools

La Extensión Redux DevTools muestra el histórico de cambios en el store, permitiendo depurar eficazmente aplicaciones con técnicas avanzadas como "depuración de viaje en el tiempo".

Términos y conceptos de Redux

Antes de ver código práctico, repasemos la terminología y conceptos esenciales para usar Redux.

Gestión del estado

Considera este pequeño componente React de contador: rastrea un número en su estado local y lo incrementa al hacer clic en un botón:

function Counter() {
// State: a counter value
const [counter, setCounter] = useState(0)

// Action: code that causes an update to the state when something happens
const increment = () => {
setCounter(prevCounter => prevCounter + 1)
}

// View: the UI definition
return (
<div>
Value: {counter} <button onClick={increment}>Increment</button>
</div>
)
}

Esta aplicación autocontenida tiene tres partes:

  • El estado: fuente de verdad que impulsa la aplicación

  • La vista: descripción declarativa de la UI según el estado actual

  • Las acciones: eventos que ocurren por interacción del usuario y actualizan el estado

Este es un ejemplo de "flujo de datos unidireccional":

  • El estado describe la condición de la aplicación en un momento específico

  • La interfaz se renderiza basándose en ese estado

  • Cuando ocurre algo (como un clic de usuario), el estado se actualiza según lo ocurrido

  • La interfaz se vuelve a renderizar basándose en el nuevo estado

Flujo de datos unidireccional

Sin embargo, esta simplicidad se complica cuando tenemos múltiples componentes que necesitan compartir y usar el mismo estado, especialmente si están ubicados en diferentes partes de la aplicación. A veces esto puede resolverse "elevando el estado" a componentes padres, pero no siempre funciona.

Una solución es extraer el estado compartido de los componentes y colocarlo en una ubicación centralizada fuera del árbol de componentes. ¡Así nuestro árbol de componentes se convierte en una gran "vista" donde cualquier componente puede acceder al estado o desencadenar acciones, sin importar su posición en el árbol!

Al definir y separar los conceptos de gestión de estado, y aplicar reglas que mantienen independencia entre vistas y estados, dotamos a nuestro código de mayor estructura y mantenibilidad.

Esta es la idea básica de Redux: un lugar centralizado para contener el estado global de tu aplicación, con patrones específicos para actualizarlo y hacer el código predecible.

Inmutabilidad

"Mutable" significa "cambiable". Si algo es "inmutable", nunca puede cambiarse.

Los objetos y arrays de JavaScript son mutables por defecto. Si creo un objeto, puedo modificar sus campos. Si creo un array, también puedo cambiar su contenido:

const obj = { a: 1, b: 2 }
// still the same object outside, but the contents have changed
obj.b = 3

const arr = ['a', 'b']
// In the same way, we can change the contents of this array
arr.push('c')
arr[1] = 'd'

Esto se llama mutar el objeto o array. Sigue siendo la misma referencia en memoria, pero su contenido interno ha cambiado.

Para actualizar valores inmutables, tu código debe hacer copias de objetos/arrays existentes y luego modificar esas copias.

Podemos hacerlo manualmente usando operadores de propagación de JavaScript o métodos de array que devuelven nuevas copias en lugar de mutar el original:

const obj = {
a: {
// To safely update obj.a.c, we have to copy each piece
c: 3
},
b: 2
}

const obj2 = {
// copy obj
...obj,
// overwrite a
a: {
// copy obj.a
...obj.a,
// overwrite c
c: 42
}
}

const arr = ['a', 'b']
// Create a new copy of arr, with "c" appended to the end
const arr2 = arr.concat('c')

// or, we can make a copy of the original array:
const arr3 = arr.slice()
// and mutate the copy:
arr3.push('c')

React y Redux esperan que todas las actualizaciones de estado se hagan inmutables. Veremos más adelante dónde y por qué esto es importante, y formas más sencillas de escribir lógica de actualización inmutable.

¿Quieres saber más?

Para más información sobre inmutabilidad en JavaScript:

Terminología

Antes de continuar, familiaricémonos con términos clave de Redux:

Acciones (Actions)

Una acción es un objeto JavaScript plano con un campo type. Puedes pensar en una acción como un evento que describe algo ocurrido en la aplicación.

El campo type debe ser un string descriptivo como "todos/todoAdded". Generalmente usamos el formato "domain/eventName", donde la primera parte es la categoría o funcionalidad, y la segunda el evento específico.

Un objeto de acción puede tener otros campos con información adicional. Por convención, esta información va en un campo llamado payload.

Un objeto de acción típico se vería así:

const addTodoAction = {
type: 'todos/todoAdded',
payload: 'Buy milk'
}

Creadores de Acciones (Action Creators)

Un creador de acciones es una función que crea y devuelve un objeto de acción. Los usamos para evitar escribir manualmente el objeto cada vez:

const addTodo = text => {
return {
type: 'todos/todoAdded',
payload: text
}
}

Reductores (Reducers)

Un reductor es una función que recibe el state actual y un objeto action, decide cómo actualizar el estado si es necesario, y devuelve el nuevo estado: (state, action) => newState. Puedes pensar en un reductor como un manejador de eventos que responde según el tipo de acción recibida.

información

El nombre "reductor" viene por su similitud con la función callback de Array.reduce().

Los reducers deben siempre seguir ciertas reglas específicas:

  • Deben calcular el nuevo valor de estado basándose únicamente en los argumentos state y action

  • No pueden modificar el state existente. En su lugar, deben realizar actualizaciones inmutables, copiando el state existente y haciendo cambios en los valores copiados

  • Deben ser "puros": no pueden ejecutar lógica asíncrona, calcular valores aleatorios ni causar otros "efectos secundarios"

Hablaremos más sobre las reglas de los reducers más adelante, incluyendo por qué son importantes y cómo seguirlas correctamente.

La lógica dentro de las funciones reducer suele seguir la misma secuencia de pasos:

  • Verificar si al reducer le importa esta acción

    • Si es así, hacer una copia del estado, actualizar la copia con nuevos valores y devolverla
  • De lo contrario, devolver el estado existente sin cambios

Aquí tienes un pequeño ejemplo de un reducer que muestra los pasos que debe seguir:

const initialState = { value: 0 }

function counterReducer(state = initialState, action) {
// Check to see if the reducer cares about this action
if (action.type === 'counter/increment') {
// If so, make a copy of `state`
return {
...state,
// and update the copy with the new value
value: state.value + 1
}
}
// otherwise return the existing state unchanged
return state
}

Los reducers pueden usar cualquier tipo de lógica interna para determinar el nuevo estado: if/else, switch, bucles, etc.

Detailed Explanation: Why Are They Called 'Reducers?'

The Array.reduce() method lets you take an array of values, process each item in the array one at a time, and return a single final result. You can think of it as "reducing the array down to one value".

Array.reduce() takes a callback function as an argument, which will be called one time for each item in the array. It takes two arguments:

  • previousResult, the value that your callback returned last time
  • currentItem, the current item in the array

The first time that the callback runs, there isn't a previousResult available, so we need to also pass in an initial value that will be used as the first previousResult.

If we wanted to add together an array of numbers to find out what the total is, we could write a reduce callback that looks like this:

const numbers = [2, 5, 8]

const addNumbers = (previousResult, currentItem) => {
console.log({ previousResult, currentItem })
return previousResult + currentItem
}

const initialValue = 0

const total = numbers.reduce(addNumbers, initialValue)
// {previousResult: 0, currentItem: 2}
// {previousResult: 2, currentItem: 5}
// {previousResult: 7, currentItem: 8}

console.log(total)
// 15

Notice that this addNumbers "reduce callback" function doesn't need to keep track of anything itself. It takes the previousResult and currentItem arguments, does something with them, and returns a new result value.

A Redux reducer function is exactly the same idea as this "reduce callback" function! It takes a "previous result" (the state), and the "current item" (the action object), decides a new state value based on those arguments, and returns that new state.

If we were to create an array of Redux actions, call reduce(), and pass in a reducer function, we'd get a final result the same way:

const actions = [
{ type: 'counter/increment' },
{ type: 'counter/increment' },
{ type: 'counter/increment' }
]

const initialState = { value: 0 }

const finalResult = actions.reduce(counterReducer, initialState)
console.log(finalResult)
// {value: 3}

We can say that Redux reducers reduce a set of actions (over time) into a single state. The difference is that with Array.reduce() it happens all at once, and with Redux, it happens over the lifetime of your running app.

Store

El estado actual de la aplicación Redux reside en un objeto llamado store.

El store se crea pasando un reducer, y tiene un método llamado getState que devuelve el valor del estado actual:

import { configureStore } from '@reduxjs/toolkit'

const store = configureStore({ reducer: counterReducer })

console.log(store.getState())
// {value: 0}

Dispatch

El store de Redux tiene un método llamado dispatch. La única forma de actualizar el estado es llamar a store.dispatch() y pasarle un objeto de acción. El store ejecutará su función reducer y guardará el nuevo valor del estado internamente, y podemos llamar a getState() para obtener el valor actualizado:

store.dispatch({ type: 'counter/increment' })

console.log(store.getState())
// {value: 1}

Puedes pensar en despachar acciones como "disparar un evento" en la aplicación. Algo sucedió y queremos que el store lo sepa. Los reducers actúan como oyentes de eventos, y cuando detectan una acción que les interesa, actualizan el estado en respuesta.

Normalmente llamamos a los creadores de acciones (action creators) para despachar la acción correcta:

const increment = () => {
return {
type: 'counter/increment'
}
}

store.dispatch(increment())

console.log(store.getState())
// {value: 2}

Selectors

Los selectors son funciones que saben extraer información específica del valor del estado del store. A medida que una aplicación crece, esto evita repetir lógica cuando diferentes partes necesitan acceder a los mismos datos:

const selectCounterValue = state => state.value

const currentValue = selectCounterValue(store.getState())
console.log(currentValue)
// 2

Flujo de datos en una aplicación Redux

Anteriormente mencionamos el "flujo de datos unidireccional", que describe esta secuencia para actualizar la aplicación:

  • El estado describe la condición de la aplicación en un momento específico

  • La interfaz se renderiza basándose en ese estado

  • Cuando ocurre algo (como un clic de usuario), el estado se actualiza según lo ocurrido

  • La interfaz se vuelve a renderizar basándose en el nuevo estado

Específicamente en Redux, podemos detallar estos pasos:

  • Configuración inicial:

    • Se crea un store de Redux usando una función reducer raíz
    • El store llama al reducer raíz una vez y guarda el valor devuelto como su state inicial
    • En el primer renderizado, los componentes acceden al estado actual del store y lo usan para decidir qué mostrar. También se suscriben a futuras actualizaciones para detectar cambios.
  • Actualizaciones:

    • Sucede algo en la aplicación, como un clic de usuario en un botón
    • El código despacha una acción al almacén Redux, como dispatch({type: 'counter/increment'})
    • El almacén ejecuta nuevamente la función reductora con el state anterior y la action actual, guardando el valor devuelto como nuevo state
    • El almacén notifica a todas las partes suscritas de la UI sobre la actualización
    • Cada componente UI que necesita datos del almacén verifica si las partes relevantes del estado cambiaron
    • Cada componente que detecta cambios en sus datos fuerza un nuevo renderizado con los datos actualizados

Así se visualiza ese flujo de datos:

Diagrama de flujo de datos en Redux

Lo que has aprendido

Redux introduce varios términos y conceptos nuevos. Como resumen, esto es lo que hemos cubierto:

Resumen
  • Redux es una biblioteca para gestionar el estado global de aplicaciones
    • Suele usarse con React-Redux para integrar Redux con React
    • Redux Toolkit es el método estándar para escribir lógica Redux
  • El patrón de actualización de Redux separa "qué sucedió" de "cómo cambia el estado"
    • Las acciones son objetos planos con campo type que describen "qué sucedió"
    • Los reductores son funciones que calculan nuevo estado basado en estado previo + acción
    • Un almacén Redux ejecuta el reductor raíz al despachar una acción
  • Redux usa estructura de "flujo de datos unidireccional"
    • El estado describe la condición de la app en un momento dado
    • La UI se renderiza basándose en ese estado
    • Cuando ocurre algo:
      • La UI despacha una acción
      • El almacén ejecuta reductores y actualiza el estado
      • El almacén notifica a la UI sobre cambios
    • La UI se vuelve a renderizar con el nuevo estado

¿Qué sigue?

Hemos visto las piezas individuales de una app Redux. Continúa con la Parte 2: Estructura de aplicación Redux Toolkit para ver un ejemplo completo donde encajan todas las piezas.