Skip to content

Commit

Permalink
Deleting useEffect and adding button
Browse files Browse the repository at this point in the history
  • Loading branch information
fdodino committed Nov 10, 2023
1 parent 4571d0a commit bcef481
Show file tree
Hide file tree
Showing 7 changed files with 37 additions and 114 deletions.
93 changes: 10 additions & 83 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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

Expand All @@ -143,25 +90,13 @@ 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

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 `<tr>` (lo interesante es que puede haber más de un tag html que cumpla ese rol)

Expand All @@ -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(<PedidoComponent />)
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
Expand Down
Binary file removed images/demo.gif
Binary file not shown.
Binary file added images/demoButton.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 0 additions & 1 deletion src/App.test.jsx
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down
1 change: 0 additions & 1 deletion src/assets/react.svg

This file was deleted.

46 changes: 26 additions & 20 deletions src/pedido/component.jsx
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -48,6 +52,8 @@ export const PedidoComponent = () => {
<Column field="direccion" header="Domicilio de entrega" sortable></Column>
<Column field="gustosPedidos" header="Gustos"></Column>
</DataTable>
<br/>
<Button data-testid="actualizar" label="Actualizar pedidos" severity="help" rounded icon="pi pi-refresh" onClick={ () => { actualizarPedidos() } }/>
<Toast ref={toast}></Toast>
</Panel>
)
Expand Down
10 changes: 1 addition & 9 deletions src/pedido/component.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,6 @@ beforeEach(() => {
])
})
)
vi.useFakeTimers({ shouldAdvanceTime: true })
})

afterEach(() => {
vi.runOnlyPendingTimers()
vi.useRealTimers()
vi.clearAllMocks()
})

test('inicialmente no tenemos pedidos', () => {
Expand All @@ -29,9 +22,8 @@ test('inicialmente no tenemos pedidos', () => {
})

test('cuando se actualiza el servidor aparecen nuevos pedidos', async () => {
vi.useFakeTimers()
render(<PedidoComponent />)
vi.advanceTimersByTime(11000)
screen.getByTestId('actualizar').click()
await waitFor(async () => {
const allRows = screen.queryAllByRole('row')
// hay que considerar el encabezado
Expand Down

0 comments on commit bcef481

Please sign in to comment.