React, React.js o ReactJS es una biblioteca de JavaScript para construir interfaces de usuario, que ayuda a los desarrolladores a construir aplicaciones en las que los datos cambian continuamente. En este post, haremos un acercamiento a React Hooks, la funcionalidad incorporada en la versión 16.8 que permite aprovechar React sin escribir clases:
Qué vas as ver en esta entrada
Gestión de estado en React Redux
Como sabemos, React distingue dos tipos de componentes, los componentes de clase y los componentes funcionales. Los componentes de clase poseen estado y tienen definido un ciclo de vida, mientras que los componentes funcionales no disponen de ninguna de estas dos cosas.
En aplicaciones no triviales es necesaria la gestión de estado para evitar efectos laterales. Pensemos, por ejemplo, en un reproductor de vídeos, en el que cada elemento sea un componente (el área de reproducción, los botones de control, la sección de comentarios, la lista de reproducción, el login, …). Estos componentes se relacionan entre sí, de modo que la acción del usuario en uno de ellos afecta el estado de los restantes. Así que el cambio en el código de un componente se hace más difícil, si esta interacción no es gestionada de una manera adecuada.
En el ecosistema React, esta tarea en general la realiza Redux. Redux es una librería para la gestión del estado global de nuestra aplicación JS cuyo objetivo es realizar los cambios de estado de nuestra aplicación de una manera predecible. Se basa en tres principios:
- Centralización de la información del estado (single source of truth): el estado global de nuestra aplicación se guarda en un objeto JavaScript llamado Archivo (store).
- Las interacciones en la UI que involucren operaciones con el estado generan unas acciones que se pasan al Archivo mediante la emisión de acciones.
- El procesamiento de estas acciones (y por tanto el cambio de estado) se implementa mediante funciones puras llamadas reducers. Estas funciones (que pueden componerse entre sí) generan un nuevo objeto con el estado a partir del estado actual y de la acción generada.
El punto crucial es que las funciones de acceso sean puras o deterministas. Una función pura no depende de ninguna información previa aparte de los argumentos (es decir, no depende de ninguna variable de estado), ni produce efectos laterales (llamadas HTTP, modificación del DOM de la página web, reproducción de contenido multimedia, etc). En suma, si se llama a una función pura con los mismos argumentos, devuelve siempre la misma salida.
Entonces, ¿cómo podemos gestionar el estado, si las funciones que se encargan de modificarlo deben ser puras y no basarse en variables de estado? Formalizando las transiciones de estado. Si definimos para cada par estado-acción el nuevo estado al que se va a transitar, esa función será determinista: definirá las transiciones de estado, pero no dependerá de una variable de estado que pueda modificarse por otros componentes. En suma, la idea es convertir los cambios de estado de nuestra aplicación en algo así como una máquina de estados finitos.
Ejemplo de gestión de estado con Redux
Vamos a ilustrar cómo lo hace Redux. Nuestra aplicación de ejemplo se limitará a un contador con botones para incrementarlo o decrementarlo en una unidad. El estado de la aplicación se limita, pues, al valor del contador.
En el fichero index.js declararemos el uso del componente principal App y su renderización:
Index.js
import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import App from './App'; import store from './app/store'; import { Provider } from 'react-redux'; ReactDOM.render( <React.StrictMode> <Provider store={store}> <App /> </Provider> </React.StrictMode>, document.getElementById('root') );
Este componente principal se define en el fichero App.js mediante JSX. Aquí usaremos otro componente, el del contador propiamente dicho, llamado Counter:
App.js
import React from 'react'; import logo from './logo.svg'; import { Counter } from './features/counter/Counter'; import './App.css'; function App() { return ( <div className="App"> <header className="App-header"> <img src={logo} className="App-logo" alt="logo" /> <Counter /> </header> </div> ); } export default App;
A su vez, el JSX del componente Counter está recogido en Counter.js:
Counter.js
import React from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { decrement, increment, selectCount, } from './counterSlice'; import styles from './Counter.module.css'; export function Counter() { const count = useSelector(selectCount); const dispatch = useDispatch();
return ( <div> <div className={styles.row}> <button className={styles.button} aria-label="Increment value" onClick={() => dispatch(increment())} > + </button> <span className={styles.value}>{count}</span> <button className={styles.button} aria-label="Decrement value" onClick={() => dispatch(decrement())} > - </button> </div> </div> ); }
Cada botón, al ser pulsado, emite (dispatch) una acción determinada, que puede ser de incremento o de decremento. Redux se encarga de generar el nuevo estado, a partir de la acción emitida y del estado actual, recurriendo a los reducers. Para la implementación de las acciones y los reducers emplearemos una herramienta llamada Redux Toolkit, que simplifica el desarrollo:
counterSlice.js
import { createSlice } from '@reduxjs/toolkit'; export const counterSlice = createSlice({ name: 'counter', initialState: { value: 0, }, reducers: { increment: state => { state.value += 1; }, decrement: state => { state.value -= 1; }, }, }); export const { increment, decrement } = counterSlice.actions; export const selectCount = state => state.counter.value; export default counterSlice.reducer;
Como podemos apreciar, el cambio de estado está centralizado y se realiza sólo mediante las funciones de acceso puras: cada vez que se llama a estas funciones se devuelve un valor nuevo en función de la entrada.
¿Qué es Hooks?
Hooks es una nueva funcionalidad ofrecida por la propia librería React a partir de la versión 16.8. Supone una forma distinta de manejar el estado y el ciclo de vida de los componentes React. Para hacerse una idea rápida, Hooks “engancha” comportamiento dependiente de estado en componentes funcionales. Además, Hooks permite compartir y desacoplar de manera sencilla la lógica de nuestros componentes para poder reutilizarla sin duplicar código.
La declaración de un hook tiene esta forma:
const [infoEstado, modificadorEstado] = nombreHook(estadoInicial);
En este caso recibimos desestructuradas dos referencias, infoEstado, que podemos usar para visualizar información de estado, y modificadorEstado, que es la función a la que llamaremos para modificar el estado. Para los lectores acostumbrados a Java, podemos pensar en estas dos referencias como algo lejanamente parecido al getter y setter de un bean.
Vamos a añadir en Counter.js un botón que nos permita establecer una cantidad base para añadir o restar al contador. Ahora el estado de la aplicación consta de dos informaciones: el valor actual del contador y la cantidad de incremento/decremento. En este caso, en vez de Redux vamos a recurrir a Hooks. En concreto vamos a usar el hook llamado useState, que nos permite acceder a información del estado:
const [incrementAmount, setIncrementAmount] = useState('2');
Con esta línea definimos incrementAmount, la cantidad de incremento/decremento, y setIncrementAmount, que nos permitirá modificar esta cantidad. Se ha definido ‘2’ como valor de estado inicial.
Counter.js
import React, { useState } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { decrement, increment, selectCount, } from './counterSlice'; import styles from './Counter.module.css'; export function Counter() { const count = useSelector(selectCount); const dispatch = useDispatch(); const [incrementAmount, setIncrementAmount] = useState('2'); return ( <div> <div className={styles.row}> <button className={styles.button} aria-label="Increment value" onClick={() => dispatch(increment())} >
+ </button> <span className={styles.value}>{count}</span> <button className={styles.button} aria-label="Decrement value" onClick={() => dispatch(decrement())} > - </button> </div> <div className={styles.row}> <input className={styles.textbox} aria-label="Set increment amount" value={incrementAmount} onChange={e => setIncrementAmount(e.target.value)} /> </div> </div> ); }
Fijémonos en el código:
onChange={e => setIncrementAmount(e.target.value)}
Ya no emitimos una acción, sino que modificamos el estado mediante el hook. No nos hace falta definir la acción, ni el reducer correspondiente. Estas funcionalidades, propias de Redux, pueden ser evitadas mediante Hooks, que es React a secas.
De cualquier modo, Hooks y Redux pueden convivir en la misma aplicación. Por ejemplo, si ahora queremos utilizar incrementAmount que hemos modificado, podríamos incluir un botón nuevo a los que teníamos empleando Redux, para lo cual debemos crear una nueva acción incrementByAmount:
En este caso Counter.js quedaría:
counter.js
import React, { useState } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { decrement, increment, incrementByAmount, selectCount, } from './counterSlice'; import styles from './Counter.module.css'; export function Counter() { const count = useSelector(selectCount); const dispatch = useDispatch(); const [incrementAmount, setIncrementAmount] = useState('2');
return ( <div> <div className={styles.row}> <button className={styles.button} aria-label="Increment value" onClick={() => dispatch(increment())} > + </button> <span className={styles.value}>{count}</span> <button className={styles.button} aria-label="Decrement value" onClick={() => dispatch(decrement())} > - </button>
</div> <div className={styles.row}> <input className={styles.textbox} aria-label="Set increment amount" value={incrementAmount} onChange={e => setIncrementAmount(e.target.value)} /> <button className={styles.button} onClick={() => dispatch(incrementByAmount(Number(incrementAmount) || 0)) } > Add Amount </button> </div> </div> ); }
Y por su parte, en counterSlice.js tendremos que añadir el correspondiente reducer para incrementByAmount:
counterSlice.js
import { createSlice } from '@reduxjs/toolkit'; export const counterSlice = createSlice({ name: 'counter', initialState: { value: 0, }, reducers: { increment: state => { state.value += 1; }, decrement: state => { state.value -= 1; }, incrementByAmount: (state, action) => { state.value += action.payload; }, }, });
export const { increment, decrement, incrementByAmount } = counterSlice.actions; export const selectCount = state => state.counter.value; export default counterSlice.reducer;
Cómo sacar el máximo potencial a React con Hooks
Hemos podido comprobar que con React Hooks la gestión del estado se simplifica enormemente. Ahora bien, Hooks ofrece otras funcionalidades que no se limitan al estado en sentido estricto. Además de useState, React ofrece otros dos hooks básicos: useEffect y useContext.
El hook useEffect está orientado a operaciones que impliquen efectos secundarios, como las peticiones de datos, el establecimiento de suscripciones o las actualizaciones manuales del DOM. El código definido como efecto se ejecuta después de que React haya actualizado el DOM. Por ejemplo, podríamos definir un efecto que modifique el título de la página en función del estado:
import React, { useState, useEffect } from 'react'; function Example() { const [count, setCount] = useState(2); useEffect(() => { document.title = `You clicked ${count} times`; }); return ( <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> </div> ); }
También mencionaremos el hook useContext, que nos permite pasar datos a través del árbol de componentes sin tener que transmitirlos a través de cada uno de los hijos. Este hook crea un estado global que pueda compartirse entre componentes, lo que puede ser muy útil para tareas de autenticación, uso de temas CSS, localización, etc.
Además de estos hooks básicos, React ofrece otros adicionales como useReducer, useCallback, useMemo o useRef, o incluso podemos definir nuestros propios hooks para extraer lógica que se use en varios componentes y atenernos al principio D.R.Y (Don’t Repeat Yourself). No tenemos espacio para hablar de todo ello; os invitamos a que visitéis la página oficial de React.
Conclusión
Hooks, la nueva funcionalidad de React, no busca reemplazar los conceptos previamente existentes en React (ciclo de vida, estado, props, contexto, refs…). Como hemos visto, las funcionalidades Redux pueden utilizarse junto con Hooks en un componente funcional que requiera acceder a información de estado en sentido general sin necesidad de redefinirlo como un componente de clase.
Redux y React Hooks, en suma, son complementarios. En proyectos pequeños nos pueden bastar funcionalidades de Hooks como useState, useContext o useReducer para gestionar adecuadamente el estado local del componente. Mientras que para proyectos más complejos (en lo que respecta al estado global) podemos recurrir a funcionalidades propias de Redux y, a la vez, mantener el estado local de componentes más pequeños mediante Hooks.
Puedes encontrar el código de ejemplo en este enlace.