Gestionar estado en React

0
104
  1. Introducción
  2. Props drilling
  3. El estado global
  4. Context
  5. Reducers
  6. Meta frameworks
  7. Gestión de estado a través de URL
  8. Conclusiones

Introducción

Si llevas bastante tiempo desarrollando aplicaciones web sabéis que se hacen muchas bromas de que cada día sale un nuevo framework de Javascript. Y es verdad 😂. Pero lo que más sorprende es la cantidad de librerías para gestionar estado en React que salen a menudo. Por poner como un ejemplo solo las más populares:

Todas las librerías de estado se puede dividir en 3 grandes grupos: basadas en reducers («reduced based»), atómicas («atomic based») y orientadas a mutar el estado («mutable based»).

Las librerías basadas en reducers utilizan las funciones («reducer») para cambiar un estado global. Los usuarios de estas librerías tienen que mandar «mensajes» («actions») al reducer para que cambie un estado centralizado («single source truth»). La más famosa (y la más compleja :=) es React-Redux. Últimamente ha ganado mucha popularidad Zustand.

Las librerías «atómicas» dividen un estado global en pequeñas piezas («átomos») que son accesibles a través de React hooks. Por ejemplo, RecoilJotai.

// atoms/counter.js
import { atom } from "recoil"; 

const counterAtom = atom({ key: "value", default: 0 }); 
export default counterAtom;
 
// index.js 
import React from "react"; 
import ReactDOM from "react-dom"; 
import { RecoilRoot } from "recoil"; 
import App from "./App"; 

ReactDOM.render(<RecoilRoot> <App /> </RecoilRoot>, 
document.getElementById("root")); 

// App.js 
import React from "react"; 
import { useRecoilState } from "recoil"; 
import counterAtom from "./atoms/counter"; 

const App = () => { 
const [value, setValue] = useRecoilState(counterAtom); 
return (<div> 
           <button onClick={() => setValue(value + 1)}>Increment</button>
           <span>{value}</span> 
           <button onClick={() => setValue(value - 1)}>Decrement</button> 
        </div>); }; 
export default App;

Un ejemplo de uso de Recoil

Las librería «mutables» nos proporcionan un objeto («proxy») que nos permite cambiar estado directamente o suscribirnos a él. Así son MobX y Valtio.

Como vemos hay muchas maneras de gestionar estado en nuestra aplicación de React.

Pero ¿es necesario realmente utilizar una librería para manejar el estado de una aplicación? Si cada componente ya tiene su propio estado para qué necesitamos crear un estado global?

Props drilling

Cada componente puede tener su propio estado y pasar parte de ello a sus hijos.

export default function Site({ username }) {
    return (<div><Article username={username}/></div>);
}

Si tenemos muchos componente dentro del árbol de nuestra aplicación que comparten estado tendremos que pasar muchas props entre diferentes niveles. Al final esta situación puede volverse insostenible.

Si tenemos el nombre de usuario en el estado del componente superior y lo tenemos que mostrar en un componente a 2 niveles «abajo» tendríamos que pasarlo a todos los componentes «descendientes».

export default function Site() {
  const { username, setUserName } = useState('')
  return (<Article>username={username}></Article>);
}

export default function Article({ username }) {
 return (<Paragraph>username={username}</Paragraph>);
}

export default function Paragraph({ username }) {
 return (<div><span>username</span></div>);
}

En una aplicación «real» el estado va a ser muchísimo más complejo y tendríamos 3, 4 y 5 niveles de componentes. El código de app se convierte demasiado verboso.  😱

Nuestra aplicación puede parecer una «cebolla» con un montón de capas. En este caso puede que sea difícil compartir la información entre un padre y sus «nietos» o «bisnietos». Tendríamos que pasar la misma información entre todos los niveles intermedios.

Un estado global

La solución más evidente es crear un estado global y compartir entre todos los componentes. Es lo que hacen las librerías tan populares como Redux o Zustand. Utilizándolas es bastante fácil compartir estado dentro de cualquier componente. También se simplifica la lógica de estado al estar un único sitio. Es un almacén global donde residen tus datos. Cada vez que necesitas actualizar tus datos, envías una acción («un mensaje») que va a la función «reducer». Dependiendo del tipo de acción, el reducer actualiza el estado de forma inmutable (devolviendo).

La desventaja de las librerías de este tipo es su complejidad. Incluso los creadores de Redux lo reconocieron y crearon un librería – helper reduxjs/toolkit que simplifica el uso de Redux.

// slices/counter.tsx

import { createSlice } from '@reduxjs/toolkit';
export const slice = createSlice({
               name:'counter',
               initialState: { value: 0 },
               reducers: {
                 increment: (state) => { state.value+=1 },
                 decrement: (state) => { state.value-=1 }},
                });

export const actions = slice.actions;
export const reducer = slice.reducer;

// store.tsx

import { configureStore } from '@reduxjs/toolkit';
import { reducer as counterReducer } from './slices/counter';

export const store = configureStore({reducer: { counter: counterReducer}});

export type CounterState = ReturnType<typeof store.getState>

// index.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import App from './App';

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

// App.tsx
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { actions } from './slices/counter';
import { store, CounterState }  from './store';
const App = () => {
const count = useSelector((state: CounterState) => state.counter.value);
const dispatch = useDispatch();
return (<div>
           <button onClick={() => dispatch(actions.increment())}>Increment</button>
           <span>{count}</span>
           <button onClick={() => dispatch(actions.decrement())}>Decrement</button>
        </div>)
};

