diff --git a/README.md b/README.md index ddf80c4..f9598fe 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,9 @@ La aplicación consiste en modelar los pedidos para una heladería: -![demo](./images/demo.gif) +![demo](./images/demoButton.gif) -Y en este ejemplo vamos a conocer el hook [`useEffect`](https://reactjs.org/docs/hooks-effect.html), asociado al ciclo de vida de los componentes de React. +Y en este ejemplo vamos a ver cómo invocar una función asincrónica, y su asociación con el ciclo de vida de los componentes de React. ## Arquitectura general de la aplicación @@ -17,9 +17,9 @@ En esta solución participan - el objeto de dominio Helado - una función asincrónica que simula pedidos pendientes -- y el componente React +- y el componente React que tiene un botón que los dispara -Dado que nuestro componente es una función, no podemos producir efectos colaterales (o "efectos"). De hecho si utilizáramos la variante con clases tampoco podemos hacerlo dentro de la función `render()` porque es cuando se están definiendo los elementos de nuestro DOM. En lugar de eso, **cada cierto tiempo, debemos disparar periódicamente la consulta al servicio para obtener los pedidos pendientes**. Esto lo vamos a resolver mediante el hook `useEffect` que terminará generando un nuevo estado (`setPedidosPendientes`). +Dado que nuestro componente es una función, no podemos producir efectos colaterales (o "efectos"). De hecho si utilizáramos la variante con clases tampoco podemos hacerlo dentro de la función `render()` porque es cuando se están definiendo los elementos de nuestro DOM. ## Dominio @@ -36,24 +36,6 @@ La función `getPedidosPendientes` exportada es asincrónica, ya que la intenci - aleatoriamente marcar/desmarcar pedidos como entregados o pendientes, para forzar un cambio en la lista de pedidos pendientes de la heladería - devolver la lista con los pedidos pendientes -```js -const cambiarEstadoPedidos = () => { - pedidos.forEach((pedido) => { - const random = Math.random() * 10 + 1 - if (random > 5) { - pedido.entregar() - } else { - pedido.cancelar() - } - }) -} - -export const getPedidosPendientes = async () => { - cambiarEstadoPedidos() - return pedidos.filter((pedido) => pedido.estaPendiente()) -} -``` - ## Componente React ### Estado @@ -97,44 +79,9 @@ Fíjense además que la definición del Toast hace referencia a nuestra variable ![React Lifecycle Methods](./images/ReactLifecycleHooks2.png) -### Component did mount / Component did update => hook useEffect - -Cuando nuestro componente comience, disparamos cada _x_ segundos la llamada asincrónica que obtiene los pedidos pendientes. Originalmente esto se hacía de esta manera: - -```js - componentDidMount() { - console.log('component did mount') - this.timerID = setInterval( - () => this.actualizarPedidosPendientes(), - 10000 - ) - } -``` - -El hook `useEffect` nos permite lograr el mismo efecto: - -```jsx -useEffect(() => { - const timerID = setInterval( - async () => { - try { - console.info('Actualizando pedidos pendientes') - const nuevosPedidosPendientes = await getPedidosPendientes() - mostrarPedidosActualizados(pedidosPendientes, nuevosPedidosPendientes) - setPedidosPendientes(nuevosPedidosPendientes) - } catch (e) { - toast.current.show({ severity: 'error', detail: e.message }) - } - }, - 10000 - ) - - // Importante quitar el timer ya que si no se siguen agregando intervalos para disparar los pedidos pendientes - return () => { clearInterval(timerID) } -}) -``` +## Disparando la consulta -El hook `useEffect` se ejecuta luego del render del componente, y **recibe como parámetro una función que es la que va a producir el efecto colateral**. +Para disparar la consulta tenemos un botón que llama a una función que **actualiza el estado**, generando así un nuevo render. ### Mostrando las diferencias @@ -143,16 +90,6 @@ Un detalle adicional que queremos mostrar es - cuántos pedidos nuevos hay (los que no estaban anteriormente y ahora aparecen = Nuevos - Viejos, según la teoría de conjuntos) - cuántos pedidos se entregaron (los que estaban anteriormente y ahora no están = Viejos - Nuevos, según la teoría de conjuntos) -```js -const mostrarPedidosActualizados = (pedidosPendientes, nuevosPedidosPendientes) => { - const idPedido = (pedido) => pedido.id - const cuantosPedidosNuevos = differenceBy(nuevosPedidosPendientes, pedidosPendientes, idPedido).length - const cuantosPedidosDespachados = differenceBy(pedidosPendientes, nuevosPedidosPendientes, idPedido).length - const detail = `Pedidos nuevos: ${cuantosPedidosNuevos}, Pedidos despachados: ${cuantosPedidosDespachados}` - toast.current.show({ severity: 'info', detail, closable: false }) -} -``` - Aquí resolvemos la diferencia de conjuntos entre los nuevos y los viejos y viceversa (gracias a la función `differenceBy` de Lodash) y mostramos el toast en caso de que haya cambios. ## Test @@ -160,8 +97,6 @@ Aquí resolvemos la diferencia de conjuntos entre los nuevos y los viejos y vice El test del componente - genera un stub del service, principalmente con fines didácticos, ya que no estamos realmente consultando a un servicio externo -- por otra parte, trabaja con **fake timers** para simular que pasaron 11 segundos y verificar que efectivamente se ve la lista de pedidos (es importante limpiar esos timers en el método `afterEach`). -- Un detalle adicional es que por defecto vitest se queda esperando esos 11 segundos, a menos de que explícitamente lo configuremos con la opción `vi.useFakeTimers({ shouldAdvanceTime: true })`. Esto es algo bastante desconcertante y una muy mala decisión de diseño ya que el timeout de 5 segundos hace que por defecto el test falle. - para testear que no hay pedidos, PrimeReact genera un div vacío cuya clase exacta estamos verificando (no es un test que tenga mucha resiliencia pero también lo mostramos con fines didácticos) - para testear que hay pedidos, estamos utilizando el queryByRole donde `row` hace referencia a un tag `` (lo interesante es que puede haber más de un tag html que cumpla ese rol) @@ -175,27 +110,19 @@ beforeEach(() => { ]) }) ) - vi.useFakeTimers({ shouldAdvanceTime: true }) -}) - -afterEach(() => { - vi.runOnlyPendingTimers() - vi.useRealTimers() - vi.clearAllMocks() }) ... test('cuando se actualiza el servidor aparecen nuevos pedidos', async () => { - vi.useFakeTimers() render() - vi.advanceTimersByTime(11000) + screen.getByTestId('actualizar').click() await waitFor(async () => { const allRows = screen.queryAllByRole('row') - expect(allRows.length).toBe(4) + // hay que considerar el encabezado + // es muy feo tener que hacer esto pero el componente DataTable no nos da data-testid + expect(allRows.length).toBe(4) }) - -}) ``` ## Bibliografía adicional diff --git a/images/demo.gif b/images/demo.gif deleted file mode 100644 index 50a9df6..0000000 Binary files a/images/demo.gif and /dev/null differ diff --git a/images/demoButton.gif b/images/demoButton.gif new file mode 100644 index 0000000..7147220 Binary files /dev/null and b/images/demoButton.gif differ diff --git a/src/App.test.jsx b/src/App.test.jsx index ceda26b..2be1b81 100644 --- a/src/App.test.jsx +++ b/src/App.test.jsx @@ -1,5 +1,4 @@ import { render, screen } from '@testing-library/react' -import React from 'react' import App from './App' test('renders a header with title', () => { diff --git a/src/assets/react.svg b/src/assets/react.svg deleted file mode 100644 index 6c87de9..0000000 --- a/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/pedido/component.jsx b/src/pedido/component.jsx index 018b46a..6685192 100644 --- a/src/pedido/component.jsx +++ b/src/pedido/component.jsx @@ -1,36 +1,40 @@ -import { differenceBy } from 'lodash' +import { useRef, useState } from 'react' + import { Column } from 'primereact/column' import { DataTable } from 'primereact/datatable' import { Panel } from 'primereact/panel' import { Toast } from 'primereact/toast' -import { useRef, useEffect, useState } from 'react' +import { Button } from 'primereact/button' + +import { differenceBy } from 'lodash' import { getPedidosPendientes } from './service' +// ============================================================================================ +// Para evitar el error +// backend.bundle.js:1 Uncaught (in promise) TypeError: Converting circular structure to JSON +// --> starting at object with constructor 'HTMLDivElement' +JSON.stringify = () => '{}' +// +// ============================================================================================ + export const PedidoComponent = () => { + console.info('render') const [pedidosPendientes, setPedidosPendientes] = useState([]) const toast = useRef(null) - useEffect(() => { - const timerID = setInterval( - async () => { - try { - console.info('Actualizando pedidos pendientes') - const nuevosPedidosPendientes = await getPedidosPendientes() - mostrarPedidosActualizados(pedidosPendientes, nuevosPedidosPendientes) - setPedidosPendientes(nuevosPedidosPendientes) - } catch (e) { - toast.current.show({ severity: 'error', detail: e.message }) - } - }, - 10000 - ) - - // Importante quitar el timer ya que si no se siguen agregando intervalos para disparar los pedidos pendientes - return () => { clearInterval(timerID) } - }) + const actualizarPedidos = async () => { + try { + console.info('Actualizando pedidos pendientes') + const nuevosPedidosPendientes = await getPedidosPendientes() + mostrarPedidosActualizados(pedidosPendientes, nuevosPedidosPendientes) + setPedidosPendientes(nuevosPedidosPendientes) + } catch (e) { + toast.current.show({ severity: 'error', detail: e.message }) + } + } const mostrarPedidosActualizados = (pedidosPendientes, nuevosPedidosPendientes) => { const idPedido = (pedido) => pedido.id @@ -48,6 +52,8 @@ export const PedidoComponent = () => { +
+