React

React

Tutorial React: Hooks para gestión de estado complejo y contexto

React Hooks: Aprende gestión de estado complejo y contexto con useContext, useReducer y useRef en React con ejemplos prácticos y buenas prácticas.

useContext para compartir estado global

El hook useContext en React permite compartir de manera eficiente el estado global entre componentes sin necesidad de pasar props manualmente en cada nivel del árbol de componentes. Esto es especialmente útil en aplicaciones de gran escala donde el estado o las funciones necesitan ser accesibles en múltiples componentes en diferentes niveles de la jerarquía.

Para utilizar useContext, primero debes crear un contexto con React.createContext. Este contexto actúa como un contenedor para el estado global que deseas compartir.

// context/GlobalContext.js
import { createContext } from 'react';

const GlobalContext = createContext();

export default GlobalContext;

Una vez creado el contexto, necesitas un proveedor (Provider) para envolver los componentes que necesitan acceso al estado global. El proveedor recibe una prop value que contiene el estado o funciones que deseas compartir.

// context/GlobalProvider.jsx
import { useState } from 'react';
import GlobalContext from '../../context/GlobalContext';

const initialState = {
  someValue: false,
};

export default function GlobalProvider({ children }) {
  const [state, setState] = useState(initialState);

  const value = {
    state,
    setState,
  };

  return (
    <GlobalContext.Provider value={value}>
      {children}
    </GlobalContext.Provider>
  );
}

Envuelve tu aplicación o la parte relevante de tu aplicación con el GlobalProvider para que los componentes hijos puedan acceder al contexto.

// App.jsx
import YourComponent from './components/YourComponent/YourComponent';
import GlobalProvider from './context/GlobalProvider';

export default function App() {
  return (
    <GlobalProvider value={true}>
      <YourComponent />
    </GlobalProvider>
  );
}

Dentro de los componentes, puedes utilizar el hook useContext para acceder al contexto y, por lo tanto, al estado global.

// components/YourComponent/YourComponent.jsx
import { useContext } from 'react';
import GlobalContext from '../../context/GlobalContext';

export default function YourComponent() {
  const { state, setState } = useContext(GlobalContext);

  const handleClick = () => {
    setState(prevState => ({
      ...prevState,
      someValue: !prevState.someValue,
    }));
  };

  return (
    <div>
      <p>{state.someValue.toString()}</p>
      <button onClick={handleClick}>Toggle Value</button>
    </div>
  );
}

En este ejemplo, YourComponent accede al estado global y a la función setState a través de useContext. El botón dentro del componente cambia el valor de someValue en el estado global, demostrando cómo useContext facilita la modificación del estado compartido.

Es importante tener en cuenta que el uso de useContext es adecuado para compartir estado que no cambia frecuentemente. Para estados que cambian con alta frecuencia, es preferible utilizar contextos más específicos o combinar useContext con otros hooks como useReducer para un manejo más eficiente del estado.

useReducer para manejo de estado completo

El hook useReducer en React es una alternativa a useState para manejar estados complejos que involucran múltiples sub-valores o cuando la lógica de actualización del estado es más avanzada. Es similar a los reducers en Redux, pero integrado en el ecosistema de React.

Para utilizar useReducer, necesitas definir un reducer, que es una función que toma el estado actual y una acción, y devuelve un nuevo estado. La estructura básica de un reducer es la siguiente:

// Counter/reducer.js
export default function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    default:
      throw new Error(`Unhandled action type: ${action.type}`);
  }
}

A continuación, puedes utilizar useReducer en tu componente para gestionar el estado. useReducer recibe el reducer y un estado inicial, y devuelve el estado actual y una función dispatch para enviar acciones.

// Counter/Counter.jsx
import { useReducer } from 'react';
import reducer from './reducer';

const initialState = { count: 0 };

export default function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>Increment</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>Decrement</button>
    </div>
  );
}

En este ejemplo, Counter utiliza useReducer para gestionar el estado del contador. Al hacer clic en los botones, se envían acciones al reducer, que actualiza el estado en consecuencia.

useReducer es especialmente útil cuando:

  • El estado tiene una estructura compleja.
  • La lógica de actualización del estado es compleja.
  • Necesitas optimizar el rendimiento para evitar recrear funciones en cada renderizado.

También puedes combinar useReducer con useContext para gestionar el estado global de manera más eficiente. Primero, define el contexto y el proveedor como en la sección de useContext.

// context/GlobalContext.js
import { createContext } from 'react';

const GlobalContext = createContext();

export default GlobalContext;

