React Context
Algoritmos III
Pero antes... hay otras opciones
Volvamos al ejemplo de Tareas
El ejemplo de Tareas
¿Qué pasa si vamos de la pantalla principal a la de asignación, cuando estamos en la segunda página?
al volver, perdemos el número de página en el que estábamos!
El ejemplo de Tareas
Necesitamos guardar las tareas y el número de página en un estado que sobreviva al alcance de la página principal de tareas.
tareas
numeroPagina
Solo vive mientras está activa la URL
El ejemplo de Tareas
En nuestro archivo de rutas vamos a definir un componente principal nuevo: PaginadorLayout, que va a tener principalmente la responsabilidad de almacenar un estado que compartirán la página de tareas y la asignación.
export const TareasRoutes = () =>
<Routes>
<Route path='/' element={<PaginadorLayout/>}>
<Route path='/' element={<TareasComponent />} />
<Route path='/asignarTarea/:id' element={<AsignarTareaComponent />} />
</Route>
</Routes>
tareas
numeroPagina
En PaginadorLayout
El ejemplo de Tareas
En el PaginadorLayout subimos las responsabilidades que antes tenía el componente principal de tareas. Eso incluye mostrar los mensajes de error cuando falla el backend.
export const PaginadorLayout = () => {
const [tareas, setTareas] = useState<Tarea[]>([])
const [hasMore, setHasMore] = useState(false)
const [page, setPage] = useState(1)
const { toast, showToast } = useToast()
const getTareas = async (newPage: number, init = false) => ...
const traerMasTareas = async () => ...
const traerTareas = async () => ...
const actualizarTarea = async (tareaActualizada: Tarea) => ...
useOnInit(traerTareas)
El ejemplo de Tareas
Y como componente no vamos a hacer mucho, simplemente vamos a
return <>
<Outlet context={{ tareas, setTareas, hasMore, traerMasTareas, actualizarTarea }}/>
<div id="toast-container">
<Toast toast={toast} />
</div>
</>
<Route path='/' element={<TareasComponent />} />
<Route path='/asignarTarea/:id' element={<AsignarTareaComponent />} />
El ejemplo de Tareas
Y como componente no vamos a hacer mucho, simplemente vamos a
return <>
<Outlet context={{ tareas, setTareas, hasMore, traerMasTareas, actualizarTarea }}/>
<div id="toast-container">
<Toast toast={toast} />
</div>
</>
<Route path='/' element={<TareasComponent />} />
<Route path='/asignarTarea/:id' element={<AsignarTareaComponent />} />
con un detalle extra: le pasamos tanto las funciones que permiten conocer el estado como las que permiten modificarlo
El ejemplo de Tareas
De esa manera, nuestro componente de tareas tiene muchas menos responsabilidades. Solo debe tomar del contexto lo que necesita:
Y usarlo dentro del JSX (es prácticamente un componente presentacional)
export const TareasComponent = () => {
const { tareas, hasMore, traerMasTareas, actualizarTarea } = useOutletContext<PaginadorContextType>()
return (
<div className='container'>
<br />
<div className="title">Tareas a realizar</div>
es el hook que provee React Router para capturar la información del store del contexto padre
El ejemplo de Tareas
El componente de asignación de tareas también aprovecha la función que actualiza una tarea, para mantener actualizado el mismo contexto que comparte con la página principal de tareas:
export const AsignarTareaComponent = () => {
const { actualizarTarea } = useOutletContext<PaginadorContextType>()
El mecanismo state/prop puede ser incómodo
Si tenemos una jerarquía extensa de componentes debemos pasar el state como prop y además una función que cambie el estado en el componente principal.
Principal
1
2
1.1
1.1.1
recibe como props el estado y la función para modificar el estado
El mecanismo state/prop puede ser incómodo
Context to the rescue
El Context actúa de memoria compartida entre los componentes.
Principal
1
2
1.1
1.1.1
Construyendo el Context
Es simplemente un objeto global que se construye con una función de React
export const Context = createContext()
Construyendo el Provider
export const Provider = ({ children }) => {
const [count, setCount] = useState(0)
const value = {
count,
increment: () => {
setCount(count + 1)
},
}
return (<Context.Provider value={value}>{children} </Context.Provider>)
Nuestros componentes son hijos del Provider
Recordemos que el Provider se define como un componente que espera children en sus props. Así que lo que hacemos es decorar nuestros componentes principales con el provider:
import { Provider } from './context/Context'
const App = () =>
<Provider>
<Contador />
<LogContador />
</Provider>
Y consumen estado y funciones del context
El hook useContext nos da la posibilidad de obtener una referencia al estado + las funciones para manipular ese estado.
const Contador = () => {
const { count, decrement, increment } =
useContext(Context)
...
<Button primary
onClick={decrement}>-</Button>
createContext permite pasarle valores por defecto
Podemos ver qué pasa si Contador está por afuera del Provider que definimos:
const App = () =>
<>
<Contador />
<Provider>
<LogContador />
</Provider>
</>
export const Context = createContext({
count: 20,
increment: () => {
console.info('hola')
},
decrement: () => {},
})
Testeo de frontend
Para utilizar el contexto en los tests solo debemos utilizar el componente que trabaja con el Provider:
test('si se presiona el botón +, el contador pasa a estar en 1', () => {
render(
<App />
)
fireEvent.click(screen.getByTestId('button_plus'))
expect(screen.getByTestId('contador')).toHaveTextContent('1')
})
Alternativas
¡Gracias!