export default App;

Un ejemplo con ‘@reduxjs/toolkit’

Como vemos incluso para un ejemplo simple tenemos que escribir mucho código. Inicializar un store («almacén»), definir un slice («un conjunto de lógica de reducers y actions») etc. Merece la pena si tienes un estado que tiene lógica interna muy compleja y necesitas plasmarla.

Context

Una de las formas en que podemos evitar el prop drilling en React es a través del ReactContext, es una forma de pasar datos entre el árbol de componentes sin tener que pasar manualmente las props en cada nivel.

Primero debemos crear un contexto con React.createContext(). Y luego podrás obtener el valor usando el hook useContext().

const ThemeContext = React.createContext()

function MyPage() {
return (
   <ThemeContext.Provider value="dark">
    <Form />
  </ThemeContext.Provider>);
}

function Form() {
  const themeValue = useContext(ThemeContext)
  //...podemos utilizar el valor de themeValue
}

Este método se puede utilizar si «el contexto» de nuestra aplicación es bastante sencillo y no necesitamos pasar mucha información ente diferentes niveles.

Los reducers

Una de la ventajas de las librerías centralizadas es «reducer«. Nos permite tener toda la lógica de cambios de estado en un solo lugar. De alguna manera «centraliza» nuestra lógica.  Tanto es así que los creadores de React introdujeron un especial hook que nos permite crear los reducers sin tener que importar las librerías de terceros. Estoy hablando de useReducer().

Resumiendo su funcionalidad, useReducer nos facilita tener nuestro propio reducer fuera nuestro componente.

import { useReducer } from 'react';

function reducer(state, action) {
  if(action.type === 'INCREASE_MY_FORTUNE') {
    return { moneyAmount: state.moneyAmount + 1.0 }
  };
  throw Error('Unknown action.');
}

function MyComponent() {
  const [state, dispatch] = useReducer(reducer, { moneyAmount: 5000.0 });
  
  function handleClick() {
    dispatch({type: 'INCREASE_MY_FORTUNE'});
  }

  return (<div>
            <button onClick = {handleClick}>Increase muy fortune!!</button>
          </div>);
}

Utilizando el hook useReducer podemos lanzar las acciones que cambian nuestro estado. Es una manera de gestionar estado muy parecida a las librerías tipo Redux pero usando el hook estándar de React. Podría ser una alternativa para los que no quieren importar las librerías externas.

Los metaframeworks

La popularidad de los frameworks «fullstack» también facilita la gestión de estado. Los frameworks «fullstack» nos permiten separar la lógica de nuestra app en dos partes: la parte de «cliente» y la parte de «back». En el mundo React el framework «fullstack» más famoso es NextJs. Este framework te permite crear los componentes que se ejecutan en el servidor (Server Components). Además existe la posibilidad de crear solo algunas funciones que se vayan a ejecutar en servidor (Server Actions). Por cierto, aquí en Adictos tenemos los tutoriales sobre Server Components y Server Actions donde se explica en profundidad el concepto de Server Components/Server Actions.

Para crear una Server Action simplemente basta con poner ‘use server’.

//Server Action
'use server'

export async function fetchApi() {
   // ...
}
//Client Component
import { fetchApi } from '@/app/actions'

export function Button() {
   return (
     <button onClick={fetchApi}></button>
   )
}

Por eso toda la lógica de las llamadas al servidor no va a afectar a tus estados internos de los componentes. Se puede sacar mucha lógica fuera de componentes que simplifica toda la gestión de estado. Muchas aplicaciones web tienen poca lógica que se ejecuta en «el cliente». Por ejemplo, los formularios, drag-n-drop, los switches etc. Pero este tipo de lógica depende de cada componente y no necesita estar «globalizada», porque no afecta a otros componentes. Así que en cuanto más lógica esté en la parte de «back-end» más simple será la lógica del «cliente».

Gestión de estado a través de URL

Además siempre se puede volver a las raíces. Muchas veces se puede guardar el estado global de nuestra app simplemente utilizando la URL y los query params. Una URL puede contener el estado requerido en forma de ruta y la cadena de parámetros de búsqueda (query params). Los query params son particularmente poderosos ya que son completamente genéricos y customizables.

Gracias a la API URLSearchParams, es posible manipular la query string sin tener que ir de ida y vuelta al servidor. Éste es un primitivo sobre el cual podemos construir. Siempre que no se exceda el límite de URL (alrededor de 2000 caracteres), somos libres de conservar el estado en una URL.

Además React-Router tiene un hook useSearchParam que simplifica muchísimo todo el trabajo con la URL.

import { useSearchParams } from "react-router-dom";

export function getProductId() {
  const [searchParams] = useSearchParams();
  const productId = searchParams.get("productId");
  return { productId };
}

Conclusiones

Sí que es verdad que en el mundo de React existen multitudes de librerías que ayudan a gestionar el estado de nuestra aplicación. Pero antes de meter cualquier dependencia de terceros y aumentar el bundle de la app tenemos que pensar si es realmente necesario.

Si tu app va a tener un estado complejo con muchas reglas puede merecer la pena implementar la lógica de estado con la ayuda de la librerías tan pesadas como Redux.

Pero existen otras alternativas más simples como Zustand o Recoil. Incluso la propia librería React ahora tiene los hooks que nos facilitan la gestión del estado para la mayoría de los casos.

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad