La gente dice que React.js hace que las cosas fáciles sean difíciles y que las difíciles sean fáciles. Me encanta ese dicho, es tan cierto 😓. Algunos ejemplos de ello:
¿Por qué es tan difícil compartir algunos datos en toda la aplicación?
¿Por qué es tan difícil pasar datos entre componentes? También conocidas props. Las props o propiedades se usan cuando quieres pasar datos entre un padre y un hijo ¿Pero qué pasa si tenemos que ir más lejos? Es un infierno.
¿¿Redux?? Es demasiado.
Tener una aplicación global centralizada: en lugar de limitarte a los estados locales en las vistas, ahora puede compartir datos en un componente principal y sus componentes relativos (hijos, nietos y así). El estado centralizado se llama store y podemos extenderlo/propagarlo utilizando el Context.Provider.
Propagación y re-renderizado de datos: cuando este estado centralizado llamado estado global (store) cambia, desencadena una re-renderización de todos los componentes hijos (tu aplicación completa) lo que genera nuevos datos para mostrar en la UI. Un setState pero central.
Si ya has trabajado con React, probablemente hayas sentido la frustración de pasar propiedades en toda tu aplicación, nosotros lo llamamos el "infierno de propiedades".
El concepto detrás es muy simple: hay un solo y gran proveedor que provee información para muchos consumidores, no hay límites en la cantidad de consumidores.
Cada vez que los datos del proveedor cambian, todos los consumidores reciben una notificación. Es muy similar a cómo funciona la señal de TV. Un canal de TV emite una señal de datos y todas las antenas de TV consumen esa señal, reciben el nuevo contenido y renderizan la imagen en los televisores.
Todo el mundo tiene acceso al contexto global ahora.
El store es ahora la pieza más delicada de información de nuestra aplicación, y es muy suceptible a malos usos, es decir, un cambio malo y toda la aplicación se vendrá abajo. Para evitar este posible escenario debemos asegurarnos que la información de nuestro store sea read-only para los consumidores, y que solo pueda actualizarse nuevamente con un conjunto limitado de funciones. Como un state normal, no cambiamos el state, establecemos uno nuevo. Este paradigma arquitectónico se llama Flux.
Debemos separar el store de las actions y las views (componentes) y asegurarnos de que todas las views o vistas llaman a acciones para actualizar el store. Nunca cambiaremos el store directamente desde una vista. Estoy siendo redundante, lo sé... lo hago a propósito...
store
y actions
.useContext()
.De acuerdo, después de un par de horas para hacer la implementación de la API de contexto más simple sin usar enlaces... ¡Esto es lo que obtuve en 5 pasos simples!:
createContext
desde React. Ese objeto se compartirá con todos los consumidores durante el tiempo de vida de la aplicación, contendrá la aplicación del store y actions.AppContext.js
1// Paso 1: Define un contexto que se compartirá dentro de toda la aplicación. 2 3import React from 'react'; 4 5const AppContext = React.createContext(null);
ContextWrapper
que utilizaremos para pasarle el context (paso 1) a los consumidores. En el estado del ContextWrapper
declaramos nuestro global state inicial, que incluye la información (store) y las funciones (actions).Nota: Debemos importar tanto el
AppContext
cómo elContextWrapper
.
1// Paso 2: Crea un componente ContextWrapper que debe ser el padre de cada consumidor 2 3import React, { useState } from 'react'; 4 5export const AppContext = React.createContext(null); 6 7export const ContextWrapper = (props) => { 8 const [ store, setStore ] = useState({ 9 todos: ["Make the bed", "Take out the trash"] 10 }); 11 const [ actions, setActions ] = useState({ 12 addTask: title => setStore({ ...store, todos: store.todos.concat(title) }) 13 }); 14 15 return ( 16 <AppContext.Provider value={{ store, actions }}> 17 {props.children} 18 </AppContext.Provider> 19 ); 20}
ContextWrapper
para que todos sus componentes hijos tengan acceso al Context.Consumer
. En este breve ejemplo usaremos el componente <TodoList />
como componente principal (la declaración está en el último paso).index.js
1// Paso 3: Ubica tu componente principal dentro del contenedor ContextWrapper, 2 3import React from 'react'; 4import ReactDOM from 'react-dom'; 5 6import { ContextWrapper } from 'path/to/AppContext.js'; 7import TodoList from 'path/to/TodoList'; 8 9const MyView = () => ( 10 <ContextWrapper> 11 <TodoList /> 12 </ContextWrapper> 13 ); 14 15ReactDOM.render(<MyView />, document.querySelector("#app"));
TodoList
sabiendo que podemos usar el hook useContext()
para leer el store desde el global state (no se necesitan props). En este caso, el componente renderizará los to-dos y también podrá añadir nuevas tareas a la lista.1// Paso 4: Declara una variable con el hook useContext(), después úsalo como un objeto para acceder al código interno 2 3import React, { useContext } from 'react'; 4import { AppContext } from 'path/to/AppContext.js'; 5 6export const TodoList = () => { 7 const context = useContext(AppContext) 8 return <div> 9 {context.store.todos.map((task, i) => (<li key={i}>{task}</li>))} 10 <button onClick={() => context.actions.addTask("I am the task " + context.store.todos.length)}> + add </button> 11 </div> 12}
Muy seguido veremos el hook useContext
del ejemplo de arriba
1const context = useContext(AppContext); 2return <div> 3 {context.store.todos.map((task, i) => (<li key={i}>{task}</li>))} 4 <button onClick={() => context.actions.addTask("I am the task " + context.store.todos.length)}> + add </button> 5</div>
En su variante desestructurada. Presta atención a cómo eso también simplifica la forma en que accedemos al store:
1const {store, actions} = useContext(AppContext); 2return <div> 3 {store.todos.map((task, i) => (<li key={i}>{task}</li>))} 4 <button onClick={() => actions.addTask("I am the task " + store.todos.length)}> + add </button> 5</div>