TechLead

Redux

Contenedor de estado predecible con acciones, reducers y un store único

Redux - Contenedor de Estado Predecible

Redux es un contenedor de estado predecible para aplicaciones JavaScript. Te ayuda a escribir aplicaciones que se comportan de manera consistente, funcionan en diferentes entornos y son fáciles de probar. Redux se basa en tres principios fundamentales: fuente única de verdad, el estado es de solo lectura y los cambios se hacen con funciones puras.

Conceptos Fundamentales

  • Store — Objeto único que contiene todo el estado de la aplicación
  • Actions — Objetos planos que describen lo que sucedió
  • Reducers — Funciones puras que especifican cómo cambia el estado
  • Dispatch — Método para enviar acciones al store
  • Selectors — Funciones para extraer datos del estado

El Flujo de Redux

Evento UI → dispatch(acción) → Reducer → Nuevo Estado → Actualización UI

┌─────────┐     ┌─────────┐     ┌─────────┐     ┌─────────┐
│   UI    │────▶│ Action  │────▶│ Reducer │────▶│  Store  │
│ (Vista) │     │ Creator │     │         │     │ (Estado)│
└─────────┘     └─────────┘     └─────────┘     └────┬────┘
     ▲                                                │
     └────────────────────────────────────────────────┘
                Subscribe y Re-renderizar

Actions

// Las acciones son objetos planos con una propiedad type
const agregarTodo = {
  type: 'todos/agregar',
  payload: {
    id: 1,
    texto: 'Aprender Redux',
    completado: false
  }
};

// Action creators - funciones que retornan acciones
function agregarTodo(texto) {
  return {
    type: 'todos/agregar',
    payload: {
      id: Date.now(),
      texto,
      completado: false
    }
  };
}

function alternarTodo(id) {
  return {
    type: 'todos/alternar',
    payload: { id }
  };
}

function eliminarTodo(id) {
  return {
    type: 'todos/eliminar',
    payload: { id }
  };
}

// Constantes de tipos de acción (previene errores tipográficos)
const AGREGAR_TODO = 'todos/agregar';
const ALTERNAR_TODO = 'todos/alternar';
const ELIMINAR_TODO = 'todos/eliminar';

Reducers

// Reducer es una función pura: (estado, acción) => nuevoEstado
const estadoInicial = {
  todos: [],
  filtro: 'todos'
};

function todoReducer(estado = estadoInicial, accion) {
  switch (accion.type) {
    case 'todos/agregar':
      return {
        ...estado,
        todos: [...estado.todos, accion.payload]
      };
      
    case 'todos/alternar':
      return {
        ...estado,
        todos: estado.todos.map(todo =>
          todo.id === accion.payload.id
            ? { ...todo, completado: !todo.completado }
            : todo
        )
      };
      
    case 'todos/eliminar':
      return {
        ...estado,
        todos: estado.todos.filter(todo => todo.id !== accion.payload.id)
      };
      
    case 'filtro/establecer':
      return {
        ...estado,
        filtro: accion.payload
      };
      
    default:
      return estado;
  }
}

// Combinar múltiples reducers
import { combineReducers } from 'redux';

const rootReducer = combineReducers({
  todos: todoReducer,
  usuario: usuarioReducer,
  configuracion: configuracionReducer
});

// La forma del estado será:
// { todos: {...}, usuario: {...}, configuracion: {...} }

Store

import { createStore } from 'redux';

// Crear el store con el root reducer
const store = createStore(rootReducer);

// Obtener estado actual
console.log(store.getState());

// Despachar acciones para actualizar el estado
store.dispatch(agregarTodo('Aprender Redux'));
store.dispatch(alternarTodo(1));

// Suscribirse a cambios de estado
const cancelarSuscripcion = store.subscribe(() => {
  console.log('Estado actualizado:', store.getState());
});

// Luego, dejar de escuchar
cancelarSuscripcion();

Integración con React-Redux

import { Provider, useSelector, useDispatch } from 'react-redux';
import { createStore } from 'redux';

// 1. Crear store
const store = createStore(rootReducer);

// 2. Envolver app con Provider
function App() {
  return (
    <Provider store={store}>
      <TodoApp />
    </Provider>
  );
}

// 3. Usar hooks para acceder al estado y dispatch
function ListaTodos() {
  // Seleccionar datos del store
  const todos = useSelector(estado => estado.todos);
  const filtro = useSelector(estado => estado.filtro);
  
  // Obtener función dispatch
  const dispatch = useDispatch();
  
  const todosFiltrados = todos.filter(todo => {
    if (filtro === 'activos') return !todo.completado;
    if (filtro === 'completados') return todo.completado;
    return true;
  });
  
  return (
    <ul>
      {todosFiltrados.map(todo => (
        <li 
          key={todo.id}
          onClick={() => dispatch(alternarTodo(todo.id))}
          style={{ textDecoration: todo.completado ? 'line-through' : 'none' }}
        >
          {todo.texto}
          <button onClick={() => dispatch(eliminarTodo(todo.id))}>
            Eliminar
          </button>
        </li>
      ))}
    </ul>
  );
}

function AgregarTodo() {
  const [texto, setTexto] = useState('');
  const dispatch = useDispatch();
  
  const manejarEnvio = (e) => {
    e.preventDefault();
    if (texto.trim()) {
      dispatch(agregarTodo(texto));
      setTexto('');
    }
  };
  
  return (
    <form onSubmit={manejarEnvio}>
      <input 
        value={texto} 
        onChange={(e) => setTexto(e.target.value)}
        placeholder="Agregar todo"
      />
      <button type="submit">Agregar</button>
    </form>
  );
}

Selectors

// Selectors básicos
const seleccionarTodos = estado => estado.todos;
const seleccionarFiltro = estado => estado.filtro;

// Selectors de datos derivados
const seleccionarTodosCompletados = estado => 
  estado.todos.filter(todo => todo.completado);

const seleccionarTodosActivos = estado => 
  estado.todos.filter(todo => !todo.completado);

const seleccionarConteoTodos = estado => estado.todos.length;

// Selector parametrizado
const seleccionarTodoPorId = (estado, todoId) =>
  estado.todos.find(todo => todo.id === todoId);

// Uso en componentes
function EstadisticasTodos() {
  const total = useSelector(seleccionarConteoTodos);
  const completados = useSelector(estado => seleccionarTodosCompletados(estado).length);
  const activos = useSelector(estado => seleccionarTodosActivos(estado).length);
  
  return (
    <div>
      Total: {total} | Activos: {activos} | Completados: {completados}
    </div>
  );
}

Pros y Contras de Redux

Pros Contras
Cambios de estado predecibles Mucho código repetitivo (boilerplate)
Excelentes DevTools Curva de aprendizaje empinada
Depuración con viaje en el tiempo Puede ser excesivo para apps pequeñas
Gran ecosistema y comunidad Configuración verbosa de action/reducer

💡 Puntos Clave

  • • Usa Redux para apps complejas con mucho estado compartido
  • • Mantén los reducers puros - sin efectos secundarios, sin mutaciones
  • • Usa selectors para encapsular el acceso al estado
  • • Considera Redux Toolkit para desarrollo moderno de Redux
  • • Instala la extensión Redux DevTools del navegador para depuración