Luego, crea un proveedor que use useReducer en lugar de useState.

// providers/GlobalProvider.jsx
import { useReducer } from 'react';
import GlobalContext from '../../context/GlobalContext';

const initialState = { count: 0 };

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    default:
      throw new Error(`Unhandled action type: ${action.type}`);
  }
}

export default function GlobalProvider({ children }) {
  const [state, dispatch] = useReducer(reducer, initialState);

  const value = { state, dispatch };

  return (
    <GlobalContext.Provider value={value}>
      {children}
    </GlobalContext.Provider>
  );
}

Envuelve tu aplicación con este proveedor.

// App.jsx
import GlobalProvider from './context/GlobalProvider';

export default function App() {
  return (
    <GlobalProvider>
      <YourComponent />
    </GlobalProvider>
  );
}

Dentro de los componentes, puedes usar useContext para acceder al contexto y despachar acciones.

// components/YourComponent/YourComponent.jsx
import { useContext } from 'react';
import GlobalContext from '../../context/GlobalContext';

export default function YourComponent() {
  const { state, dispatch } = useContext(GlobalContext);

  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>Increment</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>Decrement</button>
    </div>
  );
}

Este enfoque permite gestionar el estado global de manera eficiente utilizando useReducer, lo que es beneficioso para aplicaciones grandes con estados complejos.

useRef para acceso a referencias y persistencia de valores

El hook useRef en React se utiliza para acceder a referencias directas a elementos del DOM y para persistir valores entre renderizados sin causar re-renderizados. Este hook es especialmente útil cuando necesitas manipular directamente el DOM o almacenar valores mutables que no deberían desencadenar un ciclo de renderizado.

Acceso a referencias del DOM

useRef puede crear una referencia mutable que puedes asignar a un elemento del DOM. Esta referencia no cambia entre renderizados y no provoca un re-renderizado cuando se actualiza.

Ejemplo de cómo utilizar useRef para acceder a un elemento del DOM:

// FocusInput.jsx
import { useRef } from 'react';

export default function FocusInput() {
  const inputRef = useRef(null);

  const handleClick = () => {
    inputRef.current.focus();
  };

  return (
    <div>
      <input ref={inputRef} type="text" />
      <button onClick={handleClick}>Enfocar Input</button>
    </div>
  );
}

En este ejemplo, inputRef es una referencia que se asigna al elemento <input>. Al hacer clic en el botón, se llama a inputRef.current.focus() para enfocar el campo de entrada.

Persistencia de valores entre renderizados

Además de manejar referencias del DOM, useRef también puede almacenar valores mutables que persisten entre renderizados sin causar un re-renderizado. Esto es útil para mantener valores como contadores, temporizadores, o cualquier valor que necesite persistir sin estar en el estado.

// Timer.jsx
import { useRef, useEffect, useState } from 'react';

export default function Timer() {
  const [count, setCount] = useState(0);
  const intervalRef = useRef(null);

  useEffect(() => {
    intervalRef.current = setInterval(() => {
      setCount(prevCount => prevCount + 1);
    }, 1000);

    return () => clearInterval(intervalRef.current);
  }, []);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => clearInterval(intervalRef.current)}>Stop Timer</button>
    </div>
  );
}

En este ejemplo, intervalRef se utiliza para almacenar la referencia al intervalo. Al desmontar el componente, el intervalo se limpia utilizando clearInterval(intervalRef.current). La referencia intervalRef permite persistir el identificador del intervalo entre renderizados sin causar un re-renderizado adicional.

Casos de uso comunes

  • Manipulación del DOM: Acceder y manipular elementos del DOM directamente, como enfocar, seleccionar texto, o cambiar estilos.
  • Persistencia de valores: Almacenar valores que no deberían causar un re-renderizado, como identificadores de intervalos, contadores, o cualquier valor mutable que no afecta la UI directamente.
  • Interacción con APIs externas: Mantener referencias a objetos que interactúan con APIs externas, como WebSocket, eventos personalizados, o cualquier objeto que necesite persistir entre renderizados.

Consideraciones

  • useRef no notifica cuando su contenido cambia. Si necesitas notificaciones o desencadenar re-renderizados, considera utilizar useState o useReducer.
  • Es importante diferenciar cuándo utilizar useRef en lugar de useState. useRef es ideal para valores que no afectan la renderización de la UI, mientras que useState debería usarse para valores que sí afectan la UI.

En resumen, useRef es una herramienta versátil en React para manejar referencias del DOM y persistir valores entre renderizados, ofreciendo una forma eficiente de interactuar con elementos del DOM y almacenar valores sin causar re-renderizados innecesarios.

