1 of 21

React Context

Algoritmos III

2 of 21

Pero antes... hay otras opciones

Volvamos al ejemplo de Tareas

3 of 21

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!

4 of 21

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

5 of 21

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

6 of 21

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)

7 of 21

El ejemplo de Tareas

Y como componente no vamos a hacer mucho, simplemente vamos a

  • renderizar el componente que le corresponda a la ruta (si es ‘/’ será el componente principal de tareas, si es ‘/asignarTarea/:id’ será la asignación de una tarea
  • y agregar un div para visualizar el toast si es necesario

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 />} />

8 of 21

El ejemplo de Tareas

Y como componente no vamos a hacer mucho, simplemente vamos a

  • renderizar el componente que le corresponda a la ruta (si es ‘/’ será el componente principal de tareas, si es ‘/asignarTarea/:id’ será la asignación de una tarea
  • y agregar un div para visualizar el toast si es necesario

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

9 of 21

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

10 of 21

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>()

11 of 21

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

12 of 21

El mecanismo state/prop puede ser incómodo

13 of 21

Context to the rescue

El Context actúa de memoria compartida entre los componentes.

Principal

1

2

1.1

1.1.1

14 of 21

Construyendo el Context

Es simplemente un objeto global que se construye con una función de React

export const Context = createContext()

15 of 21

Construyendo el Provider

  • Puede definir su propio estado
  • Y llamar a funciones que modifiquen ese estado
  • Ubicamos en la referencia value lo que queremos que sea público

export const Provider = ({ children }) => {

const [count, setCount] = useState(0)

const value = {

count,

increment: () => {

setCount(count + 1)

},

}

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

16 of 21

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>

17 of 21

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>

18 of 21

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: () => {},

})

19 of 21

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')

})

20 of 21

Alternativas

  • Manejar el estado con state / props (en muchas aplicaciones con eso es suficiente)
  • Redux: una variante más compleja, que necesita de más conceptos (reducers, store, dispatch actions), tiene más herramientas de debugging y es ideal cuando tenés que trabajar con información que cambia permanentemente. Podés ver el siguiente artículo que explica cómo funciona (también podés descargar el ejemplo del contador en Redux, es un poco viejo, define los componentes como clases).

21 of 21

¡Gracias!