Buenas prácticas de useContext, useReducer, useRef

Para maximizar la eficiencia y mantenibilidad de aplicaciones React que hacen uso de useContext, useReducer y useRef, es crucial seguir una serie de buenas prácticas. Estas prácticas no solo mejoran el rendimiento, sino que también facilitan la comprensión y el mantenimiento del código.

useContext

Contextos específicos y bien definidos: Evita crear contextos demasiado generales que manejen múltiples estados dispares. En su lugar, crea contextos específicos para diferentes partes de tu aplicación. Por ejemplo, un contexto para la autenticación y otro para la configuración de la aplicación.

Memoización del valor del contexto: Utiliza useMemo para memoizar el valor pasado al proveedor del contexto. Esto evita renders innecesarios de los componentes consumidores cuando el valor del contexto no ha cambiado.

// context/AuthContext.js
import { createContext } from "react";

const AuthContext = createContext();

export default AuthContext
// providers/AuthProvider.jsx
import { useMemo, useState } from 'react';
import AuthContext from '../../context/AuthContext';

export default function AuthProvider({ children }) {
  const [user, setUser] = useState(null);

  const value = useMemo(() => ({ user, setUser }), [user]);

  return (
    <AuthContext.Provider value={value}>
      {children}
    </AuthContext.Provider>
  );
}
  • Evitar anidar múltiples contextos: Si necesitas varios contextos, considera usar un patrón de composición para evitar la anidación profunda de proveedores, lo cual puede dificultar la lectura y el mantenimiento del código.
// providers/AppProviders.jsx
const AppProviders = ({ children }) => (
  <AuthProvider>
    <GlobalProvider>
      {children}
    </GlobalProvider>
  </AuthProvider>
);

export default AppProviders
// components/YourComponent/YourComponent.jsx
import { useContext } from 'react';
import GlobalContext from '../../context/GlobalContext';
import AuthContext from '../../context/AuthContext';

export default function YourComponent() {
  const { state, dispatch } = useContext(GlobalContext);
  const { user, setUser } = useContext(AuthContext);

  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>Increment</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>Decrement</button>
      <p>User: {user}</p>
      <button onClick={() => setUser('John')}>Set User</button>
    </div>
  );
}
// App.jsx
import AppProviders from './providers/AppProviders';
import YourComponent from './components/YourComponent/YourComponent';

export default function App() {
  return (
    <AppProviders>
      <YourComponent />
    </AppProviders>
  );
}

useReducer

  • Descomponer reducers complejos: Si tu reducer maneja múltiples tipos de acciones, considera descomponerlo en varios reducers más pequeños y combinarlos utilizando el patrón de "reducers combinados".
// utils/combineReducers.js
export default function combineReducers(reducers) {
  return function combination(state = {}, action) {
    const nextState = {};
    for (const key in reducers) {
      nextState[key] = reducers[key](state[key], action);
    }
    return nextState;
  };
}
// reducers.js
import { SET_THEME, SET_USER } from "../../consts/actions.const";

export function userReducer(state, action) {
  switch (action.type) {
    case SET_USER:
      return { name: action.payload };
    default:
      return state;
  }
}

export function settingsReducer(state, action) {
  switch (action.type) {
    case SET_THEME:
      return { ...state, theme: action.payload };
    default:
      return state;
  }
}
// CombinedReducers.jsx
import { useReducer } from 'react';
import { settingsReducer, userReducer } from './reducers';
import combineReducers from '../../utils/combineReducers';
import { SET_THEME, SET_USER } from '../../consts/actions.const';

const combinedReducer = combineReducers({
  user: userReducer,
  settings: settingsReducer,
});

const initialState = {
  user: {
    name: null
  },
  settings: {
    theme: 'light'
  }
}
export default function CombinedReducers() {
  const [{ user, settings }, dispatch] = useReducer(combinedReducer, initialState)

  return (
    <>
      <div>
        <p>User: {user.name}</p>
        <p>Theme: {settings.theme}</p>
      </div>
      <button onClick={() => dispatch({ type: SET_USER, payload: 'John Doe' })}>Set User</button>
      <button onClick={() => dispatch({ type: SET_THEME, payload: 'dark' })}>Set Theme</button>
    </>
  )
}
  • Acciones bien definidas: Define tus acciones de manera clara y específica. Utiliza constantes para los tipos de acción para evitar errores tipográficos y facilitar el mantenimiento.
// const/actions.const.js
export const SET_USER = Symbol('set_user');
export const SET_THEME = Symbol('set_theme');
  • Estado inicial bien estructurado: Define un estado inicial claro y estructurado. Esto facilita la comprensión de la forma del estado y asegura que todos los valores iniciales estén correctamente establecidos.
const initialState = {
  user: {
    name: null
  },
  settings: {
    theme: 'light'
  }
}

useRef

  • Uso adecuado para referencias del DOM: Utiliza useRef para acceder a elementos del DOM cuando sea necesario, pero evita abusar de este patrón. Solo usa useRef cuando la manipulación directa del DOM sea imprescindible.
// components/Modal.jsx
import { useRef } from 'react';

export default function Modal({ children }) {
  const modalRef = useRef(null);

  const openModal = () => modalRef.current.showModal();
  const closeModal = () => modalRef.current.close();

  return (
    <>
      <button onClick={openModal}>Open</button>
      <dialog ref={modalRef}>
        {children}
        <button onClick={closeModal}>Close</button>
      </dialog>
    </>
  );
}
  • Persistencia de valores no renderizados: Usa useRef para almacenar valores que deben persistir entre renderizados sin causar un re-renderizado, como temporizadores o contadores de intentos.
// components/RetryButton.jsx
import { useRef, useState } from 'react';

export default function RetryButton() {
  const retryCount = useRef(0);
  const [message, setMessage] = useState('');

  const handleClick = () => {
    retryCount.current += 1;
    setMessage(`Intento ${retryCount.current}`);
  };

  return (
    <div>
      <button onClick={handleClick}>Retry</button>
      {message && <p>{message}</p>}
    </div>
  );
}
  • Evitar la manipulación directa del estado: No uses useRef para manipular el estado de los componentes directamente. Utiliza useState o useReducer para cualquier estado que afecte la renderización de la UI para mantener la claridad y previsibilidad del comportamiento del componente.
Certifícate en React con CertiDevs PLUS

Ejercicios de esta lección Hooks para gestión de estado complejo y contexto

Evalúa tus conocimientos de esta lección Hooks para gestión de estado complejo y contexto con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.

Todas las lecciones de React

Accede a todas las lecciones de React y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.

Introducción A React Y Su Ecosistema

React

Introducción Y Entorno

Instalar React Y Crear Nuevo Proyecto

React

Introducción Y Entorno

Introducción A Jsx

React

Componentes

Introducción A Componentes

React

Componentes

Componentes Funcionales

React

Componentes

Eventos En React

React

Componentes

Props Y Manejo De Datos Entre Componentes

React

Componentes

Renderizado Condicional

React

Componentes

Renderizado Iterativo Con Bucles

React

Componentes

Manejo De Clases Y Estilos

React

Componentes

Introducción A Los Hooks

React

Hooks

Estado Y Ciclo De Vida De Los Componentes

React

Hooks

Hooks Para Manejo De Estado Y Efectos Secundarios

React

Hooks

Hooks Para Gestión De Estado Complejo Y Contexto

React

Hooks

Hooks Para Optimización Y Actualizaciones Concurrentes

React

Hooks

Introducción A React Router

React

Navegación Y Enrutamiento

Definición Y Manejo De Rutas

React

Navegación Y Enrutamiento

Rutas Anidadas Y Rutas Dinámicas

React

Navegación Y Enrutamiento

Navegación Programática Y Redireccionamiento

React

Navegación Y Enrutamiento

Nuevos Métodos Create De React Router

React

Navegación Y Enrutamiento

Solicitudes Http Con Fetch Api

React

Interacción Http Con Backend

Solicitudes Http Con Axios

React

Interacción Http Con Backend

Estado Local Con Usestate Y Usereducer

React

Servicios Y Gestión De Estado

Estado Global Con Context Api

React

Servicios Y Gestión De Estado

Estado Global Con Redux Toolkit

React

Servicios Y Gestión De Estado

Custom Hooks Para Servicios Compartidos

React

Servicios Y Gestión De Estado

Certificados de superación de React

Supera todos los ejercicios de programación del curso de React y obtén certificados de superación para mejorar tu currículum y tu empleabilidad.

En esta lección

Objetivos de aprendizaje de esta lección

  • Comprender cómo compartir estado global entre componentes con useContext.
  • Implementar lógica de estado complejo utilizando useReducer.
  • Manipular elementos del DOM y persistir valores entre renderizados con useRef.
  • Aplicar buenas prácticas para el uso eficiente y mantenible de useContext, useReducer, y useRef.