diff --git a/src/content/5/es/part5d.md b/src/content/5/es/part5d.md
index 62171dd9d3d..53408070311 100644
--- a/src/content/5/es/part5d.md
+++ b/src/content/5/es/part5d.md
@@ -22,24 +22,16 @@ También tienen algunos inconvenientes. Configurar las pruebas E2E es más compl
Las pruebas E2E también pueden ser [flaky](https://hackernoon.com/flaky-tests-a-war-that-never-ends-9aa32fdef359) (inestables).
Algunas pruebas pueden pasar una vez y fallar en otra, incluso si el código no cambia en absoluto.
-Quizás las dos librerías más fáciles para pruebas de extremo a extremo en este momento son [Cypress](https://www.cypress.io/) y [Playwright](https://playwright.dev/).
+Quizás las dos librerías más sencillas para realizar pruebas de extremo a extremo en este momento sean [Playwright](https://playwright.dev/) y [Cypress](https://www.cypress.io/).
-De las estadísticas en [npmtrends.com](https://npmtrends.com/cypress-vs-playwright) vemos que Cypress, que dominó el mercado durante los últimos cinco años, sigue siendo claramente el número uno, pero Playwright está en un rápido ascenso:
+Las estadísticas de [npmtrends.com](https://npmtrends.com/cypress-vs-playwright) muestran que Playwright superó a Cypress en número de descargas durante 2024 y que su popularidad continúa creciendo:
-
+
-Este curso ha estado usando Cypress durante años. Ahora Playwright es una nueva adición. Puedes elegir si completar la parte de pruebas E2E del curso con Cypress o Playwright. Los principios operativos de ambas librerías son muy similares, así que tu elección no es muy importante. Sin embargo, ahora Playwright es la librería E2E preferida para el curso.
-
-Si tu elección es Playwright, por favor continúa. Si terminas usando Cypress, ve [aquí](/es/part5/pruebas_de_extremo_a_extremo_cypress).
-
-### Playwright
+Este curso utilizó Cypress durante años. Ahora nuestra elección es Playwright.
[Playwright](https://playwright.dev/) es un recién llegado a las pruebas de extremo a extremo, que comenzó a explotar en popularidad hacia finales de 2023. Playwright está aproximadamente a la par con Cypress en términos de facilidad de uso. Las librerías son ligeramente diferentes en términos de cómo funcionan. Cypress es radicalmente diferente de la mayoría de las librerías de pruebas E2E, ya que las pruebas de Cypress se ejecutan completamente dentro del navegador. Las pruebas de Playwright, por otro lado, se ejecutan en el proceso de Node, que está conectado al navegador a través de interfaces de programación.
-Se han escrito muchos blogs sobre comparaciones de librerías, por ejemplo, [este](https://www.lambdatest.com/blog/cypress-vs-playwright/) y [este](https://www.browserstack.com/guide/playwright-vs-cypress).
-
-Es difícil decir qué librería es mejor. Una ventaja de Playwright es su soporte de navegadores; Playwright soporta Chrome, Firefox y navegadores basados en Webkit como Safari. Actualmente, Cypress incluye soporte para todos estos navegadores, aunque el soporte de Webkit es experimental y no soporta todas las funcionalidades de Cypress. Al momento de escribir (1.3.2024), mi preferencia personal se inclina ligeramente hacia Playwright.
-
Ahora exploremos Playwright.
### Inicializando pruebas
@@ -54,6 +46,43 @@ El script de instalación hará algunas preguntas, responde de la siguiente mane

+Ten en cuenta que, al instalar Playwright, es posible que tu sistema operativo no admita todos los navegadores que ofrece y aparezca un mensaje de error como el siguiente:
+
+```
+Webkit 18.0 (playwright build v2070) downloaded to /home/user/.cache/ms-playwright/webkit-2070
+Playwright Host validation warning:
+╔══════════════════════════════════════════════════════╗
+║ Host system is missing dependencies to run browsers. ║
+║ Missing libraries: ║
+║ libicudata.so.66 ║
+║ libicui18n.so.66 ║
+║ libicuuc.so.66 ║
+║ libjpeg.so.8 ║
+║ libwebp.so.6 ║
+║ libpcre.so.3 ║
+║ libffi.so.7 ║
+╚══════════════════════════════════════════════════════╝
+```
+
+En ese caso, puedes especificar los navegadores que se probarán mediante `--project=` en el archivo _package.json_:
+
+```js
+ "test": "playwright test --project=chromium --project=firefox",
+```
+
+También puedes eliminar del archivo _playwright.config.js_ la entrada de los navegadores problemáticos:
+
+```js
+ projects: [
+ // ...
+ //{
+ // name: 'webkit',
+ // use: { ...devices['Desktop Safari'] },
+ //},
+ // ...
+ ]
+```
+
Definamos un script npm para ejecutar pruebas e informes de pruebas en _package.json_:
```js
@@ -107,10 +136,11 @@ Las pruebas también pueden ejecutarse a través de la interfaz gráfica con el
npm run test -- --ui
```
-Las pruebas de muestra se ven así:
+Las pruebas de muestra del archivo _tests/example.spec.js_ se ven así:
```js
-const { test, expect } = require('@playwright/test');
+// @ts-check
+import { test, expect } from '@playwright/test';
test('has title', async ({ page }) => {
await page.goto('https://playwright.dev/'); // highlight-line
@@ -144,15 +174,12 @@ Hagamos un script npm para el
, que permitirá iniciarlo en modo d
{
// ...
"scripts": {
- "start": "NODE_ENV=production node index.js",
- "dev": "NODE_ENV=development nodemon index.js",
- "build:ui": "rm -rf build && cd ../frontend/ && npm run build && cp -r build ../backend",
- "deploy": "fly deploy",
- "deploy:full": "npm run build:ui && npm run deploy",
- "logs:prod": "fly logs",
+ "start": "cross-env NODE_ENV=production node index.js",
+ "dev": "cross-env NODE_ENV=development node --watch index.js",
+ "test": "cross-env NODE_ENV=test node --test",
"lint": "eslint .",
- "test": "NODE_ENV=test node --test",
- "start:test": "NODE_ENV=test node index.js" // highlight-line
+ // ...
+ "start:test": "cross-env NODE_ENV=test node --watch index.js" // highlight-line
},
// ...
}
@@ -166,9 +193,9 @@ const { test, expect } = require('@playwright/test')
test('front page can be opened', async ({ page }) => {
await page.goto('http://localhost:5173')
- const locator = await page.getByText('Notes')
+ const locator = page.getByText('Notes')
await expect(locator).toBeVisible()
- await expect(page.getByText('Note app, Department of Computer Science, University of Helsinki 2023')).toBeVisible()
+ await expect(page.getByText('Note app, Department of Computer Science, University of Helsinki 2024')).toBeVisible()
})
```
@@ -178,21 +205,7 @@ El método [toBeVisible](https://playwright.dev/docs/api/class-locatorassertions
La segunda comprobación se realiza sin usar la variable auxiliar.
-Nos damos cuenta de que el año ha cambiado. Cambiemos la prueba de la siguiente manera:
-
-```js
-const { test, expect } = require('@playwright/test')
-
-test('front page can be opened', async ({ page }) => {
- await page.goto('http://localhost:5173')
-
- const locator = await page.getByText('Notes')
- await expect(locator).toBeVisible()
- await expect(page.getByText('Note app, Department of Computer Science, University of Helsinki 2024')).toBeVisible() // highlight-line
-})
-```
-
-Como se esperaba, la prueba falla. Playwright abre el informe de la prueba en el navegador y se hace evidente que Playwright ha realizado las pruebas con tres navegadores diferentes: Chrome, Firefox y Webkit, es decir, el motor de navegador utilizado por Safari:
+La prueba falla porque contiene un año antiguo. Playwright abre el informe en el navegador y deja claro que ha ejecutado las pruebas con tres navegadores diferentes: Chrome, Firefox y Webkit, es decir, el motor de Safari:

@@ -206,9 +219,7 @@ Generalmente, es por supuesto algo muy bueno que las pruebas se lleven a cabo co
npm test -- --project chromium
```
-Ahora corrijamos el año desactualizado en el código del frontend que causó el error.
-
-Antes de continuar, agreguemos un bloque _describe_ a las pruebas:
+Corrijamos la prueba con el año adecuado y añadamos un bloque _describe_:
```js
const { test, describe, expect } = require('@playwright/test')
@@ -217,9 +228,9 @@ describe('Note app', () => { // highlight-line
test('front page can be opened', async ({ page }) => {
await page.goto('http://localhost:5173')
- const locator = await page.getByText('Notes')
+ const locator = page.getByText('Notes')
await expect(locator).toBeVisible()
- await expect(page.getByText('Note app, Department of Computer Science, University of Helsinki 2024')).toBeVisible()
+ await expect(page.getByText('Note app, Department of Computer Science, University of Helsinki 2025')).toBeVisible()
})
})
```
@@ -229,7 +240,8 @@ Antes de continuar, rompamos las pruebas una vez más. Notamos que la ejecución
Al desarrollar pruebas, puede ser más prudente reducir el tiempo de espera a unos pocos segundos. Según la [documentación](https://playwright.dev/docs/test-timeouts), esto se puede hacer cambiando el archivo _playwright.config.js_ de la siguiente manera:
```js
-module.exports = defineConfig({
+export default defineConfig({
+ // ...
timeout: 3000, // highlight-line
fullyParallel: false, // highlight-line
workers: 1, // highlight-line
@@ -249,10 +261,10 @@ Comencemos abriendo el formulario de inicio de sesión.
describe('Note app', () => {
// ...
- test('login form can be opened', async ({ page }) => {
+ test('user can log in', async ({ page }) => {
await page.goto('http://localhost:5173')
- await page.getByRole('button', { name: 'log in' }).click()
+ await page.getByRole('button', { name: 'login' }).click()
})
})
```
@@ -279,10 +291,10 @@ Cuando se abre el formulario, la prueba debe buscar los campos de texto e introd
describe('Note app', () => {
// ...
- test('login form can be opened', async ({ page }) => {
+ test('user can log in', async ({ page }) => {
await page.goto('http://localhost:5173')
- await page.getByRole('button', { name: 'log in' }).click()
+ await page.getByRole('button', { name: 'login' }).click()
await page.getByRole('textbox').fill('mluukkai') // highlight-line
})
})
@@ -302,10 +314,10 @@ El problema ahora es que _getByRole_ encuentra dos campos de texto, y al llamar
describe('Note app', () => {
// ...
- test('login form can be opened', async ({ page }) => {
+ test('user can log in', async ({ page }) => {
await page.goto('http://localhost:5173')
- await page.getByRole('button', { name: 'log in' }).click()
+ await page.getByRole('button', { name: 'login' }).click()
// highlight-start
await page.getByRole('textbox').first().fill('mluukkai')
await page.getByRole('textbox').last().fill('salainen')
@@ -324,10 +336,10 @@ Si hubiera más de dos campos de texto, utilizar los métodos _first_ y _last_ n
```js
describe('Note app', () => {
// ...
- test('login form can be opened', async ({ page }) => {
+ test('user can log in', async ({ page }) => {
await page.goto('http://localhost:5173')
- await page.getByRole('button', { name: 'log in' }).click()
+ await page.getByRole('button', { name: 'login' }).click()
// highlight-start
const textboxes = await page.getByRole('textbox').all()
@@ -344,54 +356,50 @@ describe('Note app', () => {
Ambas versiones de la prueba funcionan. Sin embargo, ambas son problemáticas en la medida en que si el formulario de registro cambia, las pruebas pueden fallar, ya que dependen de que los campos estén en la página en un cierto orden.
-Una mejor solución es definir atributos de id de prueba únicos para los campos, y buscarlos en las pruebas utilizando el método [getByTestId](https://playwright.dev/docs/api/class-page#page-get-by-test-id).
+Si un elemento resulta difícil de localizar en las pruebas, podemos asignarle un atributo
y encontrarlo mediante [getByTestId](https://playwright.dev/docs/api/class-page#page-get-by-test-id).
-Ampliemos el formulario de inicio de sesión de la siguiente manera
+Aprovechemos ahora los elementos que ya existen en el formulario de inicio de sesión. Sus campos tienen asignados
únicos:
```js
-const LoginForm = ({ ... }) => {
- return (
-
+// ...
```
-La prueba cambia de la siguiente manera:
+Los campos pueden y deben localizarse en las pruebas mediante sus
y el método [getByLabel](https://playwright.dev/docs/api/class-page#page-get-by-label):
```js
describe('Note app', () => {
// ...
- test('login form can be opened', async ({ page }) => {
+ test('user can log in', async ({ page }) => {
await page.goto('http://localhost:5173')
- await page.getByRole('button', { name: 'log in' }).click()
- await page.getByTestId('username').fill('mluukkai') // highlight-line
- await page.getByTestId('password').fill('salainen') // highlight-line
+ await page.getByRole('button', { name: 'login' }).click()
+ await page.getByLabel('username').fill('mluukkai') // highlight-line
+ await page.getByLabel('password').fill('salainen') // highlight-line
await page.getByRole('button', { name: 'login' }).click()
@@ -400,8 +408,12 @@ describe('Note app', () => {
})
```
+Al localizar elementos conviene utilizar el contenido que el usuario ve en la interfaz, ya que así las pruebas simulan mejor cómo encontraría realmente cada campo al navegar por la aplicación.
+
Ten en cuenta que para que la prueba pase en esta etapa, es necesario que haya un usuario en la base de datos de
. ¡Crea un usuario si es necesario!
+### Inicialización de las pruebas
+
Dado que ambas pruebas comienzan de la misma manera, es decir, abriendo la página
que se ejecuta antes de cada prueba:
```js
@@ -415,15 +427,15 @@ describe('Note app', () => {
// highlight-end
test('front page can be opened', async ({ page }) => {
- const locator = await page.getByText('Notes')
+ const locator = page.getByText('Notes')
await expect(locator).toBeVisible()
- await expect(page.getByText('Note app, Department of Computer Science, University of Helsinki 2024')).toBeVisible()
+ await expect(page.getByText('Note app, Department of Computer Science, University of Helsinki 2025')).toBeVisible()
})
- test('login form can be opened', async ({ page }) => {
- await page.getByRole('button', { name: 'log in' }).click()
- await page.getByTestId('username').fill('mluukkai')
- await page.getByTestId('password').fill('salainen')
+ test('user can log in', async ({ page }) => {
+ await page.getByRole('button', { name: 'login' }).click()
+ await page.getByLabel('username').fill('mluukkai')
+ await page.getByLabel('password').fill('salainen')
await page.getByRole('button', { name: 'login' }).click()
await expect(page.getByText('Matti Luukkainen logged in')).toBeVisible()
})
@@ -442,9 +454,9 @@ describe('Note app', () => {
describe('when logged in', () => {
beforeEach(async ({ page }) => {
- await page.getByRole('button', { name: 'log in' }).click()
- await page.getByTestId('username').fill('mluukkai')
- await page.getByTestId('password').fill('salainen')
+ await page.getByRole('button', { name: 'login' }).click()
+ await page.getByLabel('username').fill('mluukkai')
+ await page.getByLabel('password').fill('salainen')
await page.getByRole('button', { name: 'login' }).click()
})
@@ -485,18 +497,18 @@ describe('Note app', () => {
// ....
test('user can log in', async ({ page }) => {
- await page.getByRole('button', { name: 'log in' }).click()
- await page.getByTestId('username').fill('mluukkai')
- await page.getByTestId('password').fill('salainen')
+ await page.getByRole('button', { name: 'login' }).click()
+ await page.getByLabel('username').fill('mluukkai')
+ await page.getByLabel('password').fill('salainen')
await page.getByRole('button', { name: 'login' }).click()
await expect(page.getByText('Matti Luukkainen logged in')).toBeVisible()
})
describe('when logged in', () => {
beforeEach(async ({ page }) => {
- await page.getByRole('button', { name: 'log in' }).click()
- await page.getByTestId('username').fill('mluukkai')
- await page.getByTestId('password').fill('salainen')
+ await page.getByRole('button', { name: 'login' }).click()
+ await page.getByLabel('username').fill('mluukkai')
+ await page.getByLabel('password').fill('salainen')
await page.getByRole('button', { name: 'login' }).click()
})
@@ -506,7 +518,7 @@ describe('Note app', () => {
await page.getByRole('button', { name: 'save' }).click()
await expect(page.getByText('a note created by playwright')).toBeVisible()
})
- })
+ })
})
```
@@ -523,18 +535,18 @@ Podemos vaciar la base de datos usando estos endpoints.
Creemos un nuevo enrutador para las pruebas dentro del directorio
```js
-const testingRouter = require('express').Router()
+const router = require('express').Router()
const Note = require('../models/note')
const User = require('../models/user')
-testingRouter.post('/reset', async (request, response) => {
+router.post('/reset', async (request, response) => {
await Note.deleteMany({})
await User.deleteMany({})
response.status(204).end()
})
-module.exports = testingRouter
+module.exports = router
```
y agrégalo al backend solo
:
@@ -574,7 +586,7 @@ Actualmente no es posible agregar nuevos usuarios a través de la interfaz de us
```js
describe('Note app', () => {
beforeEach(async ({ page, request }) => {
- await request.post('http:localhost:3001/api/testing/reset')
+ await request.post('http://localhost:3001/api/testing/reset')
await request.post('http://localhost:3001/api/users', {
data: {
name: 'Matti Luukkainen',
@@ -652,16 +664,16 @@ describe('Note app', () => {
// ...
test('login fails with wrong password', async ({ page }) => {
- await page.getByRole('button', { name: 'log in' }).click()
- await page.getByTestId('username').fill('mluukkai')
- await page.getByTestId('password').fill('wrong')
+ await page.getByRole('button', { name: 'login' }).click()
+ await page.getByLabel('username').fill('mluukkai')
+ await page.getByLabel('password').fill('wrong')
await page.getByRole('button', { name: 'login' }).click()
await expect(page.getByText('wrong credentials')).toBeVisible()
})
// ...
-)}
+})
```
La prueba verifica con el método [page.getByText](https://playwright.dev/docs/api/class-page#page-get-by-text) que la aplicación muestra un mensaje de error.
@@ -685,10 +697,10 @@ const Notification = ({ message }) => {
Podríamos refinar la prueba para asegurar que el mensaje de error se muestre exactamente en el lugar correcto, es decir, en el elemento que contiene a la clase CSS
:
```js
- test('login fails with wrong password', async ({ page }) => {
+test('login fails with wrong password', async ({ page }) => {
// ...
- const errorDiv = await page.locator('.error') // highlight-line
+ const errorDiv = page.locator('.error') // highlight-line
await expect(errorDiv).toContainText('wrong credentials')
})
```
@@ -698,13 +710,13 @@ La prueba utiliza el método [page.locator](https://playwright.dev/docs/api/clas
Es posible probar los estilos CSS de la aplicación con el comparador [toHaveCSS](https://playwright.dev/docs/api/class-locatorassertions#locator-assertions-to-have-css). Podemos, por ejemplo, asegurarnos de que el color del mensaje de error sea rojo y que haya un borde alrededor de él:
```js
- test('login fails with wrong password', async ({ page }) => {
+test('login fails with wrong password', async ({ page }) => {
// ...
- const errorDiv = await page.locator('.error')
- await expect(errorDiv).toContainText('wrong credentials')
- await expect(errorDiv).toHaveCSS('border-style', 'solid') // highlight-line
- await expect(errorDiv).toHaveCSS('color', 'rgb(255, 0, 0)') // highlight-line
+ const errorDiv = page.locator('.error')
+ await expect(errorDiv).toContainText('wrong credentials')
+ await expect(errorDiv).toHaveCSS('border-style', 'solid') // highlight-line
+ await expect(errorDiv).toHaveCSS('color', 'rgb(255, 0, 0)') // highlight-line
})
```
@@ -714,12 +726,12 @@ Terminemos la prueba para que también asegure que la aplicación **no renderiza
```js
test('login fails with wrong password', async ({ page }) =>{
- await page.getByRole('button', { name: 'log in' }).click()
- await page.getByTestId('username').fill('mluukkai')
- await page.getByTestId('password').fill('wrong')
+ await page.getByRole('button', { name: 'login' }).click()
+ await page.getByLabel('username').fill('mluukkai')
+ await page.getByLabel('password').fill('wrong')
await page.getByRole('button', { name: 'login' }).click()
- const errorDiv = await page.locator('.error')
+ const errorDiv = page.locator('.error')
await expect(errorDiv).toContainText('wrong credentials')
await expect(errorDiv).toHaveCSS('border-style', 'solid')
await expect(errorDiv).toHaveCSS('color', 'rgb(255, 0, 0)')
@@ -734,15 +746,15 @@ Por defecto, Playwright siempre ejecuta todas las pruebas, y a medida que el nú
```js
describe(() => {
- // esta es la única prueba ejecutada!
+ // this is the only test executed!
test.only('login fails with wrong password', async ({ page }) => { // highlight-line
// ...
})
- // esta prueba es omitida...
+ // this test is skipped...
test('user can login with correct credentials', async ({ page }) => {
// ...
- }
+ })
// ...
})
@@ -767,9 +779,9 @@ describe('Note app', () => {
// ...
test('user can login with correct credentials', async ({ page }) => {
- await page.getByRole('button', { name: 'log in' }).click()
- await page.getByTestId('username').fill('mluukkai')
- await page.getByTestId('password').fill('salainen')
+ await page.getByRole('button', { name: 'login' }).click()
+ await page.getByLabel('username').fill('mluukkai')
+ await page.getByLabel('password').fill('salainen')
await page.getByRole('button', { name: 'login' }).click()
await expect(page.getByText('Matti Luukkainen logged in')).toBeVisible()
})
@@ -780,9 +792,9 @@ describe('Note app', () => {
describe('when logged in', () => {
beforeEach(async ({ page, request }) => {
- await page.getByRole('button', { name: 'log in' }).click()
- await page.getByTestId('username').fill('mluukkai')
- await page.getByTestId('password').fill('salainen')
+ await page.getByRole('button', { name: 'login' }).click()
+ await page.getByLabel('username').fill('mluukkai')
+ await page.getByLabel('password').fill('salainen')
await page.getByRole('button', { name: 'login' }).click()
})
@@ -803,9 +815,9 @@ También vale la pena esforzarse por tener un código no repetitivo en las prueb
```js
const loginWith = async (page, username, password) => {
- await page.getByRole('button', { name: 'log in' }).click()
- await page.getByTestId('username').fill(username)
- await page.getByTestId('password').fill(password)
+ await page.getByRole('button', { name: 'login' }).click()
+ await page.getByLabel('username').fill(username)
+ await page.getByLabel('password').fill(password)
await page.getByRole('button', { name: 'login' }).click()
}
@@ -815,24 +827,31 @@ export { loginWith }
La prueba se vuelve mucho más simple y clara:
```js
-const { loginWith } = require('./helper')
+const { test, describe, expect, beforeEach } = require('@playwright/test')
+const { loginWith } = require('./helper') // highlight-line
describe('Note app', () => {
+ // ...
+
test('user can log in', async ({ page }) => {
await loginWith(page, 'mluukkai', 'salainen') // highlight-line
await expect(page.getByText('Matti Luukkainen logged in')).toBeVisible()
})
+ test('login fails with wrong password', async ({ page }) => {
+ await loginWith(page, 'mluukkai', 'wrong') // highlight-line
+
+ const errorDiv = page.locator('.error')
+ // ...
+ })
+
describe('when logged in', () => {
beforeEach(async ({ page }) => {
await loginWith(page, 'mluukkai', 'salainen') // highlight-line
})
- test('a new note can be created', () => {
// ...
})
-
- // ...
})
```
@@ -871,9 +890,9 @@ La creación de una nota también es aislada como una función auxiliar. El arch
```js
const loginWith = async (page, username, password) => {
- await page.getByRole('button', { name: 'log in' }).click()
- await page.getByTestId('username').fill(username)
- await page.getByTestId('password').fill(password)
+ await page.getByRole('button', { name: 'login' }).click()
+ await page.getByLabel('username').fill(username)
+ await page.getByLabel('password').fill(password)
await page.getByRole('button', { name: 'login' }).click()
}
@@ -885,12 +904,15 @@ const createNote = async (page, content) => {
}
// highlight-end
-export { loginWith, createNote }
+export { loginWith, createNote } // highlight-line
```
Las pruebas se simplifican de la siguiente manera:
```js
+const { test, describe, expect, beforeEach } = require('@playwright/test')
+const { createNote, loginWith } = require('./helper') // highlight-line
+
describe('Note app', () => {
// ...
@@ -900,13 +922,13 @@ describe('Note app', () => {
})
test('a new note can be created', async ({ page }) => {
- await createNote(page, 'a note created by playwright', true) // highlight-line
+ await createNote(page, 'a note created by playwright') // highlight-line
await expect(page.getByText('a note created by playwright')).toBeVisible()
})
describe('and a note exists', () => {
beforeEach(async ({ page }) => {
- await createNote(page, 'another note by playwright', true) // highlight-line
+ await createNote(page, 'another note by playwright') // highlight-line
})
test('importance can be changed', async ({ page }) => {
@@ -939,27 +961,28 @@ Así que podemos reemplazar todas las direcciones en las pruebas de _http://loca
Ahora podemos definir la _baseUrl_ para la aplicación en el archivo de configuración de las pruebas
:
```js
-module.exports = defineConfig({
+export default defineConfig({
// ...
use: {
baseURL: 'http://localhost:5173',
+ // ...
},
// ...
-}
+})
```
Todos los comandos en las pruebas que usan la URL de la aplicación, por ejemplo:
```js
await page.goto('http://localhost:5173')
-await page.post('http://localhost:5173/api/tests/reset')
+await request.post('http://localhost:5173/api/testing/reset')
```
se pueden transformar en:
```js
await page.goto('/')
-await page.post('/api/tests/reset')
+await request.post('/api/testing/reset')
```
El código actual para las pruebas está en [GitHub](https://github.com/fullstack-hy2020/notes-e2e/tree/part5-2), en la rama
.
@@ -973,16 +996,16 @@ Cambiemos el bloque de inicialización de la prueba para que cree dos notas en l
```js
describe('when logged in', () => {
// ...
- describe('and several notes exists', () => {
+ describe('and several notes exists', () => { // highlight-line
beforeEach(async ({ page }) => {
// highlight-start
- await createNote(page, 'first note', true)
- await createNote(page, 'second note', true)
+ await createNote(page, 'first note')
+ await createNote(page, 'second note')
// highlight-end
})
test('one of those can be made nonimportant', async ({ page }) => {
- const otherNoteElement = await page.getByText('first note')
+ const otherNoteElement = page.getByText('first note')
await otherNoteElement
.getByRole('button', { name: 'make not important' }).click()
@@ -998,7 +1021,7 @@ La prueba también podría haberse escrito sin la variable auxiliar:
```js
test('one of those can be made nonimportant', async ({ page }) => {
- await page.getByText('first note')
+ page.getByText('first note')
.getByRole('button', { name: 'make not important' }).click()
await expect(page.getByText('first note').getByText('make important'))
@@ -1028,8 +1051,8 @@ Una forma de solucionar el problema es la siguiente:
```js
test('one of those can be made nonimportant', async ({ page }) => {
- const otherNoteText = await page.getByText('first note') // highlight-line
- const otherNoteElement = await otherNoteText.locator('..') // highlight-line
+ const otherNoteText = page.getByText('first note') // highlight-line
+ const otherNoteElement = otherNoteText.locator('..') // highlight-line
await otherNoteElement.getByRole('button', { name: 'make not important' }).click()
await expect(otherNoteElement.getByText('make important')).toBeVisible()
@@ -1042,7 +1065,7 @@ Por supuesto, la prueba también puede escribirse usando solo una variable auxil
```js
test('one of those can be made nonimportant', async ({ page }) => {
- const secondNoteElement = await page.getByText('second note').locator('..')
+ const secondNoteElement = page.getByText('second note').locator('..')
await secondNoteElement.getByRole('button', { name: 'make not important' }).click()
await expect(secondNoteElement.getByText('make important')).toBeVisible()
})
@@ -1061,19 +1084,19 @@ describe('when logged in', () => {
await expect(page.getByText('a note created by playwright')).toBeVisible()
})
- describe('and a note exists', () => {
+ describe('and several notes exists', () => {
beforeEach(async ({ page }) => {
- await createNote(page, 'first note', true)
- await createNote(page, 'second note', true)
- await createNote(page, 'third note', true) // highlight-line
+ await createNote(page, 'first note')
+ await createNote(page, 'second note')
+ await createNote(page, 'third note') // highlight-line
})
- test('importance can be changed', async ({ page }) => {
- const otherNoteText = await page.getByText('second note') // highlight-line
- const otherdNoteElement = await otherNoteText.locator('..')
+ test('one of those can be made nonimportant', async ({ page }) => {
+ const otherNoteText = page.getByText('second note') // highlight-line
+ const otherNoteElement = otherNoteText.locator('..')
- await otherdNoteElement.getByRole('button', { name: 'make not important' }).click()
- await expect(otherdNoteElement.getByText('make important')).toBeVisible()
+ await otherNoteElement.getByRole('button', { name: 'make not important' }).click()
+ await expect(otherNoteElement.getByText('make important')).toBeVisible()
})
})
})
@@ -1088,7 +1111,7 @@ Si, y cuando las pruebas no pasan y sospechas que la falla está en las pruebas
El siguiente comando ejecuta la prueba problemática en modo debug:
```
-npm test -- -g'importance can be changed' --debug
+npm test -- -g'one of those can be made nonimportant' --debug
```
El inspector de Playwright muestra el progreso de las pruebas paso a paso. El botón de flecha-punto en la parte superior lleva las pruebas un paso más adelante. Los elementos encontrados por los localizadores y la interacción con el navegador se visualizan en el navegador:
@@ -1101,7 +1124,7 @@ Por defecto, el debug avanza a través de la prueba comando por comando. Si es u
describe('Note app', () => {
beforeEach(async ({ page, request }) => {
// ...
- }
+ })
describe('when logged in', () => {
beforeEach(async ({ page }) => {
@@ -1117,11 +1140,11 @@ describe('Note app', () => {
test('one of those can be made nonimportant', async ({ page }) => {
await page.pause() // highlight-line
- const otherNoteText = await page.getByText('second note')
- const otherdNoteElement = await otherNoteText.locator('..')
+ const otherNoteText = page.getByText('second note')
+ const otherNoteElement = otherNoteText.locator('..')
- await otherdNoteElement.getByRole('button', { name: 'make not important' }).click()
- await expect(otherdNoteElement.getByText('make important')).toBeVisible()
+ await otherNoteElement.getByRole('button', { name: 'make not important' }).click()
+ await expect(otherNoteElement.getByText('make important')).toBeVisible()
})
})
})
@@ -1164,7 +1187,7 @@ npm run test -- --trace on
Si es necesario, Trace puede verse con el comando
```
-npx playwright show report
+npx playwright show-report
```
o con el script npm que definimos _npm run test:report_
@@ -1256,8 +1279,8 @@ const { test, expect, beforeEach, describe } = require('@playwright/test')
describe('Blog app', () => {
beforeEach(async ({ page, request }) => {
- // vacía la base de datos aquí
- // crea un usuario para el backend aquí
+ // empty the db here
+ // create a user for the backend here
// ...
})
diff --git a/src/content/5/es/part5e.md b/src/content/5/es/part5e.md
index 77eb9e1de6a..9cee9cc8960 100644
--- a/src/content/5/es/part5e.md
+++ b/src/content/5/es/part5e.md
@@ -7,1182 +7,1104 @@ lang: es
-[Cypress](https://www.cypress.io/) ha sido la librería de pruebas E2E más popular durante los últimos años, pero Playwright está ganando terreno rápidamente. Este curso ha estado usando Cypress durante años. Ahora, Playwright es una nueva adición. Puedes elegir si completar la parte de pruebas E2E del curso con Cypress o Playwright. Los principios operativos de ambas librerías son muy similares, así que tu elección no es muy importante. Sin embargo, ahora Playwright es la librería E2E preferida para el curso.
+La interfaz de usuario de nuestra aplicación es actualmente bastante básica:
-Si tu elección es Cypress, por favor continúa. Si terminas usando Playwright, ve [aquí](/es/part5/pruebas_de_extremo_a_extremo_playwright).
+
-### Cypress
+Queremos cambiar eso. Comencemos con la estructura de navegación de la aplicación.
-La librería E2E [Cypress](https://www.cypress.io/) se ha vuelto popular durante los últimos años. Cypress es excepcionalmente fácil de usar y, en comparación con Selenium, por ejemplo, requiere mucha menos molestia y dolor de cabeza. Su principio operativo es radicalmente diferente al de la mayoría de las librerías de prueba E2E, porque las pruebas Cypress se ejecutan completamente dentro del navegador. Otras librerías ejecutan las pruebas en un proceso de Node, que está conectado al navegador a través de una API.
+Es muy común que las aplicaciones web tengan una barra de navegación que permite a los usuarios cambiar entre diferentes vistas dentro de la aplicación. Nuestra aplicación para tomar notas podría incluir una página de inicio:
-Hagamos algunas pruebas de extremo a extremo para nuestra aplicación de notas.
+
-A diferencia de las pruebas de backend o las pruebas unitarias realizadas en el front-end de React, las pruebas de extremo a extremo no necesitan estar ubicadas en el mismo proyecto npm donde está el código. Hagamos un proyecto completamente separado para las pruebas E2E con el comando _npm init_. Luego instala Cypress en
el nuevo proyecto como una dependencia de desarrollo.
+y una página separada para ver notas:
-```js
-npm install --save-dev cypress
-```
+
-y agregando un script npm para ejecutarlo:
+así como una página para crear notas:
-```js
-{
- // ...
- "scripts": {
- "cypress:open": "cypress open" // highlight-line
- },
- // ...
-}
-```
+
-También le hacemos un pequeño cambio al script que inicia la aplicación, sin este cambio Cypress no puede acceder a ella.
+[En una aplicación web tradicional](/es/part0/fundamentos_de_las_aplicaciones_web#aplicaciones-web-tradicionales), cambiar entre las páginas mostradas por la aplicación implicaba que el navegador enviara una nueva solicitud HTTP GET al servidor y luego renderizara el código HTML devuelto por el servidor, que correspondía a la nueva vista.
-A diferencia de las pruebas unitarias del frontend, las pruebas de Cypress pueden estar en el repositorio frontend o backend, o incluso en su propio repositorio separado.
+Sin embargo, en las aplicaciones de una sola página, en realidad estás en la misma página todo el tiempo y el código JavaScript ejecutado en el navegador crea la ilusión de diferentes "páginas". Si se realizan solicitudes HTTP al cambiar de vista, se utilizan únicamente para recuperar datos con formato JSON que pueden ser necesarios para mostrar la nueva vista.
-Las pruebas requieren que el sistema bajo prueba esté funcionando. A diferencia de nuestras pruebas de integración de backend, las pruebas de Cypress
no inician el sistema cuando se ejecutan.
+Una aplicación con una barra de navegación y múltiples vistas sería fácil de implementar con React, por ejemplo, haciendo que el estado de la aplicación
page recuerde en qué página se encuentra el usuario y muestre la vista correcta en función de esto:
-Agreguemos un script npm al
backend que lo inicia en modo de prueba, o para que
NODE\\_ENV sea
test .
```js
-{
- // ...
- "scripts": {
- "start": "NODE_ENV=production node index.js",
- "dev": "NODE_ENV=development nodemon index.js",
- "build:ui": "rm -rf build && cd ../frontend/ && npm run build && cp -r build ../backend",
- "deploy": "fly deploy",
- "deploy:full": "npm run build:ui && npm run deploy",
- "logs:prod": "fly logs",
- "lint": "eslint .",
- "test": "jest --verbose --runInBand",
- "start:test": "NODE_ENV=test node index.js" // highlight-line
- },
- // ...
-}
-```
+const App = () => {
+ const [page, setPage] = useState('home')
-**NB** Para conseguir que Cypress funcione con WSL2 se debe realizar una configuración preliminar. Estos dos [enlaces](https://docs.cypress.io/guides/references/advanced-installation#Windows-Subsystem-for-Linux) son buenos lugares para [iniciar](https://nickymeuleman.netlify.app/blog/gui-on-wsl2-cypress).
+ const toPage = (page) => (event) => {
+ event.preventDefault()
+ setPage(page)
+ }
-Cuando tanto el backend como el frontend están ejecutándose, podemos iniciar Cypress con el comando
+ const content = () => {
+ if (page === 'home') {
+ return
+ } else if (page === 'notes') {
+ return
+ } else if (page === 'users') {
+ return
+ }
+ }
-```js
-npm run cypress:open
+ return (
+
+ )
+}
```
-Cypress nos pregunta qué tipo de prueba realizaremos. Debemos elegir "E2E Testing":
-
-
+Sin embargo, este método no es óptimo: la URL del sitio web sigue siendo la misma incluso cuando estás en una vista diferente. Sin embargo, cada vista debe tener su propia URL para que los usuarios puedan, por ejemplo, marcar páginas como favoritas. Además, el botón Atrás del navegador no funciona lógicamente si las páginas no tienen direcciones propias; es decir, hacer clic en el botón Atrás no lo llevará a la vista de la aplicación vista anteriormente, sino a otro lugar completamente diferente.
-A continuación debemos elegir un navegador (por ejemplo Chrome) y luego debemos hacer click en "Create new spec":
+### React Router
-
+Afortunadamente, la librería [React Router](https://reactrouter.com/) ofrece una excelente solución para gestionar la navegación en una aplicación React.
-Creemos el archivo de prueba
cypress/e2e/note\_app.cy.js :
+Instalemos React Router:
-
-
-Podemos editar la prueba en Cypress, pero usemos en cambio VS Code:
-
-
-
-Ahora podemos cerrar la vista de edición de Cypress.
-
-Cambiemos el contenido de la prueba como se muestra a continuación:
-
-```js
-describe('Note app', function() {
- it('front page can be opened', function() {
- cy.visit('http://localhost:5173')
- cy.contains('Notes')
- cy.contains('Note app, Department of Computer Science, University of Helsinki 2023')
- })
-})
+```bash
+npm install react-router-dom
```
-La prueba se ejecuta haciendo clic en ella en Cypress:
+Creemos un nuevo componente que sirva como página principal de la aplicación.
-Ejecutar la prueba muestra cómo se comporta la aplicación mientras esta se ejecuta:
-
-
-
-La estructura de la prueba debería resultar familiar. Utilizan bloques
describe para agrupar diferentes casos de prueba, al igual que Jest. Los casos de prueba se han definido con el método
it . Cypress tomó estas partes de la librería de pruebas [Mocha](https://mochajs.org/) a la que utiliza bajo el capó.
+```js
+const Home = () => {
+ return (
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+
+ )
+}
-[cy.visit](https://docs.cypress.io/api/commands/visit.html) y [cy.contains](https://docs.cypress.io/api/commands/contains.html) son comandos de Cypress, y su propósito es bastante obvio.
-[cy.visit](https://docs.cypress.io/api/commands/visit.html) abre la dirección web dada como parámetro en el navegador utilizado por la prueba. [cy.contains](https://docs.cypress.io/api/commands/contains.html) busca la cadena que recibió como parámetro en la página.
+export default Home
+```
-Podríamos haber declarado la prueba usando una función de flecha
+Extraeremos la vista principal anterior de la aplicación (que estaba en el componente
App ) a su propio componente, pero moveremos el manejo del estado de las notas fuera del componente:
```js
-describe('Note app', () => { // highlight-line
- it('front page can be opened', () => { // highlight-line
- cy.visit('http://localhost:5173')
- cy.contains('Notes')
- cy.contains('Note app, Department of Computer Science, University of Helsinki 2023')
- })
-})
+// list of notes passed as a parameter
+const NoteList = ({ notes }) => { // highlight-line
+ // content mostly the same as in the App component
+ // reference to NoteForm is removed
+}
```
-Sin embargo, Mocha [recomienda](https://mochajs.org/#arrow-functions) que no se utilicen funciones de flecha, porque podrían causar algunos problemas en ciertas situaciones.
+El componente
App ahora cambia de la siguiente manera
-Si
cy.contains no encuentra el texto que está buscando, la prueba no pasa. Por lo tanto, si extendemos nuestra prueba de la siguiente manera
```js
-describe('Note app', function() {
- it('front page can be opened', function() {
- cy.visit('http://localhost:5173')
- cy.contains('Notes')
- cy.contains('Note app, Department of Computer Science, University of Helsinki 2023')
- })
+import { useState, useEffect } from 'react'
+import noteService from './services/notes'
+
+import {
+ BrowserRouter as Router,
+ Routes, Route, Link
+} from 'react-router-dom'
+import NoteList from './components/NoteList'
+import Home from './components/Home'
+import Footer from './components/Footer'
+import NoteForm from './components/NoteForm'
+
+const App = () => {
+ const [notes, setNotes] = useState([])
+
+ useEffect(() => {
+ noteService.getAll().then(initialNotes => {
+ setNotes(initialNotes)
+ })
+ }, [])
-// highlight-start
- it('front page contains random text', function() {
- cy.visit('http://localhost:5173')
- cy.contains('wtf is this app?')
- })
-// highlight-end
-})
-```
+ const addNote = noteObject => {
+ noteService.create(noteObject).then(returnedNote => {
+ setNotes(notes.concat(returnedNote))
+ })
+ }
-la prueba falla
+ const padding = {
+ padding: 5
+ }
-
+ return (
+ // highlight-start
+
+
+ home
+ notes
+ new note
+
+ // highlight-end
-Eliminemos el código que falla de la prueba.
+ // highlight-start
+
+
+ } />
+
+ } />
+ } />
+
+
+
+
+ // highlight-end
+ )
+}
-La variable _cy_ que utilizan nuestras pruebas nos da un error Eslint molesto
+export default App
+```
-
+El enrutamiento —es decir, el renderizado condicional de componentes según la
URL del navegador— se habilita colocando los componentes como hijos de [Router](https://reactrouter.com/api/declarative-routers/Router), dentro de sus etiquetas.
-Podemos deshacernos de él instalando [eslint-plugin-cypress](https://github.com/cypress-io/eslint-plugin-cypress) como una dependencia de desarrollo
+Primero, la barra de navegación de la aplicación se define utilizando el componente [Link](https://reactrouter.com/api/components/Link). El atributo
to especifica cómo se cambia la URL del navegador cuando se hace clic en el enlace:
```js
-npm install eslint-plugin-cypress --save-dev
+
+ home
+ notes
+ new note
+
```
-y cambiando la configuración en
.eslintrc.cjs de la siguiente manera:
+A continuación, el enrutamiento de la aplicación se define utilizando el componente [Routes](https://reactrouter.com/api/components/Routes). Dentro del componente usamos [Route](https://reactrouter.com/api/components/Route) para definir un conjunto de reglas y los componentes que se renderizarán para cada una:
```js
-module.exports = {
- "env": {
- browser: true,
- es2020: true,
- "jest/globals": true,
- "cypress/globals": true // highlight-line
- },
- "extends": [
- // ...
- ],
- "parserOptions": {
- // ...
- },
- "plugins": [
- "react", "jest", "cypress" // highlight-line
- ],
- "rules": {
- // ...
- }
-}
+
+
+ } />
+
+ } />
+ } />
+
```
-### Escribiendo en un formulario
+En la URL raíz de la aplicación se renderiza el componente
Home :
-Extendamos nuestras pruebas para que nuestra nueva prueba intente iniciar sesión en nuestra aplicación.
-Suponemos que nuestro backend contiene un usuario con el nombre de usuario
mluukkai y la contraseña
salainen .
+
-La prueba comienza abriendo el formulario de inicio de sesión.
+Al hacer clic en "notas" en la barra de navegación, la dirección del navegador cambia a
notes y se renderiza el componente
NoteList :
-```js
-describe('Note app', function() {
- // ...
+
- it('login form can be opened', function() {
- cy.visit('http://localhost:5173')
- cy.contains('log in').click()
- })
-})
-```
+De manera similar, al hacer clic en "nueva nota", la URL pasa a ser
create y se renderiza el componente
NoteForm .
-La prueba primero busca el botón de inicio de sesión por su texto y hace clic en el botón con el comando [cy.click](https://docs.cypress.io/api/commands/click.html#Syntax).
+En una página web normal, cambiar la dirección en la barra de direcciones del navegador hace que la página se vuelva a cargar. Sin embargo, cuando se usa React Router, esto no sucede; en cambio, el enrutamiento se maneja completamente a través de JavaScript en la interfaz.
-Nuestras dos pruebas comienzan de la misma manera, abriendo la página
http://localhost:5173 , por lo que deberíamos extraer el código compartido en un bloque
beforeEach que se ejecuta antes de cada prueba:
+El componente del enrutador que utilizamos es [BrowserRouter](https://reactrouter.com/en/main/router-components/browser-router):
```js
-describe('Note app', function() {
- // highlight-start
- beforeEach(function() {
- cy.visit('http://localhost:5173')
- })
- // highlight-end
+import {
+ BrowserRouter as Router, // highlight-line
+ Routes, Route, Link
+} from 'react-router-dom'
+```
- it('front page can be opened', function() {
- cy.contains('Notes')
- cy.contains('Note app, Department of Computer Science, University of Helsinki 2023')
- })
+Según la [documentación](https://reactrouter.com/en/main/router-components/browser-router)
- it('login form can be opened', function() {
- cy.contains('log in').click()
- })
-})
-```
+>
BrowserRouter es un
Router que utiliza la API de historial HTML5 (pushState, replaceState y el evento popstate) para mantener su interfaz de usuario sincronizada con la URL.
-El campo de inicio de sesión contiene dos campos de
input , en los que la prueba debe escribir.
+
BrowserRouter utiliza la [API de historial HTML5](https://css-tricks.com/using-the-html5-history-api/) para permitir que la URL en la barra de direcciones del navegador se use para el "enrutamiento" interno dentro de una aplicación React, lo que significa que incluso si la URL en la barra de direcciones cambia, el contenido de la página se manipula únicamente a través de JavaScript y el navegador no carga contenido nuevo desde el servidor. Sin embargo, el comportamiento del navegador con respecto a las funciones de avance y retroceso y los marcadores es intuitivo: funciona igual que en los sitios web tradicionales.
-El comando [cy.get](https://docs.cypress.io/api/commands/get.html#Syntax) permite buscar elementos mediante selectores CSS.
+El código actual de la aplicación está disponible en su totalidad en [GitHub](https://github.com/fullstack-hy2020/part2-notes-frontend/tree/part5-10), en la rama
part5-10 .
-Podemos acceda al primer y último campo de input de la página, y escribir en ellos con el comando [cy.type](https://docs.cypress.io/api/commands/type.html#Syntax) así:
+### Ruta parametrizada
-```js
-it('user can login', function () {
- cy.contains('log in').click()
- cy.get('input:first').type('mluukkai')
- cy.get('input:last').type('salainen')
-})
-```
+Movamos los detalles de una sola nota a su propia vista, a la que se puede acceder haciendo clic en el nombre de la nota:
-La prueba funciona. El problema es que si luego agregamos más campos de input, la prueba se interrumpirá porque espera que los campos que necesita sean el primero y el último en la página.
+
-Sería mejor dar a nuestros inputs
IDs únicos y usarlos para encontrarlos.
-Cambiamos nuestro formulario de inicio de sesión de la siguiente manera:
+
+La capacidad de hacer clic en el nombre se ha implementado en el componente
NoteList de la siguiente manera:
```js
-const LoginForm = ({ ... }) => {
+import { Link } from 'react-router-dom' // highlight-line
+
+const NoteList = ({ notes }) => {
+ // ...
+
return (
)
}
-```
-
-También agregamos una ID a nuestro botón submit para que podamos acceder a él en nuestras pruebas.
-
-La prueba se convierte en:
-```js
-describe('Note app', function() {
- // ..
- it('user can log in', function() {
- cy.contains('log in').click()
- cy.get('#username').type('mluukkai') // highlight-line
- cy.get('#password').type('salainen') // highlight-line
- cy.get('#login-button').click() // highlight-line
-
- cy.contains('Matti Luukkainen logged in') // highlight-line
- })
-})
+export default NoteList
```
-La última línea asegura que el inicio de sesión fue exitoso.
-
-Ten en cuenta que el [selector de ID](https://developer.mozilla.org/es/docs/Web/CSS/ID_selectors) de CSS es #, así que si queremos buscar un elemento con el ID
username el selector de CSS es
#username .
-
-Por favor, ten en cuenta que para que la prueba pase en esta etapa, es necesario que haya un usuario en la base de datos de pruebas del entorno de test del backend, cuyo nombre de usuario sea
mluukkai y la contraseña sea
salainen . ¡Crea un usuario si es necesario!
+Así volvemos a utilizar [Link](https://reactrouter.com/api/components/Link). Por ejemplo, al hacer clic en el nombre de una nota cuyo
id es 12345, la URL del navegador se actualiza a
notes/12345 .
-### Probando el formulario para agregar notas
-
-A continuación, agreguemos pruebas para probar la funcionalidad "new note":
+La URL parametrizada se define en el enrutamiento dentro del componente
App de la siguiente manera:
```js
-describe('Note app', function() {
- // ..
- // highlight-start
- describe('when logged in', function() {
- beforeEach(function() {
- cy.contains('log in').click()
- cy.get('input:first').type('mluukkai')
- cy.get('input:last').type('salainen')
- cy.get('#login-button').click()
- })
- // highlight-end
+
+ // ...
+
// highlight-start
- it('a new note can be created', function() {
- cy.contains('new note').click()
- cy.get('input').type('a note created by cypress')
- cy.contains('save').click()
-
- cy.contains('a note created by cypress')
- })
- })
- // highlight-end
-})
+
+ } />
+ // highlight-end
+ } />
+ : } />
+ } />
+ } />
+
+
```
-La prueba se ha definido en su propio bloque
describe .
-Solo los usuarios registrados pueden crear nuevas notas, por lo que agregamos el inicio de sesión en la aplicación en un bloque
beforeEach .
-
-La prueba confía en que al crear una nueva nota, la página contiene solo una entrada, por lo que la busca así:
+La ruta que representa la vista para una sola nota se define en el "estilo Express" marcando el parámetro de ruta con la notación
:id de la siguiente manera:
```js
-cy.get('input')
+
} />
```
-Si la página tuviera más inputs, la prueba se rompería
-
-
-
-Debido a esto, nuevamente sería mejor darle al input un
ID y buscar el elemento por su ID.
-
-La estructura de las pruebas se ve así:
+Cuando el navegador navega a la URL única de una nota, por ejemplo,
/notes/12345 , se representa el componente
Note , que ahora hemos tenido que modificar ligeramente:
```js
-describe('Note app', function() {
- // ...
-
- it('user can log in', function() {
- cy.contains('log in').click()
- cy.get('#username').type('mluukkai')
- cy.get('#password').type('salainen')
- cy.get('#login-button').click()
-
- cy.contains('Matti Luukkainen logged in')
- })
-
- describe('when logged in', function() {
- beforeEach(function() {
- cy.contains('log in').click()
- cy.get('input:first').type('mluukkai')
- cy.get('input:last').type('salainen')
- cy.get('#login-button').click()
- })
+import { useParams } from 'react-router-dom' // highlight-line
- it('a new note can be created', function() {
- // ...
- })
- })
-})
-```
-
-Cypress ejecuta las pruebas en el orden en que están en el código. Entonces, primero ejecuta
user can log in , donde el usuario inicia sesión. Entonces cypress ejecutará
a new note can be created para la cual el bloque
beforeEach también inicia sesión.
-¿Por qué hacer esto? ¿No inició sesión el usuario después de la primera prueba?
-No, porque
cada prueba comienza desde cero en lo que respecta al navegador.
-Todos los cambios en el estado del navegador se invierten después de cada prueba.
-
-### Controlando el estado de la base de datos
+const Note = ({ notes, toggleImportance }) => {
+ // highlight-start
+ const id = useParams().id
+ const note = notes.find(n => n.id === id)
+ // highlight-end
-Si las pruebas necesitan poder modificar la base de datos del servidor, la situación inmediatamente se vuelve más complicada. Idealmente, la base de datos del servidor debería ser la misma cada vez que ejecutamos las pruebas, para que nuestras pruebas se puedan repetir de forma fiable y sencilla.
+ const label = note.important ? 'make not important' : 'make important'
-Al igual que con las pruebas unitarias y de integración, con las pruebas E2E es mejor vaciar la base de datos y posiblemente formatearla antes de ejecutar las pruebas. El desafío con las pruebas E2E es que no tienen acceso a la base de datos.
+ return (
+
+ {note.content}
+ toggleImportance(id)}>{label}
+
+ )
+}
-La solución es crear endpoints de API en el backend para la prueba.
-Podemos vaciar la base de datos usando estos endpoints.
-Creemos un nuevo enrutador para las pruebas dentro de la carpeta
controllers , en el archivo
testing.js
+export default Note
+```
-```js
-const testingRouter = require('express').Router()
-const Note = require('../models/note')
-const User = require('../models/user')
+A diferencia de antes, el componente
Note ahora recibe todas las notas mediante la prop
notes y puede acceder a la parte variable de la URL —en concreto, el
id de la nota que se mostrará— mediante el hook [useParams](https://reactrouter.com/api/hooks/useParams) de React Router.
-testingRouter.post('/reset', async (request, response) => {
- await Note.deleteMany({})
- await User.deleteMany({})
+### useNavigate
- response.status(204).end()
-})
+El backend ya admite la eliminación de notas. Para implementar esto, agreguemos un botón a la página de notas individuales en la aplicación:
-module.exports = testingRouter
-```
+
-y agrégalo al backend solo
si la aplicación se ejecuta en modo de prueba :
+Agreguemos un controlador al componente
App que realiza la eliminación y pasémoslo al componente
Note :
```js
-// ...
-
-app.use('/api/login', loginRouter)
-app.use('/api/users', usersRouter)
-app.use('/api/notes', notesRouter)
+const App = () => {
-// highlight-start
-if (process.env.NODE_ENV === 'test') {
- const testingRouter = require('./controllers/testing')
- app.use('/api/testing', testingRouter)
-}
-// highlight-end
+ // highlight-start
+ const deleteNote = (id) => {
+ noteService.remove(id).then(() => {
+ setNotes(notes.filter(n => n.id !== id))
+ })
+ }
+ // highlight-end
-app.use(middleware.unknownEndpoint)
-app.use(middleware.errorHandler)
+ return (
+ // ...
-module.exports = app
+
+
+ } />
+
+ } />
+
+ } />
+ } />
+
+
+
+
+ )
+}
```
-Después de los cambios, una solicitud POST HTTP al endpoint
/api/testing/reset vacía la base de datos. Asegúrate de que tu backend esté ejecutándose en modo de prueba iniciándolo con este comando (previamente configurado en el archivo package.json):
+El componente
Note cambia de la siguiente manera:
```js
- npm run start:test
-```
+import { useParams, useNavigate } from 'react-router-dom'
-El código de backend modificado se puede encontrar en [GitHub](https://github.com/fullstack-hy2020/part3-notes-backend/tree/part5-1), rama
part5-1 .
+const Note = ({ notes, toggleImportanceOf, deleteNote }) => { // highlight-line
+ const id = useParams().id
+ const navigate = useNavigate() // highlight-line
+ const note = notes.find(n => n.id === id)
-A continuación, cambiaremos el bloque
beforeEach para que vacíe la base de datos del servidor antes de ejecutar las pruebas.
+ const label = note.important ? 'make not important' : 'make important'
-Actualmente no es posible agregar nuevos usuarios a través de la interfaz de usuario del frontend, por lo que agregamos un nuevo usuario al backend desde el bloque beforeEach.
-
-```js
-describe('Note app', function() {
- beforeEach(function() {
- // highlight-start
- cy.request('POST', 'http://localhost:3001/api/testing/reset')
- const user = {
- name: 'Matti Luukkainen',
- username: 'mluukkai',
- password: 'salainen'
+// highlight-start
+ const handleDelete = () => {
+ if (window.confirm(`Delete note "${note.content}"?`)) {
+ deleteNote(id)
+ navigate('/notes')
}
- cy.request('POST', 'http://localhost:3001/api/users/', user)
- // highlight-end
- cy.visit('http://localhost:5173')
- })
-
- it('front page can be opened', function() {
- // ...
- })
+ }
+ // highlight-end
- it('user can login', function() {
- // ...
- })
+ return (
+
+ {note.content}
+ toggleImportanceOf(id)}>{label}
+ delete // highlight-line
+
+ )
+}
- describe('when logged in', function() {
- // ...
- })
-})
+export default Note
```
-Durante el formateo, la prueba realiza solicitudes HTTP al backend con [cy.request](https://docs.cypress.io/api/commands/request.html).
-
-A diferencia de antes, ahora la prueba comienza con el backend en el mismo estado cada vez. El backend contendrá un usuario y ninguna nota.
+Cuando se elimina una nota, el usuario regresa a la página que enumera todas las notas. Esto se hace llamando a la función devuelta por [useNavigate](https://reactrouter.com/api/components/Navigate) de React Router con la URL deseada:
navigate('/notes') .
-Agreguemos una prueba más para verificar que podemos cambiar la importancia de notas.
+Las funciones [useParams](https://reactrouter.com/api/hooks/useParams) y [useNavigate](https://reactrouter.com/api/components/Navigate) de React Router son hooks, al igual que useState y useEffect, que ya hemos utilizado muchas veces. Como vimos en la parte 1, existen ciertas [reglas](/es/part1/un_estado_mas_complejo_depurando_aplicaciones_react#reglas-de-los-hooks) asociadas al uso de hooks.
-Anteriormente cambiamos el frontend para que una nueva nota se importante por defecto, por lo que el campo
important es
true :
+Modifiquemos también el componente
NoteForm para que después de agregar una nueva nota, el usuario acceda a la página que contiene todas las notas:
```js
+import { useState } from 'react'
+import { useNavigate } from 'react-router-dom' // highlight-line
+
const NoteForm = ({ createNote }) => {
- // ...
+ const [newNote, setNewNote] = useState('')
+ const navigate = useNavigate() // highlight-line
- const addNote = (event) => {
+ const addNote = event => {
event.preventDefault()
createNote({
content: newNote,
- important: true // highlight-line
+ important: true
})
+ navigate('/notes') // highlight-line
setNewNote('')
}
- // ...
-}
+
+ return (
+
+
Create a new note
+
+
+
+ )
+}
```
-Hay varias formas de probar esto. En el siguiente ejemplo, primero buscamos una nota y hacemos clic en su botón
make not important . Luego verificamos que la nota ahora contenga un botón
make important .
+### Ruta parametrizada revisada
+
+Hay un problema ligeramente molesto con la aplicación. El componente _Note_ recibe
all notes como props, aunque solo muestra aquella cuyo
id coincide con la parte parametrizada de la URL:
```js
-describe('Note app', function() {
+const Note = ({ notes, toggleImportance }) => {
+ const id = useParams().id
+ const note = notes.find(n => n.id === Number(id))
// ...
+}
+```
- describe('when logged in', function() {
- // ...
+¿Sería posible modificar la aplicación para que _Note_ reciba solo la nota que se mostrará como prop?
- describe('and a note exists', function () {
- beforeEach(function () {
- cy.contains('new note').click()
- cy.get('input').type('another note cypress')
- cy.contains('save').click()
- })
-
- it('it can be made not important', function () {
- cy.contains('another note cypress')
- .contains('make not important')
- .click()
-
- cy.contains('another note cypress')
- .contains('make important')
- })
- })
- })
-})
-```
+```js
+import { useParams, useNavigate } from 'react-router-dom'
-El primer comando busca un componente que contenga el texto
another note cypress , y luego busca un botón
make not important dentro de él. Luego hace clic en el botón.
+const Note = ({ note, id, toggleImportanceOf, deleteNote }) => { // highlight-line
+ const id = useParams().id
+ const navigate = useNavigate()
-El segundo comando comprueba que el texto del botón haya cambiado a
make important .
+ // ...
-### Prueba de inicio de sesión fallida
+ return (
+
+ {note.content}
+ toggleImportanceOf(id)}>{label}
+ delete
+
+ )
+}
-Hagamos una prueba para asegurarnos de que un intento de inicio de sesión falla si la contraseña es incorrecta.
+export default Note
+```
-Cypress ejecutará todas las pruebas cada vez de forma predeterminada y, a medida que aumenta el número de pruebas, comienza a consumir bastante tiempo.
-Al desarrollar una nueva prueba o al depurar una prueba rota, podemos definir la prueba con
it.only en lugar de
it , de modo que Cypress solo ejecutará la prueba requerida.
-Cuando la prueba esté funcionando, podemos eliminar
.only .
+Una posibilidad es determinar dentro del componente el
id de la nota que se mostrará mediante el hook [useMatch](https://reactrouter.com/en/main/hooks/use-match) de React Router.
-La primera versión de nuestras pruebas es la siguiente:
+No es posible utilizar el hook
useMatch en el mismo componente que define la parte enrutable de la aplicación. Movamos el componente
Router fuera de
App :
```js
-describe('Note app', function() {
- // ...
-
- it.only('login fails with wrong password', function() {
- cy.contains('log in').click()
- cy.get('#username').type('mluukkai')
- cy.get('#password').type('wrong')
- cy.get('#login-button').click()
+ReactDOM.createRoot(document.getElementById('root')).render(
+
// highlight-line
+
+ // highlight-line
+)
+```
- cy.contains('wrong credentials')
- })
+El componente
App se convierte en:
+```js
+import {
// ...
-)}
-```
+ useMatch // highlight-line
+} from 'react-router-dom'
-La prueba utiliza [cy.contains](https://docs.cypress.io/api/commands/contains.html#Syntax) para garantizar que la aplicación imprima un mensaje de error.
+const App = () => {
+ // ...
-La aplicación muestra el mensaje de error en un componente con la clase CSS
error :
+ // highlight-start
+ const match = useMatch('/notes/:id')
-```js
-const Notification = ({ message }) => {
- if (message === null) {
- return null
- }
+ const note = match
+ ? notes.find(note => note.id === match.params.id)
+ : null
+ // highlight-end
return (
-
// highlight-line
- {message}
+
+
+ home
+ // ...
+
+
+
+
+ } />
+
+ } />
+
+ } />
+ } />
+
+
+
+ Note app, Department of Computer Science 2026
+
)
-}
+}
```
-Podríamos hacer que la prueba asegure que el mensaje de error se renderiza al componente correcto, es decir, al componente con la clase CSS
error :
+Cada vez que se renderiza el componente
App (lo que, en la práctica, ocurre cada vez que cambia la URL en la barra de direcciones del navegador) se ejecuta el siguiente comando
```js
-it('login fails with wrong password', function() {
- // ...
-
- cy.get('.error').contains('wrong credentials') // highlight-line
-})
+const match = useMatch('/notes/:id')
```
-Primero usamos [cy.get](https://docs.cypress.io/api/commands/get.html#Syntax) para buscar un componente con la clase CSS
error . Luego verificamos que el mensaje de error se pueda encontrar en este componente.
-Ten en cuenta que los [selectores de clase CSS](https://developer.mozilla.org/es/docs/Web/CSS/Class_selectors) comienzan con un punto final, por lo que el selector para la clase
error es
.error .
-
-Podríamos hacer lo mismo usando la sintaxis [should](https://docs.cypress.io/api/commands/should.html):
+Si la URL tiene el formato _/notes/:id_, es decir, corresponde a la URL de una sola nota, a la variable
match se le asigna un objeto que puede usarse para determinar la parte parametrizada de la ruta, es decir, el
id de la nota. Esto nos permite recuperar la nota a renderizar:
```js
-it('login fails with wrong password', function() {
- // ...
-
- cy.get('.error').should('contain', 'wrong credentials') // highlight-line
-})
+const note = match
+ ? notes.find(note => note.id === match.params.id)
+ : null
```
-Usar should es un poco más complicado que usar
contains , pero permite pruebas más diversas que
contains , que funciona solo con contenido de texto.
-La lista de las aserciones más comunes con las que se puede usar _should_ se puede encontrar [aquí](https://docs.cypress.io/guides/references/assertions.html#Common-Assertions).
+Todavía hay un pequeño error en nuestra aplicación. Si el navegador se recarga en una sola página de notas, se produce un error:
+
+
-Podemos, por ejemplo, asegurarnos de que el mensaje de error sea rojo y tenga un borde:
+El problema surge porque se intenta representar la página antes de que se hayan obtenido las notas del backend. Podemos resolver este problema con renderizado condicional:
```js
-it('login fails with wrong password', function() {
- // ...
+const Note = ({ note, toggleImportanceOf, deleteNote }) => {
+ const id = useParams().id
+ const navigate = useNavigate()
- cy.get('.error').should('contain', 'wrong credentials')
- cy.get('.error').should('have.css', 'color', 'rgb(255, 0, 0)')
- cy.get('.error').should('have.css', 'border-style', 'solid')
-})
+// highlight-start
+ if(!note) {
+ return null
+ }
+ // highlight-end
+
+ return (
+ //...
+ )
+}
```
-Cypress requiere que los colores se den como [rgb](https://rgbcolorcode.com/color/red).
+La aplicación tiene una característica más molesta: la lógica de inicio de sesión todavía está en la página que enumera las notas. Sin embargo, dejaremos la funcionalidad en este estado algo incompleto por ahora.
-Debido a que todas las pruebas son para el mismo componente al que accedimos usando [cy.get](https://docs.cypress.io/api/commands/get.html#Syntax), podemos encadenarlos usando [and](https://docs.cypress.io/api/commands/and.html).
+El código actual de la aplicación está disponible en su totalidad en [GitHub](https://github.com/fullstack-hy2020/part2-notes-frontend/tree/part5-11), en la rama
part5-11 .
-```js
-it('login fails with wrong password', function() {
- // ...
+
- cy.get('.error')
- .should('contain', 'wrong credentials')
- .and('have.css', 'color', 'rgb(255, 0, 0)')
- .and('have.css', 'border-style', 'solid')
-})
-```
+
-Terminemos la prueba para que también verifique que la aplicación no muestre el mensaje de éxito 'Matti Luukkainen logged in' :
+### Ejercicios 5.24–5.28.
-```js
-it('login fails with wrong password', function() {
- cy.contains('log in').click()
- cy.get('#username').type('mluukkai')
- cy.get('#password').type('wrong')
- cy.get('#login-button').click()
-
- cy.get('.error')
- .should('contain', 'wrong credentials')
- .and('have.css', 'color', 'rgb(255, 0, 0)')
- .and('have.css', 'border-style', 'solid')
-
- cy.get('html').should('not.contain', 'Matti Luukkainen logged in') // highlight-line
-})
-```
+#### 5.24: blogs enrutados, paso 1
-El comando should se usa más frecuentemente encadenándolo después del comando get (o otro comando similar que pueda ser encadenado). El cy.get('html') usado en la prueba prácticamente significa el contenido visible de toda la aplicación.
+Añade React Router a la aplicación de blogs para que los enlaces de la barra de navegación controlen qué vista se muestra.
-También podríamos verificar lo mismo encadenando el comando contains con el comando should con un parámetro ligeramente diferente:
+En la raíz de la aplicación, es decir, la ruta _/_, se muestra una lista de todos los blogs:
-```js
-cy.contains('Matti Luukkainen logged in').should('not.exist')
-```
+
-**NOTA:** Algunas propiedades CSS se comportan de manera diferente en Firefox. Si ejecutas las pruebas con Firefox:
+La ruta _/login_ permite a los usuarios iniciar sesión
- 
-
- entonces las pruebas que involucran, por ejemplo, `border-style`, `border-radius` y `padding`, pasarán en Chrome o Electron, pero fallarán en Firefox:
+
- 
+Si el usuario ha iniciado sesión, aparece un botón de cierre de sesión en la barra de navegación:
-### Omitiendo la interfaz de usuario
+
-Actualmente tenemos las siguientes pruebas:
+Después de iniciar y cerrar sesión, el usuario debe ser dirigido a la página que enumera todos los blogs.
-```js
-describe('Note app', function() {
- it('user can login', function() {
- cy.contains('log in').click()
- cy.get('#username').type('mluukkai')
- cy.get('#password').type('salainen')
- cy.get('#login-button').click()
+En esta etapa, todavía no necesita preocuparse por crear blogs.
- cy.contains('Matti Luukkainen logged in')
- })
+#### 5.25: blogs enrutados, paso 2
- it('login fails with wrong password', function() {
- // ...
- })
-
- describe('when logged in', function() {
- beforeEach(function() {
- cy.contains('log in').click()
- cy.get('input:first').type('mluukkai')
- cy.get('input:last').type('salainen')
- cy.get('#login-button').click()
- })
+Implementa una vista que muestre la información de una única publicación del blog:
- it('a new note can be created', function() {
- // ...
- })
-
- })
-})
-```
+
-Primero probamos el inicio de sesión. Luego, en su propio bloque de descripción, tenemos un montón de pruebas que esperan que el usuario inicie sesión. El usuario ha iniciado sesión en el bloque beforeEach .
+Los usuarios navegan a la vista de publicación de blog única desde la lista de blogs:
-Como dijimos anteriormente, ¡cada prueba comienza desde cero! Las pruebas no comienzan en el estado donde terminaron las pruebas anteriores.
+
-La documentación de Cypress nos da el siguiente consejo: [Prueba completamente el flujo de inicio de sesión, ¡pero solo una vez!](https://docs.cypress.io/guides/end-to-end-testing/testing-your-app#Fully-test-the-login-flow----but-only-once).
-Por lo tanto, en lugar de iniciar sesión como usuario mediante el formulario en el bloque beforeEach , vamos a omitir la interfaz de usuario y realizaremos una solicitud HTTP al backend para iniciar sesión. La razón de esto es que iniciar sesión con una solicitud HTTP es mucho más rápido que completar un formulario.
+¡Asegúrate de que la función "Me gusta" para blogs todavía funcione! También modifique la funcionalidad para que sólo los usuarios que hayan iniciado sesión puedan darle "Me gusta" a un blog.
-Nuestra situación es un poco más complicada que en el ejemplo de la documentación de Cypress, porque cuando un usuario inicia sesión, nuestra aplicación guarda sus detalles en localStorage.
-Sin embargo, Cypress también puede manejar esto.
-El código es el siguiente:
+#### 5.26: blogs enrutados, paso 3
-```js
-describe('when logged in', function() {
- beforeEach(function() {
- // highlight-start
- cy.request('POST', 'http://localhost:3001/api/login', {
- username: 'mluukkai', password: 'salainen'
- }).then(response => {
- localStorage.setItem('loggedNoteappUser', JSON.stringify(response.body))
- cy.visit('http://localhost:5173')
- })
- // highlight-end
- })
+Crea una nueva vista para añadir blogs a la que puedan acceder desde la navegación los usuarios que hayan iniciado sesión:
- it('a new note can be created', function() {
- // ...
- })
+
- // ...
-})
-```
+Agregar un blog nuevo y eliminar un blog existente debería redirigir al usuario a la vista de todos los blogs.
-Podemos acceder a la respuesta de un [cy.request](https://docs.cypress.io/api/commands/request.html) con el método [_then_](https://docs.cypress.io/guides/core-concepts/introduction-to-cypress#The-Cypress-Command-Queue). Debajo del capó, cy.request , al igual que todos los comandos de Cypress, son [asíncronos](https://docs.cypress.io/guides/core-concepts/introduction-to-cypress#Commands-Are-Asynchronous).
-La función de callback guarda los detalles de un usuario conectado en localStorage y recarga la página.
-Ahora no hay diferencia con un usuario que inicia sesión a través del formulario de inicio de sesión.
+#### 5.27: blogs enrutados, paso 4
-Si cuando escribimos nuevas pruebas en nuestra aplicación, tenemos que usar el código de inicio de sesión en varios lugares, deberíamos convertirlo en un [comando personalizado](https://docs.cypress.io/api/cypress-api/custom-commands.html).
+La usabilidad y apariencia de la aplicación ahora son mejores que antes. Desafortunadamente, algunas de las pruebas han fallado.
-Los comandos personalizados se declaran en cypress/support/commands.js .
-El código para iniciar sesión es el siguiente:
+Ahora modifique las pruebas para la vista de blog única creada en Vitest de la siguiente manera
+- La información del blog y la cantidad de Me gusta se muestran a los usuarios no autenticados, los botones no se muestran
+- A los usuarios autenticados que no son los creadores del blog se les muestra solo el botón Me gusta.
+- Al creador del blog también se le muestra el botón de eliminar.
-```js
-Cypress.Commands.add('login', ({ username, password }) => {
- cy.request('POST', 'http://localhost:3001/api/login', {
- username, password
- }).then(({ body }) => {
- localStorage.setItem('loggedNoteappUser', JSON.stringify(body))
- cy.visit('http://localhost:5173')
- })
-})
-```
+#### 5.28: blogs enrutados, paso 5
-Usar nuestro comando personalizado es fácil y nuestra prueba se vuelve más limpia:
+El siguiente paso es arreglar las pruebas de un extremo a otro creadas con Playwright. Las pruebas que escribimos anteriormente no funcionan por completo y tendremos que realizarles cambios importantes.
-```js
-describe('when logged in', function() {
- beforeEach(function() {
- // highlight-start
- cy.login({ username: 'mluukkai', password: 'salainen' })
- // highlight-end
- })
+Crea pruebas para los siguientes escenarios:
+- El inicio de sesión se realizó correctamente con la combinación correcta de nombre de usuario y contraseña.
+- El inicio de sesión falla si el nombre de usuario/contraseña es incorrecto
+- Un usuario que haya iniciado sesión puede crear un blog.
+- A un usuario que haya iniciado sesión le pueden gustar los blogs.
+- Un usuario que haya iniciado sesión puede eliminar un blog.
- it('a new note can be created', function() {
- // ...
- })
+Por lo tanto, la clasificación de blogs por Me gusta no se está probando en este momento.
- // ...
-})
-```
+
-Lo mismo se aplica a la creación de una nueva nota ahora que pensamos sobre ello. Tenemos una prueba que hace una nueva nota usando el formulario. También hacemos una nueva nota en el bloque
beforeEach de la prueba que cambia la importancia de una nota:
+
-```js
-describe('Note app', function() {
- // ...
+### Librerías de UI
- describe('when logged in', function() {
- it('a new note can be created', function() {
- cy.contains('new note').click()
- cy.get('input').type('a note created by cypress')
- cy.contains('save').click()
+En la parte 2 ya vimos dos formas de añadir estilos: mediante un [único archivo CSS](/es/part2/agregar_estilos_a_la_aplicacion_react) tradicional y mediante [estilos en línea](/es/part2/agregar_estilos_a_la_aplicacion_react#estilos-en-linea). En esta sección veremos algunas formas más.
- cy.contains('a note created by cypress')
- })
+Una forma de definir los estilos de una aplicación es utilizar un framework de UI, es decir, una librería de estilos para interfaces de usuario.
- describe('and a note exists', function () {
- beforeEach(function () {
- cy.contains('new note').click()
- cy.get('input').type('another note cypress')
- cy.contains('save').click()
- })
+El primer framework de UI que alcanzó una gran popularidad fue [Bootstrap](https://getbootstrap.com/), desarrollado por Twitter. Durante los últimos años, los frameworks de UI han proliferado. La selección es tan amplia que ni siquiera merece la pena intentar enumerarlos todos aquí.
- it('it can be made important', function () {
- // ...
- })
- })
- })
-})
-```
+Muchos marcos de UI incluyen temas predefinidos para aplicaciones web, así como "componentes", como botones, menús y tablas. El término "componente" está escrito entre comillas arriba porque no se refiere a lo mismo que un componente de React. La mayoría de las veces, los marcos de UI se utilizan incluyendo las hojas de estilo CSS del marco y el código JavaScript en la aplicación.
-Creemos un nuevo comando personalizado para crear una nueva nota. El comando creará una nueva nota con una solicitud HTTP POST:
+Muchos marcos de UI se han adaptado a versiones compatibles con React, donde los "componentes" definidos por el marco de UI se han convertido en componentes de React. Por ejemplo, hay un par de versiones React de Bootstrap, la más popular de las cuales es [React-Bootstrap](https://react-bootstrap.github.io/).
-```js
-Cypress.Commands.add('createNote', ({ content, important }) => {
- cy.request({
- url: 'http://localhost:3001/api/notes',
- method: 'POST',
- body: { content, important },
- headers: {
- 'Authorization': `Bearer ${JSON.parse(localStorage.getItem('loggedNoteappUser')).token}`
- }
- })
+En lugar de Bootstrap, veamos el que quizá sea actualmente el framework de UI más popular: la librería para React [Material UI](https://mui.com/), que implementa el lenguaje de diseño [Material Design](https://material.io/) de Google.
- cy.visit('http://localhost:5173')
-})
-```
+Instalemos la librería:
-El comando espera que el usuario haya iniciado sesión y que los detalles del usuario estén guardados en localStorage.
+```bash
+npm install @mui/material @emotion/react @emotion/styled
+```
-Ahora el bloque beforeEach de la nota se convierte en:
+Cuando se utiliza Material UI, todo el contenido de la aplicación suele renderizarse dentro del componente [Container](https://mui.com/material-ui/react-container/):
```js
-describe('Note app', function() {
- // ...
+import { Container } from '@mui/material'
- describe('when logged in', function() {
- it('a new note can be created', function() {
+const App = () => {
+ // ...
+ return (
+
// ...
- })
-
- describe('and a note exists', function () {
- beforeEach(function () {
- // highlight-start
- cy.createNote({
- content: 'another note cypress',
- important: true
- })
- // highlight-end
- })
-
- it('it can be made important', function () {
- // ...
- })
- })
- })
-})
+
+ )
+}
```
-Hay otra cosa en nuestras pruebas que es molesta. La URL de nuestra aplicación
http://localhost:5173 esta codificada literalmente en varios lugares.
+#### Tabla
-Definamos la URL de nuestra aplicación
baseUrl en el [archivo de configuración](https://docs.cypress.io/guides/references/configuration) pre-generado de Cypress
cypress.config.js :
+Comencemos con el componente
NoteList y rendericemos la lista de notas como una [tabla](https://mui.com/material-ui/react-table/#simple-table), que también muestra el usuario que creó cada nota:
```js
-const { defineConfig } = require("cypress")
-
-module.exports = defineConfig({
- e2e: {
- setupNodeEvents(on, config) {
- },
- baseUrl: 'http://localhost:5173' // highlight-line
- },
-})
-```
+import { useState, useEffect } from 'react'
-Todos los comandos en las pruebas que usan la dirección de la aplicación
+import { Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper } from '@mui/material'
-```js
-cy.visit('http://localhost:5173')
-```
+//...
-se pueden cambiar a
+const NoteList = ({ notes }) => {
-```js
-cy.visit('')
-```
-
-La dirección codificada del backend,
http://localhost:3001 , todavía está en las pruebas. La [documentación](https://docs.cypress.io/guides/guides/environment-variables) de Cypress recomienda definir otras direcciones utilizadas por las pruebas como variables de entorno.
-
-Expandamos el archivo de configuración
cypress.config.js de la siguiente manera:
+ // ...
-```js
-const { defineConfig } = require("cypress")
-
-module.exports = defineConfig({
- e2e: {
- setupNodeEvents(on, config) {
- },
- baseUrl: 'http://localhost:5173',
- env: {
- BACKEND: 'http://localhost:3001/api' // highlight-line
- }
- },
-})
-```
+ return (
+
+ // ...
+
Notes
+
+
+
+
+
+ content
+ user
+ important
+
+
+
+ {notes.map(note => (
+
+
+
+ {note.content}
+
+
+
+ {note.user.name}
+
+
+ {note.important ? 'yes': ''}
+
+
+ ))}
+
+
+
-Reemplacemos todas las direcciones del backend en las pruebas de la siguiente manera:
+
+ )
+}
-```js
-describe('Note ', function() {
- beforeEach(function() {
-
- cy.request('POST', `${Cypress.env('BACKEND')}/testing/reset`) // highlight-line
- const user = {
- name: 'Matti Luukkainen',
- username: 'mluukkai',
- password: 'secret'
- }
- cy.request('POST', `${Cypress.env('BACKEND')}/users`, user) // highlight-line
- cy.visit('')
- })
- // ...
-})
+export default NoteList
```
-### Cambiando la importancia de una nota
+La tabla se ve así:
-Por último, echemos un vistazo a la prueba que hicimos para cambiar la importancia de una nota.
-Primero cambiaremos el bloque beforeEach para que cree tres notas en lugar de una:
+
-```js
-describe('when logged in', function() {
- describe('and several notes exist', function () {
- beforeEach(function () {
- // highlight-start
- cy.login({ username: 'mluukkai', password: 'salainen' })
- cy.createNote({ content: 'first note', important: false })
- cy.createNote({ content: 'second note', important: false })
- cy.createNote({ content: 'third note', important: false })
- // highlight-end
- })
-
- it('one of those can be made important', function () {
- cy.contains('second note')
- .contains('make important')
- .click()
-
- cy.contains('second note')
- .contains('make not important')
- })
- })
-})
-```
-¿Cómo funciona realmente el comando [cy.contains](https://docs.cypress.io/api/commands/contains.html)?
+#### Formulario
-Cuando hacemos clic en el comando _cy.contains('second note')_ en Cypress [Test Runner](https://docs.cypress.io/guides/core-concepts/cypress-app#Test-Runner), vemos que ese comando busca el elemento que contiene el texto
second note :
+A continuación, mejoremos la vista para crear una nueva nota
NoteForm usando los componentes [TextField](https://mui.com/components/text-fields/) y [Button](https://mui.com/api/button/):
-
+```js
+import { TextField, Button } from '@mui/material'
-Al hacer clic en la línea siguiente _.contains('make important')_ vemos que la prueba utiliza el botón 'make important' correspondiente a la
segunda nota :
+// ...
-
+const NoteForm = ({ createNote }) => {
+ // ...
-Cuando está encadenado, el segundo comando
contains continúa la búsqueda desde dentro del componente encontrado por el primer comando.
+ return (
+
+
Create a new note
+
+
+
+ )
+}
-Si no hubiéramos encadenado los comandos, y en su lugar hubiéramos escrito
+export default NoteForm
-```js
-cy.contains('second note')
-cy.contains('make important').click()
```
-el resultado habría sido totalmente diferente. La segunda línea de la prueba haría clic en el botón de una nota incorrecta:
+El resultado es elegante:
+
+
-
+#### Notificaciones
-Al escribir pruebas, ¡debes verificar en el ejecutor de pruebas que las pruebas utilicen los componentes correctos!
-Cambiemos el componente _Note_ para que el texto de la nota se renderice en un
span .
+Mejoremos el componente de notificación mediante el componente [Alert](https://mui.com/material-ui/react-alert/) de Material UI:
```js
-const Note = ({ note, toggleImportance }) => {
- const label = note.important
- ? 'make not important' : 'make important'
+import { Alert } from '@mui/material'
+
+const Notification = ({ notification }) => {
+ if (notification === null) {
+ return null
+ }
return (
-
- {note.content} // highlight-line
- {label}
-
+
+ {notification.text}
+
)
}
+
+export default Notification
```
-¡Nuestras pruebas se rompen! Como revela el test runner, _cy.contains('second note')_ ahora devuelve el componente que contiene el texto y el botón no está en él.
+Mueva el componente de notificación y su gestión de estado al componente
App :
-
+```js
+const App = () => {
+ const [notes, setNotes] = useState([])
+ const [notification, setNotification] = useState(null) // highlight-line
-Una forma de solucionarlo es la siguiente:
+ // ...
-```js
-it('one of those can be made important', function () {
- cy.contains('second note').parent().find('button').click()
- cy.contains('second note').parent().find('button')
- .should('contain', 'make not important')
-})
+ const addNote = noteObject => {
+ noteService.create(noteObject).then(returnedNote => {
+ setNotes(notes.concat(returnedNote))
+ setNotification({ text: `Note '${returnedNote.content}' added!`, type: 'success' }) // highlight-line
+ setTimeout(() => {
+ setNotification(null)
+ }, 5000)
+ })
+ }
+
+ return (
+
+
+ home
+ notes
+ new note
+
+
+ // highlight-line
+
+
+
+ } />
+
+ } />
+
+ } />
+ } />
+
+
+
+
+ )
+}
```
-En la primera línea, usamos el comando [parent](https://docs.cypress.io/api/commands/parent.html) para acceder al elemento padre del elemento que contiene
second note y buscamos el botón dentro de él.
-Luego hacemos clic en el botón y verificamos que el texto cambie.
+Alert tiene un diseño elegante:
+
+
-Ten en cuenta que usamos el comando [find](https://docs.cypress.io/api/commands/find.html#Syntax) para buscar el botón. No podemos usar [cy.get](https://docs.cypress.io/api/commands/get.html) aquí, porque siempre busca desde la página
completa y devolvería los 5 botones en la pagina.
+#### Menú de navegación
-Desafortunadamente, ahora tenemos algo de copia-pega en las pruebas, porque el código para buscar el botón correcto es siempre el mismo.
+El menú de navegación se implementa utilizando el componente [AppBar](https://mui.com/components/app-bar/).
-En este tipo de situaciones, es posible usar el comando [as](https://docs.cypress.io/api/commands/as.html):
+Si aplicamos el ejemplo de la documentación directamente
```js
-it('one of those can be made important', function () {
- cy.contains('second note').parent().find('button').as('theButton')
- cy.get('@theButton').click()
- cy.get('@theButton').should('contain', 'make not important')
-})
+
+
+ home
+ notes
+ new note
+
+
```
-Ahora la primera línea encuentra el botón correcto y usa
as para guardarlo como
theButton . Las siguientes líneas pueden usar el elemento nombrado con
cy.get('@theButton') .
+Esto proporciona una solución funcional, pero su apariencia no es la mejor posible:
-### Ejecutando y depurando tus pruebas
+
-Finalmente, algunas notas sobre cómo funciona Cypress y la depuración de tus pruebas.
+En la [documentación](https://mui.com/material-ui/guides/composition/#routing-libraries) encontramos una alternativa mejor: la [prop component](https://mui.com/material-ui/guides/composition/#component-prop), que permite cambiar cómo se renderiza el elemento raíz de un componente de Material UI.
-Debido a la forma de las pruebas de Cypress, da la impresión de que son código JavaScript normal y, por ejemplo, podríamos intentar esto:
+Al definir
```js
-const button = cy.contains('log in')
-button.click()
-debugger
-cy.contains('logout').click()
+
+ home
+
```
-Sin embargo, esto no funcionará. Cuando Cypress ejecuta una prueba, agrega cada comando _cy_ a una cola de ejecución.
-Cuando se haya ejecutado el código del método de prueba, Cypress ejecutará cada comando en la cola uno por uno.
+El componente
Button se renderiza usando como raíz el componente
Link de
react-router-dom , al que se pasa la prop
to que especifica la ruta.
-Los comandos de Cypress siempre devuelven _undefined_, por lo que _button.click()_ en el código anterior causaría un error. Un intento de iniciar el depurador no detendría el código entre la ejecución de los comandos, sino antes de que se haya ejecutado algún comando.
-
-Los comandos de Cypress son
como promesas , así que si queremos acceder a sus valores de retorno, tenemos que hacerlo usando el comando [then](https://docs.cypress.io/api/commands/then.html).
-Por ejemplo, la siguiente prueba imprime el número de botones en la aplicación y hace clic en el primer botón:
+El código completo para la barra de navegación es el siguiente
```js
-it('then example', function() {
- cy.get('button').then( buttons => {
- console.log('number of buttons', buttons.length)
- cy.wrap(buttons[0]).click()
- })
-})
+
+
+ home
+ notes
+ new note
+
+
```
-Detener la ejecución de la prueba con el depurador es [posible](https://docs.cypress.io/api/commands/debug.html). El depurador se inicia solo si la consola para desarrolladores del test runner de Cypress está abierta.
-
-La Consola para desarrolladores es muy útil para depurar tus pruebas.
-Puedes ver las solicitudes HTTP realizadas por las pruebas en la pestaña Network, y la pestaña Console te mostrará información sobre tus pruebas:
+y el resultado se ve tal como queremos:
-
+
-Hasta ahora hemos ejecutado nuestras pruebas Cypress usando el test runner gráfico. También es posible ejecutarlas [desde la línea de comandos](https://docs.cypress.io/guides/guides/command-line.html). Solo tenemos que agregarle un script npm:
+Sin embargo, notamos que cuando se mueve el mouse sobre la barra de navegación, el indicador de desplazamiento es demasiado sutil. Arreglemos esto definiendo un color de fondo ligeramente mejor para estas situaciones:
```js
- "scripts": {
- "cypress:open": "cypress open",
- "test:e2e": "cypress run" // highlight-line
- },
-```
+const style = { '&:hover': { bgcolor: 'rgba(255,255,255,0.3)' } }
+
+return (
+
+
+
+
+ home
+
+
+ notes
+
+
+ new note
+
+
+
-Ahora podemos ejecutar nuestras pruebas desde la línea de comandos con el comando npm run test:e2e
+ // ...
+)
+```
-
+Finalmente estamos satisfechos:
-Ten en cuenta que los videos de la ejecución de las pruebas se guardarán en cypress/videos/ , por lo que probablemente deberías ignorar este directorio en git. También es posible [desactivar](https://docs.cypress.io/guides/guides/screenshots-and-videos#Videos) la creación de videos.
+
-Las pruebas se encuentran en [GitHub](https://github.com/fullstack-hy2020/notes-e2e-cypress/).
+El código actual de la aplicación está disponible en su totalidad en [GitHub](https://github.com/fullstack-hy2020/part2-notes-frontend/tree/part5-12), en la rama part5-12 .
-La versión final del código frontend se puede encontrar en la rama [GitHub](https://github.com/fullstack-hy2020/part2-notes-frontend/tree/part5-9) *part5-9*.
-
+### Styled Components
-
+Además de lo que ya hemos visto, existen [otras formas](https://blog.bitsrc.io/5-ways-to-style-react-components-in-2019-30f1ccc2b5b) de aplicar estilos a una aplicación React.
-### Ejercicios 5.17.-5.23.
+La librería [styled-components](https://www.styled-components.com/), que utiliza la sintaxis [literal de plantilla etiquetada](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals) de ES6, ofrece un enfoque interesante para definir estilos.
-En los últimos ejercicios de esta parte haremos algunas pruebas E2E para nuestra aplicación de blog.
-El material de esta parte debería ser suficiente para completar los ejercicios.
-También deberías consultar la [documentación](https://docs.cypress.io/guides/overview/why-cypress.html#In-a-nutshell) de Cypress. Probablemente sea la mejor documentación que he visto para un proyecto de código abierto.
+[Instalemos](https://styled-components.com/docs/basics#installation) Styled Components y utilicémoslo para realizar algunos cambios de estilo en la aplicación de notas (la versión anterior a la instalación de Material UI). Primero, creemos dos definiciones de estilo para los componentes que usaremos:
-Recomiendo especialmente leer [Introducción a Cypress](https://docs.cypress.io/guides/core-concepts/introduction-to-cypress.html#Cypress-Can-Be-Simple-Sometimes), que afirma que
+```js
+import styled from 'styled-components'
+
+const Button = styled.button`
+ background: Bisque;
+ font-size: 1em;
+ margin: 1em;
+ padding: 0.25em 1em;
+ border: 2px solid Chocolate;
+ border-radius: 3px;
+`
+
+const Input = styled.input`
+ margin: 0.25em;
+ width: 300px;
+`
+```
->
Esta es la guía más importante para comprender cómo realizar pruebas con Cypress. Léela. Entiéndela.
+El código crea versiones de los elementos HTML
button y
input con estilo y los asigna a las variables
Button y
Input .
-#### 5.17: Pruebas de End To End de la Lista de Blogs, paso 1
+La sintaxis para definir estilos es bastante interesante, ya que las definiciones CSS se colocan entre comillas invertidas. Esta es la sintaxis de los [literales de plantilla etiquetados](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals) de ES6.
-Configura Cypress para tu proyecto. Realiza una prueba para comprobar que la aplicación muestra el formulario de inicio de sesión de forma predeterminada.
+Los componentes definidos funcionan como elementos normales
button y
input , y se utilizan en la aplicación de la forma habitual:
-La estructura de la prueba debe ser la siguiente
```js
-describe('Blog app', function() {
- beforeEach(function() {
- cy.visit('http://localhost:5173')
- })
+const NoteForm = ({ createNote }) => {
+ // ...
- it('Login form is shown', function() {
- // ...
- })
-})
+ return (
+
+
Create a new note
+
+
+
+ )
+}
```
-#### 5.18: Pruebas de End To End de la Lista de Blogs, paso 2
+El formulario ahora se ve así:
-Realiza pruebas para iniciar sesión. Prueba tanto los intentos de inicio de sesión exitosos y los no exitosos.
-Crea un nuevo usuario en el bloque
beforeEach para las pruebas.
+
-El cuerpo de las pruebas se extiende de la siguiente manera
+Definamos los siguientes componentes para agregar estilos, todos los cuales son versiones mejoradas de los elementos
div :
```js
-describe('Blog app', function() {
- beforeEach(function() {
- // vacía la base de datos aquí
- // crea un usuario para el backend aquí
- cy.visit('http://localhost:5173')
- })
-
- it('Login form is shown', function() {
- // ...
- })
+const Page = styled.div`
+ padding: 1em;
+ background: papayawhip;
+`
+
+const Navigation = styled.div`
+ background: BurlyWood;
+ padding: 1em;
+`
+
+const Footer = styled.div`
+ background: Chocolate;
+ padding: 1em;
+ margin-top: 1em;
+`
+```
- describe('Login',function() {
- it('succeeds with correct credentials', function() {
- // ...
- })
+Los nuevos componentes ahora se pueden utilizar en la aplicación:
- it('fails with wrong credentials', function() {
- // ...
- })
- })
-})
-```
+```js
+const App = () => {
+ // ...
-El bloque
beforeEach debe vaciar la base de datos utilizando, por ejemplo, el método de formateo que usamos en el [material](/es/part5/pruebas_de_extremo_a_extremo_cypress#controlando-el-estado-de-la-base-de-datos).
+ return (
+
// highlight-line
+ // highlight-line
+ home
+ notes
+ new note
+ // highlight-line
+
+
+
+ } />
+
+ } />
+
+ } />
+ } />
+
+// highlight-start
+
+ Note app, Department of Computer Science, University of Helsinki 2026
+
+
+ // highlight-end
+ )
+}
+```
-
Ejercicio adicional opcional : comprueba que la notificación que se muestra con el inicio de sesión fallido se muestra en rojo.
+El resultado final es el siguiente:
-#### 5.19: Pruebas de End To End de la Lista de Blogs, paso 3
+
-Realiza una prueba que compruebe que un usuario que ha iniciado sesión puede crear un nuevo blog.
-La estructura de la prueba podría ser la siguiente
+Styled-Components ha ido ganando popularidad últimamente y actualmente parece que mucha gente lo considera la mejor manera de definir estilos para aplicaciones React.
-```js
-describe('Blog app', function() {
- // ...
+
- describe('When logged in', function() {
- beforeEach(function() {
- // ...
- })
+
- it('A blog can be created', function() {
- // ...
- })
- })
+### Ejercicios 5.29–5.31
-})
-```
+A continuación, mejora los estilos de la aplicación de blogs utilizando Material UI o Styled Components.
-La prueba debe garantizar que se agregue un nuevo blog a la lista de todos los blogs.
+#### 5.29: blogs con estilo, paso 1
-#### 5.20: Pruebas de End To End de la Lista de Blogs, paso 4
+Añade estilos a los formularios de la aplicación.
-Haz una prueba que compruebe que al usuario le puede gustar ("like") un blog.
+Su solución podría verse así. Formulario de inicio de sesión:
-#### 5.21: Pruebas de End To End de la Lista de Blogs, paso 5
+
-Realiza una prueba para asegurarte de que el usuario que creó un blog pueda eliminarlo.
+Creando un nuevo blog:
-#### 5.22: Pruebas de End To End de la Lista de Blogs, paso 6
+
-Realiza una prueba para asegurarte de que solo el creador puede ver el botón delete de un blog, nadie más.
+#### 5.30: blogs con estilo, paso 2
-#### 5.23: Pruebas de End To End de la Lista de Blogs, paso 7
+Ahora diseña la barra de navegación de la aplicación y el componente que muestra las notificaciones. El resultado podría verse así:
-Realiza una prueba que verifique que los blogs estén ordenados de acuerdo con los likes, con el blog con más likes en primer lugar.
+
-Este ejercicio puede ser un poco más complicado que los anteriores . Una posible solución es agregar cierta clase para el elemento que cubre el contenido del blog y luego usar el método [eq](https://docs.cypress.io/api/commands/eq#Syntax) para obtener el elemento en un índice específico:
+#### 5.31: blogs con estilo, paso 3
-```js
-cy.get('.blog').eq(0).should('contain', 'The title with the most likes')
-cy.get('.blog').eq(1).should('contain', 'The title with the second most likes')
-```
+Personaliza como prefieras la apariencia del componente que muestra un único blog. Este es un ejemplo:
-Ten en cuenta que podrías terminar teniendo problemas si haces clic en el botón "Like" muchas veces seguidas. Puede ser que Cypress haga clic tan rápido que no tenga tiempo de actualizar el estado de la aplicación entre los clics. Una solución para esto es esperar a que se actualice la cantidad de Likes entre todos los clics.
+
-Este fue el último ejercicio de esta parte, y es hora de enviar tu código a GitHub y marcar los ejercicios que has completado en el [sistema de envío de ejercicios](https://studies.cs.helsinki.fi/stats/courses/fullstackopen).
+Este fue el último ejercicio de la sección; es hora de enviar el código a GitHub y marcar los ejercicios completados en el [sistema de envío de ejercicios](https://studies.cs.helsinki.fi/stats/courses/fullstackopen).
diff --git a/src/content/6/es/part6.md b/src/content/6/es/part6.md
index 2e2fa4c8f47..72c0b44d7b8 100644
--- a/src/content/6/es/part6.md
+++ b/src/content/6/es/part6.md
@@ -6,13 +6,11 @@ lang: es
-Hasta ahora, hemos colocado el estado de la aplicación y la lógica de estado directamente dentro de los componentes de React. Cuando las aplicaciones crecen, la administración del estado debe trasladarse fuera de los componentes de React. En esta parte, presentaremos la librería Redux, que actualmente es la solución más popular para administrar el estado de las aplicaciones React.
+Hasta ahora hemos colocado el estado de la aplicación y la lógica que lo gestiona directamente en los componentes de React. A medida que las aplicaciones crecen, es recomendable trasladar la gestión del estado fuera de los componentes de React. En esta parte exploraremos la librería Zustand, que actualmente es la solución de gestión del estado más popular para aplicaciones React.
-Aprenderemos sobre la versión ligera de Redux compatible directamente con React, es decir el contexto de React y el hook useReducer, también sobre la librería React Query que simplifica la gestión de estados de la aplicación.
+También veremos el enfoque más ligero de gestión del estado que React admite directamente —el contexto de React y el hook useReducer—, así como la librería React Query, que simplifica la gestión del estado del servidor.
-Parte actualizada el 12 de Octubre de 2025
-- Node actualizado a la versión 22.18.0
-- Jest reemplazado con Vitest
-- Axios reemplazado con Fetch API
+Parte actualizada el 5 de abril de 2026
+- Redux reemplazado por Zustand
diff --git a/src/content/6/es/part6a.md b/src/content/6/es/part6a.md
index 0c2b498a839..c88b9e6641a 100644
--- a/src/content/6/es/part6a.md
+++ b/src/content/6/es/part6a.md
@@ -7,1273 +7,810 @@ lang: es
-Hasta ahora, hemos seguido las convenciones de gestión de estado recomendadas por React. Hemos colocado el estado y las funciones para manejarlo en el [nivel superior](https://es.react.dev/learn/sharing-state-between-components) de la estructura de componentes de la aplicación. A menudo, la mayoría del estado de la aplicación y los métodos para modificarlo residen directamente en el componente raíz. Luego, el estado y sus métodos de control se han pasado a otros componentes con props. Esto funciona hasta cierto punto, pero cuando las aplicaciones crecen, la gestión del estado se vuelve desafiante.
+Hemos seguido la práctica recomendada de React para gestionar el estado de la aplicación: definir el estado que necesitan varios componentes y las funciones que lo manejan en los componentes [situados más arriba](https://react.dev/learn/sharing-state-between-components) de la jerarquía. La mayor parte del estado y sus funciones suelen definirse directamente en el componente raíz y pasarse mediante props a los componentes que los necesitan. Esto funciona hasta cierto punto, pero, a medida que la aplicación crece, la gestión del estado se vuelve más difícil.
-### Arquitectura de Flux
+### Arquitectura Flux
-Facebook desarrolló la arquitectura [Flux](https://facebookarchive.github.io/flux/docs/in-depth-overview/) para facilitar la gestión del estado. En Flux, el estado se separa completamente de los componentes de React en sus propios
stores (almacenes).
-El estado en el store no se cambia directamente, sino con diferentes
actions (acciones).
+Facebook desarrolló la arquitectura [Flux](https://facebookarchive.github.io/flux/docs/in-depth-overview) durante los primeros años de React para aliviar los problemas de gestión del estado. En Flux, la gestión del estado se separa por completo de los componentes de React y se traslada a
stores externos. El estado del store no se modifica directamente, sino mediante
actions específicas creadas para ello.
-Cuando una acción cambia el estado de un store, las vistas se vuelven a generar:
+Cuando una acción cambia el estado del store, las vistas se vuelven a renderizar:
-
+
-Si alguna acción en la aplicación, por ejemplo presionar un botón, provoca la necesidad de cambiar el estado, el cambio se realiza con una acción.
-Esto hace que se vuelva a renderizar la vista:
+Si una interacción con la aplicación —por ejemplo, pulsar un botón— exige cambiar el estado, el cambio se realiza mediante una acción. Esto provoca que la vista vuelva a renderizarse:
-
+
-Flux ofrece una manera estándar de cómo y dónde se mantiene el estado de la aplicación y cómo se modifica.
+Por lo tanto, Flux proporciona una forma estándar de cómo y dónde se mantiene el estado de la aplicación y de realizar cambios en ella.
### Redux
-Facebook tiene una implementación para Flux, pero usaremos la librería [Redux](https://redux.js.org). Funciona con el mismo principio, pero es un poco más sencilla. Facebook también usa Redux ahora en lugar de su Flux original.
+[Redux](https://redux.js.org), que sigue la arquitectura Flux, fue la solución de gestión de estado dominante para aplicaciones React durante casi una década. En este curso, Redux también se utilizó hasta la primavera de 2026. Redux siempre ha estado plagado de complejidad y una gran cantidad de código repetitivo. La situación mejoró significativamente con la introducción de [Redux Toolkit](https://redux-toolkit.js.org/), pero a pesar de esto, la comunidad continuó desarrollando soluciones alternativas de gestión del estado, como [MobX](https://mobx.js.org/), [Recoil](https://recoiljs.org/) y [Jotai](https://www.npmjs.com/package/jotai). Su popularidad ha variado.
-Conoceremos Redux implementando una aplicación de contador una vez más:
+El más interesante, y sin duda el más popular de los recién llegados, es [Zustand](https://zustand.docs.pmnd.rs/), y también es nuestra elección como solución de gestión del estado. Zustand parece haber alcanzado ya la popularidad de Redux:
-
+
-Crea una nueva aplicación Vite e instala
redux con el comando
+### Zustand
-```bash
-npm install redux
-```
+Familiaricémonos con Zustand implementando una vez más una aplicación de contador:
-Como en Flux, en Redux el estado también se almacena en un [store](https://redux.js.org/tutorials/essentials/part-1-overview-concepts#store).
+
-Todo el estado de la aplicación se almacena en
un objeto JavaScript en el store. Debido a que nuestra aplicación solo necesita el valor del contador, lo guardaremos directamente en el store. Si el estado fuera más complicado, diferentes elementos del estado se guardarían como campos separados del objeto.
-El estado del store se cambia con [acciones](https://redux.js.org/tutorials/essentials/part-1-overview-concepts#actions). Las acciones son objetos que tienen al menos un campo que determina el
tipo de acción.
-Nuestra aplicación necesita, por ejemplo, la siguiente acción:
+Crea una nueva aplicación Vite e instala
Zustand :
-```js
-{
- type: 'INCREMENT'
-}
+```bash
+npm install zustand
```
-Si hay datos relacionados con la acción, se pueden declarar otros campos según sea necesario. Sin embargo, nuestra aplicación de contador es tan simple que las acciones están bien con solo el campo de tipo.
+La primera versión, donde sólo funciona el incremento del contador, es la siguiente:
-El impacto de la acción sobre el estado de la aplicación se define mediante un [reducer](https://redux.js.org/tutorials/essentials/part-1-overview-concepts#reducers). En la práctica, un reducer es una función a la que se le da el estado actual y una acción como parámetros.
Devuelve un nuevo estado.
+```bash
+import { create } from 'zustand'
-Definamos ahora un reducer para nuestra aplicación:
+const useCounterStore = create(set => ({
+ counter: 0,
+ increment: () => set(state => ({ counter: state.counter + 1 })),
+}))
-```js
-const counterReducer = (state, action) => {
- if (action.type === 'INCREMENT') {
- return state + 1
- } else if (action.type === 'DECREMENT') {
- return state - 1
- } else if (action.type === 'ZERO') {
- return 0
- }
+const App = () => {
+ const counter = useCounterStore(state => state.counter)
+ const increment = useCounterStore(state => state.increment)
- return state
+ return (
+
+
{counter}
+
+ plus
+ minus
+ zero
+
+
+
+ )
}
```
-El primer parámetro es el
estado en el store. El reducer devuelve un
nuevo estado basado en el tipo de _acción_. Entonces, por ejemplo, cuando el tipo de acción es
INCREMENT , el estado obtiene el valor antiguo más uno. Si el tipo de acción es
ZERO , el nuevo valor del estado es cero.
+La aplicación comienza creando un
store , es decir, el estado global, mediante la función [create](https://zustand.docs.pmnd.rs/reference/apis/create) de Zustand:
+
+```bash
+import { create } from 'zustand'
-Cambiemos un poco el código. Hemos utilizado declaraciones if-else para responder a una acción y cambiar el estado. Sin embargo, la declaración [switch](https://developer.mozilla.org/es/docs/Web/JavaScript/Reference/Statements/switch) es el enfoque más común para escribir un reducer.
+const useCounterStore = create(set => ({
+ counter: 0,
+ increment: () => set(state => ({ counter: state.counter + 1 })),
+}))
+```
-También definamos un [valor predeterminado](https://developer.mozilla.org/es/docs/Web/JavaScript/Reference/Functions/Default_parameters) de 0 para el parámetro
state . Ahora, el reducer funciona incluso si el estado del store aún no se ha inicializado.
+La función recibe como parámetro una
función que devuelve el estado que se definirá para la aplicación. El parámetro es, por tanto, el siguiente:
```js
-// highlight-start
-const counterReducer = (state = 0, action) => {
- // highlight-end
- switch (action.type) {
- case 'INCREMENT':
- return state + 1
- case 'DECREMENT':
- return state - 1
- case 'ZERO':
- return 0
- default: // if none of the above matches, code comes here
- return state
- }
-}
+set => ({
+ counter: 0,
+ increment: () => set(state => ({ counter: state.counter + 1 })),
+})
```
-El reducer nunca debe ser llamado directamente desde el código de la aplicación. Solo es proporcionado como parámetro a la función _createStore_ que crea el store:
+Por tanto, el estado tiene
counter definido con un valor de cero y
increment que es una función.
+
+Los componentes de la aplicación pueden acceder a los valores y funciones definidos en el estado a través de la función
useCounterStore definida usando
create de Zustand. El componente
App utiliza
selectors para recuperar el valor
counter y la función
increment del estado:
```js
-// highlight-start
-import { createStore } from 'redux'
-// highlight-end
+const App = () => {
+ // highlight-start
+ // using selector to pick right part of the store state
+ const counter = useCounterStore(state => state.counter)
+ const increment = useCounterStore(state => state.increment)
+ // highlight-end
-const counterReducer = (state = 0, action) => {
- // ...
+ return (
+
+
{counter}
// highlight-line
+
+ plus // highlight-line
+ minus
+ zero
+
+
+
+ )
}
-
-// highlight-start
-const store = createStore(counterReducer)
-// highlight-end
```
-El store ahora usa el reducer para manejar
acciones , que son
dispatched o 'enviadas' al store con su método [dispatch](https://redux.js.org/tutorials/essentials/part-1-overview-concepts#dispatch)(envío).
+El código almacena el valor del contador del store en una variable de la siguiente manera:
```js
-store.dispatch({type: 'INCREMENT'})
+const counter = useCounterStore(state => state.counter)
```
-Puedes averiguar el estado del store utilizando el método [getState](https://redux.js.org/api/store#getstate).
+Se utiliza una función selectora
state => state.counter , que determina lo que se devuelve del contenido del store. De la misma forma, la función almacenada en el store se recupera en la variable
increment .
-Por ejemplo, el siguiente código:
+La función de estado
increment , que se definió de la siguiente manera, se proporciona como controlador de clic para el botón "más":
```js
-const store = createStore(counterReducer)
-console.log(store.getState())
-store.dispatch({ type: 'INCREMENT' })
-store.dispatch({ type: 'INCREMENT' })
-store.dispatch({ type: 'INCREMENT' })
-console.log(store.getState())
-store.dispatch({ type: 'ZERO' })
-store.dispatch({ type: 'DECREMENT' })
-console.log(store.getState())
+const useCounterStore = create(set => ({
+ counter: 0,
+ increment: () => set(state => ({ counter: state.counter + 1 })), // highlight-line
+}))
```
-imprimiría lo siguiente en la consola
+Veamos la definición de la función por separado:
-```
-0
-3
--1
+```js
+() => set(state => ({ counter: state.counter + 1 }))
```
-porque al principio el estado del store es 0. Después de tres acciones
INCREMENT el estado es 3. Al final, después de las acciones
ZERO y
DECREMENT , el estado es -1.
+Esta es una función que llama a la función [set](https://zustand.docs.pmnd.rs/learn/guides/updating-state) dando otra función como parámetro. Esta función pasada como parámetro define cómo cambia el estado:
-El tercer método importante que tiene el store es [subscribe](https://redux.js.org/api/store#subscribelistener), que se utiliza para crear funciones callback que el store llama cuando cambia su estado.
+```js
+state => ({ counter: state.counter + 1 })
+```
-Si, por ejemplo, añadiéramos la siguiente función para suscribirnos,
todos los cambios en el store se imprimirían en la consola.
+que es una abreviatura de:
```js
-store.subscribe(() => {
- const storeNow = store.getState()
- console.log(storeNow)
-})
+state => {
+ return { counter: state.counter + 1 }
+}
```
-entonces el código
+La función devuelve un nuevo estado, que calcula en función del estado anterior al que puede acceder mediante el parámetro
state . Entonces, si el antiguo estado es, por ejemplo:
```js
-const store = createStore(counterReducer)
+{
+ counter: 1,
+ increment: // function definition
+}
+```
-store.subscribe(() => {
- const storeNow = store.getState()
- console.log(storeNow)
-})
+el nuevo estado se convierte en:
-store.dispatch({ type: 'INCREMENT' })
-store.dispatch({ type: 'INCREMENT' })
-store.dispatch({ type: 'INCREMENT' })
-store.dispatch({ type: 'ZERO' })
-store.dispatch({ type: 'DECREMENT' })
+```js
+{
+ counter: 2,
+ increment: // function definition
+}
```
-causaría que se imprima lo siguiente:
+El estado siempre contiene también la función de cambio de estado
increment .
+La función de transición de estado
+
+```js
+state => ({ counter: state.counter + 1 })
```
-1
-2
-3
-0
--1
-```
-El código de nuestra aplicación de contador es el siguiente. Todo el código se ha escrito en el mismo archivo, por lo que
store está directamente disponible para el código React. Más adelante conoceremos mejores formas de estructurar el código React/Redux.
+solo afecta el valor
counter en el estado.
+
+Nada impediría cambiar la función en el estado dentro de la función de transición de estado; por ejemplo, si lo definimos de la siguiente manera:
```js
-import React from 'react'
-import ReactDOM from 'react-dom/client'
-
-import { createStore } from 'redux'
-
-const counterReducer = (state = 0, action) => {
- switch (action.type) {
- case 'INCREMENT':
- return state + 1
- case 'DECREMENT':
- return state - 1
- case 'ZERO':
- return 0
- default:
- return state
+state => {
+ return {
+ counter: state.counter + 1 ,
+ increment: console.log('increment broken')
}
}
+```
-const store = createStore(counterReducer)
+el botón de incremento sólo funcionaría la primera vez; después de eso, presionar el botón solo imprimiría en la consola.
-const App = () => {
- return (
-
-
- {store.getState()}
-
-
store.dispatch({ type: 'INCREMENT' })}
- >
- plus
-
-
store.dispatch({ type: 'DECREMENT' })}
- >
- minus
-
-
store.dispatch({ type: 'ZERO' })}
- >
- zero
-
-
- )
-}
+Cuando el nuevo estado se establece como:
-const root = ReactDOM.createRoot(document.getElementById('root'))
+```js
+state => ({ counter: state.counter + 1 })
+```
-const renderApp = () => {
- root.render(
)
-}
+solo se actualiza el valor de la clave
counter en el estado; el nuevo estado se obtiene fusionando el estado anterior con el valor devuelto por la función de cambio de estado. Es por eso que la siguiente función de transición de estado:
-renderApp()
-store.subscribe(renderApp)
+```js
+state => ({})
```
-Hay algunas cosas notables en el código.
-
App muestra el valor del contador solicitándolo al store con el método _store.getState()_. Los controladores de acciones de los botones envían (
dispatch ) las acciones correctas al store.
-
-Cuando se cambia el estado del store, React no puede volver a re-renderizar automáticamente la aplicación. Por lo tanto, hemos registrado una función _renderApp_ , que renderiza toda la aplicación, para escuchar cambios en el store con el método _store.subscribe_. Ten en cuenta que tenemos que invocar inmediatamente al método _renderApp_. Sin la invocación, el primer renderizado de la aplicación nunca se produciría.
+No afecta en absoluto al estado.
-### Una nota sobre el uso de createStore
+Completemos también la solicitud para los botones restantes:
-Los más atentos notarán que el nombre de la función createStore está tachado. Si pasas el mouse sobre el nombre, aparecerá una explicación
+```js
+const useCounterStore = create(set => ({
+ counter: 0,
+ increment: () => set(state => ({ counter: state.counter + 1 })),
+ decrement: () => set(state => ({ counter: state.counter - 1 })),
+ zero: () => set(() => ({ counter: 0 })),
+}))
-
+const App = () => {
+ const counter = useCounterStore(state => state.counter)
+ const increment = useCounterStore(state => state.increment)
+ const decrement = useCounterStore(state => state.decrement)
+ const zero = useCounterStore(state => state.zero)
-La explicación completa es la siguiente:
+ return (
+
+
{counter}
+
+ plus
+ minus
+ zero
+
+
+
+ )
+}
+```
->
Recomendamos utilizar el método configureStore del paquete @reduxjs/toolkit, que reemplaza a createStore.
->
->
Redux Toolkit es nuestro enfoque recomendado para escribir la lógica de Redux hoy, incluida la configuración de store, reducers, la obtención de datos y más.
->
->
Para obtener más detalles, lea esta página de documentación de Redux:
+> #### ¿De dónde vienen set y state?
>
->
configureStore de Redux Toolkit es una versión mejorada de createStore que simplifica la configuración y ayuda a evitar errores comunes.
+> ¿De dónde viene
set ? Es una función auxiliar que proporciona
create para actualizar el estado.
create llama a la función que recibe como parámetro y le pasa
set automáticamente. No necesitas llamarla ni importarla; Zustand se encarga de ello.
>
->
No deberías usar el paquete principal de redux por sí solo hoy en día, excepto con fines de aprendizaje. El método createStore del paquete core de redux no se eliminará, pero alentamos a todos los usuarios a migrar al uso de Redux Toolkit para todo el código de Redux.
+> ¿De dónde viene
state ? Cuando se proporciona una función como parámetro para
set (en lugar de un nuevo objeto de estado directamente), Zustand llama a esa función con el estado actual del store como argumento. De esta manera, las funciones de actualización de estado pueden acceder al estado anterior para calcular el nuevo.
-Entonces, en lugar de la función
createStore , se recomienda usar la función un poco más "avanzada"
configureStore , y también la usaremos cuando nos hayamos hecho cargo de la funcionalidad básica de Redux.
+### Uso del estado desde distintos componentes
-Nota adicional:
createStore se define como "obsoleto", lo que generalmente significa que la función se eliminará en alguna versión más nueva de la librería. La explicación anterior y esta [discusión](https://stackoverflow.com/questions/71944111/redux-createstore-is-deprecated-cannot-get-state-from-getstate-in-redux-ac) revelan que
createStore no se eliminará y se le ha dado el estado
obsoleto , quizás por motivos ligeramente incorrectos. Por lo tanto, la función no está obsoleta, pero hoy en día existe una forma nueva y preferible de hacer casi lo mismo.
+Refactoricemos la aplicación para que la definición del store se mueva a su propio archivo
store.js y la vista se divida en varios componentes, cada uno definido en sus propios archivos.
-### Redux-notas
-
-Nuestro objetivo es modificar nuestra aplicación de notas para utilizar Redux para la gestión del estado. Sin embargo, primero cubramos algunos conceptos clave a través de una aplicación de notas simplificada.
-
-La primera versión de nuestra aplicación, escrita en el archivo
main.jsx , se ve de la siguiente manera:
+El contenido de
store.js es sencillo:
```js
-import ReactDOM from 'react-dom/client'
-import { createStore } from 'redux'
-
-const noteReducer = (state = [], action) => {
- switch (action.type) {
- case 'NEW_NOTE':
- state.push(action.payload)
- return state
- default:
- return state
- }
-}
-
-const store = createStore(noteReducer)
+export const useCounterStore = create(set => ({
+ counter: 0,
+ increment: () => set(state => ({ counter: state.counter + 1 })),
+ decrement: () => set(state => ({ counter: state.counter - 1 })),
+ zero: () => set(() => ({ counter: 0 })),
+}))
+```
-store.dispatch({
- type: 'NEW_NOTE',
- payload: {
- content: 'the app state is in redux store',
- important: true,
- id: 1
- }
-})
+El componente
App se simplifica de la siguiente manera:
-store.dispatch({
- type: 'NEW_NOTE',
- payload: {
- content: 'state changes are made with actions',
- important: false,
- id: 2
- }
-})
+```js
+import Display from './Display'
+import Controls from './Controls'
const App = () => {
return (
-
- {store.getState().map(note => (
-
- {note.content} {note.important ? 'important' : ''}
-
- ))}
-
+
+
)
}
-const root = ReactDOM.createRoot(document.getElementById('root'))
-
-const renderApp = () => {
- root.render(
)
-}
-
-renderApp()
-store.subscribe(renderApp)
+export default App
```
-Hasta el momento la aplicación no tiene la funcionalidad para agregar nuevas notas, aunque es posible hacerlo enviando acciones
NEW\_NOTE .
+Lo que es digno de mención aquí es que el componente
App ya no pasa el estado a sus componentes secundarios. De hecho, el componente no afecta el estado de ninguna manera y la definición del store se ha separado completamente fuera del componente.
-Ahora las acciones tienen un tipo y un campo
payload (carga), que contiene la nota a agregar:
+El componente que renderiza el valor del contador es sencillo:
```js
-{
- type: 'NEW_NOTE',
- payload: {
- content: 'state changes are made with actions',
- important: false,
- id: 2
- }
-}
-```
+import { useCounterStore } from './store'
-La elección del nombre del campo es arbitraria. La convención es que las acciones tengan exactamente dos campos,
type diciendo el tipo y
payload conteniendo los datos incluidos en la acción.
+const Display = () => {
+ const counter = useCounterStore(state => state.counter)
-### Funciones puras, inmutables
-
-La versión inicial del reducer es muy sencilla:
-
-```js
-const noteReducer = (state = [], action) => {
- switch (action.type) {
- case 'NEW_NOTE':
- state.push(action.payload)
- return state
- default:
- return state
- }
+ return (
+
{counter}
+ )
}
-```
-El estado ahora es un Array. Las acciones de tipo
NEW\_NOTE hacen que se agregue una nueva nota al estado con el método [push](https://developer.mozilla.org/es/docs/Web/JavaScript/Reference/Global_Objects/Array/push).
+export default Display
+```
-La aplicación parece estar funcionando, pero el reducer que hemos declarado es malo. Rompe el [supuesto básico](https://redux.js.org/tutorials/essentials/part-1-overview-concepts#reducers) de que los reducers deben ser [funciones puras](https://es.wikipedia.org/wiki/Programaci%C3%B3n_funcional#Funciones_puras).
+El componente accede al valor del contador a través de la función
useCounterStore que define el store. Esto es conveniente en muchos sentidos, por ejemplo, no es necesario pasar el estado al componente a través de props.
-Las funciones puras son aquellas que
no causan ningún efecto secundario y siempre deben devolver la misma respuesta cuando se llaman con los mismos parámetros.
+El componente que define los botones tiene este aspecto:
-Agregamos una nueva nota al estado con el método _state.push(action.payload)_ que
cambia el estado del objeto-estado. Esto no está permitido. El problema se resuelve fácilmente utilizando el método [concat](https://developer.mozilla.org/es/docs/Web/JavaScript/Reference/Global_Objects/Array/concat), que crea un
nuevo array , que contiene todos los elementos del array anterior y el nuevo elemento:
-
```js
-const noteReducer = (state = [], action) => {
- switch (action.type) {
- case 'NEW_NOTE':
- return state.concat(action.payload) // highlight-line
- default:
- return state
- }
-}
-```
-
-El estado de un reducer debe estar compuesto por objetos [inmutables](https://es.wikipedia.org/wiki/Objeto_inmutable). Si hay un cambio en el estado, el objeto antiguo no se cambia, sino que se
reemplaza por un objeto nuevo modificado . Esto es exactamente lo que hicimos con el nuevo reducer: el array anterior se reemplaza por el nuevo.
+import { useCounterStore } from './store'
-Ampliemos nuestro reducer para que pueda manejar el cambio de importancia de una nota:
+const Controls = () => {
+ const increment = useCounterStore(state => state.increment)
+ const decrement = useCounterStore(state => state.decrement)
+ const zero = useCounterStore(state => state.zero)
-```js
-{
- type: 'TOGGLE_IMPORTANCE',
- payload: {
- id: 2
- }
+ return (
+
+ plus
+ minus
+ zero
+
+ )
}
-```
-
-Dado que todavía no tenemos ningún código que utilice esta funcionalidad, estamos expandiendo el reducer en la forma 'test driven' (guiada por pruebas).
-### Configurando el entorno de pruebas
+export default Controls
+```
-Tenemos que configurar primero la biblioteca de pruebas [Vitest](https://vitest.dev/) para el proyecto. Vamos a instalarla como una dependencia de desarrollo para la aplicación:
+La función
useCounterStore toma una función selectora como parámetro, que determina qué parte del estado usar. Por ejemplo:
```js
-npm install --save-dev vitest
+ const increment = useCounterStore(state => state.increment)
```
-Expandamos
package.json con un script para ejecutar las pruebas:
-
-```json
-{
- // ...
- "scripts": {
- "dev": "vite",
- "build": "vite build",
- "lint": "eslint .",
- "preview": "vite preview",
- "test": "vitest" // highlight-line
- },
- // ...
-}
-```
+Aquí, la función selectora
state => state.increment selecciona el valor de la clave
increment del estado (la función que incrementa el contador) y lo almacena en la variable
increment .
-Para hacer las pruebas más fáciles, primero trasladaremos el código del reducer a su propio módulo, al archivo
src/reducers/noteReducer.js :
+También podríamos acceder a todo el estado de la siguiente manera:
```js
-const noteReducer = (state = [], action) => {
- switch (action.type) {
- case 'NEW_NOTE':
- return state.concat(action.payload)
- default:
- return state
- }
-}
-
-export default noteReducer
+ const state = useCounterStore()
+ // does the same as useCounterStore(state => state), i.e., selects the entire state
```
-El archivo
main.jsx cambia de la siguiente manera:
+Luego podríamos referirnos al valor del contador y las funciones usando notación de puntos, es decir,
state.counter y
state.increment .
-```js
-import ReactDOM from 'react-dom/client'
-import { createStore } from 'redux'
-import noteReducer from './reducers/noteReducer' // highlight-line
+Surge una pregunta natural: ¿sería posible utilizar múltiples partes del estado mediante la desestructuración?
-const store = createStore(noteReducer)
+```js
+import { useCounterStore } from './store'
-// ...
-```
+const Controls = () => {
+ const { increment, decrement, zero } = useCounterStore() // highlight-line
-También agregaremos la librería [deep-freeze](https://www.npmjs.com/package/deep-freeze), que se puede usar para garantizar que el reducer se haya definido correctamente como una función inmutable.
-Instalemos la librería como una dependencia de desarrollo:
+ return (
+
+ plus
+ minus
+ zero
+
+ )
+}
-```js
-npm install --save-dev deep-freeze
+export default Controls
```
-Ahora estamos listos para escribir pruebas.
-
-### Pruebas para noteReducer
+La solución funciona, pero tiene un inconveniente importante. La desestructuración hace que el componente
Controls se vuelva a representar cada vez que cambia el valor del contador, aunque el componente solo muestra los botones y no el valor en sí.
-Comencemos creando una prueba para manejar la acción
NEW\_NOTE . La prueba, que definimos en el archivo
src/reducers/noteReducer.test.js , tiene el siguiente contenido:
+Por lo tanto, la mejor práctica en Zustand es seleccionar del estado exactamente sólo aquellas piezas que se necesitan en el componente dado. Un componente se vuelve a renderizar solo cuando cambia la parte del estado que ha seleccionado. Cuando en lugar de escribir:
```js
-import deepFreeze from 'deep-freeze'
-import { describe, expect, test } from 'vitest'
-import noteReducer from './noteReducer'
-
-describe('noteReducer', () => {
- test('returns new state with action NEW_NOTE', () => {
- const state = []
- const action = {
- type: 'NEW_NOTE',
- payload: {
- content: 'the app state is in redux store',
- important: true,
- id: 1
- }
- }
-
- deepFreeze(state)
- const newState = noteReducer(state, action)
-
- expect(newState).toHaveLength(1)
- expect(newState).toContainEqual(action.payload)
- })
-})
+ const { increment, decrement, zero } = useCounterStore()
```
-Ejecuta la prueba con
npm test . La prueba asegura que el nuevo estado devuelto por el reducer es un array que contiene un solo elemento, que es el mismo objeto que el que está en el campo
payload de la acción.
-
-El comando
deepFreeze(state) asegura que el reducer no cambie el estado del store que se le dio como parámetro. Si el reducer usa el comando _push_ para manipular el estado, la prueba no pasará
+el componente ya no reacciona a los cambios en el valor del contador porque no lo ha seleccionado del estado.
-
+### Reorganización del estado
-Ahora crearemos una prueba para la acción
TOGGLE\_IMPORTANCE :
+Sin embargo, podemos obtener una solución bastante clara reorganizando el estado de la siguiente manera:
```js
-test('returns new state with action TOGGLE_IMPORTANCE', () => {
- const state = [
- {
- content: 'the app state is in redux store',
- important: true,
- id: 1
- },
- {
- content: 'state changes are made with actions',
- important: false,
- id: 2
- }]
-
- const action = {
- type: 'TOGGLE_IMPORTANCE',
- payload: {
- id: 2
- }
- }
-
- deepFreeze(state)
- const newState = noteReducer(state, action)
-
- expect(newState).toHaveLength(2)
-
- expect(newState).toContainEqual(state[0])
-
- expect(newState).toContainEqual({
- content: 'state changes are made with actions',
- important: true,
- id: 2
- })
-})
+export const useCounterStore = create(set => ({
+ counter: 0,
+ actions: {
+ increment: () => set(state => ({ counter: state.counter + 1 })),
+ decrement: () => set(state => ({ counter: state.counter - 1 })),
+ zero: () => set(() => ({ counter: 0 })),
+ }
+}))
```
-Entonces la siguiente acción
+Las funciones de cambio de estado ahora están agrupadas bajo su propia clave
actions , y pueden seleccionarse como un todo y desestructurarse:
```js
-{
- type: 'TOGGLE_IMPORTANCE',
- payload: {
- id: 2
- }
-}
-```
-
-tiene que cambiar la importancia de la nota con el id 2.
-
-El reducer se expande de la siguiente manera
+const Controls = () => {
+
+ const { increment, decrement, zero } = useCounterStore(state => state.actions)
-```js
-const noteReducer = (state = [], action) => {
- switch(action.type) {
- case 'NEW_NOTE':
- return state.concat(action.payload)
- case 'TOGGLE_IMPORTANCE': {
- const id = action.payload.id
- const noteToChange = state.find(n => n.id === id)
- const changedNote = {
- ...noteToChange,
- important: !noteToChange.important
- }
- return state.map(note =>
- note.id !== id ? note : changedNote
- )
- }
- default:
- return state
- }
+ return (
+
+ plus
+ minus
+ zero
+
+ )
}
```
-Creamos una copia de la nota cuya importancia ha cambiado con la sintaxis [de la parte 2](/es/part2/alterando_datos_en_el_servidor#cambiar-la-importancia-de-las-notas), y reemplazamos el estado con un nuevo estado que contiene todas las notas que no han cambiado y la copia de la nota cambiada
changedNote .
+Ahora no se vuelve a renderizar, ya que solo se han seleccionado las funciones del estado y permanecen iguales durante toda la vida útil del store.
-Recapitulemos lo que sucede en el código. Primero, buscamos un objeto de nota específico, cuya importancia queremos cambiar:
+Según algunas [buenas prácticas](https://tkdodo.eu/blog/working-with-zustand#only-export-custom-hooks), no conviene exportar la función que da acceso al estado completo para utilizarla por toda la aplicación. Es preferible crear a partir de ella vistas más pequeñas que expongan solo las partes necesarias del estado. Modifiquemos
store.js de la siguiente manera:
```js
-const noteToChange = state.find(n => n.id === id)
-```
+import { create } from 'zustand'
-luego creamos un nuevo objeto, que es una
copia de la nota original, solo el valor del campo
important se ha cambiado a lo opuesto de lo que era:
+const useCounterStore = create(set => ({
+ counter: 0,
+ actions: {
+ increment: () => set(state => ({ counter: state.counter + 1 })),
+ decrement: () => set(state => ({ counter: state.counter - 1 })),
+ zero: () => set(() => ({ counter: 0 })),
+ }
+}))
-```js
-const changedNote = {
- ...noteToChange,
- important: !noteToChange.important
-}
+// the hook functions that are used elsewhere in app
+export const useCounter = () => useCounterStore(state => state.counter)
+export const useCounterControls = () => useCounterStore(state => state.actions)
```
-Entonces se devuelve un nuevo estado. Lo creamos tomando todas las notas del estado anterior, excepto la nota deseada, que reemplazamos con su copia ligeramente alterada:
+Ahora, fuera del módulo que define el estado, están disponibles las funciones
useCounter , que devuelve el valor del contador cuando se llama, y
useCounterControls , que devuelve las funciones que modifican el valor del contador. El uso cambia ligeramente:
```js
-state.map(note =>
- note.id !== id ? note : changedNote
-)
-```
+import { useCounter } from './store' // highlight-line
-### Array spread syntax
+const Display = () => {
+ const counter = useCounter() // highlight-line
-Debido a que ahora tenemos pruebas bastante buenas para el reducer, podemos refactorizar el código de forma segura.
-
-Agregar una nueva nota crea el estado devuelto por la función de Arrays _concat_. Echemos un vistazo a cómo podemos lograr lo mismo usando la sintaxis [array spread](https://developer.mozilla.org/es/docs/Web/JavaScript/Reference/Operators/Spread_syntax) de JavaScript:
-
-```js
-const noteReducer = (state = [], action) => {
- switch(action.type) {
- case 'NEW_NOTE':
- // highlight-start
- return [...state, action.payload]
- // highlight-end
- case 'TOGGLE_IMPORTANCE':
- // ...
- default:
- return state
- }
+ return (
+
{counter}
+ )
}
```
-La sintaxis spread funciona de la siguiente manera. Si declaramos
-
-```js
-const numbers = [1, 2, 3]
-```
-
-
...numbers divide el array en elementos individuales, que se pueden colocar en otro array.
-
```js
-[...numbers, 4, 5]
-```
-
-y el resultado es un array
[1, 2, 3, 4, 5] .
+import { useCounterControls } from './store' // highlight-line
-Si hubiéramos colocado el array en otro array sin el spread
+const Controls = () => {
+ const { increment, decrement, zero } = useCounterControls() // highlight-line
-```js
-[numbers, 4, 5]
+ return (
+
+ plus
+ minus
+ zero
+
+ )
+}
```
-el resultado habría sido
[ [1, 2, 3], 4, 5] .
-
-Cuando tomamos elementos de un array mediante la [desestructuración](https://developer.mozilla.org/es/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment), se usa una sintaxis similar para
juntar el resto de los elementos:
-
-```js
-const numbers = [1, 2, 3, 4, 5, 6]
+Cuando se usa el estado de esta manera, ya no es necesario usar funciones selectoras, ya que su uso está oculto dentro de la definición de las nuevas funciones auxiliares.
-const [first, second, ...rest] = numbers
+Los más observadores han notado que las funciones relacionadas con Zustand se nombran comenzando con la palabra
use . La razón de esto es que la función devuelta por la función
create de Zustand (en nuestro ejemplo
useCounterStore ) es una función React [enganche personalizado](https://react.dev/learn/reusing-logic-with-custom-hooks). Nuestras propias funciones auxiliares
useCounter y
useCounterControls también son esencialmente hooks personalizados, porque ocultan el uso del hook personalizado
useCounterStore dentro de ellas.
-console.log(first) // imprime 1
-console.log(second) // imprime 2
-console.log(rest) // imprime [3, 4, 5, 6]
-```
+Los hooks personalizados están sujetos a una serie de reglas; por ejemplo, sus nombres deben comenzar siempre por
use . Las [reglas de los hooks](https://react.dev/warnings/invalid-hook-call-warning) estudiadas en la [parte 1](/es/part1/un_estado_mas_complejo_depurando_aplicaciones_react#reglas-de-los-hooks) también se aplican a los hooks personalizados.
-### Ejercicios 6.1.-6.2.
+### Ejercicio 6.1.
-Hagamos una versión simplificada del ejercicio unicafe de la parte 1. Manejemos la administración del estado con Redux.
+Hagamos una nueva versión del ejercicio de Unicafe de la parte 1. Gestionaremos el estado de la aplicación con Zustand.
-Puedes tomar el código de este repositorio https://github.com/fullstack-hy2020/unicafe-redux para la base de tu proyecto.
+Puedes utilizar el proyecto https://github.com/fullstack-hy2020/unicafe-zustand como base para tu aplicación.
-Comienza eliminando la configuración git del repositorio clonado e instalando dependencias
+Comience eliminando la configuración de Git de la aplicación clonada e instalando las dependencias:
```bash
-cd unicafe-redux // go to the directory of cloned repository
+cd unicafe-zustand // go to the cloned repository directory
rm -rf .git
npm install
```
-#### 6.1: Unicafe Revisitado, paso 1
-
-Antes de implementar la funcionalidad de la UI(interfaz de usuario), implementemos la funcionalidad requerida por el store.
-
-Tenemos que guardar el número de cada tipo de feedback en el store, por lo que la forma del estado en el store es:
-
-```js
-{
- good: 5,
- ok: 4,
- bad: 2
-}
-```
-
-El proyecto tiene la siguiente base para un reducer:
-
-```js
-const initialState = {
- good: 0,
- ok: 0,
- bad: 0
-}
-
-const counterReducer = (state = initialState, action) => {
- console.log(action)
- switch (action.type) {
- case 'GOOD':
- return state
- case 'OK':
- return state
- case 'BAD':
- return state
- case 'ZERO':
- return state
- default: return state
- }
-
-}
-
-export default counterReducer
-```
-
-y una base para sus pruebas
-
-```js
-import deepFreeze from 'deep-freeze'
-import { describe, expect, test } from 'vitest'
-import counterReducer from './reducer'
-
-describe('unicafe reducer', () => {
- const initialState = {
- good: 0,
- ok: 0,
- bad: 0
- }
-
- test('should return a proper initial state when called with undefined state', () => {
- const state = {}
- const action = {
- type: 'DO_NOTHING'
- }
-
- const newState = counterReducer(undefined, action)
- expect(newState).toEqual(initialState)
- })
-
- test('good is incremented', () => {
- const action = {
- type: 'GOOD'
- }
- const state = initialState
-
- deepFreeze(state)
- const newState = counterReducer(state, action)
- expect(newState).toEqual({
- good: 1,
- ok: 0,
- bad: 0
- })
- })
-})
-```
-
-**Implementa el reducer y sus pruebas.**
-
-En las pruebas, asegúrate de que el reducer sea una función inmutable con la librería deep-freeze .
-Asegúrate de que la primera prueba proporcionada pase, porque Redux espera que el reducer devuelva el estado original cuando se llama con un primer parámetro - que representa el estado previo - con el valor undefined .
-
-Comienza expandiendo el reducer para que pasen ambas pruebas. Luego agrega el resto de las pruebas y finalmente la funcionalidad que están probando.
-
-Un buen modelo para el reducer es el ejemplo anterior de [redux-notas](/es/part6/flux_architecture_y_redux#redux-notas).
-
-#### 6.2: Unicafe Revisitado, paso 2
+#### 6.1: Unicafé revisitado
-Ahora implementa la funcionalidad real de la aplicación.
+Luego implemente la funcionalidad original completa de la aplicación.
-Tu aplicación puede tener una apariencia modesta, nada más se necesitan 3 botones y el número de calificaciones para cada tipo:
+La apariencia y la funcionalidad de tu aplicación deben ser las mismas que en la parte 1:
-
+
-### Formulario no controlado
+### Notas de Zustand
+
+Nuestro objetivo es crear una versión basada en Zustand de la antigua aplicación de notas.
-Agreguemos la funcionalidad para agregar nuevas notas y cambiar su importancia:
+La primera versión de la aplicación es la siguiente. El componente
App :
```js
-// highlight-start
-const generateId = () =>
- Number((Math.random() * 1000000).toFixed(0))
-// highlight-end
+import { useNotes } from './store'
const App = () => {
- // highlight-start
- const addNote = (event) => {
- event.preventDefault()
- const content = event.target.note.value
- event.target.note.value = ''
- store.dispatch({
- type: 'NEW_NOTE',
- payload: {
- content,
- important: false,
- id: generateId()
- }
- })
- }
- // highlight-end
-
- // highlight-start
- const toggleImportance = (id) => {
- store.dispatch({
- type: 'TOGGLE_IMPORTANCE',
- payload: { id }
- })
- }
- // highlight-end
+ const notes = useNotes()
return (
- // highlight-start
-
- // highlight-end
- {store.getState().map(note =>
- toggleImportance(note.id)} // highlight-line
- >
- {note.content} {note.important ? 'important' : ''}
+ {notes.map(note => (
+
+ {note.important ? {note.content} : note.content}
- )}
+ ))}
)
}
+export default App
```
-La implementación de ambas funcionalidades es sencilla. Cabe señalar que
no hemos vinculado el estado de los campos del formulario al estado del componente
App como lo hicimos anteriormente. React llama a este tipo de formulario [no controlado](https://es.react.dev/reference/react-dom/components/input#controlling-an-input-with-a-state-variable).
-
->Los formularios no controlados tienen ciertas limitaciones (por ejemplo, no son posibles los mensajes de error dinámicos o la desactivación del botón de envío en función de input). Sin embargo, son adecuados para nuestras necesidades actuales.
-
-Puedes leer más sobre formularios no controlados [aquí](https://goshakkk.name/controlled-vs-uncontrolled-inputs-react/).
-
-El método para agregar nuevas notas es simple, simplemente envía la acción para agregar notas:
-
-```js
-addNote = (event) => {
- event.preventDefault()
- const content = event.target.note.value // highlight-line
- event.target.note.value = ''
- store.dispatch({
- type: 'NEW_NOTE',
- payload: {
- content,
- important: false,
- id: generateId()
- }
- })
-}
-```
-
-Podemos obtener el contenido de la nueva nota directamente desde el campo del formulario. Debido a que el campo tiene un nombre, podemos acceder al contenido a través del objeto del evento
event.target.note.value .
+El store se define inicialmente de la siguiente manera:
```js
-
-```
+import { create } from 'zustand'
-La importancia de una nota se puede cambiar haciendo clic en su nombre. El controlador de eventos es muy simple:
+const useNoteStore = create(set => ({
+ notes: [
+ {
+ id: 1,
+ content: 'Zustand is less complex than Redux',
+ important: true,
+ },
+ ],
+}))
-```js
-toggleImportance = (id) => {
- store.dispatch({
- type: 'TOGGLE_IMPORTANCE',
- payload: { id }
- })
-}
+export const useNotes = () => useNoteStore(state => state.notes)
```
-### Action creators
-
-Comenzamos a notar que, incluso en aplicaciones tan simples como la nuestra, usar Redux puede simplificar el código de la interfaz. Sin embargo, podemos hacerlo mucho mejor.
-
-En realidad, no es necesario que los componentes de React conozcan los tipos y formas de acción de Redux.
-Separemos la creación de acciones en sus propias funciones:
-
-```js
-const createNote = (content) => {
- return {
- type: 'NEW_NOTE',
- payload: {
- content,
- important: false,
- id: generateId()
- }
- }
-}
-
-const toggleImportanceOf = (id) => {
- return {
- type: 'TOGGLE_IMPORTANCE',
- payload: { id }
- }
-}
-```
+Por ahora, la aplicación no permite añadir notas nuevas y el store tampoco lo admite todavía. El estado se ha inicializado con una nota para que podamos comprobar que la aplicación lo renderiza correctamente.
-Las funciones que crean acciones se denominan [action creators](https://redux.js.org/tutorials/essentials/part-1-overview-concepts#action-creators) (creadores de acciones).
+### Funciones puras y objetos inmutables
-El componente
App ya no tiene que saber nada sobre la representación interna de las acciones, solo obtiene la acción correcta llamando a la función creadora:
+El primer intento de acción que añade una nota es el siguiente:
```js
-const App = () => {
- const addNote = (event) => {
- event.preventDefault()
- const content = event.target.note.value
- event.target.note.value = ''
- store.dispatch(createNote(content)) // highlight-line
-
- }
-
- const toggleImportance = (id) => {
- store.dispatch(toggleImportanceOf(id))// highlight-line
- }
-
- // ...
-}
+note => set(
+ state => {
+ state.notes.push(note)
+ return state
+ }
+ )
```
-### Reenviando Redux-Store a varios componentes
-
-Aparte del reducer, nuestra aplicación está en un solo archivo. Esto, por supuesto, no es sensato, y deberíamos separar
App en su propio módulo.
-
-Ahora la pregunta es, ¿cómo puede
App acceder al store después de moverlo? Y en términos más generales, cuando un componente está compuesto por muchos componentes más pequeños, debe haber una forma para que todos los componentes accedan al store.
-Hay varias formas de compartir el store redux con los componentes. Primero veremos la forma más nueva, y posiblemente la más fácil, usando la api de [hooks](https://react-redux.js.org/api/hooks) de la librería [react-redux](https://react-redux.js.org/).
-
-Primero instalamos react-redux
-
-```bash
-npm install react-redux
-```
+La función recibe una nota como parámetro y devuelve un estado en el que se ha agregado una nueva nota al estado anterior
state .
-A continuación movemos el componente _App_ en su propio archivo _App.jsx_. Veamos cómo afecta esto al resto de los archivos de la aplicación.
+Sin embargo, nuestro intento infringe las reglas. La [documentación de Zustand](https://zustand.docs.pmnd.rs/learn/guides/immutable-state-and-merging) indica que, al igual que con useState de React, debemos actualizar el estado de forma inmutable. Como sabemos,
state.notes.push muta el objeto de estado, así que debemos modificar la solución.
-_main.jsx_ se convierte en:
+La forma correcta es usar, por ejemplo, la función [Array.concat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/concat), que no modifica el estado existente sino que crea una nueva copia del mismo con la nueva nota agregada:
```js
-import React from 'react'
-import ReactDOM from 'react-dom/client'
-import { createStore } from 'redux'
-import { Provider } from 'react-redux' // highlight-line
-
-import App from './App'
-import noteReducer from './reducers/noteReducer'
-
-const store = createStore(noteReducer)
-
-ReactDOM.createRoot(document.getElementById('root')).render(
-
// highlight-line
-
- // highlight-line
-)
+note => set(
+ state => {
+ return { notes: state.notes.concat(note) }
+ }
+ )
```
-Ten en cuenta que la aplicación ahora se define como un elemento secundario de un componente [Provider](https://react-redux.js.org/api/provider) (proveedor) proporcionado por la librería react-redux.
-El store de la aplicación se entrega al Provider como su atributo store.
-
-La definición de los action creators se ha movido al archivo
reducers/noteReducer.js donde se define el reducer. El archivo se ve así:
+La definición de store ahora tiene el siguiente aspecto:
```js
-const noteReducer = (state = [], action) => {
- // ...
-}
-
-const generateId = () =>
- Number((Math.random() * 1000000).toFixed(0))
-
-export const createNote = (content) => { // highlight-line
- return {
- type: 'NEW_NOTE',
- payload: {
- content,
- important: false,
- id: generateId()
- }
- }
-}
+import { create } from 'zustand'
-export const toggleImportanceOf = (id) => { // highlight-line
- return {
- type: 'TOGGLE_IMPORTANCE',
- payload: { id }
+const useNoteStore = create(set => ({
+ notes: [],
+ actions: {
+ add: note => set(
+ state => ({ notes: state.notes.concat(note) })
+ )
}
-}
-
-export default noteReducer
-```
+}))
-Si la aplicación tiene muchos componentes que necesitan el store, el componente
App debe pasar
store como props a todos esos componentes.
-
-El módulo ahora tiene varios comandos de [export](https://developer.mozilla.org/es/docs/Web/JavaScript/Reference/Statements/export).
-
-La función del reducer todavía se devuelve con el comando de
export default , por lo que el reducer se puede importar de la forma habitual:
-
-```js
-import noteReducer from './reducers/noteReducer'
+export const useNotes = () => useNoteStore(state => state.notes)
+export const useNoteActions = () => useNoteStore(state => state.actions)
```
-Un módulo solo puede tener
un default export , pero varias exportaciones "normales"
-
-```js
-export const createNote = (content) => {
- // ...
-}
+> #### Sintaxis de extensión de array
+>
+> Otra forma comúnmente vista de hacer lo mismo es usar la sintaxis de array [spread](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax):
+>
+> ```js
+> state => ({ notes: [...state.notes, note] })
+> ```
+>
+> Aquí se forma un array expandiendo los elementos del array
state.notes mediante la sintaxis spread y añadiendo después la nueva nota al final. Elegir entre spread y
concat es una cuestión de preferencia.
-export const toggleImportanceOf = (id) => {
- // ...
-}
-```
+Técnicamente hablando, el estado creado con Zustand es [inmutable](https://developer.mozilla.org/en-US/docs/Glossary/Immutable), y las funciones de acción que modifican el estado deben ser [funciones puras](https://en.wikipedia.org/wiki/Pure_function).
-Las funciones exportadas normalmente (no como los default) se pueden importar con la sintaxis de llaves:
+Las funciones puras son aquellas que
no producen efectos secundarios y siempre devuelven el mismo resultado cuando se llaman con los mismos parámetros.
-```js
-import { createNote } from '../../reducers/noteReducer'
-```
+### Formulario no controlado
-Código para el componente
App
+Agreguemos la capacidad de crear nuevas notas a la aplicación:
```js
-import { createNote, toggleImportanceOf } from './reducers/noteReducer' // highlight-line
-import { useSelector, useDispatch } from 'react-redux' // highlight-line
+import { useNotes, useNoteActions } from './store'
const App = () => {
- const dispatch = useDispatch() // highlight-line
- const notes = useSelector(state => state) // highlight-line
-
- const addNote = (event) => {
- event.preventDefault()
- const content = event.target.note.value
- event.target.note.value = ''
- dispatch(createNote(content)) // highlight-line
- }
+ const notes = useNotes()
+ const { add } = useNoteActions() // highlight-line
+
+ const generateId = () => Number((Math.random() * 1000000).toFixed(0)) // highlight-line
- const toggleImportance = (id) => {
- dispatch(toggleImportanceOf(id)) // highlight-line
+ // highlight-start
+ const addNote = (e) => {
+ e.preventDefault()
+ const content = e.target.note.value
+ add({ id: generateId(), content, important: false })
+ e.target.reset()
}
+ // highlight-end
return (
+ // highlight-start
+ // highlight-end
- {notes.map(note => // highlight-line
- toggleImportance(note.id)}
- >
- {note.content} {note.important ? 'important' : ''}
+ {notes.map(note => (
+
+ {note.important ? {note.content} : note.content}
- )}
+ ))}
)
}
-
-export default App
```
-Hay algunas cosas a tener en cuenta en el código. Anteriormente, el código despachaba acciones invocando al método dispatch de redux-store:
+La implementación es bastante sencilla. Lo que es digno de mención acerca de agregar una nueva nota es que, a diferencia de los formularios anteriores implementados con React, tenemos
not vinculado el valor del campo del formulario al estado del componente
App . React llama a dichas formas [incontroladas](https://react.dev/learn/sharing-state-between-components#controlled-and-uncontrolled-components).
-```js
-store.dispatch({
- type: 'TOGGLE_IMPORTANCE',
- payload: { id }
-})
-```
+> Las formas no controladas tienen ciertas limitaciones. No permiten, por ejemplo, proporcionar mensajes de validación sobre la marcha, desactivar el botón de envío según el contenido, etc. Sin embargo, esta vez son adecuados para nuestro caso de uso.
+Si lo deseas, puedes leer más sobre el tema [aquí](https://goshakkk.name/controlled-vs-uncontrolled-inputs-react/).
-Ahora lo hace con la función
dispatch del hook [useDispatch](https://react-redux.js.org/api/hooks#usedispatch).
+El formulario es muy sencillo:
```js
-import { useSelector, useDispatch } from 'react-redux' // highlight-line
-
-const App = () => {
- const dispatch = useDispatch() // highlight-line
- // ...
-
- const toggleImportance = (id) => {
- dispatch(toggleImportanceOf(id)) // highlight-line
- }
-
- // ...
-}
+
```
-El hook
useDispatch proporciona acceso a cualquier componente de React a la función dispatch de redux-store definida en
main.jsx . Esto permite que todos los componentes realicen cambios en el estado de Redux store.
+Lo que llama la atención del formulario es que el campo de entrada tiene un nombre. Esto permite que la función del controlador acceda al valor del campo.
-El componente puede acceder a las notas almacenadas en el store con el hook [useSelector](https://react-redux.js.org/api/hooks#useselector) de la librería react-redux.
+El controlador de sumas también es sencillo:
```js
-import { useSelector, useDispatch } from 'react-redux' // highlight-line
-
-const App = () => {
- // ...
- const notes = useSelector(state => state) // highlight-line
- // ...
-}
+ const addNote = (e) => {
+ e.preventDefault()
+ const content = e.target.note.value
+ add({ id: generateId(), content, important: false })
+ e.target.reset()
+ }
```
-
useSelector recibe una función como parámetro. La función busca o selecciona datos del store de Redux.
-Aquí necesitamos todas las notas, por lo que nuestra función de selector devuelve el estado completo:
+El contenido se recupera del campo de texto del formulario usando
e.target.note.value en una variable, que se usa como parámetro en la llamada a la función de agregar notas
add .
+La última línea,
e.target.reset() , borra el formulario.
-```js
-state => state
-```
+El código actual de la aplicación está disponible en su totalidad en [GitHub](https://github.com/fullstack-hy2020/zustand-notes/tree/part6-1), en la rama
part6-1 .
-que es una abreviatura de
+### Más componentes y funcionalidades
-```js
-(state) => {
- return state
-}
-```
+Dividamos la aplicación en más componentes. Separaremos la creación de una nueva nota, la lista de notas y la visualización de una sola nota en sus propios componentes.
-Por lo general, las funciones de selector son un poco más interesantes y solo devuelven partes seleccionadas del contenido del store redux. Por ejemplo, podríamos devolver solo notas marcadas como importantes:
+El componente
App después del cambio es simple:
```js
-const importantNotes = useSelector(state => state.filter(note => note.important))
+const App = () => (
+
+
+
+
+)
```
-La versión actual de la aplicación se puede encontrar en [GitHub](https://github.com/fullstack-hy2020/redux-notes/tree/part6-0), en la rama
part6-0 .
-
-### Más componentes
+La creación de notas, es decir,
NoteForm , no contiene nada dramático, por lo que el código no se muestra aquí.
-Separemos el formulario responsable de crear una nueva nota en su propio componente en el archivo
src/components/NoteForm.jsx :
+El componente responsable de enumerar las notas,
NoteList , tiene el siguiente aspecto:
```js
-import { useDispatch } from 'react-redux'
-import { createNote } from '../reducers/noteReducer'
-
-const NoteForm = () => {
- const dispatch = useDispatch()
+import { useNotes } from './store'
+import Note from './Note'
- const addNote = (event) => {
- event.preventDefault()
- const content = event.target.note.value
- event.target.note.value = ''
- dispatch(createNote(content))
- }
+const NoteList = () => {
+ const notes = useNotes()
return (
-
+
+ {notes.map(note => (
+
+ ))}
+
)
}
-
-export default NoteForm
```
-A diferencia del código de React que hicimos sin Redux, el controlador de eventos para cambiar el estado de la aplicación (que ahora vive en Redux) se ha movido de
App a un componente hijo. La lógica para cambiar el estado en Redux todavía está claramente separada de toda la parte de React de la aplicación.
+El componente recupera la lista de notas del store y crea un componente
Note correspondiente para cada una, pasando los datos de la nota como props:
+
+```js
+const Note = ({ note }) => (
+
+ {note.important ? {note.content} : note.content}
+
+)
+```
-También separaremos la lista de notas y mostraremos una sola nota en sus propios componentes. Coloquemos ambos en el archivo
src/components/Notes.jsx :
+Agreguemos también la capacidad de alternar la importancia de una nota. El componente después del cambio es el siguiente:
```js
-import { useDispatch, useSelector } from 'react-redux' // highlight-line
-import { toggleImportanceOf } from '../reducers/noteReducer' // highlight-line
-
-const Note = ({ note, handleClick }) => {
- return(
-
- {note.content}
- {note.important ? 'important' : ''}
-
- )
-}
+import { useNoteActions } from './store'
-const Notes = () => {
- const dispatch = useDispatch() // highlight-line
- const notes = useSelector(state => state) // highlight-line
+const Note = ({ note }) => {
+ const { toggleImportance } = useNoteActions() // highlight-line
- return(
-
- {notes.map(note =>
-
- dispatch(toggleImportanceOf(note.id))
- }
- />
- )}
-
+ return (
+
+ {note.important ? {note.content} : note.content}
+ // highlight-start
+ toggleImportance(note.id)}>
+ {note.important ? 'make not important' : 'make important'}
+
+ // highlight-end
+
)
}
-
-export default Notes
```
-La lógica para cambiar la importancia de una nota ahora está en el componente que administra la lista de notas.
+El componente desestructura la función de alternancia de importancia a partir del valor de retorno de
useNoteActions y la llama cuando se hace clic en el botón de alternancia.
-Solo queda una pequeña cantidad de código en el archivo
App.jsx :
+La implementación de la función de cambio de importancia se parece a la siguiente:
```js
-import NoteForm from './components/NoteForm'
-import Notes from './components/Notes'
+import { create } from 'zustand'
-const App = () => {
- return (
-
-
-
-
- )
-}
+const useNoteStore = create(set => ({
+ notes: [],
+ actions: {
+ add: note => set(
+ state => ({ notes: state.notes.concat(note) })
+ ),
+ // highlight-start
+ toggleImportance: id => set(
+ state => ({
+ notes: state.notes.map(note =>
+ note.id === id ? { ...note, important: !note.important } : note
+ )
+ })
+ )
+ // highlight-end
+ }
+}))
-export default App
```
-
Note , responsable de representar una sola nota, es muy simple y no es consciente de que el controlador de eventos que obtiene como props despacha una acción. Este tipo de componentes se denominan [presentacionales](https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0) en la terminología de React.
+La función recibe como parámetro el id de la nota a modificar. El nuevo estado se forma a partir del estado anterior utilizando la función
map de modo que se incluyan todas las notas antiguas, excepto la nota que se va a modificar, para la cual se crea una versión donde se alterna su importancia:
-
Notes , por otro lado, es un componente [contenedor](https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0), ya que contiene cierta lógica de aplicación: define lo que hacen los controladores de eventos de los componentes
Note y coordina la configuración de los componentes
presentacionales , es decir, los
Note s.
+```js
+{ ...note, important: !note.important }
+```
-El código de la aplicación Redux se puede encontrar en [GitHub](https://github.com/fullstack-hy2020/redux-notes/tree/part6-1), en la rama
part6-1 .
+El código actual de la aplicación está disponible en su totalidad en [GitHub](https://github.com/fullstack-hy2020/zustand-notes/tree/part6-2), en la rama
part6-2 .
-### Ejercicios 6.3.-6.8.
+### Ejercicios 6.2.-6.6.
-Hagamos una nueva versión de la aplicación de votación de anécdotas de la parte 1. Toma el proyecto de este repositorio https://github.com/fullstack-hy2020/redux-anecdotes como base de tu solución.
+Implementemos una nueva versión de la aplicación de votación de anécdotas de la parte 1. Utiliza el proyecto https://github.com/fullstack-hy2020/zustand-anecdotes como base para tu solución.
-Si clonas el proyecto en un repositorio de git existente, elimina la configuración de git de la aplicación clonada:
+Si clona el proyecto dentro de un repositorio Git existente, elimine la configuración de Git de la aplicación clonada:
```bash
-cd redux-anecdotes // go to the cloned repository
+cd zustand-anecdotes // go to the cloned repository directory
rm -rf .git
```
-La aplicación se puede iniciar como de costumbre, pero primero debes instalar las dependencias:
+La aplicación se inicia normalmente, pero primero debes instalar las dependencias:
```bash
npm install
npm run dev
```
-Después de completar estos ejercicios, tu aplicación debería verse así:
-
-
-
-#### 6.3: Anécdotas, paso 1
-
-Implementa la funcionalidad para votar anécdotas. La cantidad de votos debe guardarse en una store de Redux.
+Al completar los siguientes ejercicios, la aplicación debería verse así:
-#### 6.4: Anécdotas, paso 2
+
-Implementa la funcionalidad para agregar nuevas anécdotas.
+#### 6.2: anécdotas, paso 1
-Puedes mantener el formulario no controlado, como hicimos [antes](/es/part6/flux_architecture_y_redux#formulario-no-controlado).
+Implementar la posibilidad de votar por anécdotas. El número de votos debe almacenarse en el store de Zustand.
-#### 6.5: Anécdotas, paso 3
+#### 6.3: anécdotas, paso 2
-Asegúrate de que las anécdotas estén ordenadas por número de votos.
+Añade la posibilidad de añadir nuevas anécdotas a la aplicación.
-#### 6.6: Anécdotas, paso 4
+Puedes mantener el formulario para añadir anécdotas [sin controlar](/es/part6/arquitectura_flux_y_zustand#formulario-no-controlado), como en el ejemplo anterior.
-Si aún no lo haz hecho, separa la creación de objetos de acción en funciones [action creator](https://read.reduxbook.com/markdown/part1/04-action-creators.html) y colócalos en el archivo src/reducers/anecdoteReducer.js , así que haz lo que hemos estado haciendo desde el capítulo [action creators](/es/part6/flux_architecture_y_redux#action-creators).
+#### 6.4: anécdotas, paso 3
-#### 6.7: Anécdotas, paso 5
+Separe la creación de una nueva anécdota en su propio componente llamado AnecdoteForm y separe la visualización de la lista de anécdotas en su propio componente llamado AnecdoteList .
-Separa la creación de nuevas anécdotas en su propio componente llamado AnecdoteForm . Mueve toda la lógica para crear una nueva anécdota en este nuevo componente.
-
-#### 6.8: Anécdotas, paso 6
-
-Separa el renderizado de la lista de anécdotas en su propio componente llamado AnecdoteList . Mueve toda la lógica relacionada con la votación de una anécdota a este nuevo componente.
-
-Ahora, el componente App debería verse así:
+Después de este ejercicio, el componente App debería tener el siguiente aspecto:
```js
import AnecdoteForm from './components/AnecdoteForm'
@@ -1292,4 +829,10 @@ const App = () => {
export default App
```
+#### 6.5: anécdotas, paso 4
+
+Asegúrate de que las anécdotas se mantengan en orden descendente según su número de votos.
+
+**NOTA** En este ejercicio es recomendable utilizar la función [Array.toSorted](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/toSorted), que no ordena el array original sino que crea una copia ordenada del mismo. ¡Esto se debe a que el estado de Zustand no debe sufrir mutaciones!
+
diff --git a/src/content/6/es/part6b.md b/src/content/6/es/part6b.md
index 3ae82e5434c..a4018c40784 100644
--- a/src/content/6/es/part6b.md
+++ b/src/content/6/es/part6b.md
@@ -4,835 +4,1296 @@ part: 6
letter: b
lang: es
---
-
+
-Continuemos nuestro trabajo con la [versión Redux](/es/part6/flux_architecture_y_redux#redux-notas) simplificada de nuestra aplicación de notas.
+Sigamos ampliando la versión Zustand de la aplicación de notas.
-Para facilitar nuestro desarrollo, cambiemos nuestro reducer para que el store se inicialice con un estado que contenga un par de notas:
+Para facilitar el desarrollo, cambiemos el estado inicial para que ya contenga algunas notas:
```js
-const initialState = [
- {
- content: 'reducer defines how redux store works',
- important: true,
- id: 1,
- },
- {
- content: 'state of store can contain any data',
- important: false,
- id: 2,
- },
-]
+// highlight-start
+const initialNotes = [
+ {
+ id: 1,
+ content: 'Zustand is less complex than Redux',
+ important: true,
+ }, {
+ id: 2,
+ content: 'React app benefits from custom hooks',
+ important: false,
+ }, {
+ id: 3,
+ content: 'Remember to sleep well',
+ important: true,
+ }
+ ]
+
-const noteReducer = (state = initialState, action) => {
+//highlight-end
+
+const useNoteStore = create((set) => ({
+ notes: initialNotes,
// ...
}
-
-// ...
-export default noteReducer
```
-### Store con estado complejo
+### Estado más complejo
-Implementemos el filtrado de las notas que se muestran al usuario. La interfaz de usuario para los filtros se implementará con [botones de radio](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/radio):
+Implementemos el filtrado de las notas que se muestran en la aplicación, permitiendo restringir las notas visibles. El filtro se implementa mediante [botones de opción](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/radio):
-
+
-Comencemos con una implementación muy simple y directa:
+Surge la pregunta de cuál es la mejor forma de gestionar el estado del filtro. Hay dos opciones: crear un store de Zustand separado para el filtro o añadirlo al store existente. Ambas son razonables. Las [buenas prácticas](https://tkdodo.eu/blog/working-with-zustand#keep-the-scope-of-your-store-small) recomiendan mantener los elementos que no guardan relación en stores separados. Sin embargo, la lista de notas y el filtro están tan vinculados que colocaremos ambos en el mismo store:
```js
-import NoteForm from './components/NoteForm'
-import Notes from './components/Notes'
-
-const App = () => {
-//highlight-start
- const filterSelected = (value) => {
- console.log(value)
+const useNoteStore = create((set) => ({
+ notes: initialNotes,
+ filter: 'all', // highlight-line
+ actions: {
+ add: note => set(
+ state => ({ notes: state.notes.concat(note) })
+ ),
+ toggleImportance: id => set(
+ state => ({
+ notes: state.notes.map(note =>
+ note.id === id ? { ...note, important: !note.important } : note
+ )
+ })
+ ),
+ setFilter: value => set(() => ({ filter: value })) // highlight-line
}
-//highlight-end
+}))
+
+export const useNotes = () => useNoteStore((state) => state.notes)
+export const useFilter = () => useNoteStore((state) => state.filter) // highlight-line
+export const useNoteActions = () => useNoteStore((state) => state.actions)
+```
+
+El componente que establece el valor del filtro:
+
+```js
+import { useNoteActions } from './store'
+
+const VisibilityFilter = () => {
+ const { setFilter } = useNoteActions()
return (
)
}
+
+export default VisibilityFilter
```
-Dado que el atributo
name de todos los botones de radio es el mismo, estos forman un
button group (grupo de botones) en el que solo se puede seleccionar una opción.
+El componente
App renderiza el filtro:
-Los botones tienen un controlador de cambios que actualmente solo imprime el string asociado con el botón en el que se hizo clic en la consola.
+```js
+const App = () => (
+
+
+ // highlight-line
+
+
+)
+```
-En la siguiente sección, vamos a implementar el filtrado almacenando las notas y
el valor del filtro en el store de redux. Cuando terminemos, nos gustaría que el estado del store se viera así:
+El filtrado de las notas mostradas podría manejarse en el componente
NoteList , por ejemplo de la siguiente manera:
```js
-{
- notes: [
- { content: 'reducer defines how redux store works', important: true, id: 1},
- { content: 'state of store can contain any data', important: false, id: 2}
- ],
- filter: 'IMPORTANT'
+import { useNotes, useFilter } from './store'
+import Note from './Note'
+
+const NoteList = () => {
+ const notes = useNotes()
+ const filter = useFilter() // highlight-line
+
+ // highlight-start
+ const notesToShow = notes.filter(note => {
+ if (filter === 'important') return note.important
+ if (filter === 'nonimportant') return !note.important
+ return true
+ })
+ // highlight-end
+
+ return (
+
+ {notesToShow.map(note => ( // highlight-line
+
+ ))}
+
+ )
}
```
-Solo el array de notas se almacenaba en el estado de la implementación anterior de nuestra aplicación. En la nueva implementación, el objeto de estado tiene dos propiedades,
notes que contienen el array de notas y
filter que contiene un string que indica qué notas deben mostrarse al usuario.
+Se llega a una mejor solución incluyendo la lógica de filtrado directamente en la función
useNotes del store:
+
+```js
+import { create } from 'zustand'
+
+const useNoteStore = create((set) => ({
+ // ...
+}))
-### Reducers combinados
+// highlight-start
+export const useNotes = () => {
+ const notes = useNoteStore((state) => state.notes)
+ const filter = useNoteStore((state) => state.filter)
-Podríamos modificar nuestro reducer actual para hacer frente a la nueva forma del estado. Sin embargo, una mejor solución en esta situación es definir un nuevo reducer separado para el estado del filtro:
+ if (filter === 'important') return notes.filter(n => n.important)
+ if (filter === 'nonimportant') return notes.filter(n => !n.important)
-```js
-const filterReducer = (state = 'ALL', action) => {
- switch (action.type) {
- case 'SET_FILTER':
- return action.payload
- default:
- return state
- }
+ return notes
}
+// highlight-end
```
-Las acciones para cambiar el estado del filtro se ven así:
+La función
useNotes devuelve siempre una lista de notas filtradas de la forma deseada. El consumidor de la función, el componente
NoteList , ni siquiera necesita ser consciente de la existencia del filtro:
```js
-{
- type: 'SET_FILTER',
- payload: 'IMPORTANT'
+import { useNotes } from './store'
+import Note from './Note'
+
+const NoteList = () => {
+ // component gets always the properly filtered set of notes
+ const notes = useNotes()
+
+ return (
+
+ {notes.map(note => (
+
+ ))}
+
+ )
}
```
-Creemos también una nueva función de _action creator_. Escribiremos su código en un nuevo módulo
src/reducers/filterReducer.js :
+¡La solución es elegante!
+
+> #### Una posible solución alternativa
+>
+> Una alternativa sería implementar el filtrado directamente dentro de una función selectora, de modo que tanto las notas como el filtro se lean en una sola llamada
useNoteStore :
+>
+>```js
+>export const useNotes = () => useNoteStore(({ notes, filter }) => {
+> if (filter === 'important') return notes.filter(n => n.important)
+> if (filter === 'nonimportant') return notes.filter(n => !n.important)
+> return notes
+>})
+>```
+>
+> Sin embargo, este enfoque no funciona, ya que conduce a un bucle infinito de renderizado cuando se cambia el filtro.
+>
+> El motivo es el siguiente: Zustand compara el valor de retorno del selector utilizando el operador
=== . Dado que
notes.filter(...) crea una nueva array en cada renderizado, React siempre lo interpreta como un nuevo estado y activa otro renderizado, que nuevamente crea una nueva array, y así sucesivamente.
+>
+> La solución consiste en añadir [useShallow](https://zustand.docs.pmnd.rs/reference/hooks/use-shallow), que sustituye la comparación
=== por una comparación superficial: compara uno a uno los elementos del array. Si el contenido no ha cambiado, devuelve la referencia anterior en lugar de crear una nueva, por lo que React considera estable el estado y no vuelve a renderizar.
+>
+>```js
+>import { useShallow } from 'zustand/react/shallow'
+>
+>//...
+>
+>export const useNotes = () => useNoteStore(useShallow(({ notes, filter }) => {
+> if (filter === 'important') return notes.filter(n => n.important)
+> if (filter === 'nonimportant') return notes.filter(n => !n.important)
+> return notes
+>}))
+>```
+>
+> La solución funciona, pero es un poco más difícil de entender. En el material del curso utilizamos la versión presentada anteriormente con dos llamadas
useNoteStore independientes.
+
+El código actual de la aplicación está disponible en su totalidad en [GitHub](https://github.com/fullstack-hy2020/zustand-notes/tree/part6-3), en la rama
part6-3 .
-```js
-const filterReducer = (state = 'ALL', action) => {
- // ...
-}
+
+
+
+
+### Ejercicio 6.6
+
+Sigamos con la aplicación de anécdotas.
-export const filterChange = filter => {
- return {
- type: 'SET_FILTER',
- payload: filter,
+#### 6.6 anécdotas, paso 5
+
+Implementar filtrado de las anécdotas mostradas en la aplicación:
+
+
+
+Crea un componente
Filter para mostrar el filtro en la pantalla. Puedes utilizar lo siguiente como punto de partida:
+
+```js
+const Filter = () => {
+ const handleChange = (event) => {
+ // the value of the input field is in event.target.value
}
+ const style = {
+ marginBottom: 10
+ }
+
+ return (
+
+ filter
+
+ )
}
-export default filterReducer
+export default Filter
```
-Podemos crear el reducer que nuestra aplicación realmente utilizara al combinar los dos reducers existentes con la función [combineReducers](https://redux.js.org/api/combinereducers).
+
+
+
+
+### Datos al servidor
+
+Extendamos la aplicación para que las notas se almacenen en un backend. Utilizaremos el [JSON Server](/es/part2/obteniendo_datos_del_servidor) que conocemos de la parte 2.
+
+Guarde el estado inicial de la base de datos en el archivo
db.json en la raíz del proyecto:
+
+```json
+{
+ "notes": [
+ {
+ "id": 1,
+ "content": "Zustand is less complex than Redux",
+ "important": true
+ },
+ {
+ "id": 2,
+ "content": "React app benefits from custom hooks",
+ "important": false
+ },
+ {
+ "id": 3,
+ "content": "Remember to sleep well",
+ "important": true
+ }
+ ]
+}
+```
-Definamos el reducer combinado en el archivo
main.jsx :
+Instalar el servidor JSON:
+
+```bash
+npm install json-server --save-dev
+```
+
+y agregue la siguiente línea a la sección
scripts de
package.json :
```js
-import ReactDOM from 'react-dom/client'
-import { createStore, combineReducers } from 'redux' // highlight-line
-import { Provider } from 'react-redux'
-import App from './App'
+"scripts": {
+ "server": "json-server -p 3001 db.json",
+ // ...
+}
+```
-import noteReducer from './reducers/noteReducer'
-import filterReducer from './reducers/filterReducer' // highlight-line
+Inicie el servidor JSON con el comando _npm run server_.
- // highlight-start
-const reducer = combineReducers({
- notes: noteReducer,
- filter: filterReducer
-})
- // highlight-end
+### Fetch API
-const store = createStore(reducer) // highlight-line
+En el desarrollo de software, a menudo hay que considerar si implementar una determinada característica utilizando una librería externa o aprovechar las soluciones nativas proporcionadas por el entorno. Ambos enfoques tienen sus propias ventajas y desafíos.
-console.log(store.getState())
+En partes anteriores del curso hemos utilizado la librería [Axios](https://axios-http.com/docs/intro) para realizar solicitudes HTTP. Veamos ahora una alternativa basada en la [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) nativa.
-/*
-ReactDOM.createRoot(document.getElementById('root')).render(
-
-
-
-)*/
+Es típico que una librería externa como
Axios se implemente utilizando otras librerías externas. Por ejemplo, si instala Axios en un proyecto con el comando
npm install axios , la salida de la consola es:
-ReactDOM.createRoot(document.getElementById('root')).render(
-
-
-
-)
+```bash
+$ npm install axios
+
+added 23 packages, and audited 302 packages in 1s
+
+71 packages are looking for funding
+ run `npm fund` for details
+
+found 0 vulnerabilities
```
-Dado que nuestra aplicación se rompe por completo en este punto, renderizamos un elemento
div vacío en lugar del componente
App .
+Por lo tanto, el comando instalaría no solo la librería de Axios sino también más de 20 paquetes npm más que Axios necesita para funcionar.
-El estado del store se imprime en la consola:
+La
Fetch API permite realizar solicitudes HTTP de forma similar a Axios, pero sin instalar librerías externas. Mantener una aplicación resulta más sencillo cuando hay menos librerías que actualizar y la seguridad también mejora al reducirse su superficie de ataque potencial. La seguridad y el mantenimiento se tratan en la [parte 7](/es/part7/miscelanea#seguridad-en-aplicaciones-reactnode) del curso.
-
+En la práctica, la realización de solicitudes se realiza mediante la función
fetch() . La sintaxis utilizada tiene algunas diferencias respecto a Axios. Pronto también notaremos que Axios se encargó de algunas cosas por nosotros y nos hizo la vida más fácil. Sin embargo, ahora usaremos la API Fetch porque es una solución nativa ampliamente utilizada con la que todo desarrollador Full Stack debería estar familiarizado.
-Como podemos ver en el resultado, ¡el store tiene la forma exacta que queríamos!
+### Obtención de datos del servidor
-Echemos un vistazo más de cerca a cómo se crea el reducer combinado:
+Creemos una función que obtenga datos del backend en el archivo
src/services/notes.js :
```js
-const reducer = combineReducers({
- notes: noteReducer,
- filter: filterReducer,
-})
+const baseUrl = 'http://localhost:3001/notes'
+
+const getAll = async () => {
+ const response = await fetch(baseUrl)
+
+ if (!response.ok) {
+ throw new Error('Failed to fetch notes')
+ }
+
+ const data = await response.json()
+ return data
+}
+
+export default { getAll }
```
-El estado del store definido por este reducer es un objeto con dos propiedades:
notes y
filter . El valor de la propiedad
notes es definido por
noteReducer , que no tiene que lidiar con las otras propiedades del estado. Asimismo, la propiedad
filter es administrada por
filterReducer .
+Veamos más de cerca la implementación de la función
getAll . Las notas ahora se obtienen del backend llamando a la función
fetch() , a la que se le ha dado la URL del backend como argumento. El tipo de solicitud no se especifica por separado, por lo que
fetch realiza la acción predeterminada, que es una solicitud GET.
-Antes de realizar más cambios en el código, echemos un vistazo a cómo las diferentes acciones cambian el estado del store definido por el reducer combinado. Agreguemos lo siguiente al archivo
main.jsx :
+Cuando llega la respuesta, comprobamos si la solicitud se realizó correctamente mirando el campo
response.ok y arrojamos un error si es necesario:
```js
-import { createNote } from './reducers/noteReducer'
-import { filterChange } from './reducers/filterReducer'
-//...
-store.subscribe(() => console.log(store.getState()))
-store.dispatch(filterChange('IMPORTANT'))
-store.dispatch(createNote('combineReducers forms one reducer from many simple reducers'))
+if (!response.ok) {
+ throw new Error('Failed to fetch notes')
+}
```
-Al simular la creación de una nota y cambiar el estado del filtro de esta manera, el estado del store se muestra en la consola después de cada cambio que se realiza en el store:
+El atributo
response.ok obtiene el valor
true si la solicitud se realizó correctamente, es decir, si el código de estado de respuesta está en el rango 200-299. Para todos los demás códigos de estado, como 404 o 500, obtiene el valor
false .
-
+Ten en cuenta que
fetch no genera automáticamente un error aunque el código de estado de la respuesta sea, por ejemplo, 404. El manejo de errores debe implementarse manualmente, como acabamos de hacer.
-En este punto es bueno darse cuenta de un pequeño pero importante detalle. Si agregamos un console log
al comienzo de ambos reducers (noteReducer y filterReducer) :
+Si la solicitud tuvo éxito, los datos contenidos en la respuesta se convierten al formato JSON:
```js
-const filterReducer = (state = 'ALL', action) => {
- console.log('ACTION: ', action)
- // ...
-}
+const data = await response.json()
```
+
fetch no convierte automáticamente los datos que puedan acompañar a la respuesta al formato JSON; la conversión debe realizarse manualmente. También vale la pena señalar que
response.json() es una función asincrónica, por lo que se debe usar la palabra clave
await con ella.
+
+Simplifiquemos un poco el código devolviendo los datos devueltos por la función
response.json() directamente:
+
```js
-const noteReducer = (state = initialState, action) => {
- console.log('ACTION: ', action)
- // ...
+const getAll = async () => {
+ const response = await fetch(baseUrl)
+
+ if (!response.ok) {
+ throw new Error('Failed to fetch notes')
+ }
+
+ return await response.json() // highlight-line
}
```
-Según el resultado de la consola, uno podría tener la impresión de que cada acción se duplica:
+Agreguemos una función al store que se puede usar para inicializar el estado con notas obtenidas del servidor:
-
+```js
+const useNoteStore = create((set) => ({
+ notes: [], // highlight-line
+ filter: '',
+ actions: {
+ // ...
+ setFilter: value => set(() => ({ filter: value })),
+ initialize: notes => set(() => ({ notes })) // highlight-line
+ }
+}))
+```
-¿Hay algún bug en nuestro código? No. El reducer combinado funciona de tal manera que cada
acción es controlada en
cada parte del reducer combinado, o en otras palabras, cada reducer "escucha" a todas las acciones despachadas y hace algo con ellas si así se lo hemos instruido. Normalmente, solo un reducer está interesado en una acción determinada, pero hay situaciones en las que varios reducers cambian sus respectivas partes del estado en función de la misma acción.
+Implementemos la inicialización de notas en el componente
App ; como es habitual al recuperar datos de un servidor, utilizamos el hook
useEffect :
-### Terminando los filtros
+```js
+const App = () => {
+ const { initialize } = useNoteActions()
-Terminemos la aplicación para que utilice el reducer combinado. Comenzamos cambiando la renderización de la aplicación y conectando el store a la aplicación en el archivo
main.jsx :
+ // highlight-start
+ useEffect(() => {
+ noteService.getAll().then(notes => initialize(notes))
+ }, [initialize])
+ // highlight-end
-```js
-ReactDOM.createRoot(document.getElementById('root')).render(
-
-
-
-)
+ return (
+
+
+
+
+
+ )
+}
```
-A continuación, solucionemos un error causado por el código que espera que la store de aplicaciones sea un array de notas:
+Por lo tanto, las notas se obtienen del servidor usando la función
getAll() que definimos y luego se almacenan usando la función
initialize del store. Estas acciones se realizan en el hook
useEffect , lo que significa que se ejecutan durante el primer renderizado del componente de la aplicación.
+
+Veamos un pequeño detalle. Hemos añadido la función
initialize al array de dependencias del hook
useEffect . Si intentamos utilizar un array de dependencias vacío, ESLint muestra la advertencia
React Hook useEffect has a missing dependency: 'initialize' . ¿Qué está ocurriendo?
-
+El código funcionaría igual aunque utilizáramos un array de dependencias vacío, porque
initialize hace referencia a la misma función durante toda la ejecución. Sin embargo, es una buena práctica incluir como dependencias todas las variables y funciones utilizadas por _useEffect_ que estén definidas dentro del componente. Esto ayuda a evitar errores inesperados.
-Es una solución fácil. Debido a que las notas están en el campo
notes del store, solo tenemos que hacer un pequeño cambio en la función de selector:
+### Envío de datos al servidor
+
+A continuación, implementemos la funcionalidad para enviar una nueva nota al servidor. Al mismo tiempo podemos practicar cómo realizar una solicitud POST usando la función
fetch() .
+
+Extendamos el código de comunicación del servidor en
src/services/notes.js de la siguiente manera:
```js
-const Notes = () => {
- const dispatch = useDispatch()
- const notes = useSelector(state => state.notes) // highlight-line
+const baseUrl = 'http://localhost:3001/notes'
- return(
-
- {notes.map(note =>
-
- dispatch(toggleImportanceOf(note.id))
- }
- />
- )}
-
- )
+const getAll = async () => {
+ const response = await fetch(baseUrl)
+
+ if (!response.ok) {
+ throw new Error('Failed to fetch notes')
+ }
+
+ return await response.json()
+}
+
+// highlight-start
+const createNew = async (content) => {
+ const response = await fetch(baseUrl, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ content, important: false }),
+ })
+
+ if (!response.ok) {
+ throw new Error('Failed to create note')
+ }
+
+ return await response.json()
}
+// highlight-end
+
+export default { getAll, createNew } // highlight-line
```
-Anteriormente, la función de selector devolvía el estado completo del store:
+Veamos más de cerca la implementación de la función
createNew . El primer parámetro de la función
fetch() especifica la URL a la que se realiza la solicitud. El segundo parámetro es un objeto que define los demás detalles de la solicitud, como el tipo de solicitud, los encabezados y los datos enviados con la solicitud. Podemos aclarar aún más el código almacenando el objeto que define los detalles de la solicitud en una variable auxiliar
options separada:
```js
-const notes = useSelector(state => state)
+const createNew = async (content) => {
+ // highlight-start
+ const options = {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ content, important: false }),
+ }
+
+ const response = await fetch(baseUrl, options)
+ // highlight-end
+
+ if (!response.ok) {
+ throw new Error('Failed to create note')
+ }
+
+ return await response.json()
+}
```
-Y ahora devuelve solo su campo
notes
+Miremos más de cerca el objeto
options :
+
+-
method define el tipo de solicitud, que en este caso es
POST
+-
headers define los encabezados de solicitud. Adjuntamos el encabezado
'Content-Type': 'application/json' a la solicitud para que el servidor sepa que los datos incluidos con la solicitud están en formato JSON y pueda manejar la solicitud correctamente.
+-
body contiene los datos que se enviarán con la solicitud. El campo no puede contener directamente un objeto JavaScript; primero debe convertirse en una cadena JSON llamando a
JSON.stringify() .
+
+Al igual que con la solicitud GET, aquí también verificamos el código de estado de respuesta para detectar errores:
```js
-const notes = useSelector(state => state.notes)
+if (!response.ok) {
+ throw new Error('Failed to create note')
+}
```
-Extraigamos el filtro de visibilidad en su propio componente
src/components/VisibilityFilter.jsx :
+Si la solicitud tiene éxito,
JSON Server devuelve la nota recién creada, para la cual también generó un
id único. Los datos contenidos en la respuesta aún deben convertirse al formato JSON usando la función
response.json() :
```js
-import { filterChange } from '../reducers/filterReducer'
-import { useDispatch } from 'react-redux'
+return await response.json()
+```
-const VisibilityFilter = (props) => {
- const dispatch = useDispatch()
+Luego cambiemos el componente
NoteForm de nuestra aplicación para que se envíe una nueva nota al backend. La función
addNote del componente cambia ligeramente:
+
+```js
+import { useNoteActions } from './store'
+import noteService from './services/notes'
+
+const NoteForm = () => {
+ const { add } = useNoteActions()
+
+ const addNote = async (e) => {
+ e.preventDefault()
+ const content = e.target.note.value
+ const newNote = await noteService.createNew(content) // highlight-line
+ add(newNote)
+ e.target.reset()
+ }
return (
-
- all
- dispatch(filterChange('ALL'))}
- />
- important
- dispatch(filterChange('IMPORTANT'))}
- />
- nonimportant
- dispatch(filterChange('NONIMPORTANT'))}
- />
-
+
)
}
-export default VisibilityFilter
+export default NoteForm
```
-Con el nuevo componente,
App se puede simplificar de la siguiente manera:
+Cuando se crea una nueva nota en el backend llamando a la función
createNew() , obtenemos un objeto que describe la nota, para la cual el backend ha generado un
id .
-```js
-import NoteForm from './components/NoteForm'
-import Notes from './components/Notes'
-import VisibilityFilter from './components/VisibilityFilter'
+El código actual de la aplicación está disponible en su totalidad en [GitHub](https://github.com/fullstack-hy2020/zustand-notes/tree/part6-4), en la rama
part6-4 .
+
+### Acciones asíncronas
+
+Nuestro enfoque es bastante bueno, pero en cierto sentido desafortunado, ya que la comunicación con el servidor ocurre dentro del código de las funciones que definen los componentes. Sería mejor si la comunicación pudiera abstraerse de los componentes, de modo que solo necesiten llamar a una función apropiada que proporciona el store.
+
+Queremos que
App inicialice el estado de la aplicación de la siguiente manera:
+```js
const App = () => {
+ const { initialize } = useNoteActions() // highlight-line
+
+ useEffect(() => {
+ initialize() // highlight-line
+ }, [initialize])
+
return (
-
+
)
}
+```
+
-export default App
+
NoteForm a su vez crea una nueva nota como esta:
+
+```js
+const NoteForm = () => {
+ const { add } = useNoteActions() // highlight-line
+
+ const addNote = async (e) => {
+ e.preventDefault()
+ const content = e.target.note.value
+ await add(content) // highlight-line
+ e.target.reset()
+ }
+
+ return (
+
+ )
+}
+```
+
+El cambio a
store.js es el siguiente:
+
+```js
+import { create } from 'zustand'
+import noteService from './services/notes' // highlight-line
+
+const useNoteStore = create((set) => ({
+ notes: [],
+ filter: '',
+ actions: {
+ add: async (content) => { // highlight-line
+ const newNote = await noteService.createNew(content) // highlight-line
+ set(state => ({ notes: state.notes.concat(newNote) }))
+ },
+ initialize: async () => { // highlight-line
+ const notes = await noteService.getAll() // highlight-line
+ set(() => ({ notes }))
+ },
+ // ...
+ }
+}))
```
-La implementación es bastante sencilla. Al hacer clic en los diferentes radio buttons, cambia el estado de la propiedad
filter del store.
+Las funciones
add y
initialize se han convertido en funciones asincrónicas, que primero llaman a la función noteService adecuada y luego actualizan el estado.
-Cambiemos el componente
Notes para incorporar el filtro:
+La solución es elegante; La gestión del estado y la comunicación con el servidor están completamente separadas fuera de los componentes de React.
+
+Finalicemos la aplicación sincronizando el cambio de importancia con el servidor.
+
+
noteService.js se amplía de la siguiente manera:
```js
-const Notes = () => {
- const dispatch = useDispatch()
- // highlight-start
- const notes = useSelector(state => {
- if ( state.filter === 'ALL' ) {
- return state.notes
- }
- return state.filter === 'IMPORTANT'
- ? state.notes.filter(note => note.important)
- : state.notes.filter(note => !note.important)
+const update = async (id, note) => {
+ const response = await fetch(`${baseUrl}/${id}`, {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(note),
})
- // highlight-end
- return(
-
- {notes.map(note =>
-
- dispatch(toggleImportanceOf(note.id))
- }
- />
- )}
-
- )
+ if (!response.ok) {
+ throw new Error('Failed to update note')
+ }
+
+ return await response.json()
+}
+
+export default { getAll, createNew, update }
+```
+
+El cambio a la función
toggleImportance del store es el siguiente:
+
+```js
+const useNoteStore = create((set) => ({
+ notes: [],
+ filter: '',
+ actions: {
+ add: async (content) => {
+ const newNote = await noteService.createNew(content)
+ set(state => ({ notes: state.notes.concat(newNote) }))
+ },
+ // highlight-start
+ toggleImportance: async (id) => {
+ const note = useNoteStore.getState().notes.find(n => n.id === id)
+ const updated = await noteService.update(
+ id, { ...note, important: !note.important }
+ )
+ set(state => ({
+ notes: state.notes.map(n => n.id === id ? updated : n)
+ }))
+ },
+ // highlight-end
+ setFilter: value => set(() => ({ filter: value })),
+ initialize: async () => {
+ const notes = await noteService.getAll()
+ set(() => ({ notes }))
+ }
+ }
+}))
```
-Solo realizamos cambios en la función de selector, que solía ser
+Hay un detalle digno de mención en la nueva función. La función recibe el id de la nota como parámetro. Sin embargo, la nota modificada debe enviarse al backend. Se puede encontrar llamando a la función
getState del store:
```js
-useSelector(state => state.notes)
+const note = useNoteStore.getState().notes.find(n => n.id === id)
```
-Simplifiquemos el selector desestructurando los campos del estado que recibe como parámetro:
+Los stores de Zustand también tienen otras [funciones auxiliares](https://zustand.docs.pmnd.rs/reference/apis/create#returns), que pueden resultar útiles en algunas situaciones.
+
+Sin embargo, cambiemos también la definición del store para que también pasemos el parámetro
get a la función dada a
create , a través de la cual podemos acceder a los valores de estado cuando sea necesario:
```js
-const notes = useSelector(({ filter, notes }) => {
- if ( filter === 'ALL' ) {
- return notes
+const useNoteStore = create((set, get) => ({ // highlight-line
+ notes: [],
+ filter: '',
+ actions: {
+ toggleImportance: async (id) => {
+ const note = get().notes.find(n => n.id === id) // highlight-line
+ const updated = await noteService.update(
+ id, { ...note, important: !note.important }
+ )
+ set(state => ({
+ notes: state.notes.map(n => n.id === id ? updated : n)
+ }))
+ },
+ // ...
}
- return filter === 'IMPORTANT'
- ? notes.filter(note => note.important)
- : notes.filter(note => !note.important)
-})
+}))
```
-Hay un pequeño defecto cosmético en nuestra aplicación. Aunque el filtro está configurado en
ALL de forma predeterminada, el radio button asociado no está seleccionado. Naturalmente, este problema se puede solucionar, pero como se trata de un error desagradable pero, en última instancia, inofensivo, dejaremos la solución para más adelante.
+La función
get devuelve el estado actual del store. Por ejemplo, la llamada
get().notes proporciona las notas actuales del store. La función
get es funcionalmente equivalente a llamar a
useNoteStore.getState() , pero es la forma más idiomática de referirse al estado del store desde las propias funciones del store.
-La versión actual de la aplicación se puede encontrar en [GitHub](https://github.com/fullstack-hy2020/redux-notes/tree/part6-2), en la rama
part6-2 .
+El código de la aplicación está en [GitHub](https://github.com/fullstack-hy2020/zustand-notes/tree/part6-5) en la rama
part6-5 .
-### Ejercicio 6.9
+### Ejercicios 6.7.-6.11.
-#### 6.9 Mejores Anécdotas, paso 7
+#### 6.7 anécdotas, paso 6
-Implementa el filtrado para las anécdotas que se muestran al usuario.
+Obtén las anécdotas del backend de JSON Server cuando se inicie la aplicación. Utiliza la Fetch API para realizar la solicitud HTTP.
-
+Puedes encontrar contenido inicial para el backend [aquí](https://github.com/fullstack-hy2020/misc/blob/master/anecdotes.json).
-Almacena el estado del filtro en el store de Redux. Se recomienda crear un nuevo reducer, action creators y un reducer combinado para el store utilizando la función
combineReducers .
+#### 6.8 anécdotas, paso 7
-Crea un nuevo componente
Filter para mostrar los filtros. Puedes utilizar el siguiente código como punto de partida:
+Modifica la creación de nuevas anécdotas para que se almacenen en el backend. Utiliza la Fetch API en tu implementación.
+
+#### 6.9 anécdotas, paso 8
+
+La votación aún no guarda los cambios en el backend. Arreglar la situación.
+
+#### 6.10 anécdotas, paso 9
+
+La aplicación tiene un esqueleto listo para usar para el componente
Notification :
```js
-const Filter = () => {
- const handleChange = (event) => {
- // input-field value is in variable event.target.value
- }
+const Notification = () => {
const style = {
+ border: 'solid',
+ padding: 10,
+ borderWidth: 1,
marginBottom: 10
}
return (
- filter
+ render here notification...
)
}
-export default Filter
+export default Notification
```
+Amplíe la aplicación para que muestre una notificación utilizando el componente
Notification durante cinco segundos cuando se votan anécdotas o se crean nuevas anécdotas:
+
+
+
+Utiliza Zustand para gestionar el estado de las notificaciones. Puede ser conveniente crear un store de Zustand separado para ellas, ya que su uso podría extenderse a otras áreas a medida que la aplicación crezca, como el inicio de sesión.
+
+#### 6.11 anécdotas, paso 10
+
+Notamos que algunas de las anécdotas añadidas por los usuarios no son muy buenas. Implementar una característica que permita eliminar anécdotas que tengan cero votos.
+
-### Redux Toolkit y refactorizando la configuración del Store
+### Middlewares
-Como hemos visto hasta ahora, la implementación de la gestión del estado y la configuración de Redux requiere bastante esfuerzo. Esto se manifiesta, por ejemplo, en el código relacionado con el reducer y el action creator, que tiene un código un tanto repetitivo. [Redux Toolkit](https://redux-toolkit.js.org/) es una librería que resuelve estos problemas comunes relacionados con Redux. La librería, por ejemplo, simplifica enormemente la configuración del store de Redux y ofrece una gran variedad de herramientas para facilitar la gestión del estado.
+Al desarrollar una aplicación, a menudo nos encontramos con situaciones en las que es difícil entender por qué la aplicación se comporta de forma inesperada. El estado cambia como resultado de alguna llamada a una función de acción, pero no está claro qué llamada cambió qué y en qué orden. El registro tradicional de funciones individuales en la consola sólo ayuda de forma limitada.
-Comencemos a usar Redux Toolkit en nuestra aplicación refactorizando el código existente. Primero, necesitaremos instalar la librería:
+Zustand admite los llamados middlewares, que se pueden utilizar para agregar funcionalidad a los stores de forma transparente, sin tocar la propia lógica del store. La idea del middleware es simple: "envuelve" el store y puede, por ejemplo, registrar automáticamente cada cambio de estado.
-```bash
-npm install @reduxjs/toolkit
+La forma de las funciones del middleware es algo críptica. A continuación se muestra un
logger que siempre imprime el estado antiguo y nuevo del store cada vez que cambia el estado:
+
+```js
+const logger = (config) => (set, get) => config(
+ (...args) => {
+ console.log('prev state', get());
+ set(...args);
+ console.log('next state', get());
+ },
+ get
+);
```
-A continuación, abre el archivo
main.jsx que actualmente crea la store de Redux. En lugar de la función
createStore de Redux, creemos el Store usando la función [configureStore](https://redux-toolkit.js.org/api/configureStore) de Redux Toolkit:
+El middleware se activa "envolviendo" la función dada al
create de Zustand como parámetro:
```js
-import ReactDOM from 'react-dom/client'
-import { Provider } from 'react-redux'
-import { configureStore } from '@reduxjs/toolkit' // highlight-line
-import App from './App'
+const useNoteStore = create(logger((set, get) => ({ // highlight-line
+ notes: [],
+ filter: '',
+ actions: {
+ // ...
+ }
+}))) // highlight-line
+```
-import noteReducer from './reducers/noteReducer'
-import filterReducer from './reducers/filterReducer'
+Ahora, cada vez que cambia el estado del store, siempre podemos ver en la consola cómo cambia el estado:
- // highlight-start
-const store = configureStore({
- reducer: {
- notes: noteReducer,
- filter: filterReducer
- }
-})
-// highlight-end
+
-console.log(store.getState())
+En la práctica, nuestro middleware definido funciona reemplazando la función original
set con la función
-ReactDOM.createRoot(document.getElementById('root')).render(
-
-
-
-)
+```js
+ (...args) => {
+ console.log('prev state', get());
+ set(...args);
+ console.log('next state', get());
+ }
```
-Ya nos deshicimos de algunas líneas de código, ya no necesitamos la función
combineReducers para crear el reducer del store. Pronto veremos que la función
configureStore tiene muchos beneficios adicionales, como la integración sin esfuerzo de herramientas de desarrollo y muchas librerías de uso común sin necesidad de configuración adicional.
+que además de llamar a
set , también imprime el estado antiguo y nuevo (accesible a través de la función
get ) en la consola. El segundo parámetro es el antiguo
get sin cambios.
-Limpiemos aún más el archivo
main.jsx moviendo el código relacionado con la creación del store de Redux a su propio archivo. Creemos un nuevo archivo
src/store.js :
-```js
-import { configureStore } from '@reduxjs/toolkit'
+Zustand también tiene un middleware
devtools listo para usar que integra el store con la extensión [Redux DevTools](https://chromewebstore.google.com/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd) del navegador. Devtools es una herramienta de desarrollo extremadamente útil, ya que le permite realizar un seguimiento visual de los cambios de estado.
-import noteReducer from './reducers/noteReducer'
-import filterReducer from './reducers/filterReducer'
+La configuración es sencilla:
-const store = configureStore({
- reducer: {
- notes: noteReducer,
- filter: filterReducer
- }
-})
+```js
+import { create } from 'zustand'
+import { devtools } from 'zustand/middleware' // highlight-line
-export default store
+const useNoteStore = create(devtools((set, get) => ({ // highlight-line
+ notes: [],
+ filter: '',
+ actions: {
+ // ...
+ }
+}))) // highlight-line
```
-Después de los cambios, el contenido del archivo
main.jsx es el siguiente:
-
-```js
-import ReactDOM from 'react-dom/client'
-import { Provider } from 'react-redux'
+Cuando la extensión Redux DevTools está instalada en el navegador, el estado del store y sus cambios se pueden inspeccionar en las herramientas de desarrollo del navegador:
-import App from './App'
-import store from './store'
+
-ReactDOM.createRoot(document.getElementById('root')).render(
-
-
-
-)
-```
+### Pruebas de los stores de Zustand
-### Redux Toolkit y refactorizando los reducers
+Finalmente, veamos cómo probar los stores de Zustand con Vitest.
-Pasemos a refactorizar los reducers, lo que trae consigo los beneficios de Redux Toolkit. Con Redux Toolkit, podemos crear fácilmente reducers y action creators relacionados utilizando la función [createSlice](https://redux-toolkit.js.org/api/createSlice). Podemos usar la función
createSlice para refactorizar el reducer y los action creators en el archivo
reducers/noteReducer.js de la siguiente manera:
+Para simplificar, comencemos con el store del contador:
```js
-import { createSlice } from '@reduxjs/toolkit' // highlight-line
+import { create } from 'zustand'
-const initialState = [
- {
- content: 'reducer defines how redux store works',
- important: true,
- id: 1,
- },
- {
- content: 'state of store can contain any data',
- important: false,
- id: 2,
- },
-]
-
-const generateId = () =>
- Number((Math.random() * 1000000).toFixed(0))
+const useCounterStore = create(set => ({
+ counter: 0,
+ actions: {
+ increment: () => set(state => ({ counter: state.counter + 1 })),
+ decrement: () => set(state => ({ counter: state.counter - 1 })),
+ zero: () => set(() => ({ counter: 0 })),
+ }
+}))
-// highlight-start
-const noteSlice = createSlice({
- name: 'notes',
- initialState,
- reducers: {
- createNote(state, action) {
- const content = action.payload
-
- state.push({
- content,
- important: false,
- id: generateId(),
- })
- },
- toggleImportanceOf(state, action) {
- const id = action.payload
+export const useCounter = () => useCounterStore(state => state.counter)
+export const useCounterControls = () => useCounterStore(state => state.actions)
- const noteToChange = state.find(n => n.id === id)
+export default useCounterStore // highlight-line
+```
- const changedNote = {
- ...noteToChange,
- important: !noteToChange.important
- }
+Agregamos una exportación a la definición de las pruebas, a través de la cual la prueba puede acceder al store.
- return state.map(note =>
- note.id !== id ? note : changedNote
- )
- }
- },
-})
-// highlight-end
+Instalemos Vitest:
-// highlight-start
-export const { createNote, toggleImportanceOf } = noteSlice.actions
-export default noteSlice.reducer
-// highlight-end
+```
+npm install --save-dev vitest
```
-El parámetro
name de la función
createSlice define el prefijo que se utiliza en los valores de tipo de la acción. Por ejemplo, la acción
createNote definida más adelante tendrá el valor de tipo
notes/createNote . Es una buena práctica dar al parámetro un valor que sea único entre los reducers. De esta forma no habrá colisiones inesperadas entre los valores de tipo de acción de la aplicación.
-El parámetro
initialState define el estado inicial del reducer.
-El parámetro
reducers toma al propio reducer como un objeto, cuyas funciones manejan los cambios de estado causados por ciertas acciones. Ten en cuenta que
action.payload en la función contiene el argumento proporcionado al llamar al creador de la acción:
+Implementemos la prueba en el archivo
store.test.js :
```js
-dispatch(createNote('Redux Toolkit is awesome!'))
-```
+import { beforeEach, describe, expect, it } from 'vitest'
+import useCounterStore from './store'
-Esta llamada a dispatch equivale a enviar el siguiente objeto:
+beforeEach(() => {
+ useCounterStore.setState({ counter: 0 })
+})
-```js
-dispatch({ type: 'notes/createNote', payload: 'Redux Toolkit is awesome!' })
-```
+describe('counter store', () => {
+ it('initial state is 0', () => {
+ expect(useCounterStore.getState().counter).toBe(0)
+ })
-Si has prestado atención, es posible que hayas notado que dentro de la acción
createNote , parece suceder algo que viola el principio de inmutabilidad de los reducers mencionado anteriormente:
+ it('increment increases counter by 1', () => {
+ useCounterStore.getState().actions.increment()
+ expect(useCounterStore.getState().counter).toBe(1)
+ })
-```js
-createNote(state, action) {
- const content = action.payload
+ it('decrement decreases counter by 1', () => {
+ useCounterStore.getState().actions.decrement()
+ expect(useCounterStore.getState().counter).toBe(-1)
+ })
- state.push({
- content,
- important: false,
- id: generateId(),
+ it('zero resets counter to 0', () => {
+ useCounterStore.getState().actions.increment()
+ useCounterStore.getState().actions.increment()
+ useCounterStore.getState().actions.zero()
+ expect(useCounterStore.getState().counter).toBe(0)
})
-}
+})
```
-Estamos mutando el array del argumento
state al llamar al método
push en lugar de devolver una nueva instancia del array. ¿De qué se trata todo esto?
+Las pruebas son bastante sencillas y utilizan la función [getState](https://zustand.docs.pmnd.rs/reference/apis/create#returns) del store, que les permite leer el estado del store y ejecutar las funciones del store.
+
+Antes de cada prueba, el store se restablece a su estado inicial en el bloque
beforeEach usando la función [setState](https://zustand.docs.pmnd.rs/reference/apis/create#returns) del store.
-Redux Toolkit utiliza la librería [Immer](https://immerjs.github.io/immer/) con reducers creados por la función
createSlice , lo que hace posible mutar el argumento
state dentro del reducer. Immer usa el estado mutado para producir un nuevo estado inmutable y, por lo tanto, los cambios de estado permanecen inmutables. Ten en cuenta que
state se puede cambiar sin "mutarlo", como hemos hecho con la acción
toggleImportanceOf . En este caso, la función
devuelve el nuevo estado directamente. Sin embargo, mutar el estado a menudo será útil, especialmente cuando se necesita actualizar un estado complejo.
+En nuestro caso, restablecer el store a su estado inicial es sencillo, aunque no siempre lo es. La [documentación de Zustand](https://zustand.docs.pmnd.rs/learn/guides/testing#vitest) describe cómo crear una versión de los stores para las pruebas que se restablece automáticamente antes de cada una. Sin embargo, el método es lo bastante complejo e innecesario para nuestro caso como para omitirlo por ahora.
+
+Por tanto, las pruebas utilizan el store directamente. Si se ha implementado una lógica más compleja mediante hooks personalizados, puede ser necesario escribir pruebas que también los utilicen. En el contador se accede al store mediante los hooks
useCounter y
useCounterControls :
-La función
createSlice devuelve un objeto que contiene al reducer así como a los action creators definidos por el parámetro
reducers . Se puede acceder al reducer mediante la propiedad
noteSlice.reducer , mientras que a los action creators mediante la propiedad
noteSlice.actions . Podemos producir las exportaciones del archivo de la siguiente manera:
```js
-const noteSlice = createSlice(/* ... */)
+const useCounterStore = create(set => ({
+ // ...
+}))
-// highlight-start
-export const { createNote, toggleImportanceOf } = noteSlice.actions
+// hightlight-start
+export const useCounter = () => useCounterStore(state => state.counter)
+export const useCounterControls = () => useCounterStore(state => state.actions)
+// hightlight-end
+```
-export default noteSlice.reducer
-// highlight-end
+En este caso, los hooks no contienen ninguna lógica, simplemente exponen por separado el valor almacenado en el store y las funciones del store. Por lo tanto, el método de prueba que utilizamos anteriormente es perfectamente correcto.
+
+Sin embargo, hagamos otra versión de las pruebas a modo de ejemplo, donde el store se usa exactamente de la misma manera que la aplicación.
+
+
useCounter y
useCounterControls son hooks de React, por lo que probarlos requiere la [Librería de prueba de React](https://github.com/testing-library/react-testing-library) y la librería [jsdom](https://github.com/jsdom/jsdom):
+
+```
+npm install --save-dev @testing-library/react jsdom
```
-Las importaciones en otros archivos funcionarán igual que antes:
+Agreguemos la configuración del entorno de prueba a
vite.config.js :
```js
-import noteReducer, { createNote, toggleImportanceOf } from './reducers/noteReducer'
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+
+export default defineConfig({
+ plugins: [react()],
+ // highlight-start
+ test: {
+ environment: 'jsdom',
+ },
+ // highlight-end
+})
```
-Necesitamos modificar los nombres de los tipos de las acciones en las pruebas debido a las convenciones de ReduxToolkit:
+Las pruebas son las siguientes:
-```js
-import deepFreeze from 'deep-freeze'
-import { describe, expect, test } from 'vitest'
-import noteReducer from './noteReducer'
+```js
+import { beforeEach, describe, expect, it } from 'vitest'
+import { renderHook, act } from '@testing-library/react'
+import useCounterStore, { useCounter, useCounterControls } from './store'
-describe('noteReducer', () => {
- test('returns new state with action notes/createNote', () => { // highlight-line
- const state = []
- const action = {
- type: 'notes/createNote', // highlight-line
- payload: 'the app state is in redux store' // highlight-line
- }
+beforeEach(() => {
+ useCounterStore.setState({ counter: 0 })
+})
- deepFreeze(state)
- const newState = noteReducer(state, action)
+describe('counter hooks', () => {
+ it('useCounter returns initial value of 0', () => {
+ const { result } = renderHook(() => useCounter())
+ expect(result.current).toBe(0)
+ })
- expect(newState).toHaveLength(1)
- expect(newState.map(note => note.content)).toContainEqual(action.payload) // highlight-line
+ it('increment updates counter', () => {
+ const { result: counter } = renderHook(() => useCounter())
+ const { result: controls } = renderHook(() => useCounterControls())
+
+ act(() => controls.current.increment())
+
+ expect(counter.current).toBe(1)
})
-})
-test('returns new state with action notes/toggleImportanceOf', () => { // highlight-line
- const state = [
- {
- content: 'the app state is in redux store',
- important: true,
- id: 1
- },
- {
- content: 'state changes are made with actions',
- important: false,
- id: 2
- }
- ]
+ it('decrement updates counter', () => {
+ const { result: counter } = renderHook(() => useCounter())
+ const { result: controls } = renderHook(() => useCounterControls())
- const action = {
- type: 'notes/toggleImportanceOf', // highlight-line
- payload: 2 // highlight-line
- }
+ act(() => controls.current.decrement())
- deepFreeze(state)
- const newState = noteReducer(state, action)
+ expect(counter.current).toBe(-1)
+ })
- expect(newState).toHaveLength(2)
+ it('zero resets counter', () => {
+ const { result: counter } = renderHook(() => useCounter())
+ const { result: controls } = renderHook(() => useCounterControls())
- expect(newState).toContainEqual(state[0])
+ act(() => {
+ controls.current.increment()
+ controls.current.increment()
+ controls.current.zero()
+ })
- expect(newState).toContainEqual({
- content: 'state changes are made with actions',
- important: true,
- id: 2
+ expect(counter.current).toBe(0)
})
})
```
-Puedes encontrar el código de nuestra aplicación actual en su totalidad en la rama
part6-3 de [este repositorio de GitHub](https://github.com/fullstack-hy2020/redux-notes/tree/part6-3).
+Hay algunas cosas interesantes en la prueba. Al comienzo de las pruebas, los hooks se representan usando la función [renderHook](https://testing-library.com/docs/react-testing-library/api/#renderhook):
-### Redux Toolkit y console.log
+```js
+const { result: counter } = renderHook(() => useCounter())
+const { result: controls } = renderHook(() => useCounterControls())
+```
-Como hemos aprendido, console.log es una herramienta extremadamente poderosa, por lo general siempre nos salva de problemas.
+De esta forma la prueba obtiene acceso a los valores devueltos por los hooks, que se almacenan en las variables
counter y
controls .
-Intentemos imprimir el estado del store de Redux en la consola en medio del reducer creado con la función createSlice:
+Los hooks se llaman envolviendo la llamada dentro de la función [act](https://testing-library.com/docs/react-testing-library/api/#act):
```js
-const noteSlice = createSlice({
- name: 'notes',
- initialState,
- reducers: {
- // ...
- toggleImportanceOf(state, action) {
- const id = action.payload
+act(() => {
+ controls.current.increment()
+ controls.current.increment()
+ controls.current.zero()
+})
+```
+
+Finalmente, se produce la expectativa de la prueba:
+
+```js
+expect(counter.current).toBe(0)
+```
+
+Como podemos ver, para acceder al hook en sí aún necesitamos tomar el campo
current del objeto devuelto por
renderHook , que corresponde al valor actual del hook.
+
+> #### ¿Qué es acto?
+>
+>
act es una función auxiliar que garantiza que todas las actualizaciones de estado y sus efectos secundarios se hayan procesado antes de que continúe el código de prueba.
+>
+> Cuando se produce un cambio de estado en un componente o enlace de React, React no actualiza el estado inmediatamente sino que pone las actualizaciones en cola. act obliga a ejecutar estas actualizaciones en cola.
+>
+> Sin actuar, una prueba podría verificar el estado antes de que React haya tenido tiempo de actualizarlo, lo que provocaría que la prueba falle o dé resultados incorrectos.
+>
+> La librería de pruebas de React incluye muchas de sus funciones (como fireEvent, userEvent) automáticamente, pero cuando se prueban enlaces directamente, generalmente es necesario.
+
+Las pruebas mediante hooks utilizan la librería de pruebas de React y representan los hooks en un contexto real de React usando jsdom. Este enfoque es considerablemente más lento que las pruebas que utilizan el store directamente, por lo que si los enlaces no contienen lógica compleja, puede ser suficiente ejecutar las pruebas utilizando el store directamente.
- const noteToChange = state.find(n => n.id === id)
+El código que contiene las pruebas de contador de Zustand está disponible en [GitHub](https://github.com/fullstack-hy2020/zustand-counter).
- const changedNote = {
- ...noteToChange,
- important: !noteToChange.important
- }
+### Pruebas del store de notas
- console.log(state) // highlight-line
+Probar el store de la aplicación de notas es un caso algo más desafiante, ya que el store contiene funciones asincrónicas que llaman al servidor:
- return state.map(note =>
- note.id !== id ? note : changedNote
- )
+```js
+import { create } from 'zustand'
+import noteService from './services/notes'
+
+const useNoteStore = create(set => ({
+ notes: [],
+ filter: '',
+ actions: {
+ add: async (content) => {
+ const newNote = await noteService.createNew(content) // highlight-line
+ set(state => ({ notes: state.notes.concat(newNote) }))
+ },
+ toggleImportance: async (id) => {
+ const note = useNoteStore.getState().notes.find(n => n.id === id)
+ // highlight-start
+ const updated = await noteService.update(
+ id, { ...note, important: !note.important }
+ )
+ // highlight-end
+ set(state => ({
+ notes: state.notes.map(n => n.id === id ? updated : n)
+ }))
+ },
+ setFilter: value => set(() => ({ filter: value })),
+ initialize: async () => {
+ const notes = await noteService.getAll() // highlight-line
+ set(() => ({ notes }))
}
- },
-})
-```
+ }
+}))
-Lo siguiente se imprime en la consola
+export const useNotes = () => {
+ const notes = useNoteStore((state) => state.notes)
+ const filter = useNoteStore((state) => state.filter)
-
+ if (filter === 'important') return notes.filter(n => n.important)
+ if (filter === 'nonimportant') return notes.filter(n => !n.important)
+ return notes
+}
-Lo que vemos es interesante pero no muy útil. Esto tiene que ver con la librería Immer que mencionamos anteriormente y es utilizada por Redux Toolkit internamente para guardar el estado de la Tienda.
+export const useFilter = () => useNoteStore((state) => state.filter)
+export const useNoteActions = () => useNoteStore((state) => state.actions)
+```
-El estado se puede convertir a un formato legible por humanos utilizando la función [current](https://redux-toolkit.js.org/api/other-exports#current) de la librería immer.
+Esta vez
useNotes también contiene una cantidad significativa de lógica, por lo que las pruebas probablemente deberían realizarse mediante enlaces con la librería de pruebas React.
-Actualicemos las importaciones para incluir a la función "current" de la librería immer:
+Instalemos las librerías necesarias:
-```js
-import { createSlice, current } from '@reduxjs/toolkit' // highlight-line
+```
+npm install --save-dev vitest @testing-library/react jsdom
```
-Luego actualicemos el llamado a la función console.log:
+Agreguemos la configuración del entorno de prueba a
vite.config.js :
```js
-console.log(current(state)) // highlight-line
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+
+export default defineConfig({
+ plugins: [react()],
+ // highlight-start
+ test: {
+ environment: 'jsdom',
+ },
+ // highlight-end
+})
```
-Ahora lo que imprime la consola es legible para humanos
+La primera parte de las pruebas es la siguiente:
-
+```js
+import { describe, it, expect, beforeEach, vi } from 'vitest'
+import { renderHook, act } from '@testing-library/react'
+
+vi.mock('./services/notes', () => ({
+ default: {
+ getAll: vi.fn(),
+ createNew: vi.fn(),
+ update: vi.fn(),
+ }
+}))
-### Redux DevTools
+import noteService from './services/notes'
+import useNoteStore, { useNotes, useFilter, useNoteActions } from './store'
-[Redux DevTools](https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd) es una extension de Chrome, que ofrece útiles herramientas de desarrollo para Redux. Se puede usar, por ejemplo, para inspeccionar el estado del store de Redux y enviar acciones (dispatch) a través de la consola del navegador. Cuando el store se crea usando la función
configureStore de Redux Toolkit, no se necesita ninguna configuración adicional para que Redux DevTools funcione.
+beforeEach(() => {
+ useNoteStore.setState({ notes: [], filter: '' })
+ vi.clearAllMocks()
+})
-Una vez instalada la extension, al hacer clic en la pestaña de
Redux en las herramientas de desarrollo del navegador, Redux DevTools debería abrirse:
+describe('useNoteActions', () => {
+ it('initialize loads notes from service', async () => {
+ const mockNotes = [{ id: 1, content: 'Test', important: false }]
+ noteService.getAll.mockResolvedValue(mockNotes)
-
+ const { result } = renderHook(() => useNoteActions())
-Puedes inspeccionar cómo el envío de una determinada acción cambia el estado haciendo clic en la acción:
+ await act(async () => {
+ await result.current.initialize()
+ })
-
+ const { result: notesResult } = renderHook(() => useNotes())
+ expect(notesResult.current).toEqual(mockNotes)
+ })
-También es posible enviar acciones (dispatch) a la store utilizando las herramientas de desarrollo:
+ it('add appends a new note', async () => {
+ const newNote = { id: 2, content: 'New note', important: false }
+ noteService.createNew.mockResolvedValue(newNote)
-
+ const { result } = renderHook(() => useNoteActions())
-El código actual de la aplicación se puede encontrar en [GitHub](https://github.com/fullstack-hy2020/redux-notes/tree/part6-3), en la rama
part6-3 .
+ await act(async () => {
+ await result.current.add('New note')
+ })
-
+ const { result: notesResult } = renderHook(() => useNotes())
+ expect(notesResult.current).toContainEqual(newNote)
+ })
-
+ it('toggleImportance flips important flag', async () => {
+ const note = { id: 1, content: 'Test', important: false }
+ useNoteStore.setState({ notes: [note] })
+ noteService.update.mockResolvedValue({ ...note, important: true })
-### Ejercicios 6.10.-6.13.
+ const { result } = renderHook(() => useNoteActions())
-Continuemos trabajando en la aplicación de anécdotas que comenzamos en el ejercicio 6.3, usando Redux Toolkit.
+ await act(async () => {
+ await result.current.toggleImportance(1)
+ })
-#### 6.10 Mejores Anécdotas, paso 8
+ const { result: notesResult } = renderHook(() => useNotes())
+ expect(notesResult.current[0].important).toBe(true)
+ })
+})
+```
-Instala Redux Toolkit en el proyecto. Mueve la creación del store de Redux a su propio archivo
store.js y utiliza la función
configureStore para crear el store.
+Hay mucho que digerir en las pruebas. Las pruebas crean, usando Vitest, una versión [simulada](https://vitest.dev/guide/mocking) del
noteService responsable de comunicarse con el servidor:
+
+```js
+import { describe, it, expect, beforeEach, vi } from 'vitest'
-Cambia la definición del
filter reducer y sus action creators para usar la función
createSlice de Redux Toolkit.
+vi.mock('./services/notes', () => ({
+ default: {
+ getAll: vi.fn(),
+ createNew: vi.fn(),
+ update: vi.fn(),
+ }
+}))
+```
-También, comienza a utilizar Redux DevTools para depurar el estado de la aplicación fácilmente.
+[vi.mock](https://vitest.dev/api/vi.html#vi-mock) reemplaza el
noteService en el módulo
./services/notes con su propia versión, donde todas las funciones se reemplazan con funciones simuladas devueltas por [vi.fn](https://vitest.dev/api/vi.html#vi-fn).
-#### 6.11 Mejores Anécdotas, paso 9
+Antes de cada prueba, el store se restablece a su estado inicial y se borran las funciones simuladas:
-Cambia también la definición de
anecdote reducer y sus action creators para usar la función
createSlice de Redux Toolkit.
+```js
+beforeEach(() => {
+ useNoteStore.setState({ notes: [], filter: '' })
+ vi.clearAllMocks()
+})
+```
-Nota de implementación: cuando utilices Redux Toolkit para devolver el estado inicial de las anécdotas, será inmutable, por lo que tendrás que copiarlo para ordenarlas, o te encontraras con el error "TypeError: Cannot assign to read only property". Puedes usar la sintaxis spread para hacer una copia del array. En vez de:
+Al comienzo de cada prueba, al
noteService simulado se le indica a través de la función [mockResolvedValue](https://vitest.dev/api/mock.html#mockresolvedvalue) cómo debe comportarse en el contexto de la prueba:
```js
+it('initialize loads notes from service', async () => {
+ // highlight-start
+ const mockNotes = [{ id: 1, content: 'Test', important: false }]
+ noteService.getAll.mockResolvedValue(mockNotes)
+ // highlight-end
+
+ const { result } = renderHook(() => useNoteActions())
-anecdotes.sort()
+ await act(async () => {
+ await result.current.initialize()
+ })
+ const { result: notesResult } = renderHook(() => useNotes())
+ expect(notesResult.current).toEqual(mockNotes)
+})
```
-Escribe:
+Primero, la prueba establece que, cuando se llama a
noteService.getAll , se devuelven al store las notas del array
mockNotes .
+
+Lo que se está probando es la llamada a la función
initialize :
```js
+await act(async () => {
+ await result.current.initialize()
+})
+```
+
+Dado que se trata de una función asincrónica, se debe esperar la finalización de la llamada con la palabra clave
await .
-[...anecdotes].sort()
+Finalmente, la prueba verifica que el estado del store contiene la misma lista de notas que devolvió la función simulada
noteService.getAll :
+```js
+const { result: notesResult } = renderHook(() => useNotes())
+expect(notesResult.current).toEqual(mockNotes)
```
-#### 6.12 Mejores Anécdotas, paso 10
+Las otras pruebas siguen el mismo patrón: primero, se define lo que devuelve la función llamada
noteService del store y luego se ejecuta la prueba real.
-La aplicación tiene el esqueleto del componente
Notification listo para utilizarlo:
+La segunda parte de las pruebas verifica que el filtrado funciona correctamente:
```js
-const Notification = () => {
- const style = {
- border: 'solid',
- padding: 10,
- borderWidth: 1,
- marginBottom: 10
- }
+describe('useNotes filtering', () => {
+ const notes = [
+ { id: 1, content: 'A', important: true },
+ { id: 2, content: 'B', important: false },
+ ]
- return (
-
- render here notification...
-
- )
-}
+ beforeEach(() => {
+ useNoteStore.setState({ notes })
+ })
-export default Notification
+ it('returns all notes with no filter', () => {
+ const { result } = renderHook(() => useNotes())
+ expect(result.current).toHaveLength(2)
+ })
+
+ it('filters important notes', () => {
+ useNoteStore.setState({ notes, filter: 'important' })
+ const { result } = renderHook(() => useNotes())
+ expect(result.current).toEqual([notes[0]])
+ })
+
+ it('filters nonimportant notes', () => {
+ useNoteStore.setState({ notes, filter: 'nonimportant' })
+ const { result } = renderHook(() => useNotes())
+ expect(result.current).toEqual([notes[1]])
+ })
+})
```
-Extiende el componente para que muestre el mensaje almacenado en el store de redux, haciendo que el componente tome la siguiente forma:
+El estado se inicializa con dos notas, una de las cuales es importante y la otra no. Los tres casos de prueba verifican que
useNotes devuelva las notas correctas para todos los valores de filtro.
-```js
-import { useSelector } from 'react-redux' // highlight-line
+El código final de la aplicación está en [GitHub](https://github.com/fullstack-hy2020/zustand-notes/tree/part6-6) en la rama
part6-6 .
-const Notification = () => {
- const notification = useSelector(/* something here */) // highlight-line
- const style = {
- border: 'solid',
- padding: 10,
- borderWidth: 1
- }
- return (
-
- {notification} // highlight-line
-
- )
-}
-```
+
+
+
+
+### Ejercicios 6.12.-6.15.
+
+#### 6.12 Anécdotas, paso 11
+
+Escribe una prueba que compruebe que el estado se inicializa con las anécdotas devueltas por el backend.
-Tendrás que realizar cambios en el reducer existente de la aplicación. Crea un reducer separado para la nueva funcionalidad usando la función createSlice de Redux Toolkit.
+#### 6.13 Anécdotas, paso 12
-La aplicación no tiene que utilizar el componente Notification completamente en este punto de los ejercicios. Es suficiente con que la aplicación muestre el valor inicial establecido para el mensaje en el notificationReducer .
+Escribe una prueba que compruebe que el componente que muestra las anécdotas las recibe del store ordenadas por votos.
-#### 6.13 Mejores Anécdotas, paso 11
+#### 6.14 Anécdotas, paso 13
-Extiende la aplicación para que utilice el componente Notification para mostrar un mensaje durante cinco segundos cuando el usuario vote por una anécdota o cree una nueva anécdota:
+Escribe una prueba que compruebe que el componente de React adecuado recibe una lista de anécdotas correctamente filtrada.
-
+#### 6.15 Anécdotas, paso 14
-Se recomienda crear [action creators](https://redux-toolkit.js.org/api/createSlice#reducers) independientes para configurar y eliminar notificaciones.
+Escribe una prueba que verifique que votar aumenta el número de votos para una anécdota.
diff --git a/src/content/6/es/part6c.md b/src/content/6/es/part6c.md
index d10064d453d..d9e49e85e19 100644
--- a/src/content/6/es/part6c.md
+++ b/src/content/6/es/part6c.md
@@ -7,630 +7,1023 @@ lang: es
-Expandamos la aplicación, de modo que las notas se almacenen en el backend. Usaremos [json-server](/es/part2/obteniendo_datos_del_servidor), de la parte 2.
-
-El estado inicial de la base de datos se almacena en el archivo
db.json , que se coloca en la raíz del proyecto:
-
-```json
-{
- "notes": [
- {
- "content": "the app state is in redux store",
- "important": true,
- "id": 1
- },
- {
- "content": "state changes are made with actions",
- "important": false,
- "id": 2
- }
- ]
-}
-```
+Al final de esta parte, analizaremos algunas formas diferentes de administrar el estado de una aplicación.
-Instalaremos json-server en nuestro proyecto...
+Continuemos con la aplicación de notas. Nos centraremos en la comunicación con el servidor. Comencemos la aplicación desde cero. La primera versión es la siguiente:
```js
-npm install json-server --save-dev
-```
+const App = () => {
+ const addNote = async (event) => {
+ event.preventDefault()
+ const content = event.target.note.value
+ event.target.reset()
+ console.log(content)
+ }
-y agregaremos la siguiente línea a la parte de
scripts del archivo
package.json
+ const toggleImportance = (note) => {
+ console.log('toggle importance of', note.id)
+ }
-```js
-"scripts": {
- "server": "json-server -p 3001 db.json",
- // ...
+ const notes = []
+
+ return (
+
+
Notes app
+
+ {notes.map((note) => (
+ toggleImportance(note)}>
+ {note.important ? {note.content} : note.content}
+ toggleImportance(note.id)}>
+ {note.important ? 'make not important' : 'make important'}
+
+
+ ))}
+
+ )
}
-```
-Ahora iniciemos json-server con el comando _npm run server_.
+export default App
+```
-### Fetch API
+El código inicial está en GitHub en este [repositorio](https://github.com/fullstack-hy2020/query-notes/tree/part6-0), en la rama
part6-0 .
-En el desarrollo de software, a menudo es necesario considerar si una cierta funcionalidad debe implementarse usando una librería externa o si es mejor utilizar las soluciones nativas proporcionadas por el entorno. Ambos enfoques tienen sus propias ventajas y desafíos.
+### Gestión de datos del servidor con la librería TanStack Query
-En las partes anteriores de este curso, usamos la librería [Axios](https://axios-http.com/docs/intro) para hacer peticiones HTTP. Ahora, exploremos una forma alternativa de hacer peticiones HTTP usando la [Fetch API](https://developer.mozilla.org/es/docs/Web/API/Fetch_API) nativa.
+Ahora utilizaremos la librería [TanStack Query](https://tanstack.com/query/latest) para almacenar y gestionar los datos obtenidos del servidor.
-Es típico que una librería externa como
Axios se implemente usando otras librerías externas. Por ejemplo, si instalas Axios en tu proyecto con el comando _npm install axios_, la salida de la consola será:
+Instala la librería con el comando
```bash
-$ npm install axios
+npm install @tanstack/react-query
+```
-added 23 packages, and audited 302 packages in 1s
+Se necesitan agregar algunas cosas en el archivo
main.jsx para pasar las funciones de la librería a toda la aplicación:
-71 packages are looking for funding
- run `npm fund` for details
+```js
+import { createRoot } from 'react-dom/client'
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query' // highlight-line
-found 0 vulnerabilities
-```
+import App from './App.jsx'
-Por lo tanto, además de la librería Axios, el comando instalaría más de 20 paquetes npm adicionales que Axios necesita para funcionar.
+const queryClient = new QueryClient() // highlight-line
-La
Fetch API proporciona una forma similar de hacer peticiones HTTP como Axios, pero usar la Fetch API no requiere instalar ninguna librería externa. El mantenimiento de la aplicación se vuelve más fácil cuando hay menos librerías que actualizar, y la seguridad también mejora porque la superficie de ataque potencial de la aplicación se reduce. La seguridad y el mantenimiento de las aplicaciones se discute más a fondo en la [parte 7](https://fullstackopen.com/es/part7/class_components_miscellaneous#react-node-application-security) del curso.
+createRoot(document.getElementById('root')).render(
+
// highlight-line
+
+ // highlight-line
+)
+```
-En la práctica, las peticiones se realizan usando la función _fetch()_. La sintaxis utilizada difiere algo de Axios. También notaremos pronto que Axios se ha encargado de algunas cosas por nosotros y nos ha facilitado la vida. Sin embargo, ahora usaremos la Fetch API, ya que es una solución nativa ampliamente utilizada que todo desarrollador Full Stack debería conocer.
+Usemos [JSON Server](https://github.com/typicode/json-server) como en las partes anteriores para simular el backend. JSON Server está preconfigurado en el proyecto de ejemplo, y la raíz del proyecto contiene un archivo
db.json que por defecto tiene dos notas. Puedes iniciar el servidor con:
-### Obteniendo datos del backend
+```js
+npm run server
+```
-Creemos un método para obtener datos del backend en el archivo
src/services/notes.js :
+Ahora podemos recuperar las notas en el componente
App . El código se expande de la siguiente manera:
```js
-const baseUrl = 'http://localhost:3001/notes'
+import { useQuery } from '@tanstack/react-query' // highlight-line
-const getAll = async () => {
- const response = await fetch(baseUrl)
+const App = () => {
+ const addNote = async (event) => {
+ event.preventDefault()
+ const content = event.target.note.value
+ event.target.reset()
+ console.log(content)
+ }
- if (!response.ok) {
- throw new Error('Failed to fetch notes')
+ const toggleImportance = (note) => {
+ console.log('toggle importance of', note.id)
}
- const data = await response.json()
- return data
+ // highlight-start
+ const result = useQuery({
+ queryKey: ['notes'],
+ queryFn: async () => {
+ const response = await fetch('http://localhost:3001/notes')
+ if (!response.ok) {
+ throw new Error('Failed to fetch notes')
+ }
+ return await response.json()
+ }
+ })
+
+ console.log(JSON.parse(JSON.stringify(result)))
+
+ if (result.isPending) {
+ return
loading data...
+ }
+
+ const notes = result.data
+ // highlight-end
+
+ return (
+ // ...
+ )
}
+```
+
+La obtención de datos del servidor se realiza, como en el capítulo anterior, usando el método
fetch de la Fetch API. Sin embargo, la llamada al método ahora está envuelta en una [query](https://tanstack.com/query/latest/docs/react/guides/queries) (consulta) formada con la función [useQuery](https://tanstack.com/query/latest/docs/react/reference/useQuery). La llamada a
useQuery toma como parámetro un objeto con los campos
queryKey y
queryFn . El valor del campo
queryKey es un array que contiene el string
notes . Actúa como la [clave](https://tanstack.com/query/latest/docs/react/guides/query-keys) para la query definida, es decir, la lista de notas.
-export default { getAll }
+El valor devuelto por la función
useQuery es un objeto que indica el estado de la query. La salida a la consola ilustra la situación:
+
+
+
+Es decir, la primera vez que se renderiza el componente, la query todavía está en estado
loading , es decir, la solicitud HTTP asociada está pendiente. En esta etapa, solo se procesa lo siguiente:
+
+```html
+
loading data...
```
-Examinemos más de cerca la implementación del método _getAll_. Las notas ahora se obtienen del backend llamando a la función _fetch()_, a la cual se le da la URL del backend como argumento. El tipo de petición no se define explícitamente, por lo que _fetch_ realiza su acción predeterminada, que es una petición GET.
+Sin embargo, la solicitud HTTP se completa tan rápido que ni siquiera Max Verstappen podría ver el texto. Cuando se completa la solicitud, el componente se renderiza de nuevo. La query está en el estado
success en la segunda renderización, y el campo
data del objeto de la query contiene los datos devueltos por la solicitud, es decir, la lista de notas que se muestran en la pantalla.
-Una vez que la respuesta ha llegado, se verifica el éxito de la petición usando la propiedad _response.ok_, y se lanza un error si es necesario:
+Entonces, la aplicación recupera datos del servidor y los renderiza en la pantalla sin usar los Hooks de React
useState y
useEffect utilizados en los capítulos 2-5. Los datos en el servidor ahora están completamente bajo la administración de la librería TanStack Query, ¡y la aplicación no necesita el estado definido con el Hook de React
useState en absoluto!
+
+Movamos la función que realiza la solicitud HTTP a su propio archivo
src/requests.js
```js
-if (!response.ok) {
- throw new Error('Failed to fetch notes')
+const baseUrl = 'http://localhost:3001/notes'
+
+export const getNotes = async () => {
+ const response = await fetch(baseUrl)
+ if (!response.ok) {
+ throw new Error('Failed to fetch notes')
+ }
+ return await response.json()
}
```
-El atributo _response.ok_ se establece en _true_ si la petición fue exitosa, es decir, el código de estado de la respuesta está entre 200 y 299. Para todos los demás códigos de estado, como 404 o 500, se establece en _false_.
+El componente
App ahora se ha simplificado un poco:
+
+```js
+import { useQuery } from '@tanstack/react-query'
+import { getNotes } from './requests' // highlight-line
-Ten en cuenta que _fetch_ no lanza automáticamente un error incluso si el código de estado de la respuesta es, por ejemplo, 404. El manejo de errores debe implementarse manualmente, como lo hemos hecho aquí.
+const App = () => {
+ // ...
-Si la petición es exitosa, los datos contenidos en la respuesta se convierten a formato JSON:
+ const result = useQuery({
+ queryKey: ['notes'],
+ queryFn: getNotes // highlight-line
+ })
-```js
-const data = await response.json()
+ // ...
+}
```
-_fetch_ no convierte automáticamente ningún dato incluido en la respuesta a formato JSON; la conversión debe hacerse manualmente. También es importante notar que _response.json()_ es un método asíncrono, por lo que se requiere la palabra clave
await .
+El código actual de la aplicación está en [GitHub](https://github.com/fullstack-hy2020/query-notes/tree/part6-1) en la rama
part6-1 .
+
+### Sincronización de datos con el servidor mediante TanStack Query
-Simplifiquemos aún más el código devolviendo directamente los datos devueltos por el método _response.json()_:
+Los datos ya se han recuperado correctamente del servidor. A continuación, nos aseguraremos de que los datos agregados y modificados se almacenen en el servidor. Comencemos agregando nuevas notas.
+
+Hagamos una función
createNote en el archivo
requests.js para guardar nuevas notas:
```js
-const getAll = async () => {
- const response = await fetch(baseUrl)
+const baseUrl = 'http://localhost:3001/notes'
+export const getNotes = async () => {
+ const response = await fetch(baseUrl)
if (!response.ok) {
throw new Error('Failed to fetch notes')
}
+ return await response.json()
+}
- return await response.json() // highlight-line
+// highlight-start
+export const createNote = async (newNote) => {
+ const options = {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(newNote)
+ }
+
+ const response = await fetch(baseUrl, options)
+
+ if (!response.ok) {
+ throw new Error('Failed to create note')
+ }
+
+ return await response.json()
}
+// highlight-end
```
-### Inicializando el store con datos obtenidos del servidor
+El componente
App cambiará de la siguiente manera
-Modifiquemos ahora nuestra aplicación para que el estado de la aplicación se inicialice con las notas obtenidas del servidor.
+```js
+import { useQuery, useMutation } from '@tanstack/react-query' // highlight-line
+import { getNotes, createNote } from './requests' // highlight-line
-En el archivo
noteReducer.js , cambiemos la inicialización del estado de las notas para que por defecto no haya notas:
+const App = () => {
+ //highlight-start
+ const newNoteMutation = useMutation({
+ mutationFn: createNote,
+ })
+ // highlight-end
-```js
-const noteSlice = createSlice({
- name: 'notes',
- initialState: [], // highlight-line
- // ...
-})
+ const addNote = async (event) => {
+ event.preventDefault()
+ const content = event.target.note.value
+ event.target.reset()
+ newNoteMutation.mutate({ content, important: true }) // highlight-line
+ }
+
+ //
+
+}
```
-Agreguemos un action creator llamado
setNotes , que nos permita reemplazar directamente el array de notas. Podemos crear el action creator deseado usando la función
createSlice de la siguiente manera:
+Para crear una nueva nota, se define una [mutación](https://tanstack.com/query/latest/docs/react/guides/mutations) usando la función [useMutation](https://tanstack.com/query/latest/docs/react/reference/useMutation):
```js
-// ...
-
-const noteSlice = createSlice({
- name: 'notes',
- initialState: [],
- reducers: {
- createNote(state, action) {
- const content = action.payload
- state.push({
- content,
- important: false,
- id: generateId()
- })
- },
- toggleImportanceOf(state, action) {
- const id = action.payload
- const noteToChange = state.find(n => n.id === id)
- const changedNote = {
- ...noteToChange,
- important: !noteToChange.important
- }
- return state.map(note => (note.id !== id ? note : changedNote))
- },
- // highlight-start
- setNotes(state, action) {
- return action.payload
- }
- // highlight-end
- }
+const newNoteMutation = useMutation({
+ mutationFn: createNote,
})
-
-export const { createNote, toggleImportanceOf, setNotes } = noteSlice.actions // highlight-line
-export default noteSlice.reducer
```
-Implementemos la inicialización de las notas en el componente
App . Como es habitual al obtener datos de un servidor, usaremos el hook
useEffect :
+El parámetro es la función que agregamos al archivo
requests.js , que usa la Fetch API para enviar una nueva nota al servidor.
+El controlador de eventos
addNote realiza la mutación llamando a la función
mutate del objeto de mutación y pasando la nueva nota como parámetro:
```js
-import { useEffect } from 'react' // highlight-line
-import { useDispatch } from 'react-redux' // highlight-line
+newNoteMutation.mutate({ content, important: true })
+```
+
+Nuestra solución es buena. Excepto que no funciona. La nueva nota se guarda en el servidor, pero no se actualiza en la pantalla.
+
+Para renderizar una nueva nota también, debemos decirle a TanStack Query que el resultado antiguo de la query cuya clave es el string
notes debe ser [invalidado](https://tanstack.com/query/latest/docs/react/guides/invalidations-from-mutations).
-import NoteForm from './components/NoteForm'
-import Notes from './components/Notes'
-import VisibilityFilter from './components/VisibilityFilter'
-import { setNotes } from './reducers/noteReducer' // highlight-line
-import noteService from './services/notes' // highlight-line
+Afortunadamente, la invalidación es fácil, se puede hacer definiendo la función de callback
onSuccess apropiada para la mutación:
+
+```js
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' // highlight-line
+import { getNotes, createNote } from './requests'
const App = () => {
- const dispatch = useDispatch() // highlight-line
+ const queryClient = useQueryClient() // highlight-line
- // highlight-start
- useEffect(() => {
- noteService.getAll().then(notes => dispatch(setNotes(notes)))
- }, [dispatch])
- // highlight-end
+ const newNoteMutation = useMutation({
+ mutationFn: createNote,
+ onSuccess: () => { // highlight-line
+ queryClient.invalidateQueries({ queryKey: ['notes'] }) // highlight-line
+ }, // highlight-line
+ })
- return (
-
-
-
-
-
- )
+ // ...
}
-
-export default App
```
-### Enviando datos al backend
+Ahora que la mutación se ha ejecutado con éxito, se realiza una llamada a la función
-A continuación, implementemos la funcionalidad para enviar una nueva nota al servidor. Esto también nos dará una oportunidad de practicar cómo hacer una petición POST usando el método _fetch()_.
+```js
+queryClient.invalidateQueries({ queryKey: ['notes'] })
+```
-Expandamos el código en
src/services/notes.js que maneja la comunicación con el servidor de la siguiente manera:
+Esto a su vez hace que TanStack Query actualice automáticamente una query con la clave
notes , es decir, obtenga las notas del servidor. Como resultado, la aplicación renderiza el estado actualizado en el servidor, es decir, la nota agregada también se renderiza.
+
+Implementemos también el cambio en la importancia de las notas. Se agrega una función para actualizar notas al archivo
requests.js :
```js
-const baseUrl = 'http://localhost:3001/notes'
+export const updateNote = async (updatedNote) => {
+ const options = {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(updatedNote)
+ }
-const getAll = async () => {
- const response = await fetch(baseUrl)
+ const response = await fetch(`${baseUrl}/${updatedNote.id}`, options)
if (!response.ok) {
- throw new Error('Failed to fetch notes')
+ throw new Error('Failed to update note')
}
return await response.json()
}
+```
-// highlight-start
-const createNew = async (content) => {
- const response = await fetch(baseUrl, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ content, important: false }),
+Actualizar la nota también se hace mediante una mutación. El componente
App se expande de la siguiente manera:
+
+```js
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
+import { getNotes, createNote, updateNote } from './requests' // highlight-line
+
+const App = () => {
+ const queryClient = useQueryClient()
+
+ const newNoteMutation = useMutation({
+ mutationFn: createNote,
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['notes'] })
+ }
})
-
- if (!response.ok) {
- throw new Error('Failed to create note')
+
+ // highlight-start
+ const updateNoteMutation = useMutation({
+ mutationFn: updateNote,
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['notes'] })
+ }
+ })
+ // highlight-end
+
+ const addNote = async (event) => {
+ event.preventDefault()
+ const content = event.target.note.value
+ event.target.reset()
+ newNoteMutation.mutate({ content, important: true })
}
-
- return await response.json()
+
+ const toggleImportance = (note) => {
+ updateNoteMutation.mutate({...note, important: !note.important }) // highlight-line
+ }
+
+ // ...
}
-// highlight-end
+```
-export default { getAll, createNew } // highlight-line
+De nuevo, se creó una mutación que invalidó la query
notes para que la nota actualizada se renderice correctamente. Usar mutaciones es fácil, el método
mutate recibe una nota como parámetro, cuya importancia se cambia a la negación del valor antiguo.
+
+El código actual de la aplicación está en [GitHub](https://github.com/fullstack-hy2020/query-notes/tree/part6-2) en la rama
part6-2 .
+
+### Optimizando el rendimiento
+
+La aplicación funciona bien y el código es relativamente simple. La facilidad para realizar cambios en la lista de notas es particularmente sorprendente. Por ejemplo, cuando cambiamos la importancia de una nota, invalidar la query
notes es suficiente para que los datos de la aplicación se actualicen:
+
+```js
+const updateNoteMutation = useMutation({
+ mutationFn: updateNote,
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['notes'] }) // highlight-line
+ }
+})
```
-Examinemos más de cerca la implementación del método _createNew_. El primer parámetro de la función _fetch()_ especifica la URL a la que se realiza la petición. El segundo parámetro es un objeto que define otros detalles de la petición, como el tipo de petición, encabezados y los datos enviados con la petición. Podemos aclarar aún más el código almacenando el objeto que define los detalles de la petición en una variable
options separada:
+La consecuencia de esto, por supuesto, es que después de la solicitud PUT que causa el cambio de nota, la aplicación realiza una nueva solicitud GET para recuperar los datos de la query desde el servidor:
+
+
+
+Si la cantidad de datos obtenidos por la aplicación no es grande, realmente no importa. Después de todo, desde el punto de vista de la funcionalidad del lado del navegador, hacer una solicitud HTTP GET adicional realmente no importa, pero en algunas situaciones podría generar una carga en el servidor.
+
+Si fuera necesario, es posible también [optimizar el rendimiento manualmente](https://tanstack.com/query/latest/docs/react/guides/updates-from-mutation-responses), actualizando el estado de la query mantenido por TanStack Query.
+
+El cambio para la mutación que agrega una nueva nota es el siguiente:
```js
-const createNew = async (content) => {
- // highlight-start
+const App = () => {
+ const queryClient = useQueryClient()
+
+ const newNoteMutation = useMutation({
+ mutationFn: createNote,
+ // highlight-start
+ onSuccess: (newNote) => {
+ const notes = queryClient.getQueryData(['notes'])
+ queryClient.setQueryData(['notes'], notes.concat(newNote))
+ // highlight-end
+ }
+ })
+
+ // ...
+}
+```
+
+Es decir, en el callback de
onSuccess , el objeto
queryClient primero lee el estado existente de
notes de la query y lo actualiza agregando una nueva nota, que se obtiene como parámetro de la función de callback. El valor del parámetro es el valor devuelto por la función
createNote , definida en el archivo
requests.js de la siguiente manera:
+
+```js
+export const createNote = async (newNote) => {
const options = {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ content, important: false }),
+ body: JSON.stringify(newNote)
}
-
+
const response = await fetch(baseUrl, options)
- // highlight-end
if (!response.ok) {
throw new Error('Failed to create note')
}
-
- return await response.json()
+
+ return await response.json() // highlight-line
}
```
-Examinemos más de cerca el objeto
options :
+Sería relativamente fácil hacer un cambio similar a una mutación que cambia la importancia de la nota, pero lo dejamos como un ejercicio opcional.
+
+Finalmente, nota un detalle interesante. TanStack Query vuelve a obtener todas las notas cuando cambiamos a otra pestaña del navegador y luego regresamos a la pestaña de la aplicación. Esto se puede observar en la pestaña de Red de la Consola de Desarrollador:
--
method define el tipo de petición, que en este caso es
POST
--
headers define los encabezados de la petición. Agregamos el encabezado _'Content-Type': 'application/json'_ para informar al servidor que los datos enviados con la petición están en formato JSON, para que pueda manejar la petición correctamente
--
body contiene los datos enviados con la petición. No puedes asignar directamente un objeto JavaScript a este campo; primero debe convertirse a una cadena JSON llamando a la función _JSON.stringify()_
+
-Al igual que con una petición GET, el código de estado de la respuesta se verifica para detectar errores:
+¿Qué está pasando? Al leer la [documentación](https://tanstack.com/query/latest/docs/react/reference/useQuery), nos damos cuenta de que la funcionalidad predeterminada de las queries de TanStack Query es que las queries (cuyo estado es
stale ) se actualicen cuando cambia el
window focus . Si queremos, podemos desactivar la funcionalidad creando una consulta de la siguiente manera:
```js
-if (!response.ok) {
- throw new Error('Failed to create note')
+const App = () => {
+ // ...
+ const result = useQuery({
+ queryKey: ['notes'],
+ queryFn: getNotes,
+ refetchOnWindowFocus: false // highlight-line
+ })
+
+ // ...
}
```
-Si la petición es exitosa,
JSON Server devuelve la nota recién creada, para la cual también ha generado un
id único. Sin embargo, los datos contenidos en la respuesta aún deben convertirse a formato JSON usando el método _response.json()_:
+Si colocas un console.log en el código, podrás ver desde la consola del navegador cuántas veces TanStack Query hace que la aplicación se vuelva a renderizar. La regla general es que el renderizado ocurre al menos cada vez que es necesario, es decir, cuando cambia el estado de la query. Puedes leer más al respecto por ejemplo [aquí](https://tkdodo.eu/blog/react-query-render-optimizations).
+
+### Hook personalizado useNotes
+
+Nuestra solución es bastante buena, pero resulta algo incómodo que muchos detalles de implementación de TanStack Query estén directamente en el componente de React. Extraigámoslos a un hook personalizado:
```js
-return await response.json()
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
+import { getNotes, createNote, updateNote } from '../requests'
+
+export const useNotes = () => {
+ const queryClient = useQueryClient()
+
+ const result = useQuery({
+ queryKey: ['notes'],
+ queryFn: getNotes,
+ refetchOnWindowFocus: false
+ })
+
+ const newNoteMutation = useMutation({
+ mutationFn: createNote,
+ onSuccess: (newNote) => {
+ const notes = queryClient.getQueryData(['notes'])
+ queryClient.setQueryData(['notes'], notes.concat(newNote))
+ }
+ })
+
+ const updateNoteMutation = useMutation({
+ mutationFn: updateNote,
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['notes'] })
+ }
+ })
+
+ return {
+ notes: result.data,
+ isPending: result.isPending,
+ addNote: (content) => newNoteMutation.mutate({ content, important: true }),
+ toggleImportance: (note) => updateNoteMutation.mutate({
+ ...note, important: !note.important
+ }),
+ }
+}
```
-Luego modificaremos el componente de nuestra aplicación
NoteForm para que una nueva nota se envíe al backend. El método _addNote_ del componente cambiará ligeramente:
+El hook encapsula todo el código relacionado con TanStack Query: la query que obtiene las notas y las dos mutaciones que las crean y actualizan. Estos detalles quedan ocultos para quien utiliza el hook, ya que la función devuelve un objeto sencillo con:
+
+-
notes : la lista de notas
+-
isPending : indica si los datos siguen cargándose
+-
addNote : una función para añadir una nota a partir de su contenido
+-
toggleImportance : una función para cambiar la importancia de una nota
+
+El componente
App se simplifica considerablemente:
```js
-import { useDispatch } from 'react-redux'
-import { createNote } from '../reducers/noteReducer'
-import noteService from '../services/notes' // highlight-line
+import { useNotes } from './hooks/useNotes'
-const NoteForm = (props) => {
- const dispatch = useDispatch()
-
- const addNote = async (event) => { // highlight-line
+const App = () => {
+ const { notes, isPending, addNote: addNoteToServer, toggleImportance } = useNotes()
+
+ const addNote = async (event) => {
event.preventDefault()
const content = event.target.note.value
- event.target.note.value = ''
- const newNote = await noteService.createNew(content) // highlight-line
- dispatch(createNote(newNote)) // highlight-line
+ event.target.reset()
+ addNoteToServer(content)
+ }
+
+ if (isPending) {
+ return
loading data...
}
return (
-
+
+
Notes app
+
+ {notes.map((note) => (
+
+ {note.important ? {note.content} : note.content}
+ toggleImportance(note)}>
+ {note.important ? 'make not important' : 'make important'}
+
+
+ ))}
+
)
}
-
-export default NoteForm
```
-Cuando se crea una nueva nota en el backend llamando al método _createNew()_, el valor de retorno es un objeto que representa la nota, al cual el backend ha generado un
id único. Por lo tanto, modifiquemos el action creator
createNote definido en
notesReducer.js de la siguiente manera:
+El código de la aplicación está en [GitHub](https://github.com/fullstack-hy2020/query-notes/tree/part6-3) en la rama
part6-3 .
-```js
-const noteSlice = createSlice({
- name: 'notes',
- initialState: [],
- reducers: {
- createNote(state, action) {
- state.push(action.payload) // highlight-line
- },
- // ..
- },
-})
-```
+TanStack Query es una librería versátil que, basándonos en lo que ya hemos visto, simplifica la aplicación. ¿Hace TanStack Query que soluciones de gestión de estado más complejas como Redux sean innecesarias? No. TanStack Query puede reemplazar parcialmente el estado de la aplicación en algunos casos, pero como lo indica la [documentación](https://tanstack.com/query/latest/docs/react/guides/does-this-replace-client-state):
+
+- TanStack Query es una
librería de estado del servidor , responsable de la gestión de operaciones asíncronas entre el servidor y el cliente
+- Zustand y otras soluciones similares son
librerías de estado del cliente que pueden almacenar datos asíncronos, aunque con menor eficiencia que una herramienta como TanStack Query
-El cambio de importancia de las notas podría implementarse utilizando el mismo principio, haciendo una llamada asíncrona al servidor y luego enviando una acción apropiada.
+Entonces, TanStack Query es una librería que mantiene el
estado del servidor en el frontend, es decir, actúa como una caché para lo que se almacena en el servidor. TanStack Query simplifica el procesamiento de datos en el servidor y, en algunos casos, puede eliminar la necesidad de que los datos en el servidor se guarden en el estado del frontend.
-El estado actual del código para la aplicación se puede encontrar en [GitHub](https://github.com/fullstack-hy2020/redux-notes/tree/part6-4) en la rama
part6-4 .
+La mayoría de las aplicaciones de React no necesitan solo una forma de almacenar temporalmente los datos servidos, sino también alguna solución para cómo se maneja el resto del estado del frontend (por ejemplo, el estado de los formularios o las notificaciones).
-### Ejercicios 6.14.-6.15.
+### Ejercicios 6.16.-6.19.
+
+Ahora hagamos una nueva versión de la aplicación de anécdotas que use la librería TanStack Query. Usa [este proyecto](https://github.com/fullstack-hy2020/query-anecdotes) como punto de partida. El proyecto tiene un JSON Server instalado, la operación del cual se ha modificado ligeramente (Revisa el archivo _server.js_ para más detalles. Asegúrate de estar conectándote al _PORT_ correcto). Inicia el servidor con npm run server .
+
+Usa la Fetch API para hacer las peticiones.
-#### 6.14 Anécdotas y el Backend, paso 1
+#### Ejercicio 6.16
-Cuando la aplicación se inicie, obtén las anécdotas del backend implementado usando json-server. Usa la Fetch API para hacer la petición HTTP.
+Implementa la obtención de anécdotas del servidor usando TanStack Query.
-Como datos de backend iniciales, puedes usar, por ejemplo, [esto](https://github.com/fullstack-hy2020/misc/blob/master/anecdotes.json).
+La aplicación debe funcionar de tal manera que si hay problemas para comunicarse con el servidor, solo se mostrará una página de error:
-#### 6.15 Anécdotas y el Backend, paso 2
+
+
+Puedes encontrar [aquí](https://tanstack.com/query/latest/docs/react/guides/queries) información sobre cómo detectar posibles errores.
+
+Puedes simular un problema con el servidor apagando el JSON Server. Ten en cuenta que en una situación problemática, la consulta primero está en el estado isLoading durante un tiempo, porque si una solicitud falla, TanStack Query intenta la solicitud varias veces antes de que indique que la solicitud no es exitosa. Opcionalmente, puedes especificar que no se realicen reintentos:
+
+```js
+const result = useQuery(
+ {
+ queryKey: ['anecdotes'],
+ queryFn: getAnecdotes,
+ retry: false
+ }
+)
+```
+
+o que la solicitud se vuelva a intentar solo una vez más:
+
+```js
+const result = useQuery(
+ {
+ queryKey: ['anecdotes'],
+ queryFn: getAnecdotes,
+ retry: 1
+ }
+)
+```
-Modifica la creación de nuevas anécdotas, de forma que las anécdotas se almacenen en el backend. Utiliza la Fetch API en tu implementación una vez más.
+#### Ejercicio 6.17
+
+Implementa la adición de nuevas anécdotas al servidor usando TanStack Query. La aplicación debe renderizar una nueva anécdota por defecto. Ten en cuenta que el contenido de la anécdota debe tener al menos 5 caracteres de longitud, de lo contrario el servidor rechazará la solicitud POST. No tienes que preocuparte por el control de errores ahora.
+
+#### Ejercicio 6.18
+
+Implementa la votación de anécdotas usando nuevamente TanStack Query. La aplicación debe renderizar automáticamente el número aumentado de votos para la anécdota votada.
+
+#### Ejercicio 6.19
+
+Extrae los detalles de TanStack Query a un hook personalizado.
-### Acciones asíncronas y Redux Thunk
+### Context API
-Nuestro enfoque es bastante bueno, pero no es muy bueno que la comunicación con el servidor suceda dentro de las funciones de los componentes. Sería mejor si la comunicación pudiera abstraerse de los componentes para que no tengan que hacer nada más que llamar al action creator apropiado. Como ejemplo,
App inicializaría el estado de la aplicación de la siguiente manera:
+Volvamos a la conocida aplicación de contador. La aplicación se define de la siguiente manera:
```js
+import { useState } from 'react'
+import Display from './components/Display'
+import Controls from './components/Controls'
+
const App = () => {
- const dispatch = useDispatch()
+ const [counter, setCounter] = useState(0)
- useEffect(() => {
- dispatch(initializeNotes())
- }, [dispatch])
-
- // ...
+ return (
+
+
+
+
+ )
}
```
-y
NoteForm crearía una nueva nota de la siguiente manera:
+El componente
App define el estado de la aplicación y se lo pasa a
Display , que renderiza el valor del contador:
```js
-const NoteForm = () => {
- const dispatch = useDispatch()
-
- const addNote = async (event) => {
- event.preventDefault()
- const content = event.target.note.value
- event.target.note.value = ''
- dispatch(createNote(content))
- }
+const Display = ({ counter }) => {
- // ...
+ return (
+
{counter}
+ )
+}
+```
+
+y al componente
Controls , que renderiza los botones:
+
+```js
+const Controls = ({ counter, setCounter }) => {
+ const increment = () => setCounter(counter + 1)
+ const decrement = () => setCounter(counter - 1)
+ const zero = () => setCounter(0)
+
+ return (
+
+ plus
+ minus
+ zero
+
+ )
}
```
-En esta implementación, ambos componentes enviarían una acción sin necesidad de saber sobre la comunicación con el servidor que sucede detrás de escena. Estos tipos de
acciones asíncronas se pueden implementar utilizando la librería [Redux Thunk](https://github.com/reduxjs/redux-thunk). El uso de la librería no requiere ninguna configuración adicional o incluso instalación cuando el store de Redux se ha creado utilizando la función
configureStore del kit de herramientas de Redux (Redux Toolkit).
+La aplicación crece:
-Con Redux Thunk, es posible implementar
action creators que devuelven una función en lugar de un objeto. La función recibe los métodos
dispatch y
getState del store de Redux como parámetros. Esto permite, por ejemplo, implementaciones de action creators asíncronos, que primero esperan la finalización de una cierta operación asíncrona y luego despachan alguna acción, que cambia el estado del store.
+
-Podemos definir un action creator llamado
initializeNotes en el archivo
noteReducer.js , que obtiene las notas iniciales del servidor, de la siguiente manera:
+El papel de
App cambia: sigue conservando el estado, pero ya no renderiza directamente los componentes que lo utilizan:
```js
-import { createSlice } from '@reduxjs/toolkit'
-import noteService from '../services/notes' // highlight-line
+const App = () => {
+ const [counter, setCounter] = useState(0)
-const noteSlice = createSlice({
- name: 'notes',
- initialState: [],
- reducers: {
- createNote(state, action) {
- state.push(action.payload)
- },
- toggleImportanceOf(state, action) {
- const id = action.payload
- const noteToChange = state.find((n) => n.id === id)
- const changedNote = {
- ...noteToChange,
- important: !noteToChange.important,
- }
- return state.map((note) => (note.id !== id ? note : changedNote))
- },
- setNotes(state, action) {
- return action.payload
- },
- },
-})
+ return (
+
+ )
+}
+```
-const { setNotes } = noteSlice.actions // highlight-line
+El nuevo componente
Panel renderiza los componentes que muestran el contador y los botones:
-// highlight-start
-export const initializeNotes = () => {
- return async (dispatch) => {
- const notes = await noteService.getAll()
- dispatch(setNotes(notes))
- }
+```js
+import Display from './Display'
+import Controls from './Controls'
+
+const Panel = ({ counter, setCounter }) => {
+ return (
+
+
+
+
+ )
}
-// highlight-end
+```
-export const { createNote, toggleImportanceOf } = noteSlice.actions // highlight-line
+La jerarquía de componentes es la siguiente:
-export default noteSlice.reducer
```
+App (state)
+ ├── Panel
+ │ ├── Display
+ │ └── Controls
+ └── Footer
+```
+
+El estado continúa en
App . Para que
Display y
Controls accedan al contador, tanto el estado como su función de actualización deben atravesar
Panel como props, aunque
Panel no los necesite. Esta situación aparece fácilmente al utilizar estado creado con
useState y se denomina [prop drilling](https://kentcdodds.com/blog/prop-drilling).
-En su función interna, es decir, en la
acción asíncrona , la operación primero obtiene todas las notas del servidor y luego
despacha la acción para agregarlas al store. Es importante destacar que Redux pasa automáticamente una referencia al método _dispatch_ como argumento a la función, por lo que el action creator _initializeNotes_ no requiere ningún parámetro.
+La [Context API](https://react.dev/learn/passing-data-deeply-with-context) integrada en React ofrece una solución. Un contexto de React es una especie de estado global de la aplicación al que se puede dar acceso directo a cualquier componente.
-El action creator _setNotes_ ya no se exporta fuera del módulo, ya que el estado inicial de las notas ahora se establecerá usando el action creator asíncrono _initializeNotes_ que creamos. Sin embargo, todavía usamos el action creator _setNotes_ dentro del módulo.
+Creemos un contexto que almacene la gestión del estado del contador.
-El componente
App ahora puede definirse de la siguiente manera:
+El contexto se crea con [createContext](https://react.dev/reference/react/createContext). Lo definiremos en el archivo
src/CounterContext.jsx :
```js
-import { useEffect } from 'react'
-import { useDispatch } from 'react-redux'
+import { createContext } from 'react'
+
+const CounterContext = createContext()
-import NoteForm from './components/NoteForm'
-import Notes from './components/Notes'
-import VisibilityFilter from './components/VisibilityFilter'
-import { initializeNotes } from './reducers/noteReducer' // highlight-line
+export default CounterContext
+```
+
+El componente
App puede
proporcionar el contexto a sus componentes hijos así:
+
+```js
+// ...
+import CounterContext from './components/CounterContext'
const App = () => {
- const dispatch = useDispatch()
+ const [counter, setCounter] = useState(0)
+
+ return (
+
// highlight-line
+ // highlight-line
+
+ // highlight-line
+ )
+}
+```
- useEffect(() => {
- dispatch(initializeNotes()) // highlight-line
- }, [dispatch])
+El contexto se proporciona envolviendo los componentes hijos con
CounterContext.Provider y asignándole un valor adecuado.
+Su valor es ahora un objeto con los atributos
counter y
setCounter : el estado del contador y la función que lo actualiza.
+
+Como
Panel ya no recibe props relacionadas con el contador, se simplifica:
+
+```js
+const Panel = () => {
return (
-
-
-
+
+
)
}
+```
-export default App
+Los demás componentes pueden acceder al contexto mediante el hook [useContext](https://react.dev/reference/react/useContext).
Display cambia de la siguiente manera:
+
+```js
+import { useContext } from 'react' // highlight-line
+import CounterContext from './CounterContext' // highlight-line
+
+const Display = () => { // highlight-line
+ const { counter } = useContext(CounterContext) // highlight-line
+
+ return
{counter}
+}
```
-La solución es bastante elegante. La lógica de inicialización de las notas se ha separado completamente del componente React.
+
Display ya no necesita props. Obtiene el contador llamando a
useContext con el objeto
CounterContext como parámetro.
-A continuación, creemos un action creator asíncrono llamado _appendNote_:
+De forma análoga,
Controls pasa a ser:
```js
-import { createSlice } from '@reduxjs/toolkit'
-import noteService from '../services/notes'
+import { useContext } from 'react' // highlight-line
+import CounterContext from './CounterContext' // highlight-line
-const noteSlice = createSlice({
- name: 'notes',
- initialState: [],
- reducers: {
- createNote(state, action) {
- state.push(action.payload)
- },
- toggleImportanceOf(state, action) {
- const id = action.payload
- const noteToChange = state.find((n) => n.id === id)
- const changedNote = {
- ...noteToChange,
- important: !noteToChange.important,
- }
- return state.map((note) => (note.id !== id ? note : changedNote))
- },
- setNotes(state, action) {
- return action.payload
- },
- },
-})
+const Controls = () => {
+ const { counter, setCounter } = useContext(CounterContext) // highlight-line
-const { createNote, setNotes } = noteSlice.actions // highlight-line
+ const increment = () => setCounter(counter + 1)
+ const decrement = () => setCounter(counter - 1)
+ const zero = () => setCounter(0)
-export const initializeNotes = () => {
- return async (dispatch) => {
- const notes = await noteService.getAll()
- dispatch(setNotes(notes))
- }
+ return (
+
+ plus
+ minus
+ zero
+
+ )
}
+export default Controls
+```
+
+Los componentes ya tienen acceso al contenido establecido por el proveedor: el estado del contador y su función de actualización.
+
+Extraen los atributos que necesitan mediante la desestructuración de JavaScript:
+
+```js
+const { counter } = useContext(CounterContext)
+```
+
+### Definir el contexto del contador en su propio archivo
+
+La aplicación aún tiene un aspecto poco agradable: la gestión del estado del contador está definida dentro de
App . Traslademos todo el código relacionado al archivo
CounterContext.jsx :
+
+```js
+import { createContext, useState } from 'react'
+
+const CounterContext = createContext()
+
+export default CounterContext
+
// highlight-start
-export const appendNote = (content) => {
- return async (dispatch) => {
- const newNote = await noteService.createNew(content)
- dispatch(createNote(newNote))
- }
+export const CounterContextProvider = (props) => {
+ const [counter, setCounter] = useState(0)
+
+ return (
+
+ {props.children}
+
+ )
}
// highlight-end
+```
+
+El archivo exporta tanto
CounterContext como
CounterContextProvider , que es esencialmente un proveedor cuyo valor contiene el contador y su función de actualización.
+
+Utilicemos el proveedor directamente en
main.jsx :
-export const { toggleImportanceOf } = noteSlice.actions // highlight-line
+```js
+import { StrictMode } from 'react'
+import { createRoot } from 'react-dom/client'
+
+import App from './App'
+import { CounterContextProvider } from './CounterContext' // highlight-line
-export default noteSlice.reducer
+createRoot(document.getElementById('root')).render(
+
// highlight-line
+
+ // highlight-line
+)
```
-El principio es el mismo una vez más. Primero se realiza una operación asíncrona y, una vez completada, se
despacha una acción que actualiza el estado del store. El action creator _createNote_ ya no se exporta fuera del archivo; solo se usa internamente en la implementación de la función _appendNote_.
+El contexto que define el valor y la funcionalidad del contador está ahora disponible para
todos los componentes.
-El componente
NoteForm cambia de la siguiente manera:
+
App se simplifica:
```js
-import { useDispatch } from 'react-redux'
-import { appendNote } from '../reducers/noteReducer' // highlight-line
+import Panel from './components/Panel'
+import Footer from './components/Footer'
-const NoteForm = () => {
- const dispatch = useDispatch()
+const App = () => {
- const addNote = async (event) => {
- event.preventDefault()
- const content = event.target.note.value
- event.target.note.value = ''
- dispatch(appendNote(content)) // highlight-line
- }
+ return (
+
+ )
+}
+
+export default App
+```
+
+El contexto sigue utilizándose igual y los demás componentes no necesitan cambios. Por ejemplo,
Controls permanece así:
+
+```js
+const Controls = () => {
+ const { counter, setCounter } = useContext(CounterContext)
+ const increment = () => setCounter(counter + 1)
+ const decrement = () => setCounter(counter - 1)
+ const zero = () => setCounter(0)
return (
-
+
+ plus
+ minus
+ zero
+
)
}
```
-Finalmente, limpiemos un poco el archivo
main.jsx moviendo el código relacionado con la creación del store de Redux a su propio archivo
store.js :
+La solución es bastante buena. Todo el estado de la aplicación —el valor del contador— queda aislado en
CounterContext . Cada componente accede exactamente a la parte que necesita mediante
useContext y la desestructuración.
+
+Hagamos una pequeña mejora y definamos también las funciones
increment ,
decrement y
zero dentro del contexto:
```js
-import { configureStore } from '@reduxjs/toolkit'
+import { createContext, useState } from 'react'
-import noteReducer from './reducers/noteReducer'
-import filterReducer from './reducers/filterReducer'
+const CounterContext = createContext()
-const store = configureStore({
- reducer: {
- notes: noteReducer,
- filter: filterReducer
- }
-})
+export default CounterContext
+
+export const CounterContextProvider = (props) => {
+ const [counter, setCounter] = useState(0)
+
+// highlight-start
+ const increment = () => setCounter(counter + 1)
+ const decrement = () => setCounter(counter - 1)
+ const zero = () => setCounter(0)
+// highlight-end
-export default store
+ return (
+
// highlight-line
+ {props.children}
+
+ )
+}
```
-Luego de los cambios, el contenido del archivo
main.jsx es el siguiente:
+Ahora podemos usar las funciones obtenidas del contexto directamente como controladores de eventos:
```js
-import React from 'react'
-import ReactDOM from 'react-dom/client'
-import { Provider } from 'react-redux'
-import store from './store' // highlight-line
-import App from './App'
+import { useContext } from 'react'
+import CounterContext from '../CounterContext'
-ReactDOM.createRoot(document.getElementById('root')).render(
-
-
-
-)
+const Controls = () => {
+ const { increment, decrement, zero } = useContext(CounterContext) // highlight-line
+
+ return (
+
+ plus
+ minus
+ zero
+
+ )
+}
+```
+
+Todavía podemos mejorarlo. Al observar el uso del contexto, vemos el mismo código repetitivo en ambos componentes que lo consumen:
+
+```js
+import { useContext } from 'react'
+import CounterContext from '../CounterContext'
+
+const Display = () => {
+ const { counter } = useContext(CounterContext)
+ // ...
+}
```
-El estado actual del código de la aplicación se puede encontrar en [GitHub](https://github.com/fullstack-hy2020/redux-notes/tree/part6-5) en la rama
part6-5 .
+```js
+import { useContext } from 'react'
+import CounterContext from '../CounterContext'
-Redux Toolkit ofrece una gran cantidad de herramientas para simplificar la administración de estado asíncrono. Las herramientas adecuadas para este caso de uso son, por ejemplo, la función [createAsyncThunk](https://redux-toolkit.js.org/api/createAsyncThunk) y la API [RTK Query](https://redux-toolkit.js.org/rtk-query/overview).
+const Controls = () => {
+ const { increment, decrement, zero } = useContext(CounterContext) // highlight-line
+ // ...
+}
+```
+
+Podemos avanzar un paso más creando un hook personalizado que devuelva directamente el contexto. Añadámoslo al archivo
hooks/useCounter.js :
+
+```js
+import { useContext } from 'react'
+import CounterContext from '../CounterContext'
+
+const useCounter = () => useContext(CounterContext)
+
+export default useCounter
+
+```
+
+El uso del contexto queda aún más sencillo:
+
+```js
+import { useCounter } from '../hooks/useCounter'
+const Display = () => {
+ const { counter } = useCounter()
+ // ...
+}
+
+import { useCounter } from '../hooks/useCounter'
+
+const Controls = () => {
+ const { increment, decrement, zero } = useCounter()
+ // ...
+}
+```
+
+La solución nos satisface. Aísla toda la gestión del estado dentro del contexto. Los componentes que lo usan desconocen cómo se implementa; gracias al hook personalizado, ni siquiera necesitan saber que la solución se basa en la Context API.
+
+El código de la aplicación está en el repositorio de GitHub [https://github.com/fullstack-hy2020/context-counter](https://github.com/fullstack-hy2020/context-counter).
-### Ejercicios 6.16.-6.19.
+### Ejercicios 6.20.-6.22.
-#### 6.16 Anécdotas y el Backend, paso 3
+#### Ejercicio 6.20.
-Modifica la inicialización de la store de Redux para que suceda utilizando action creators asíncronos, los cuales son posibles gracias a la librería Redux Thunk.
+La aplicación tiene un componente Notification para mostrar notificaciones al usuario.
-#### 6.17 Anécdotas y el Backend, paso 4
+Implementa la gestión del estado de las notificaciones de la aplicación utilizando la Context API. La notificación debe informar al usuario cuando se crea una nueva anécdota o cuando se vota por ella:
-También modifica la creación de una nueva anécdota para que suceda usando action creators asíncronos, hecho posible por la librería Redux Thunk.
+
-#### 6.18 Anécdotas y el Backend, paso 5
+La notificación se muestra durante cinco segundos.
-La votación aún no guarda los cambios en el backend. Arregla la situación con la ayuda de la librería Redux Thunk y la Fetch API.
+#### Ejercicio 6.21.
-#### 6.19 Anécdotas y el Backend, paso 6
+Como se indicó en el ejercicio 6.17, el servidor exige que el contenido de la anécdota que se añade tenga al menos 5 caracteres. Implementa ahora el manejo de errores de la inserción. En la práctica basta con mostrar una notificación cuando falle la solicitud POST:
-La creación de notificaciones sigue siendo un poco tediosa, ya que hay que realizar dos acciones y utilizar la función _setTimeout_:
+
-```js
-dispatch(setNotification(`new anecdote '${content}'`))
-setTimeout(() => {
- dispatch(clearNotification())
-}, 5000)
-```
+La condición de error debe manejarse en la función de callback registrada para ello, consulta [aquí](https://tanstack.com/query/latest/docs/react/reference/useMutation) cómo registrar una función.
-Crea un action creator, que te permita proveer la notificación de la siguiente manera:
+#### Ejercicio 6.22.
-```js
-dispatch(setNotification(`you voted '${anecdote.content}'`, 10))
-```
+Si aún no lo has hecho, mueve el contexto de las notificaciones a su propio archivo NotificationContext.jsx , del mismo modo que el contexto de la aplicación del contador se trasladó a CounterContext.jsx . Crea también un hook personalizado useNotify que encapsule la lógica de las notificaciones. Simplifica los componentes que las utilizan para que llamen directamente al hook en lugar de llamar a useContext por separado.
+
+Este fue el último ejercicio para esta parte del curso y es hora de enviar tu código a GitHub y marcar todos tus ejercicios completados en el [sistema de envío de ejercicios](https://studies.cs.helsinki.fi/stats/courses/fullstackopen).
+
+
+
+
+
+### ¿Qué solución de gestión de estado elegir?
+
+En los capítulos 1-5, toda la gestión de estado de la aplicación se realizó utilizando el hook de React useState . Las llamadas asíncronas al backend requerían el uso del hook useEffect en algunas situaciones. En principio, no se necesita nada más.
+
+Un problema sutil con una solución basada en un estado creado con el hook useState es que si alguna parte del estado de la aplicación se necesita en varios componentes de la aplicación, el estado y las funciones para manipularlo deben pasarse via props a todos los componentes que manejan el estado. A veces, las props deben pasar por varios componentes, y los componentes a lo largo del camino pueden ni siquiera estar interesados en el estado de ninguna manera. Este fenómeno algo desagradable se llama prop drilling .
+
+A lo largo de los años, se han desarrollado varias soluciones alternativas para la gestión de estado de aplicaciones React, que se pueden usar para aliviar situaciones problemáticas (por ejemplo, prop drilling). Sin embargo, ninguna solución ha sido "final", todas tienen sus propias ventajas y desventajas, y se están desarrollando nuevas soluciones todo el tiempo.
+
+La situación puede confundir a un principiante e incluso a un desarrollador web experimentado. ¿Qué solución se debe usar?
+
+Para una aplicación simple, useState es sin duda un buen punto de partida. Si la aplicación está comunicándose con el servidor, la comunicación se puede manejar de la misma manera que en los capítulos 1-5, utilizando el estado de la aplicación misma. Sin embargo, recientemente se ha vuelto más común mover la comunicación y la gestión asociada del estado al menos parcialmente bajo el control de TanStack Query (o alguna otra librería similar). Si estás preocupado por useState y el prop drilling que conlleva, usar context puede ser una buena opción. También hay situaciones donde puede tener sentido manejar parte del estado con useState y parte con contextos.
+
+Durante mucho tiempo, la solución más popular y completa fue Redux, una forma de implementar la arquitectura [Flux](https://facebookarchive.github.io/flux/). Sin embargo, Redux es conocido por su complejidad y la abundancia de código repetitivo, lo que motivó la aparición de alternativas más recientes. En este curso Redux se ha sustituido por [Zustand](https://zustand.docs.pmnd.rs/), que ofrece una funcionalidad equivalente con una API mucho más sencilla. Zustand se ha convertido en una opción popular cuando hace falta algo más que useState, pero toda la maquinaria de Redux resulta excesiva. Parte de las críticas a la rigidez de Redux han quedado obsoletas gracias a [Redux Toolkit](https://redux-toolkit.js.org/), y Redux todavía se usa mucho, especialmente en proyectos grandes.
-El primer parámetro es el texto que sera renderizado y el segundo parámetro es el tiempo durante el cual se mostrara la notificación en segundos.
+Ni Zustand ni Redux tienen que utilizarse en toda la aplicación. Por ejemplo, puede ser razonable gestionar fuera de ellos el estado de los formularios cuando no afecta al resto de la aplicación. También es perfectamente posible combinar Zustand o Redux con TanStack Query.
-Implementa el uso de esta notificación mejorada en tu aplicación.
+La pregunta de qué solución de gestión de estado se debe usar no es para nada sencilla. Es imposible dar una sola respuesta correcta. También es probable que la solución de gestión de estado seleccionada pueda resultar ser subóptima a medida que la aplicación crece hasta tal punto que la solución tenga que cambiarse incluso si la aplicación ya ha sido puesta en uso de producción.
diff --git a/src/content/6/es/part6d.md b/src/content/6/es/part6d.md
index 85ef8a110cb..1397054a8d9 100644
--- a/src/content/6/es/part6d.md
+++ b/src/content/6/es/part6d.md
@@ -5,541 +5,2349 @@ letter: d
lang: es
---
+
+
+Este es el material relacionado con Redux que se ha retirado del curso. Puedes continuar con este material y sus ejercicios si ya habías empezado esta parte utilizando Redux. En caso contrario, se recomienda seguir el material nuevo. Este material se eliminará en junio de 2026.
+
+
+
+
+
+Hasta ahora, hemos seguido las convenciones de gestión de estado recomendadas por React. Hemos colocado el estado y las funciones para manejarlo en el [nivel superior](https://es.react.dev/learn/sharing-state-between-components) de la estructura de componentes de la aplicación. A menudo, la mayoría del estado de la aplicación y los métodos para modificarlo residen directamente en el componente raíz. Luego, el estado y sus métodos de control se han pasado a otros componentes con props. Esto funciona hasta cierto punto, pero cuando las aplicaciones crecen, la gestión del estado se vuelve desafiante.
+
+### Arquitectura de Flux
+
+Facebook desarrolló la arquitectura [Flux](https://facebookarchive.github.io/flux/docs/in-depth-overview/) para facilitar la gestión del estado. En Flux, el estado se separa completamente de los componentes de React en sus propios
stores (almacenes).
+El estado en el store no se cambia directamente, sino con diferentes
actions (acciones).
+
+Cuando una acción cambia el estado de un store, las vistas se vuelven a generar:
+
+
+
+Si alguna acción en la aplicación, por ejemplo presionar un botón, provoca la necesidad de cambiar el estado, el cambio se realiza con una acción.
+Esto hace que se vuelva a renderizar la vista:
+
+
+
+Flux ofrece una manera estándar de cómo y dónde se mantiene el estado de la aplicación y cómo se modifica.
+
+### Redux
+
+Facebook tiene una implementación para Flux, pero usaremos la librería [Redux](https://redux.js.org). Funciona con el mismo principio, pero es un poco más sencilla. Facebook también usa Redux ahora en lugar de su Flux original.
+
+Conoceremos Redux implementando una aplicación de contador una vez más:
+
+
+
+Crea una nueva aplicación Vite e instala
redux con el comando
+
+```bash
+npm install redux
+```
+
+Como en Flux, en Redux el estado también se almacena en un [store](https://redux.js.org/tutorials/essentials/part-1-overview-concepts#store).
+
+Todo el estado de la aplicación se almacena en
un objeto JavaScript en el store. Debido a que nuestra aplicación solo necesita el valor del contador, lo guardaremos directamente en el store. Si el estado fuera más complicado, diferentes elementos del estado se guardarían como campos separados del objeto.
+
+El estado del store se cambia con [acciones](https://redux.js.org/tutorials/essentials/part-1-overview-concepts#actions). Las acciones son objetos que tienen al menos un campo que determina el
tipo de acción.
+Nuestra aplicación necesita, por ejemplo, la siguiente acción:
+
+```js
+{
+ type: 'INCREMENT'
+}
+```
+
+Si hay datos relacionados con la acción, se pueden declarar otros campos según sea necesario. Sin embargo, nuestra aplicación de contador es tan simple que las acciones están bien con solo el campo de tipo.
+
+El impacto de la acción sobre el estado de la aplicación se define mediante un [reducer](https://redux.js.org/tutorials/essentials/part-1-overview-concepts#reducers). En la práctica, un reducer es una función a la que se le da el estado actual y una acción como parámetros.
Devuelve un nuevo estado.
+
+Definamos ahora un reducer para nuestra aplicación:
+
+```js
+const counterReducer = (state, action) => {
+ if (action.type === 'INCREMENT') {
+ return state + 1
+ } else if (action.type === 'DECREMENT') {
+ return state - 1
+ } else if (action.type === 'ZERO') {
+ return 0
+ }
+
+ return state
+}
+```
+
+El primer parámetro es el
estado en el store. El reducer devuelve un
nuevo estado basado en el tipo de _acción_. Entonces, por ejemplo, cuando el tipo de acción es
INCREMENT , el estado obtiene el valor antiguo más uno. Si el tipo de acción es
ZERO , el nuevo valor del estado es cero.
+
+Cambiemos un poco el código. Hemos utilizado declaraciones if-else para responder a una acción y cambiar el estado. Sin embargo, la declaración [switch](https://developer.mozilla.org/es/docs/Web/JavaScript/Reference/Statements/switch) es el enfoque más común para escribir un reducer.
+
+También definamos un [valor predeterminado](https://developer.mozilla.org/es/docs/Web/JavaScript/Reference/Functions/Default_parameters) de 0 para el parámetro
state . Ahora, el reducer funciona incluso si el estado del store aún no se ha inicializado.
+
+```js
+// highlight-start
+const counterReducer = (state = 0, action) => {
+ // highlight-end
+ switch (action.type) {
+ case 'INCREMENT':
+ return state + 1
+ case 'DECREMENT':
+ return state - 1
+ case 'ZERO':
+ return 0
+ default: // if none of the above matches, code comes here
+ return state
+ }
+}
+```
+
+El reducer nunca debe ser llamado directamente desde el código de la aplicación. Solo es proporcionado como parámetro a la función _createStore_ que crea el store:
+
+```js
+import { createStore } from 'redux' // highlight-line
+
+const counterReducer = (state = 0, action) => {
+ switch (action.type) {
+ case 'INCREMENT':
+ return state + 1
+ case 'DECREMENT':
+ return state - 1
+ case 'ZERO':
+ return 0
+ default:
+ return state
+ }
+}
+
+const store = createStore(counterReducer) // highlight-line
+```
+
+El store ahora usa el reducer para manejar
acciones , que son
dispatched o 'enviadas' al store con su método [dispatch](https://redux.js.org/tutorials/essentials/part-1-overview-concepts#dispatch)(envío).
+
+```js
+store.dispatch({ type: 'INCREMENT' })
+```
+
+Puedes averiguar el estado del store utilizando el método [getState](https://redux.js.org/api/store#getstate).
+
+Por ejemplo, el siguiente código:
+
+```js
+// ...
+
+const store = createStore(counterReducer)
+
+// highlight-start
+console.log(store.getState())
+store.dispatch({type: 'INCREMENT'})
+store.dispatch({type: 'INCREMENT'})
+store.dispatch({type: 'INCREMENT'})
+console.log(store.getState())
+store.dispatch({type: 'ZERO'})
+store.dispatch({type: 'DECREMENT'})
+console.log(store.getState())
+// highlight-end
+```
+
+imprimiría lo siguiente en la consola
+
+```
+0
+3
+-1
+```
+
+porque al principio el estado del store es 0. Después de tres acciones
INCREMENT el estado es 3. Al final, después de las acciones
ZERO y
DECREMENT , el estado es -1.
+
+El tercer método importante que tiene el store es [subscribe](https://redux.js.org/api/store#subscribelistener), que se utiliza para crear funciones callback que el store llama cuando cambia su estado.
+
+Si, por ejemplo, añadiéramos la siguiente función para suscribirnos,
todos los cambios en el store se imprimirían en la consola.
+
+```js
+store.subscribe(() => {
+ const storeNow = store.getState()
+ console.log(storeNow)
+})
+```
+
+entonces el código
+
+```js
+// ...
+
+const store = createStore(counterReducer)
+
+// highlight-start
+store.subscribe(() => {
+ const storeNow = store.getState()
+ console.log(storeNow)
+})
+// highlight-end
+
+// highlight-start
+store.dispatch({ type: 'INCREMENT' })
+store.dispatch({ type: 'INCREMENT' })
+store.dispatch({ type: 'INCREMENT' })
+store.dispatch({ type: 'ZERO' })
+store.dispatch({ type: 'DECREMENT' })
+// highlight-end
+```
+
+causaría que se imprima lo siguiente:
+
+```
+1
+2
+3
+0
+-1
+```
+
+El código de nuestra aplicación de contador es el siguiente. Todo el código se ha escrito en el mismo archivo, por lo que
store está directamente disponible para el código React. Más adelante conoceremos mejores formas de estructurar el código React/Redux.
+
+```js
+import ReactDOM from 'react-dom/client'
+import { createStore } from 'redux'
+
+const counterReducer = (state = 0, action) => {
+ switch (action.type) {
+ case 'INCREMENT':
+ return state + 1
+ case 'DECREMENT':
+ return state - 1
+ case 'ZERO':
+ return 0
+ default:
+ return state
+ }
+}
+
+const store = createStore(counterReducer)
+
+const App = () => {
+ return (
+
+
{store.getState()}
+
store.dispatch({ type: 'INCREMENT' })}>
+ plus
+
+
store.dispatch({ type: 'DECREMENT' })}>
+ minus
+
+
store.dispatch({ type: 'ZERO' })}>
+ zero
+
+
+ )
+}
+
+const root = ReactDOM.createRoot(document.getElementById('root'))
+
+const renderApp = () => {
+ root.render(
)
+}
+
+renderApp()
+store.subscribe(renderApp)
+```
+
+Hay algunas cosas notables en el código.
+
App muestra el valor del contador solicitándolo al store con el método _store.getState()_. Los controladores de acciones de los botones envían (
dispatch ) las acciones correctas al store.
+
+Cuando se cambia el estado del store, React no puede volver a re-renderizar automáticamente la aplicación. Por lo tanto, hemos registrado una función _renderApp_ , que renderiza toda la aplicación, para escuchar cambios en el store con el método _store.subscribe_. Ten en cuenta que tenemos que invocar inmediatamente al método _renderApp_. Sin la invocación, el primer renderizado de la aplicación nunca se produciría.
+
+### Una nota sobre el uso de createStore
+
+Los más atentos notarán que el nombre de la función createStore está tachado. Si pasas el mouse sobre el nombre, aparecerá una explicación
+
+
+
+La explicación completa es la siguiente:
+
+>
Recomendamos utilizar el método configureStore del paquete @reduxjs/toolkit, que reemplaza a createStore.
+>
+>
Redux Toolkit es nuestro enfoque recomendado para escribir la lógica de Redux hoy, incluida la configuración de store, reducers, la obtención de datos y más.
+>
+>
Para obtener más detalles, lea esta página de documentación de Redux:
+>
+>
configureStore de Redux Toolkit es una versión mejorada de createStore que simplifica la configuración y ayuda a evitar errores comunes.
+>
+>
No deberías usar el paquete principal de redux por sí solo hoy en día, excepto con fines de aprendizaje. El método createStore del paquete core de redux no se eliminará, pero alentamos a todos los usuarios a migrar al uso de Redux Toolkit para todo el código de Redux.
+
+Entonces, en lugar de la función
createStore , se recomienda usar la función un poco más "avanzada"
configureStore , y también la usaremos cuando nos hayamos hecho cargo de la funcionalidad básica de Redux.
+
+Nota adicional:
createStore se define como "obsoleto", lo que generalmente significa que la función se eliminará en alguna versión más nueva de la librería. La explicación anterior y esta [discusión](https://stackoverflow.com/questions/71944111/redux-createstore-is-deprecated-cannot-get-state-from-getstate-in-redux-ac) revelan que
createStore no se eliminará y se le ha dado el estado
obsoleto , quizás por motivos ligeramente incorrectos. Por lo tanto, la función no está obsoleta, pero hoy en día existe una forma nueva y preferible de hacer casi lo mismo.
+
+### Redux-notas
+
+Nuestro objetivo es modificar nuestra aplicación de notas para utilizar Redux para la gestión del estado. Sin embargo, primero cubramos algunos conceptos clave a través de una aplicación de notas simplificada.
+
+La primera versión de nuestra aplicación, escrita en el archivo
main.jsx , se ve de la siguiente manera:
+
+```js
+import ReactDOM from 'react-dom/client'
+import { createStore } from 'redux'
+
+const noteReducer = (state = [], action) => {
+ switch (action.type) {
+ case 'NEW_NOTE':
+ state.push(action.payload)
+ return state
+ default:
+ return state
+ }
+}
+
+const store = createStore(noteReducer)
+
+store.dispatch({
+ type: 'NEW_NOTE',
+ payload: {
+ content: 'the app state is in redux store',
+ important: true,
+ id: 1
+ }
+})
+
+store.dispatch({
+ type: 'NEW_NOTE',
+ payload: {
+ content: 'state changes are made with actions',
+ important: false,
+ id: 2
+ }
+})
+
+const App = () => {
+ return (
+
+
+ {store.getState().map(note => (
+
+ {note.content} {note.important ? 'important' : ''}
+
+ ))}
+
+
+ )
+}
+
+const root = ReactDOM.createRoot(document.getElementById('root'))
+
+const renderApp = () => {
+ root.render(
)
+}
+
+renderApp()
+store.subscribe(renderApp)
+```
+
+Hasta el momento la aplicación no tiene la funcionalidad para agregar nuevas notas, aunque es posible hacerlo enviando acciones
NEW\_NOTE .
+
+Ahora las acciones tienen un tipo y un campo
payload (carga), que contiene la nota a agregar:
+
+```js
+{
+ type: 'NEW_NOTE',
+ payload: {
+ content: 'state changes are made with actions',
+ important: false,
+ id: 2
+ }
+}
+```
+
+La elección del nombre del campo es arbitraria. La convención es que las acciones tengan exactamente dos campos,
type diciendo el tipo y
payload conteniendo los datos incluidos en la acción.
+
+### Funciones puras, inmutables
+
+La versión inicial del reducer es muy sencilla:
+
+```js
+const noteReducer = (state = [], action) => {
+ switch (action.type) {
+ case 'NEW_NOTE':
+ state.push(action.payload)
+ return state
+ default:
+ return state
+ }
+}
+```
+
+El estado ahora es un Array. Las acciones de tipo
NEW\_NOTE hacen que se agregue una nueva nota al estado con el método [push](https://developer.mozilla.org/es/docs/Web/JavaScript/Reference/Global_Objects/Array/push).
+
+La aplicación parece estar funcionando, pero el reducer que hemos declarado es malo. Rompe el [supuesto básico](https://redux.js.org/tutorials/essentials/part-1-overview-concepts#reducers) de que los reducers deben ser [funciones puras](https://es.wikipedia.org/wiki/Programaci%C3%B3n_funcional#Funciones_puras).
+
+Las funciones puras son aquellas que
no causan ningún efecto secundario y siempre deben devolver la misma respuesta cuando se llaman con los mismos parámetros.
+
+Agregamos una nueva nota al estado con el método _state.push(action.payload)_ que
cambia el estado del objeto-estado. Esto no está permitido. El problema se resuelve fácilmente utilizando el método [concat](https://developer.mozilla.org/es/docs/Web/JavaScript/Reference/Global_Objects/Array/concat), que crea un
nuevo array , que contiene todos los elementos del array anterior y el nuevo elemento:
+
+```js
+const noteReducer = (state = [], action) => {
+ switch (action.type) {
+ case 'NEW_NOTE':
+ return state.concat(action.payload) // highlight-line
+ default:
+ return state
+ }
+}
+```
+
+El estado de un reducer debe estar compuesto por objetos [inmutables](https://es.wikipedia.org/wiki/Objeto_inmutable). Si hay un cambio en el estado, el objeto antiguo no se cambia, sino que se
reemplaza por un objeto nuevo modificado . Esto es exactamente lo que hicimos con el nuevo reducer: el array anterior se reemplaza por el nuevo.
+
+Ampliemos nuestro reducer para que pueda manejar el cambio de importancia de una nota:
+
+```js
+{
+ type: 'TOGGLE_IMPORTANCE',
+ payload: {
+ id: 2
+ }
+}
+```
+
+Dado que todavía no tenemos ningún código que utilice esta funcionalidad, estamos expandiendo el reducer en la forma 'test driven' (guiada por pruebas).
+
+### Configurando el entorno de pruebas
+
+Tenemos que configurar primero la biblioteca de pruebas [Vitest](https://vitest.dev/) para el proyecto. Vamos a instalarla como una dependencia de desarrollo para la aplicación:
+
+```js
+npm install --save-dev vitest
+```
+
+Expandamos
package.json con un script para ejecutar las pruebas:
+
+```json
+{
+ // ...
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "lint": "eslint .",
+ "preview": "vite preview",
+ "test": "vitest" // highlight-line
+ },
+ // ...
+}
+```
+
+Para hacer las pruebas más fáciles, primero trasladaremos el código del reducer a su propio módulo, al archivo
src/reducers/noteReducer.js :
+
+```js
+const noteReducer = (state = [], action) => {
+ switch (action.type) {
+ case 'NEW_NOTE':
+ return state.concat(action.payload)
+ default:
+ return state
+ }
+}
+
+export default noteReducer
+```
+
+El archivo
main.jsx cambia de la siguiente manera:
+
+```js
+import ReactDOM from 'react-dom/client'
+import { createStore } from 'redux'
+import noteReducer from './reducers/noteReducer' // highlight-line
+
+const store = createStore(noteReducer)
+
+// ...
+```
+
+También agregaremos la librería [deep-freeze](https://www.npmjs.com/package/deep-freeze), que se puede usar para garantizar que el reducer se haya definido correctamente como una función inmutable.
+Instalemos la librería como una dependencia de desarrollo:
+
+```js
+npm install --save-dev deep-freeze
+```
+
+Ahora estamos listos para escribir pruebas.
+
+### Pruebas para noteReducer
+
+Comencemos creando una prueba para manejar la acción
NEW\_NOTE . La prueba, que definimos en el archivo
src/reducers/noteReducer.test.js , tiene el siguiente contenido:
+
+```js
+import deepFreeze from 'deep-freeze'
+import { describe, expect, test } from 'vitest'
+import noteReducer from './noteReducer'
+
+describe('noteReducer', () => {
+ test('returns new state with action NEW_NOTE', () => {
+ const state = []
+ const action = {
+ type: 'NEW_NOTE',
+ payload: {
+ content: 'the app state is in redux store',
+ important: true,
+ id: 1
+ }
+ }
+
+ deepFreeze(state)
+ const newState = noteReducer(state, action)
+
+ expect(newState).toHaveLength(1)
+ expect(newState).toContainEqual(action.payload)
+ })
+})
+```
+
+Ejecuta la prueba con
npm test . La prueba asegura que el nuevo estado devuelto por el reducer es un array que contiene un solo elemento, que es el mismo objeto que el que está en el campo
payload de la acción.
+
+El comando
deepFreeze(state) asegura que el reducer no cambie el estado del store que se le dio como parámetro. Si el reducer usa el comando _push_ para manipular el estado, la prueba no pasará
+
+
+
+Ahora crearemos una prueba para la acción
TOGGLE\_IMPORTANCE :
+
+```js
+test('returns new state with action TOGGLE_IMPORTANCE', () => {
+ const state = [
+ {
+ content: 'the app state is in redux store',
+ important: true,
+ id: 1
+ },
+ {
+ content: 'state changes are made with actions',
+ important: false,
+ id: 2
+ }
+ ]
+
+ const action = {
+ type: 'TOGGLE_IMPORTANCE',
+ payload: {
+ id: 2
+ }
+ }
+
+ deepFreeze(state)
+ const newState = noteReducer(state, action)
+
+ expect(newState).toHaveLength(2)
+
+ expect(newState).toContainEqual(state[0])
+
+ expect(newState).toContainEqual({
+ content: 'state changes are made with actions',
+ important: true,
+ id: 2
+ })
+})
+```
+
+Entonces la siguiente acción
+
+```js
+{
+ type: 'TOGGLE_IMPORTANCE',
+ payload: {
+ id: 2
+ }
+}
+```
+
+tiene que cambiar la importancia de la nota con el id 2.
+
+El reducer se expande de la siguiente manera
+
+```js
+const noteReducer = (state = [], action) => {
+ switch(action.type) {
+ case 'NEW_NOTE':
+ return state.concat(action.payload)
+ // highlight-start
+ case 'TOGGLE_IMPORTANCE': {
+ const id = action.payload.id
+ const noteToChange = state.find(n => n.id === id)
+ const changedNote = {
+ ...noteToChange,
+ important: !noteToChange.important
+ }
+ return state.map(note => (note.id !== id ? note : changedNote))
+ }
+ // highlight-end
+ default:
+ return state
+ }
+}
+```
+
+Creamos una copia de la nota cuya importancia ha cambiado con la sintaxis [de la parte 2](/es/part2/alterando_datos_en_el_servidor#cambiar-la-importancia-de-las-notas), y reemplazamos el estado con un nuevo estado que contiene todas las notas que no han cambiado y la copia de la nota cambiada
changedNote .
+
+Recapitulemos lo que sucede en el código. Primero, buscamos un objeto de nota específico, cuya importancia queremos cambiar:
+
+```js
+const noteToChange = state.find(n => n.id === id)
+```
+
+luego creamos un nuevo objeto, que es una
copia de la nota original, solo el valor del campo
important se ha cambiado a lo opuesto de lo que era:
+
+```js
+const changedNote = {
+ ...noteToChange,
+ important: !noteToChange.important
+}
+```
+
+Entonces se devuelve un nuevo estado. Lo creamos tomando todas las notas del estado anterior, excepto la nota deseada, que reemplazamos con su copia ligeramente alterada:
+
+```js
+state.map(note => (note.id !== id ? note : changedNote))
+```
+
+### Array spread syntax
+
+Debido a que ahora tenemos pruebas bastante buenas para el reducer, podemos refactorizar el código de forma segura.
+
+Agregar una nueva nota crea el estado devuelto por la función de Arrays _concat_. Echemos un vistazo a cómo podemos lograr lo mismo usando la sintaxis [array spread](https://developer.mozilla.org/es/docs/Web/JavaScript/Reference/Operators/Spread_syntax) de JavaScript:
+
+```js
+const noteReducer = (state = [], action) => {
+ switch(action.type) {
+ case 'NEW_NOTE':
+ return [...state, action.payload] // highlight-line
+ case 'TOGGLE_IMPORTANCE': {
+ // ...
+ }
+ default:
+ return state
+ }
+}
+```
+
+La sintaxis spread funciona de la siguiente manera. Si declaramos
+
+```js
+const numbers = [1, 2, 3]
+```
+
+
...numbers divide el array en elementos individuales, que se pueden colocar en otro array.
+
+```js
+[...numbers, 4, 5]
+```
+
+y el resultado es un array
[1, 2, 3, 4, 5] .
+
+Si hubiéramos colocado el array en otro array sin el spread
+
+```js
+[numbers, 4, 5]
+```
+
+el resultado habría sido
[ [1, 2, 3], 4, 5] .
+
+Cuando tomamos elementos de un array mediante la [desestructuración](https://developer.mozilla.org/es/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment), se usa una sintaxis similar para
juntar el resto de los elementos:
+
+```js
+const numbers = [1, 2, 3, 4, 5, 6]
+
+const [first, second, ...rest] = numbers
+
+console.log(first) // prints 1
+console.log(second) // prints 2
+console.log(rest) // prints [3, 4, 5, 6]
+```
+
+
+
+
+
+### Ejercicios 6.1.-6.2.
+
+Hagamos una versión simplificada del ejercicio unicafe de la parte 1. Manejemos la administración del estado con Redux.
+
+Puedes tomar el código de este repositorio https://github.com/fullstack-hy2020/unicafe-redux para la base de tu proyecto.
+
+Comienza eliminando la configuración git del repositorio clonado e instalando dependencias
+
+```bash
+cd unicafe-redux // go to the directory of cloned repository
+rm -rf .git
+npm install
+```
+
+#### 6.1: Unicafe Revisitado, paso 1
+
+Antes de implementar la funcionalidad de la UI(interfaz de usuario), implementemos la funcionalidad requerida por el store.
+
+Tenemos que guardar el número de cada tipo de feedback en el store, por lo que la forma del estado en el store es:
+
+```js
+{
+ good: 5,
+ ok: 4,
+ bad: 2
+}
+```
+
+El proyecto tiene la siguiente base para un reducer:
+
+```js
+const initialState = {
+ good: 0,
+ ok: 0,
+ bad: 0
+}
+
+const counterReducer = (state = initialState, action) => {
+ console.log(action)
+ switch (action.type) {
+ case 'GOOD':
+ return state
+ case 'OK':
+ return state
+ case 'BAD':
+ return state
+ case 'RESET':
+ return state
+ default:
+ return state
+ }
+}
+
+export default counterReducer
+```
+
+y una base para sus pruebas
+
+```js
+import deepFreeze from 'deep-freeze'
+import { describe, expect, test } from 'vitest'
+import counterReducer from './reducer'
+
+describe('unicafe reducer', () => {
+ const initialState = {
+ good: 0,
+ ok: 0,
+ bad: 0
+ }
+
+ test('should return a proper initial state when called with undefined state', () => {
+ const action = {
+ type: 'DO_NOTHING'
+ }
+
+ const newState = counterReducer(undefined, action)
+ expect(newState).toEqual(initialState)
+ })
+
+ test('good is incremented', () => {
+ const action = {
+ type: 'GOOD'
+ }
+ const state = initialState
+
+ deepFreeze(state)
+ const newState = counterReducer(state, action)
+ expect(newState).toEqual({
+ good: 1,
+ ok: 0,
+ bad: 0
+ })
+ })
+})
+```
+
+**Implementa el reducer y sus pruebas.**
+
+En las pruebas, asegúrate de que el reducer sea una función inmutable con la librería deep-freeze .
+Asegúrate de que la primera prueba proporcionada pase, porque Redux espera que el reducer devuelva el estado original cuando se llama con un primer parámetro - que representa el estado previo - con el valor undefined .
+
+Comienza expandiendo el reducer para que pasen ambas pruebas. Luego agrega el resto de las pruebas y finalmente la funcionalidad que están probando.
+
+Un buen modelo para el reducer es el ejemplo anterior de [redux-notas](/es/part6/redux_heredado#redux-notas).
+
+#### 6.2: Unicafe Revisitado, paso 2
+
+Ahora implementa la funcionalidad real de la aplicación.
+
+Tu aplicación puede tener una apariencia modesta, nada más se necesitan 3 botones y el número de calificaciones para cada tipo:
+
+
+
+
+
-Al final de esta parte, analizaremos algunas formas diferentes de administrar el estado de una aplicación.
+### Formulario no controlado
+
+Agreguemos la funcionalidad para agregar nuevas notas y cambiar su importancia:
+
+```js
+// ...
+
+const generateId = () => Number((Math.random() * 1000000).toFixed(0)) // highlight-line
+
+const App = () => {
+ // highlight-start
+ const addNote = event => {
+ event.preventDefault()
+ const content = event.target.note.value
+ event.target.note.value = ''
+ store.dispatch({
+ type: 'NEW_NOTE',
+ payload: {
+ content,
+ important: false,
+ id: generateId()
+ }
+ })
+ }
+ // highlight-end
+
+ // highlight-start
+ const toggleImportance = id => {
+ store.dispatch({
+ type: 'TOGGLE_IMPORTANCE',
+ payload: { id }
+ })
+ }
+ // highlight-end
+
+ return (
+
+ // highlight-start
+
+ // highlight-end
+
+ {store.getState().map(note => (
+ toggleImportance(note.id)}> // highlight-line
+ {note.content} {note.important ? 'important' : ''}
+
+ ))}
+
+
+ )
+}
+
+// ...
+```
+
+La implementación de ambas funcionalidades es sencilla. Cabe señalar que
no hemos vinculado el estado de los campos del formulario al estado del componente
App como lo hicimos anteriormente. React llama a este tipo de formulario [no controlado](https://es.react.dev/reference/react-dom/components/input#controlling-an-input-with-a-state-variable).
+
+>Los formularios no controlados tienen ciertas limitaciones (por ejemplo, no son posibles los mensajes de error dinámicos o la desactivación del botón de envío en función de input). Sin embargo, son adecuados para nuestras necesidades actuales.
+
+Puedes leer más sobre formularios no controlados [aquí](https://goshakkk.name/controlled-vs-uncontrolled-inputs-react/).
+
+El método para agregar nuevas notas es simple, simplemente envía la acción para agregar notas:
+
+```js
+addNote = event => {
+ event.preventDefault()
+ const content = event.target.note.value
+ event.target.note.value = ''
+ store.dispatch({
+ type: 'NEW_NOTE',
+ payload: {
+ content,
+ important: false,
+ id: generateId()
+ }
+ })
+}
+```
+
+Podemos obtener el contenido de la nueva nota directamente desde el campo del formulario. Debido a que el campo tiene un nombre, podemos acceder al contenido a través del objeto del evento
event.target.note.value .
+
+```js
+const content = event.target.note.value
+```
+
+```js
+
+```
+
+La importancia de una nota se puede cambiar haciendo clic en su nombre. El controlador de eventos es muy simple:
+
+```js
+toggleImportance = id => {
+ store.dispatch({
+ type: 'TOGGLE_IMPORTANCE',
+ payload: { id }
+ })
+}
+```
+
+### Action creators
+
+Comenzamos a notar que, incluso en aplicaciones tan simples como la nuestra, usar Redux puede simplificar el código de la interfaz. Sin embargo, podemos hacerlo mucho mejor.
+
+En realidad, no es necesario que los componentes de React conozcan los tipos y formas de acción de Redux.
+Separemos la creación de acciones en sus propias funciones:
+
+```js
+const createNote = content => {
+ return {
+ type: 'NEW_NOTE',
+ payload: {
+ content,
+ important: false,
+ id: generateId()
+ }
+ }
+}
+
+const toggleImportanceOf = id => {
+ return {
+ type: 'TOGGLE_IMPORTANCE',
+ payload: { id }
+ }
+}
+```
+
+Las funciones que crean acciones se denominan [action creators](https://redux.js.org/tutorials/essentials/part-1-overview-concepts#action-creators) (creadores de acciones).
+
+El componente
App ya no tiene que saber nada sobre la representación interna de las acciones, solo obtiene la acción correcta llamando a la función creadora:
+
+```js
+const App = () => {
+ const addNote = event => {
+ event.preventDefault()
+ const content = event.target.note.value
+ event.target.note.value = ''
+ store.dispatch(createNote(content)) // highlight-line
+
+ }
+
+ const toggleImportance = id => {
+ store.dispatch(toggleImportanceOf(id))// highlight-line
+ }
+
+ // ...
+}
+```
+
+### Reenviando Redux-Store a varios componentes
+
+Aparte del reducer, nuestra aplicación está en un solo archivo. Esto, por supuesto, no es sensato, y deberíamos separar
App en su propio módulo.
+
+Ahora la pregunta es, ¿cómo puede
App acceder al store después de moverlo? Y en términos más generales, cuando un componente está compuesto por muchos componentes más pequeños, debe haber una forma para que todos los componentes accedan al store.
+Hay varias formas de compartir el store redux con los componentes. Primero veremos la forma más nueva, y posiblemente la más fácil, usando la api de [hooks](https://react-redux.js.org/api/hooks) de la librería [react-redux](https://react-redux.js.org/).
+
+Primero instalamos react-redux
+
+```bash
+npm install react-redux
+```
+
+A continuación, organicemos el código de la aplicación de forma más sensata en varios archivos. Después de los cambios, _main.jsx_ queda así:
+
+```js
+import ReactDOM from 'react-dom/client'
+import { createStore } from 'redux'
+import { Provider } from 'react-redux'
+
+import App from './App'
+import noteReducer from './reducers/noteReducer'
+
+const store = createStore(noteReducer)
+
+ReactDOM.createRoot(document.getElementById('root')).render(
+
+
+
+)
+```
+
+Ten en cuenta que la aplicación ahora se define como un elemento secundario de un componente [Provider](https://react-redux.js.org/api/provider) (proveedor) proporcionado por la librería react-redux.
+El store de la aplicación se entrega al Provider como su atributo store.
+
+```js
+const store = createStore(noteReducer)
+
+ReactDOM.createRoot(document.getElementById('root')).render(
+
// highlight-line
+
+ // highlight-line
+)
+```
+
+La definición de los action creators se ha movido al archivo
reducers/noteReducer.js donde se define el reducer. El archivo se ve así:
+
+```js
+const noteReducer = (state = [], action) => {
+ switch (action.type) {
+ case 'NEW_NOTE':
+ return [...state, action.payload]
+ case 'TOGGLE_IMPORTANCE': {
+ const id = action.payload.id
+ const noteToChange = state.find(n => n.id === id)
+ const changedNote = {
+ ...noteToChange,
+ important: !noteToChange.important
+ }
+ return state.map(note => (note.id !== id ? note : changedNote))
+ }
+ default:
+ return state
+ }
+}
+
+const generateId = () =>
+ Number((Math.random() * 1000000).toFixed(0))
+
+export const createNote = (content) => {
+ return {
+ type: 'NEW_NOTE',
+ payload: {
+ content,
+ important: false,
+ id: generateId()
+ }
+ }
+}
+
+export const toggleImportanceOf = (id) => {
+ return {
+ type: 'TOGGLE_IMPORTANCE',
+ payload: { id }
+ }
+}
+
+export default noteReducer
+```
+
+Si la aplicación tiene muchos componentes que necesitan el store, el componente
App debe pasar
store como props a todos esos componentes.
+
+El módulo ahora tiene varios comandos de [export](https://developer.mozilla.org/es/docs/Web/JavaScript/Reference/Statements/export).
+
+La función del reducer todavía se devuelve con el comando de
export default , por lo que el reducer se puede importar de la forma habitual:
+
+```js
+import noteReducer from './reducers/noteReducer'
+```
+
+Un módulo solo puede tener
un default export , pero varias exportaciones "normales"
+
+```js
+export const createNote = (content) => {
+ // ...
+}
+
+export const toggleImportanceOf = (id) => {
+ // ...
+}
+```
+
+Las funciones exportadas normalmente (no como los default) se pueden importar con la sintaxis de llaves:
+
+```js
+import { createNote } from '../../reducers/noteReducer'
+```
+
+Código para el componente
App
+
+```js
+import { createNote, toggleImportanceOf } from './reducers/noteReducer'
+import { useSelector, useDispatch } from 'react-redux'
+
+
+const App = () => {
+ const dispatch = useDispatch()
+ const notes = useSelector(state => state)
+
+ const addNote = (event) => {
+ event.preventDefault()
+ const content = event.target.note.value
+ event.target.note.value = ''
+ dispatch(createNote(content))
+ }
+
+ const toggleImportance = (id) => {
+ dispatch(toggleImportanceOf(id))
+ }
+
+ return (
+
+
+
+ {notes.map(note =>
+ toggleImportance(note.id)}
+ >
+ {note.content} {note.important ? 'important' : ''}
+
+ )}
+
+
+ )
+}
+
+export default App
+```
+
+Hay algunas cosas a tener en cuenta en el código. Anteriormente, el código despachaba acciones invocando al método dispatch de redux-store:
+
+```js
+store.dispatch({
+ type: 'TOGGLE_IMPORTANCE',
+ payload: { id }
+})
+```
-Continuemos con la aplicación de notas. Nos centraremos en la comunicación con el servidor. Comencemos la aplicación desde cero. La primera versión es la siguiente:
+Ahora lo hace con la función
dispatch del hook [useDispatch](https://react-redux.js.org/api/hooks#usedispatch).
```js
+import { useSelector, useDispatch } from 'react-redux' // highlight-line
+
const App = () => {
- const addNote = async (event) => {
+ const dispatch = useDispatch() // highlight-line
+ // ...
+
+ const toggleImportance = (id) => {
+ dispatch(toggleImportanceOf(id)) // highlight-line
+ }
+
+ // ...
+}
+```
+
+El hook
useDispatch proporciona acceso a cualquier componente de React a la función dispatch de redux-store definida en
main.jsx . Esto permite que todos los componentes realicen cambios en el estado de Redux store.
+
+El componente puede acceder a las notas almacenadas en el store con el hook [useSelector](https://react-redux.js.org/api/hooks#useselector) de la librería react-redux.
+
+```js
+import { useSelector, useDispatch } from 'react-redux' // highlight-line
+
+const App = () => {
+ // ...
+ const notes = useSelector(state => state) // highlight-line
+ // ...
+}
+```
+
+
useSelector recibe una función como parámetro. La función busca o selecciona datos del store de Redux.
+Aquí necesitamos todas las notas, por lo que nuestra función de selector devuelve el estado completo:
+
+
+```js
+state => state
+```
+
+que es una abreviatura de
+
+```js
+(state) => {
+ return state
+}
+```
+
+Por lo general, las funciones de selector son un poco más interesantes y solo devuelven partes seleccionadas del contenido del store redux. Por ejemplo, podríamos devolver solo notas marcadas como importantes:
+
+```js
+const importantNotes = useSelector(state => state.filter(note => note.important))
+```
+
+La versión actual de la aplicación se puede encontrar en [GitHub](https://github.com/fullstack-hy2020/redux-notes/tree/part6-0), en la rama
part6-0 .
+
+### Más componentes
+
+Separemos el formulario responsable de crear una nueva nota en su propio componente en el archivo
src/components/NoteForm.jsx :
+
+```js
+import { useDispatch } from 'react-redux'
+import { createNote } from '../reducers/noteReducer'
+
+const NoteForm = () => {
+ const dispatch = useDispatch()
+
+ const addNote = (event) => {
event.preventDefault()
const content = event.target.note.value
event.target.note.value = ''
- console.log(content)
+ dispatch(createNote(content))
+ }
+
+ return (
+
+ )
+}
+
+export default NoteForm
+```
+
+A diferencia del código de React que hicimos sin Redux, el controlador de eventos para cambiar el estado de la aplicación (que ahora vive en Redux) se ha movido de
App a un componente hijo. La lógica para cambiar el estado en Redux todavía está claramente separada de toda la parte de React de la aplicación.
+
+También separaremos la lista de notas y mostraremos una sola nota en sus propios componentes. Coloquemos ambos en el archivo
src/components/Notes.jsx :
+
+```js
+import { useDispatch, useSelector } from 'react-redux'
+import { toggleImportanceOf } from '../reducers/noteReducer'
+
+const Note = ({ note, handleClick }) => {
+ return (
+
+ {note.content}
+ {note.important ? 'important' : ''}
+
+ )
+}
+
+const Notes = () => {
+ const dispatch = useDispatch()
+ const notes = useSelector(state => state)
+
+ return (
+
+ {notes.map(note => (
+ dispatch(toggleImportanceOf(note.id))}
+ />
+ ))}
+
+ )
+}
+
+export default Notes
+```
+
+La lógica para cambiar la importancia de una nota ahora está en el componente que administra la lista de notas.
+
+Solo queda una pequeña cantidad de código en el archivo
App.jsx :
+
+```js
+import NoteForm from './components/NoteForm'
+import Notes from './components/Notes'
+
+const App = () => {
+ return (
+
+
+
+
+ )
+}
+
+export default App
+```
+
+
Note , responsable de representar una sola nota, es muy simple y no es consciente de que el controlador de eventos que obtiene como props despacha una acción. Este tipo de componentes se denominan [presentacionales](https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0) en la terminología de React.
+
+
Notes , por otro lado, es un componente [contenedor](https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0), ya que contiene cierta lógica de aplicación: define lo que hacen los controladores de eventos de los componentes
Note y coordina la configuración de los componentes
presentacionales , es decir, los
Note s.
+
+El código de la aplicación Redux se puede encontrar en [GitHub](https://github.com/fullstack-hy2020/redux-notes/tree/part6-1), en la rama
part6-1 .
+
+
+
+
+
+### Ejercicios 6.3.-6.8.
+
+Hagamos una nueva versión de la aplicación de votación de anécdotas de la parte 1. Toma el proyecto de este repositorio https://github.com/fullstack-hy2020/redux-anecdotes como base de tu solución.
+
+Si clonas el proyecto en un repositorio de git existente,
elimina la configuración de git de la aplicación clonada:
+
+```bash
+cd redux-anecdotes // go to the cloned repository
+rm -rf .git
+```
+
+La aplicación se puede iniciar como de costumbre, pero primero debes instalar las dependencias:
+
+```bash
+npm install
+npm run dev
+```
+
+Después de completar estos ejercicios, tu aplicación debería verse así:
+
+
+
+#### 6.3: Anécdotas, paso 1
+
+Implementa la funcionalidad para votar anécdotas. La cantidad de votos debe guardarse en una store de Redux.
+
+#### 6.4: Anécdotas, paso 2
+
+Implementa la funcionalidad para agregar nuevas anécdotas.
+
+Puedes mantener el formulario no controlado, como hicimos [antes](/es/part6/redux_heredado#formulario-no-controlado).
+
+#### 6.5: Anécdotas, paso 3
+
+Asegúrate de que las anécdotas estén ordenadas por número de votos.
+
+#### 6.6: Anécdotas, paso 4
+
+Si aún no lo has hecho, separa la creación de objetos de acción en funciones [action creator](https://read.reduxbook.com/markdown/part1/04-action-creators.html) y colócalas en el archivo
src/reducers/anecdoteReducer.js , como hemos hecho desde la sección [action creators](/es/part6/redux_heredado#action-creators).
+
+#### 6.7: Anécdotas, paso 5
+
+Separa la creación de nuevas anécdotas en su propio componente llamado
AnecdoteForm . Mueve toda la lógica para crear una nueva anécdota en este nuevo componente.
+
+#### 6.8: Anécdotas, paso 6
+
+Separa el renderizado de la lista de anécdotas en su propio componente llamado
AnecdoteList . Mueve toda la lógica relacionada con la votación de una anécdota a este nuevo componente.
+
+Ahora, el componente
App debería verse así:
+
+```js
+import AnecdoteForm from './components/AnecdoteForm'
+import AnecdoteList from './components/AnecdoteList'
+
+const App = () => {
+ return (
+
+ )
+}
+
+export default App
+```
+
+
+
+
+
+Continuemos nuestro trabajo con la [versión Redux](/es/part6/redux_heredado#redux-notas) simplificada de nuestra aplicación de notas.
+
+Para facilitar nuestro desarrollo, cambiemos nuestro reducer para que el store se inicialice con un estado que contenga un par de notas:
+
+```js
+// highlight-start
+const initialState = [
+ {
+ content: 'reducer defines how redux store works',
+ important: true,
+ id: 1,
+ },
+ {
+ content: 'state of store can contain any data',
+ important: false,
+ id: 2,
+ },
+]
+//highlight-end
+
+const noteReducer = (state = initialState, action) => { // highlight-line
+ // ...
+}
+
+// ...
+
+export default noteReducer
+```
+
+### Store con estado complejo
+
+Implementemos el filtrado de las notas que se muestran al usuario. La interfaz de usuario para los filtros se implementará con [botones de radio](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/radio):
+
+
+
+Comencemos con una implementación muy simple y directa:
+
+```js
+import NoteForm from './components/NoteForm'
+import Notes from './components/Notes'
+
+const App = () => {
+//highlight-start
+ const filterSelected = (value) => {
+ console.log(value)
+ }
+//highlight-end
+
+ return (
+
+ )
+}
+```
+
+Dado que el atributo
name de todos los botones de radio es el mismo, estos forman un
button group (grupo de botones) en el que solo se puede seleccionar una opción.
+
+Los botones tienen un controlador de cambios que actualmente solo imprime el string asociado con el botón en el que se hizo clic en la consola.
+
+En la siguiente sección, vamos a implementar el filtrado almacenando las notas y
el valor del filtro en el store de redux. Cuando terminemos, nos gustaría que el estado del store se viera así:
+
+```js
+{
+ notes: [
+ { content: 'reducer defines how redux store works', important: true, id: 1},
+ { content: 'state of store can contain any data', important: false, id: 2}
+ ],
+ filter: 'IMPORTANT'
+}
+```
+
+Solo el array de notas se almacenaba en el estado de la implementación anterior de nuestra aplicación. En la nueva implementación, el objeto de estado tiene dos propiedades,
notes que contienen el array de notas y
filter que contiene un string que indica qué notas deben mostrarse al usuario.
+
+### Reducers combinados
+
+Podríamos modificar nuestro reducer actual para hacer frente a la nueva forma del estado. Sin embargo, una mejor solución en esta situación es definir un nuevo reducer separado para el estado del filtro:
+
+```js
+const filterReducer = (state = 'ALL', action) => {
+ switch (action.type) {
+ case 'SET_FILTER':
+ return action.payload
+ default:
+ return state
+ }
+}
+
+export const filterChange = filter => {
+ return {
+ type: 'SET_FILTER',
+ payload: filter
}
+}
+
+export default filterReducer
+```
+
+Las acciones para cambiar el estado del filtro se ven así:
+
+```js
+{
+ type: 'SET_FILTER',
+ payload: 'IMPORTANT'
+}
+```
+
+Podemos crear el reducer que nuestra aplicación realmente utilizara al combinar los dos reducers existentes con la función [combineReducers](https://redux.js.org/api/combinereducers).
+
+Definamos el reducer combinado en el archivo
main.jsx :
+
+```js
+import ReactDOM from 'react-dom/client'
+import { Provider } from 'react-redux'
+import { createStore, combineReducers } from 'redux'
+
+import App from './App'
+import filterReducer from './reducers/filterReducer'
+import noteReducer from './reducers/noteReducer'
+
+const reducer = combineReducers({
+ notes: noteReducer,
+ filter: filterReducer
+})
+
+const store = createStore(reducer)
+
+console.log(store.getState())
+
+ReactDOM.createRoot(document.getElementById('root')).render(
+
+
+
+)
+```
+
+Dado que nuestra aplicación se rompe por completo en este punto, renderizamos un elemento
div vacío en lugar del componente
App .
+
+El estado del store se imprime en la consola:
+
+
+
+Como podemos ver en el resultado, ¡el store tiene la forma exacta que queríamos!
+
+Echemos un vistazo más de cerca a cómo se crea el reducer combinado:
+
+```js
+const reducer = combineReducers({
+ notes: noteReducer,
+ filter: filterReducer,
+})
+```
+
+El estado del store definido por este reducer es un objeto con dos propiedades:
notes y
filter . El valor de la propiedad
notes es definido por
noteReducer , que no tiene que lidiar con las otras propiedades del estado. Asimismo, la propiedad
filter es administrada por
filterReducer .
+
+Antes de realizar más cambios en el código, echemos un vistazo a cómo las diferentes acciones cambian el estado del store definido por el reducer combinado. Agreguemos lo siguiente al archivo
main.jsx :
+
+```js
+// ...
+
+const store = createStore(reducer)
+
+console.log(store.getState())
+
+// highlight-start
+import { createNote } from './reducers/noteReducer'
+import { filterChange } from './reducers/filterReducer'
+// highlight-end
+
+// highlight-start
+store.subscribe(() => console.log(store.getState()))
+store.dispatch(filterChange('IMPORTANT'))
+store.dispatch(createNote('combineReducers forms one reducer from many simple reducers'))
+// highlight-end
+
+ReactDOM.createRoot(document.getElementById('root')).render(
+
+
+
+)
+```
+
+Al simular la creación de una nota y cambiar el estado del filtro de esta manera, el estado del store se muestra en la consola después de cada cambio que se realiza en el store:
+
+
+
+En este punto es bueno darse cuenta de un pequeño pero importante detalle. Si agregamos un console log
al comienzo de ambos reducers (noteReducer y filterReducer) :
+
+```js
+const filterReducer = (state = 'ALL', action) => {
+ console.log('ACTION: ', action) // highlight-line
+ // ...
+}
+```
+
+Según el resultado de la consola, uno podría tener la impresión de que cada acción se duplica:
+
+
+
+¿Hay algún bug en nuestro código? No. El reducer combinado funciona de tal manera que cada
acción es controlada en
cada parte del reducer combinado, o en otras palabras, cada reducer "escucha" a todas las acciones despachadas y hace algo con ellas si así se lo hemos instruido. Normalmente, solo un reducer está interesado en una acción determinada, pero hay situaciones en las que varios reducers cambian sus respectivas partes del estado en función de la misma acción.
+
+### Terminando los filtros
+
+Terminemos la aplicación para que utilice el reducer combinado. Comenzamos cambiando la renderización de la aplicación y conectando el store a la aplicación en el archivo
main.jsx :
+
+```js
+import ReactDOM from 'react-dom/client'
+import { Provider } from 'react-redux'
+import { createStore, combineReducers } from 'redux'
+
+import App from './App'
+import filterReducer from './reducers/filterReducer'
+import noteReducer from './reducers/noteReducer'
- const toggleImportance = (note) => {
- console.log('toggle importance of', note.id)
- }
+const reducer = combineReducers({
+ notes: noteReducer,
+ filter: filterReducer
+})
- const notes = []
+const store = createStore(reducer)
- return (
-
-
Notes app
-
- {notes.map((note) => (
- toggleImportance(note)}>
- {note.content}
- {note.important ? 'important' : ''}
-
- ))}
-
- )
-}
+console.log(store.getState())
-export default App
+ReactDOM.createRoot(document.getElementById('root')).render(
+
+
+
+)
```
-El código inicial está en GitHub en este [repositorio](https://github.com/fullstack-hy2020/query-notes/tree/part6-0), en la rama
part6-0 .
-
-### Administrando datos en el servidor con la librería React Query
+A continuación, solucionemos un error causado por el código que espera que la store de aplicaciones sea un array de notas:
-Ahora usaremos la librería [React Query](https://tanstack.com/query/latest) para almacenar y administrar los datos obtenidos del servidor. La última versión de la librería también es llamada TanStack Query, pero seguiremos usando su nombre tradicional.
+
-Instala la librería con el comando
+Es una solución fácil. Debido a que las notas están en el campo
notes del store, solo tenemos que hacer un pequeño cambio en la función de selector:
-```bash
-npm install @tanstack/react-query
+```js
+const Notes = () => {
+ const dispatch = useDispatch()
+ const notes = useSelector(state => state.notes) // highlight-line
+
+ return(
+
+ {notes.map(note =>
+
+ dispatch(toggleImportanceOf(note.id))
+ }
+ />
+ )}
+
+ )
+}
```
-Se necesitan agregar algunas cosas en el archivo
main.jsx para pasar las funciones de la librería a toda la aplicación:
+Anteriormente, la función de selector devolvía el estado completo del store:
```js
-import { createRoot } from 'react-dom/client'
-import { QueryClient, QueryClientProvider } from '@tanstack/react-query' // highlight-line
-
-import App from './App.jsx'
+const notes = useSelector(state => state)
+```
-const queryClient = new QueryClient() // highlight-line
+Y ahora devuelve solo su campo
notes
-createRoot(document.getElementById('root')).render(
-
// highlight-line
-
- // highlight-line
-)
+```js
+const notes = useSelector(state => state.notes)
```
-Usemos [JSON Server](https://github.com/typicode/json-server) como en las partes anteriores para simular el backend. JSON Server está preconfigurado en el proyecto de ejemplo, y la raíz del proyecto contiene un archivo
db.json que por defecto tiene dos notas. Puedes iniciar el servidor con:
+Extraigamos el filtro de visibilidad en su propio componente
src/components/VisibilityFilter.jsx :
```js
-npm run server
+import { useDispatch } from 'react-redux'
+import { filterChange } from '../reducers/filterReducer'
+
+const VisibilityFilter = () => {
+ const dispatch = useDispatch()
+
+ return (
+
+ dispatch(filterChange('ALL'))}
+ />
+ all
+ dispatch(filterChange('IMPORTANT'))}
+ />
+ important
+ dispatch(filterChange('NONIMPORTANT'))}
+ />
+ nonimportant
+
+ )
+}
+
+export default VisibilityFilter
```
-Ahora podemos recuperar las notas en el componente
App . El código se expande de la siguiente manera:
+Con el nuevo componente,
App se puede simplificar de la siguiente manera:
```js
-import { useQuery } from '@tanstack/react-query' // highlight-line
+import NoteForm from './components/NoteForm'
+import Notes from './components/Notes'
+import VisibilityFilter from './components/VisibilityFilter'
const App = () => {
- const addNote = async (event) => {
- event.preventDefault()
- const content = event.target.note.value
- event.target.note.value = ''
- console.log(content)
- }
+ return (
+
+
+
+
+
+ )
+}
- const toggleImportance = (note) => {
- console.log('toggle importance of', note.id)
- }
+export default App
+```
+
+La implementación es bastante sencilla. Al hacer clic en los diferentes radio buttons, cambia el estado de la propiedad
filter del store.
+
+Cambiemos el componente
Notes para incorporar el filtro:
+```js
+const Notes = () => {
+ const dispatch = useDispatch()
// highlight-start
- const result = useQuery({
- queryKey: ['notes'],
- queryFn: async () => {
- const response = await fetch('http://localhost:3001/notes')
- if (!response.ok) {
- throw new Error('Failed to fetch notes')
- }
- return await response.json()
+ const notes = useSelector(state => {
+ if (state.filter === 'ALL') {
+ return state.notes
}
+ return state.filter === 'IMPORTANT'
+ ? state.notes.filter(note => note.important)
+ : state.notes.filter(note => !note.important)
})
-
- console.log(JSON.parse(JSON.stringify(result)))
-
- if (result.isLoading) {
- return
loading data...
- }
-
- const notes = result.data
// highlight-end
return (
- // ...
+
+ {notes.map(note => (
+ dispatch(toggleImportanceOf(note.id))}
+ />
+ ))}
+
)
}
```
-La obtención de datos del servidor se realiza, como en el capítulo anterior, usando el método
fetch de la Fetch API. Sin embargo, la llamada al método ahora está envuelta en una [query](https://tanstack.com/query/latest/docs/react/guides/queries) (consulta) formada con la función [useQuery](https://tanstack.com/query/latest/docs/react/reference/useQuery). La llamada a
useQuery toma como parámetro un objeto con los campos
queryKey y
queryFn . El valor del campo
queryKey es un array que contiene el string
notes . Actúa como la [clave](https://tanstack.com/query/latest/docs/react/guides/query-keys) para la query definida, es decir, la lista de notas.
+Solo realizamos cambios en la función de selector, que solía ser
-El valor devuelto por la función
useQuery es un objeto que indica el estado de la query. La salida a la consola ilustra la situación:
-
-
+```js
+useSelector(state => state.notes)
+```
-Es decir, la primera vez que se renderiza el componente, la query todavía está en estado
loading , es decir, la solicitud HTTP asociada está pendiente. En esta etapa, solo se procesa lo siguiente:
+Simplifiquemos el selector desestructurando los campos del estado que recibe como parámetro:
-```html
-
loading data...
+```js
+const notes = useSelector(({ filter, notes }) => {
+ if ( filter === 'ALL' ) {
+ return notes
+ }
+ return filter === 'IMPORTANT'
+ ? notes.filter(note => note.important)
+ : notes.filter(note => !note.important)
+})
```
-Sin embargo, la solicitud HTTP se completa tan rápido que ni siquiera Max Verstappen podría ver el texto. Cuando se completa la solicitud, el componente se renderiza de nuevo. La query está en el estado
success en la segunda renderización, y el campo
data del objeto de la query contiene los datos devueltos por la solicitud, es decir, la lista de notas que se muestran en la pantalla.
+Hay un pequeño defecto cosmético en nuestra aplicación. Aunque el filtro está configurado en
ALL de forma predeterminada, el radio button asociado no está seleccionado. Naturalmente, este problema se puede solucionar, pero como se trata de un error desagradable pero, en última instancia, inofensivo, dejaremos la solución para más adelante.
-Entonces, la aplicación recupera datos del servidor y los renderiza en la pantalla sin usar los Hooks de React
useState y
useEffect utilizados en los capítulos 2-5. Los datos en el servidor ahora están completamente bajo la administración de la librería React Query, ¡y la aplicación no necesita el estado definido con el Hook de React
useState en absoluto!
+La versión actual de la aplicación se puede encontrar en [GitHub](https://github.com/fullstack-hy2020/redux-notes/tree/part6-2), en la rama
part6-2 .
-Movamos la función que realiza la solicitud HTTP a su propio archivo
src/requests.js
+
-```js
-const baseUrl = 'http://localhost:3001/notes'
+
-export const getNotes = async () => {
- const response = await fetch(baseUrl)
- if (!response.ok) {
- throw new Error('Failed to fetch notes')
- }
- return await response.json()
-}
-```
+### Ejercicio 6.9
-El componente
App ahora se ha simplificado un poco:
+#### 6.9 Mejores Anécdotas, paso 7
-```js
-import { useQuery } from '@tanstack/react-query'
-import { getNotes } from './requests' // highlight-line
+Implementa el filtrado para las anécdotas que se muestran al usuario.
-const App = () => {
- // ...
+
- const result = useQuery({
- queryKey: ['notes'],
- queryFn: getNotes // highlight-line
- })
+Almacena el estado del filtro en el store de Redux. Se recomienda crear un nuevo reducer, action creators y un reducer combinado para el store utilizando la función
combineReducers .
- // ...
+Crea un nuevo componente
Filter para mostrar los filtros. Puedes utilizar el siguiente código como punto de partida:
+
+```js
+const Filter = () => {
+ const handleChange = (event) => {
+ // input-field value is in variable event.target.value
+ }
+ const style = {
+ marginBottom: 10
+ }
+
+ return (
+
+ filter
+
+ )
}
+
+export default Filter
```
-El código actual de la aplicación está en [GitHub](https://github.com/fullstack-hy2020/query-notes/tree/part6-1) en la rama
part6-1 .
+
-### Sincronizando datos con el servidor usando React Query
+
-Los datos ya se han recuperado correctamente del servidor. A continuación, nos aseguraremos de que los datos agregados y modificados se almacenen en el servidor. Comencemos agregando nuevas notas.
+### Redux Toolkit y refactorizando la configuración del Store
-Hagamos una función
createNote en el archivo
requests.js para guardar nuevas notas:
+Como hemos visto hasta ahora, la implementación de la gestión del estado y la configuración de Redux requiere bastante esfuerzo. Esto se manifiesta, por ejemplo, en el código relacionado con el reducer y el action creator, que tiene un código un tanto repetitivo. [Redux Toolkit](https://redux-toolkit.js.org/) es una librería que resuelve estos problemas comunes relacionados con Redux. La librería, por ejemplo, simplifica enormemente la configuración del store de Redux y ofrece una gran variedad de herramientas para facilitar la gestión del estado.
-```js
-const baseUrl = 'http://localhost:3001/notes'
+Comencemos a usar Redux Toolkit en nuestra aplicación refactorizando el código existente. Primero, necesitaremos instalar la librería:
-export const getNotes = async () => {
- const response = await fetch(baseUrl)
- if (!response.ok) {
- throw new Error('Failed to fetch notes')
- }
- return await response.json()
-}
+```bash
+npm install @reduxjs/toolkit
+```
-// highlight-start
-export const createNote = async (newNote) => {
- const options = {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(newNote)
- }
-
- const response = await fetch(baseUrl, options)
-
- if (!response.ok) {
- throw new Error('Failed to create note')
+A continuación, abre el archivo
main.jsx que actualmente crea la store de Redux. En lugar de la función
createStore de Redux, creemos el Store usando la función [configureStore](https://redux-toolkit.js.org/api/configureStore) de Redux Toolkit:
+
+```js
+import ReactDOM from 'react-dom/client'
+import { Provider } from 'react-redux'
+import { configureStore } from '@reduxjs/toolkit' // highlight-line
+
+import App from './App'
+import filterReducer from './reducers/filterReducer'
+import noteReducer from './reducers/noteReducer'
+
+ // highlight-start
+const store = configureStore({
+ reducer: {
+ notes: noteReducer,
+ filter: filterReducer
}
-
- return await response.json()
-}
+})
// highlight-end
+
+console.log(store.getState())
+
+ReactDOM.createRoot(document.getElementById('root')).render(
+
+
+
+)
```
-El componente
App cambiará de la siguiente manera
+Ya nos deshicimos de algunas líneas de código, ya no necesitamos la función
combineReducers para crear el reducer del store. Pronto veremos que la función
configureStore tiene muchos beneficios adicionales, como la integración sin esfuerzo de herramientas de desarrollo y muchas librerías de uso común sin necesidad de configuración adicional.
+Limpiemos aún más el archivo
main.jsx moviendo el código relacionado con la creación del store de Redux a su propio archivo. Creemos un nuevo archivo
src/store.js :
```js
-import { useQuery, useMutation } from '@tanstack/react-query' // highlight-line
-import { getNotes, createNote } from './requests' // highlight-line
+import { configureStore } from '@reduxjs/toolkit'
-const App = () => {
- //highlight-start
- const newNoteMutation = useMutation({
- mutationFn: createNote,
- })
- // highlight-end
+import noteReducer from './reducers/noteReducer'
+import filterReducer from './reducers/filterReducer'
- const addNote = async (event) => {
- event.preventDefault()
- const content = event.target.note.value
- event.target.note.value = ''
- newNoteMutation.mutate({ content, important: true }) // highlight-line
+const store = configureStore({
+ reducer: {
+ notes: noteReducer,
+ filter: filterReducer
}
+})
- //
-
-}
+export default store
```
-Para crear una nueva nota, se define una [mutación](https://tanstack.com/query/latest/docs/react/guides/mutations) usando la función [useMutation](https://tanstack.com/query/latest/docs/react/reference/useMutation):
+Después de los cambios, el contenido del archivo
main.jsx es el siguiente:
```js
-const newNoteMutation = useMutation({
- mutationFn: createNote,
-})
+import ReactDOM from 'react-dom/client'
+import { Provider } from 'react-redux'
+
+import App from './App'
+import store from './store'
+
+ReactDOM.createRoot(document.getElementById('root')).render(
+
+
+
+)
```
-El parámetro es la función que agregamos al archivo
requests.js , que usa la Fetch API para enviar una nueva nota al servidor.
+### Redux Toolkit y refactorizando los reducers
-El controlador de eventos
addNote realiza la mutación llamando a la función
mutate del objeto de mutación y pasando la nueva nota como parámetro:
+Pasemos a refactorizar los reducers, lo que trae consigo los beneficios de Redux Toolkit. Con Redux Toolkit, podemos crear fácilmente reducers y action creators relacionados utilizando la función [createSlice](https://redux-toolkit.js.org/api/createSlice). Podemos usar la función
createSlice para refactorizar el reducer y los action creators en el archivo
reducers/noteReducer.js de la siguiente manera:
```js
-newNoteMutation.mutate({ content, important: true })
-```
+import { createSlice } from '@reduxjs/toolkit' // highlight-line
-Nuestra solución es buena. Excepto que no funciona. La nueva nota se guarda en el servidor, pero no se actualiza en la pantalla.
+const initialState = [
+ {
+ content: 'reducer defines how redux store works',
+ important: true,
+ id: 1,
+ },
+ {
+ content: 'state of store can contain any data',
+ important: false,
+ id: 2,
+ },
+]
-Para renderizar una nueva nota también, debemos decirle a React Query que el resultado antiguo de la query cuya clave es el string
notes debe ser [invalidado](https://tanstack.com/query/latest/docs/react/guides/invalidations-from-mutations).
+const generateId = () =>
+ Number((Math.random() * 1000000).toFixed(0))
-Afortunadamente, la invalidación es fácil, se puede hacer definiendo la función de callback
onSuccess apropiada para la mutación:
+// highlight-start
+const noteSlice = createSlice({
+ name: 'notes',
+ initialState,
+ reducers: {
+ createNote(state, action) {
+ const content = action.payload
+
+ state.push({
+ content,
+ important: false,
+ id: generateId(),
+ })
+ },
+ toggleImportanceOf(state, action) {
+ const id = action.payload
+
+ const noteToChange = state.find(n => n.id === id)
+
+ const changedNote = {
+ ...noteToChange,
+ important: !noteToChange.important
+ }
-```js
-import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' // highlight-line
-import { getNotes, createNote } from './requests'
+ return state.map(note =>
+ note.id !== id ? note : changedNote
+ )
+ }
+ },
+})
+// highlight-end
-const App = () => {
- const queryClient = useQueryClient() // highlight-line
+// highlight-start
+export const { createNote, toggleImportanceOf } = noteSlice.actions
+export default noteSlice.reducer
+// highlight-end
+```
- const newNoteMutation = useMutation({
- mutationFn: createNote,
- onSuccess: () => { // highlight-line
- queryClient.invalidateQueries({ queryKey: ['notes'] }) // highlight-line
- }, // highlight-line
- })
+El parámetro
name de la función
createSlice define el prefijo que se utiliza en los valores de tipo de la acción. Por ejemplo, la acción
createNote definida más adelante tendrá el valor de tipo
notes/createNote . Es una buena práctica dar al parámetro un valor que sea único entre los reducers. De esta forma no habrá colisiones inesperadas entre los valores de tipo de acción de la aplicación.
+El parámetro
initialState define el estado inicial del reducer.
+El parámetro
reducers toma al propio reducer como un objeto, cuyas funciones manejan los cambios de estado causados por ciertas acciones. Ten en cuenta que
action.payload en la función contiene el argumento proporcionado al llamar al creador de la acción:
- // ...
-}
+```js
+dispatch(createNote('Redux Toolkit is awesome!'))
```
-Ahora que la mutación se ha ejecutado con éxito, se realiza una llamada a la función
+Esta llamada a dispatch equivale a enviar el siguiente objeto:
```js
-queryClient.invalidateQueries({ queryKey: ['notes'] })
+dispatch({ type: 'notes/createNote', payload: 'Redux Toolkit is awesome!' })
```
-Esto a su vez hace que React Query actualice automáticamente una query con la clave
notes , es decir, obtenga las notas del servidor. Como resultado, la aplicación renderiza el estado actualizado en el servidor, es decir, la nota agregada también se renderiza.
-
-Implementemos también el cambio en la importancia de las notas. Se agrega una función para actualizar notas al archivo
requests.js :
+Si has prestado atención, es posible que hayas notado que dentro de la acción
createNote , parece suceder algo que viola el principio de inmutabilidad de los reducers mencionado anteriormente:
```js
-export const updateNote = async (updatedNote) => {
- const options = {
- method: 'PUT',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(updatedNote)
- }
+createNote(state, action) {
+ const content = action.payload
+
+ state.push({
+ content,
+ important: false,
+ id: generateId(),
+ })
+}
+```
- const response = await fetch(`${baseUrl}/${updatedNote.id}`, options)
+Estamos mutando el array del argumento
state al llamar al método
push en lugar de devolver una nueva instancia del array. ¿De qué se trata todo esto?
- if (!response.ok) {
- throw new Error('Failed to update note')
- }
+Redux Toolkit utiliza la librería [Immer](https://immerjs.github.io/immer/) con reducers creados por la función
createSlice , lo que hace posible mutar el argumento
state dentro del reducer. Immer usa el estado mutado para producir un nuevo estado inmutable y, por lo tanto, los cambios de estado permanecen inmutables. Ten en cuenta que
state se puede cambiar sin "mutarlo", como hemos hecho con la acción
toggleImportanceOf . En este caso, la función
devuelve el nuevo estado directamente. Sin embargo, mutar el estado a menudo será útil, especialmente cuando se necesita actualizar un estado complejo.
- return await response.json()
-}
+La función
createSlice devuelve un objeto que contiene al reducer así como a los action creators definidos por el parámetro
reducers . Se puede acceder al reducer mediante la propiedad
noteSlice.reducer , mientras que a los action creators mediante la propiedad
noteSlice.actions . Podemos producir las exportaciones del archivo de la siguiente manera:
+
+```js
+const noteSlice = createSlice({
+ // ...
+})
+
+// highlight-start
+export const { createNote, toggleImportanceOf } = noteSlice.actions
+export default noteSlice.reducer
+// highlight-end
```
-Actualizar la nota también se hace mediante una mutación. El componente
App se expande de la siguiente manera:
+Las importaciones en otros archivos funcionarán igual que antes:
```js
-import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
-import { getNotes, createNote, updateNote } from './requests' // highlight-line
+import noteReducer, { createNote, toggleImportanceOf } from './reducers/noteReducer'
+```
-const App = () => {
- const queryClient = useQueryClient()
+Necesitamos modificar los nombres de los tipos de las acciones en las pruebas debido a las convenciones de ReduxToolkit:
+
+```js
+import deepFreeze from 'deep-freeze'
+import { describe, expect, test } from 'vitest'
+import noteReducer from './noteReducer'
- const newNoteMutation = useMutation({
- mutationFn: createNote,
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['notes'] })
+describe('noteReducer', () => {
+ test('returns new state with action notes/createNote', () => { // highlight-line
+ const state = []
+ const action = {
+ type: 'notes/createNote', // highlight-line
+ payload: 'the app state is in redux store' // highlight-line
}
+
+ deepFreeze(state)
+ const newState = noteReducer(state, action)
+
+ expect(newState).toHaveLength(1)
+ expect(newState.map(note => note.content)).toContainEqual(action.payload) // highlight-line
})
+})
- // highlight-start
- const updateNoteMutation = useMutation({
- mutationFn: updateNote,
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['notes'] })
+test('returns new state with action notes/toggleImportanceOf', () => { // highlight-line
+ const state = [
+ {
+ content: 'the app state is in redux store',
+ important: true,
+ id: 1
+ },
+ {
+ content: 'state changes are made with actions',
+ important: false,
+ id: 2
}
- })
- // highlight-end
+ ]
- const addNote = async (event) => {
- event.preventDefault()
- const content = event.target.note.value
- event.target.note.value = ''
- newNoteMutation.mutate({ content, important: true })
+ const action = {
+ type: 'notes/toggleImportanceOf', // highlight-line
+ payload: 2 // highlight-line
}
- const toggleImportance = (note) => {
- updateNoteMutation.mutate({...note, important: !note.important }) // highlight-line
- }
+ deepFreeze(state)
+ const newState = noteReducer(state, action)
- // ...
-}
+ expect(newState).toHaveLength(2)
+
+ expect(newState).toContainEqual(state[0])
+
+ expect(newState).toContainEqual({
+ content: 'state changes are made with actions',
+ important: true,
+ id: 2
+ })
+})
```
-De nuevo, se creó una mutación que invalidó la query
notes para que la nota actualizada se renderice correctamente. Usar mutaciones es fácil, el método
mutate recibe una nota como parámetro, cuya importancia se cambia a la negación del valor antiguo.
+Puedes encontrar el código de nuestra aplicación actual en su totalidad en la rama
part6-3 de [este repositorio de GitHub](https://github.com/fullstack-hy2020/redux-notes/tree/part6-3).
-El código actual de la aplicación está en [GitHub](https://github.com/fullstack-hy2020/query-notes/tree/part6-2) en la rama
part6-2 .
+### Redux Toolkit y console.log
-### Optimizando el rendimiento
+Como hemos aprendido, console.log es una herramienta extremadamente poderosa, por lo general siempre nos salva de problemas.
-La aplicación funciona bien y el código es relativamente simple. La facilidad para realizar cambios en la lista de notas es particularmente sorprendente. Por ejemplo, cuando cambiamos la importancia de una nota, invalidar la query
notes es suficiente para que los datos de la aplicación se actualicen:
+Intentemos imprimir el estado del store de Redux en la consola en medio del reducer creado con la función createSlice:
```js
-const updateNoteMutation = useMutation({
- mutationFn: updateNote,
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['notes'] }) // highlight-line
- }
+const noteSlice = createSlice({
+ name: 'notes',
+ initialState,
+ reducers: {
+ // ...
+ toggleImportanceOf(state, action) {
+ const id = action.payload
+
+ const noteToChange = state.find(n => n.id === id)
+
+ const changedNote = {
+ ...noteToChange,
+ important: !noteToChange.important
+ }
+
+ console.log(state) // highlight-line
+
+ return state.map(note =>
+ note.id !== id ? note : changedNote
+ )
+ }
+ },
})
```
-La consecuencia de esto, por supuesto, es que después de la solicitud PUT que causa el cambio de nota, la aplicación realiza una nueva solicitud GET para recuperar los datos de la query desde el servidor:
+Lo siguiente se imprime en la consola
-
+
-Si la cantidad de datos obtenidos por la aplicación no es grande, realmente no importa. Después de todo, desde el punto de vista de la funcionalidad del lado del navegador, hacer una solicitud HTTP GET adicional realmente no importa, pero en algunas situaciones podría generar una carga en el servidor.
+Lo que vemos es interesante pero no muy útil. Esto tiene que ver con la librería Immer que mencionamos anteriormente y es utilizada por Redux Toolkit internamente para guardar el estado de la Tienda.
-Si fuera necesario, es posible también [optimizar el rendimiento manualmente](https://tanstack.com/query/latest/docs/react/guides/updates-from-mutation-responses), actualizando el estado de la query mantenido por React Query.
+El estado se puede convertir a un formato legible por humanos utilizando la función [current](https://redux-toolkit.js.org/api/other-exports#current) de la librería immer.
-El cambio para la mutación que agrega una nueva nota es el siguiente:
+Actualicemos las importaciones para incluir a la función "current" de la librería immer:
```js
-const App = () => {
- const queryClient = useQueryClient()
+import { current } from '@reduxjs/toolkit'
+```
- const newNoteMutation = useMutation({
- mutationFn: createNote,
- // highlight-start
- onSuccess: (newNote) => {
- const notes = queryClient.getQueryData(['notes'])
- queryClient.setQueryData(['notes'], notes.concat(newNote))
- // highlight-end
- }
- })
+Luego actualicemos el llamado a la función console.log:
- // ...
-}
+```js
+console.log(current(state))
```
-Es decir, en el callback de
onSuccess , el objeto
queryClient primero lee el estado existente de
notes de la query y lo actualiza agregando una nueva nota, que se obtiene como parámetro de la función de callback. El valor del parámetro es el valor devuelto por la función
createNote , definida en el archivo
requests.js de la siguiente manera:
+Ahora lo que imprime la consola es legible para humanos
+
+
+
+### Redux DevTools
+
+[Redux DevTools](https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd) es una extension de Chrome, que ofrece útiles herramientas de desarrollo para Redux. Se puede usar, por ejemplo, para inspeccionar el estado del store de Redux y enviar acciones (dispatch) a través de la consola del navegador. Cuando el store se crea usando la función
configureStore de Redux Toolkit, no se necesita ninguna configuración adicional para que Redux DevTools funcione.
+
+Una vez instalada la extension, al hacer clic en la pestaña de
Redux en las herramientas de desarrollo del navegador, Redux DevTools debería abrirse:
+
+
+
+Puedes inspeccionar cómo el envío de una determinada acción cambia el estado haciendo clic en la acción:
+
+
+
+También es posible enviar acciones (dispatch) a la store utilizando las herramientas de desarrollo:
+
+
+
+El código actual de la aplicación se puede encontrar en [GitHub](https://github.com/fullstack-hy2020/redux-notes/tree/part6-3), en la rama
part6-3 .
+
+
+
+
+
+### Ejercicios 6.10.-6.13.
+
+Continuemos trabajando en la aplicación de anécdotas que comenzamos en el ejercicio 6.3, usando Redux Toolkit.
+
+#### 6.10 Mejores Anécdotas, paso 8
+
+Instala Redux Toolkit en el proyecto. Mueve la creación del store de Redux a su propio archivo
store.js y utiliza la función
configureStore para crear el store.
+
+Cambia la definición del
filter reducer y sus action creators para usar la función
createSlice de Redux Toolkit.
+
+También, comienza a utilizar Redux DevTools para depurar el estado de la aplicación fácilmente.
+
+#### 6.11 Mejores Anécdotas, paso 9
+
+Cambia también la definición de
anecdote reducer y sus action creators para usar la función
createSlice de Redux Toolkit.
+
+#### 6.12 Mejores Anécdotas, paso 10
+
+La aplicación tiene el esqueleto del componente
Notification listo para utilizarlo:
```js
-export const createNote = async (newNote) => {
- const options = {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(newNote)
+const Notification = () => {
+ const style = {
+ border: 'solid',
+ padding: 10,
+ borderWidth: 1,
+ marginBottom: 10
}
- const response = await fetch(baseUrl, options)
+ return (
+
+ render here notification...
+
+ )
+}
+
+export default Notification
+```
+
+Extiende el componente para que muestre el mensaje almacenado en el store de Redux. Crea un reducer separado para la nueva funcionalidad mediante la función
createSlice de Redux Toolkit.
+
+La aplicación no tiene que utilizar el componente
Notification completamente en este punto de los ejercicios. Es suficiente con que la aplicación muestre el valor inicial establecido para el mensaje en el
notificationReducer .
+
+#### 6.13 Mejores Anécdotas, paso 11
+
+Extiende la aplicación para que utilice el componente
Notification para mostrar un mensaje durante cinco segundos cuando el usuario vote por una anécdota o cree una nueva anécdota:
+
+
+
+Se recomienda crear [action creators](https://redux-toolkit.js.org/api/createSlice#reducers) independientes para configurar y eliminar notificaciones.
+
+
+
+
+
+### Configuración de JSON Server
+
+Expandamos la aplicación, de modo que las notas se almacenen en el backend. Usaremos [json-server](/es/part2/obteniendo_datos_del_servidor), de la parte 2.
- if (!response.ok) {
- throw new Error('Failed to create note')
- }
+El estado inicial de la base de datos se almacena en el archivo db.json , que se coloca en la raíz del proyecto:
- return await response.json()
+```json
+{
+ "notes": [
+ {
+ "content": "the app state is in redux store",
+ "important": true,
+ "id": 1
+ },
+ {
+ "content": "state changes are made with actions",
+ "important": false,
+ "id": 2
+ }
+ ]
}
```
-Sería relativamente fácil hacer un cambio similar a una mutación que cambia la importancia de la nota, pero lo dejamos como un ejercicio opcional.
-
-Finalmente, nota un detalle interesante. React Query vuelve a obtener todas las notas cuando cambiamos a otra pestaña del navegador y luego regresamos a la pestaña de la aplicación. Esto se puede observar en la pestaña de Red de la Consola de Desarrollador:
+Instalaremos json-server en nuestro proyecto...
-
+```js
+npm install json-server --save-dev
+```
-¿Qué está pasando? Al leer la [documentación](https://tanstack.com/query/latest/docs/react/reference/useQuery), nos damos cuenta de que la funcionalidad predeterminada de las queries de React Query es que las queries (cuyo estado es stale ) se actualicen cuando cambia el window focus . Si queremos, podemos desactivar la funcionalidad creando una consulta de la siguiente manera:
+y agregaremos la siguiente línea a la parte de scripts del archivo package.json
```js
-const App = () => {
- // ...
- const result = useQuery({
- queryKey: ['notes'],
- queryFn: getNotes,
- refetchOnWindowFocus: false // highlight-line
- })
-
+"scripts": {
+ "server": "json-server -p 3001 db.json",
// ...
}
```
-Si colocas un console.log en el código, podrás ver desde la consola del navegador cuántas veces React Query hace que la aplicación se vuelva a renderizar. La regla general es que el renderizado ocurre al menos cada vez que es necesario, es decir, cuando cambia el estado de la query. Puedes leer más al respecto por ejemplo [aquí](https://tkdodo.eu/blog/react-query-render-optimizations).
+Ahora iniciemos json-server con el comando _npm run server_.
-El código de la aplicación está en [GitHub](https://github.com/fullstack-hy2020/query-notes/tree/part6-3) en la rama part6-3 .
+### Fetch API
-React Query es una librería versátil que, basándonos en lo que ya hemos visto, simplifica la aplicación. ¿Hace React Query que soluciones de gestión de estado más complejas como Redux sean innecesarias? No. React Query puede reemplazar parcialmente el estado de la aplicación en algunos casos, pero como lo indica la [documentación](https://tanstack.com/query/latest/docs/react/guides/does-this-replace-client-state):
+En el desarrollo de software, a menudo es necesario considerar si una cierta funcionalidad debe implementarse usando una librería externa o si es mejor utilizar las soluciones nativas proporcionadas por el entorno. Ambos enfoques tienen sus propias ventajas y desafíos.
-- React Query es una librería de estado del servidor , responsable de la gestión de operaciones asíncronas entre el servidor y el cliente
-- Redux, etc. son librerías de estado del cliente que se pueden usar para almacenar datos asíncronos, aunque de manera menos eficiente cuando se comparan con una herramienta como React Query
+En las partes anteriores de este curso, usamos la librería [Axios](https://axios-http.com/docs/intro) para hacer peticiones HTTP. Ahora, exploremos una forma alternativa de hacer peticiones HTTP usando la [Fetch API](https://developer.mozilla.org/es/docs/Web/API/Fetch_API) nativa.
-Entonces, React Query es una librería que mantiene el estado del servidor en el frontend, es decir, actúa como una caché para lo que se almacena en el servidor. React Query simplifica el procesamiento de datos en el servidor y, en algunos casos, puede eliminar la necesidad de que los datos en el servidor se guarden en el estado del frontend.
+Es típico que una librería externa como Axios se implemente usando otras librerías externas. Por ejemplo, si instalas Axios en tu proyecto con el comando _npm install axios_, la salida de la consola será:
-La mayoría de las aplicaciones de React no necesitan solo una forma de almacenar temporalmente los datos servidos, sino también alguna solución para cómo se maneja el resto del estado del frontend (por ejemplo, el estado de los formularios o las notificaciones).
+```bash
+$ npm install axios
-
+added 23 packages, and audited 302 packages in 1s
-
+71 packages are looking for funding
+ run `npm fund` for details
-### Ejercicios 6.20.-6.22.
+found 0 vulnerabilities
+```
-Ahora hagamos una nueva versión de la aplicación de anécdotas que use la librería React Query. Usa [este proyecto](https://github.com/fullstack-hy2020/query-anecdotes) como punto de partida. El proyecto tiene un JSON Server instalado, la operación del cual se ha modificado ligeramente (Revisa el archivo _server.js_ para más detalles. Asegúrate de estar conectándote al _PORT_ correcto). Inicia el servidor con npm run server .
+Por lo tanto, además de la librería Axios, el comando instalaría más de 20 paquetes npm adicionales que Axios necesita para funcionar.
-Usa la Fetch API para hacer las peticiones.
+La Fetch API proporciona una forma similar de hacer peticiones HTTP como Axios, pero usar la Fetch API no requiere instalar ninguna librería externa. El mantenimiento de la aplicación se vuelve más fácil cuando hay menos librerías que actualizar, y la seguridad también mejora porque la superficie de ataque potencial de la aplicación se reduce. La seguridad y el mantenimiento de las aplicaciones se discute más a fondo en la [parte 7](https://fullstackopen.com/es/part7/class_components_miscellaneous#react-node-application-security) del curso.
-NOTA: La parte 6 se actualizó el 12 de octubre de 2025 para usar la Fetch API, que se introduce en la parte 6c. Si comenzaste a trabajar en esta parte antes de esa fecha, aún puedes usar Axios en los ejercicios si lo prefieres.
+En la práctica, las peticiones se realizan usando la función _fetch()_. La sintaxis utilizada difiere algo de Axios. También notaremos pronto que Axios se ha encargado de algunas cosas por nosotros y nos ha facilitado la vida. Sin embargo, ahora usaremos la Fetch API, ya que es una solución nativa ampliamente utilizada que todo desarrollador Full Stack debería conocer.
-#### Ejercicio 6.20
+### Obteniendo datos del backend
-Implementa la obtención de anécdotas del servidor usando React Query.
+Creemos un método para obtener datos del backend en el archivo src/services/notes.js :
-La aplicación debe funcionar de tal manera que si hay problemas para comunicarse con el servidor, solo se mostrará una página de error:
+```js
+const baseUrl = 'http://localhost:3001/notes'
-
+const getAll = async () => {
+ const response = await fetch(baseUrl)
-Puedes encontrar [aquí](https://tanstack.com/query/latest/docs/react/guides/queries) información sobre cómo detectar posibles errores.
+ if (!response.ok) {
+ throw new Error('Failed to fetch notes')
+ }
-Puedes simular un problema con el servidor apagando el JSON Server. Ten en cuenta que en una situación problemática, la consulta primero está en el estado isLoading durante un tiempo, porque si una solicitud falla, React Query intenta la solicitud varias veces antes de que indique que la solicitud no es exitosa. Opcionalmente, puedes especificar que no se realicen reintentos:
+ const data = await response.json()
+ return data
+}
-```js
-const result = useQuery(
- {
- queryKey: ['anecdotes'],
- queryFn: getAnecdotes,
- retry: false
- }
-)
+export default { getAll }
```
-o que la solicitud se vuelva a intentar solo una vez más:
+Examinemos más de cerca la implementación del método _getAll_. Las notas ahora se obtienen del backend llamando a la función _fetch()_, a la cual se le da la URL del backend como argumento. El tipo de petición no se define explícitamente, por lo que _fetch_ realiza su acción predeterminada, que es una petición GET.
+
+Una vez que la respuesta ha llegado, se verifica el éxito de la petición usando la propiedad _response.ok_, y se lanza un error si es necesario:
```js
-const result = useQuery(
- {
- queryKey: ['anecdotes'],
- queryFn: getAnecdotes,
- retry: 1
- }
-)
+if (!response.ok) {
+ throw new Error('Failed to fetch notes')
+}
```
-#### Ejercicio 6.21
+El atributo _response.ok_ se establece en _true_ si la petición fue exitosa, es decir, el código de estado de la respuesta está entre 200 y 299. Para todos los demás códigos de estado, como 404 o 500, se establece en _false_.
-Implementa la adición de nuevas anécdotas al servidor usando React Query. La aplicación debe renderizar una nueva anécdota por defecto. Ten en cuenta que el contenido de la anécdota debe tener al menos 5 caracteres de longitud, de lo contrario el servidor rechazará la solicitud POST. No tienes que preocuparte por el control de errores ahora.
+Ten en cuenta que _fetch_ no lanza automáticamente un error incluso si el código de estado de la respuesta es, por ejemplo, 404. El manejo de errores debe implementarse manualmente, como lo hemos hecho aquí.
-#### Ejercicio 6.22
+Si la petición es exitosa, los datos contenidos en la respuesta se convierten a formato JSON:
-Implementa la votación de anécdotas usando nuevamente React Query. La aplicación debe renderizar automáticamente el número aumentado de votos para la anécdota votada.
+```js
+const data = await response.json()
+```
-
+_fetch_ no convierte automáticamente ningún dato incluido en la respuesta a formato JSON; la conversión debe hacerse manualmente. También es importante notar que _response.json()_ es un método asíncrono, por lo que se requiere la palabra clave
await .
-
+Simplifiquemos aún más el código devolviendo directamente los datos devueltos por el método _response.json()_:
-### useReducer
+```js
+const getAll = async () => {
+ const response = await fetch(baseUrl)
-Entonces, incluso si la aplicación usa React Query, generalmente se necesita alguna solución para manejar el resto del estado del frontend (por ejemplo, el estado de los formularios). Con bastante frecuencia, el estado creado con
useState es una solución suficiente. Usar Redux es, por supuesto, posible, pero hay otras alternativas.
+ if (!response.ok) {
+ throw new Error('Failed to fetch notes')
+ }
-Veamos una aplicación de contador sencilla. La aplicación muestra el valor del contador y ofrece tres botones para actualizar su estado:
+ return await response.json() // highlight-line
+}
+```
-
+### Inicializando el store con datos obtenidos del servidor
-Ahora implementaremos la gestión del estado del contador usando un mecanismo de gestión de estado similar a Redux proporcionado por el hook integrado de React [useReducer](https://react.dev/reference/react/useReducer).
+Modifiquemos ahora nuestra aplicación para que el estado de la aplicación se inicialice con las notas obtenidas del servidor.
-El código inicial de la aplicación está en [GitHub](https://github.com/fullstack-hy2020/hook-counter/tree/part6-1) en la rama
part6-1 . El archivo
App.jsx se ve de la siguiente manera:
+En el archivo
noteReducer.js , cambiemos la inicialización del estado de las notas para que por defecto no haya notas:
```js
-import { useReducer } from 'react'
+const noteSlice = createSlice({
+ name: 'notes',
+ initialState: [], // highlight-line
+ // ...
+})
+```
-const counterReducer = (state, action) => {
- switch (action.type) {
- case 'INC':
- return state + 1
- case 'DEC':
- return state - 1
- case 'ZERO':
- return 0
- default:
- return state
+Agreguemos un action creator llamado
setNotes , que nos permita reemplazar directamente el array de notas. Podemos crear el action creator deseado usando la función
createSlice de la siguiente manera:
+
+```js
+// ...
+
+const noteSlice = createSlice({
+ name: 'notes',
+ initialState: [],
+ reducers: {
+ createNote(state, action) {
+ const content = action.payload
+ state.push({
+ content,
+ important: false,
+ id: generateId()
+ })
+ },
+ toggleImportanceOf(state, action) {
+ const id = action.payload
+ const noteToChange = state.find(n => n.id === id)
+ const changedNote = {
+ ...noteToChange,
+ important: !noteToChange.important
+ }
+ return state.map(note => (note.id !== id ? note : changedNote))
+ },
+ // highlight-start
+ setNotes(state, action) {
+ return action.payload
+ }
+ // highlight-end
}
-}
+})
+
+export const { createNote, toggleImportanceOf, setNotes } = noteSlice.actions // highlight-line
+export default noteSlice.reducer
+```
+
+Implementemos la inicialización de las notas en el componente
App . Como es habitual al obtener datos de un servidor, usaremos el hook
useEffect :
+
+
+```js
+import { useEffect } from 'react' // highlight-line
+import { useDispatch } from 'react-redux' // highlight-line
+
+import NoteForm from './components/NoteForm'
+import Notes from './components/Notes'
+import VisibilityFilter from './components/VisibilityFilter'
+import { setNotes } from './reducers/noteReducer' // highlight-line
+import noteService from './services/notes' // highlight-line
const App = () => {
- const [counter, counterDispatch] = useReducer(counterReducer, 0)
+ const dispatch = useDispatch() // highlight-line
+
+ // highlight-start
+ useEffect(() => {
+ noteService.getAll().then(notes => dispatch(setNotes(notes)))
+ }, [dispatch])
+ // highlight-end
return (
-
{counter}
-
- counterDispatch({ type: 'INC' })}>+
- counterDispatch({ type: 'DEC' })}>-
- counterDispatch({ type: 'ZERO' })}>0
-
+
+
+
)
}
@@ -547,289 +2355,262 @@ const App = () => {
export default App
```
-El hook [useReducer](https://react.dev/reference/react/useReducer) proporciona un mecanismo para crear un estado para la aplicación. El parámetro para crear un estado es la función del reducer que maneja los cambios de estado y el valor inicial del estado:
+### Enviando datos al backend
-```js
-const [counter, counterDispatch] = useReducer(counterReducer, 0)
-```
+A continuación, implementemos la funcionalidad para enviar una nueva nota al servidor. Esto también nos dará una oportunidad de practicar cómo hacer una petición POST usando el método _fetch()_.
-La función del reducer que maneja los cambios de estado es similar a los reducers de Redux, es decir, la función obtiene como parámetros el estado actual y la acción que cambia el estado. La función devuelve el nuevo estado actualizado en función del tipo y el posible contenido de la acción:
+Expandamos el código en
src/services/notes.js que maneja la comunicación con el servidor de la siguiente manera:
```js
-const counterReducer = (state, action) => {
- switch (action.type) {
- case 'INC':
- return state + 1
- case 'DEC':
- return state - 1
- case 'ZERO':
- return 0
- default:
- return state
- }
-}
-```
+const baseUrl = 'http://localhost:3001/notes'
-En nuestro ejemplo, las acciones no tienen nada más que un tipo. Si el tipo de acción es
INC , aumenta el valor del contador en uno, etc. Como los reducers de Redux, las acciones también pueden contener datos arbitrarios, que generalmente se colocan en el campo
payload de la acción.
+const getAll = async () => {
+ const response = await fetch(baseUrl)
-La función
useReducer devuelve un array que contiene un elemento para acceder al valor actual del estado (primer elemento del array) y una función
dispatch (segundo elemento del array) para cambiar el estado:
+ if (!response.ok) {
+ throw new Error('Failed to fetch notes')
+ }
-```js
-const App = () => {
- const [counter, counterDispatch] = useReducer(counterReducer, 0) // highlight-line
+ return await response.json()
+}
- return (
-
-
{counter}
// highlight-line
-
- counterDispatch({ type: 'INC' })}>+ // highlight-line
- counterDispatch({ type: 'DEC' })}>-
- counterDispatch({ type: 'ZERO' })}>0
-
-
- )
+// highlight-start
+const createNew = async (content) => {
+ const response = await fetch(baseUrl, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ content, important: false }),
+ })
+
+ if (!response.ok) {
+ throw new Error('Failed to create note')
+ }
+
+ return await response.json()
}
+// highlight-end
+
+export default { getAll, createNew } // highlight-line
```
-Como se puede ver, el cambio de estado se realiza exactamente como en Redux, la función de dispatch recibe la acción apropiada para cambiar el estado como parámetro:
+Examinemos más de cerca la implementación del método _createNew_. El primer parámetro de la función _fetch()_ especifica la URL a la que se realiza la petición. El segundo parámetro es un objeto que define otros detalles de la petición, como el tipo de petición, encabezados y los datos enviados con la petición. Podemos aclarar aún más el código almacenando el objeto que define los detalles de la petición en una variable
options separada:
```js
-counterDispatch({ type: "INC" })
+const createNew = async (content) => {
+ // highlight-start
+ const options = {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ content, important: false }),
+ }
+
+ const response = await fetch(baseUrl, options)
+ // highlight-end
+
+ if (!response.ok) {
+ throw new Error('Failed to create note')
+ }
+
+ return await response.json()
+}
```
-### Pasando el estado via props
-Cuando la aplicación se divide en varios componentes, el valor del contador y la función de dispatch utilizada para gestionarlo deben pasarse de alguna manera a los otros componentes también. Una solución es pasar estos como props de la manera habitual.
+Examinemos más de cerca el objeto
options :
-Definamos un componente
Display separado para la aplicación, cuya responsabilidad es mostrar el valor del contador. El contenido del archivo
src/components/Display.jsx debe ser:
+-
method define el tipo de petición, que en este caso es
POST
+-
headers define los encabezados de la petición. Agregamos el encabezado _'Content-Type': 'application/json'_ para informar al servidor que los datos enviados con la petición están en formato JSON, para que pueda manejar la petición correctamente
+-
body contiene los datos enviados con la petición. No puedes asignar directamente un objeto JavaScript a este campo; primero debe convertirse a una cadena JSON llamando a la función _JSON.stringify()_
+Al igual que con una petición GET, el código de estado de la respuesta se verifica para detectar errores:
```js
-const Display = ({ counter }) => {
- return
{counter}
+if (!response.ok) {
+ throw new Error('Failed to create note')
}
-
-export default Display
```
-Además, definamos un componente
Button que sea responsable de los botones de la aplicación:
-```js
-const Button = ({ dispatch, type, label }) => {
- return (
-
dispatch({ type })}>
- {label}
-
- )
-}
+Si la petición es exitosa,
JSON Server devuelve la nota recién creada, para la cual también ha generado un
id único. Sin embargo, los datos contenidos en la respuesta aún deben convertirse a formato JSON usando el método _response.json()_:
-export default Button
+```js
+return await response.json()
```
-El archivo
App.jsx cambia de la siguiente manera:
+Luego modificaremos el componente de nuestra aplicación
NoteForm para que una nueva nota se envíe al backend. El método _addNote_ del componente cambiará ligeramente:
```js
-import { useReducer } from 'react'
-
-import Button from './components/Button' // highlight-line
-import Display from './components/Display' // highlight-line
-
-const counterReducer = (state, action) => {
- switch (action.type) {
- case 'INC':
- return state + 1
- case 'DEC':
- return state - 1
- case 'ZERO':
- return 0
- default:
- return state
+import { useDispatch } from 'react-redux'
+import { createNote } from '../reducers/noteReducer'
+import noteService from '../services/notes' // highlight-line
+
+const NoteForm = (props) => {
+ const dispatch = useDispatch()
+
+ const addNote = async (event) => { // highlight-line
+ event.preventDefault()
+ const content = event.target.note.value
+ event.target.note.value = ''
+ const newNote = await noteService.createNew(content) // highlight-line
+ dispatch(createNote(newNote)) // highlight-line
}
-}
-
-const App = () => {
- const [counter, counterDispatch] = useReducer(counterReducer, 0)
return (
-
-
// highlight-line
-
- // highlight-start
-
-
-
- // highlight-end
-
-
+
)
}
-```
-La aplicación ahora se ha dividido en varios componentes. La gestión del estado está definida en el archivo
App.jsx , desde donde los valores y funciones necesarios para la gestión del estado se pasan a los componentes hijos como props.
-
-La solución funciona, pero no es óptima. Si la estructura de los componentes se complica, por ejemplo, el despachador debe transmitirse usando props a través de muchos componentes para llegar a los componentes que lo necesitan, aunque los componentes intermedios en el árbol de componentes no necesiten al despachador. Este fenómeno se llama
prop drilling .
-### Usando context para pasar el estado a los componentes
-
-La [API de Contexto](https://react.dev/learn/passing-data-deeply-with-context) integrada en React proporciona una solución para nosotros. El contexto de React es un tipo de estado global de la aplicación, al que se puede dar acceso directo a cualquier componente de la aplicación.
-
-Creemos ahora un contexto en la aplicación que almacene la gestión de estado del contador.
+export default NoteForm
+```
-El contexto se crea con el hook [createContext](https://react.dev/reference/react/createContext) de React. Creemos un contexto en el archivo
src/CounterContext.jsx :
+Cuando se crea una nueva nota en el backend llamando al método _createNew()_, el valor de retorno es un objeto que representa la nota, al cual el backend ha generado un
id único. Por lo tanto, modifiquemos el action creator
createNote definido en
notesReducer.js de la siguiente manera:
```js
-import { createContext } from 'react'
-
-const CounterContext = createContext()
-
-export default CounterContext
+const noteSlice = createSlice({
+ name: 'notes',
+ initialState: [],
+ reducers: {
+ createNote(state, action) {
+ state.push(action.payload) // highlight-line
+ },
+ // ..
+ },
+})
```
-El componente
App ahora puede
proveer un contexto a sus componentes hijos de la siguiente manera:
+El cambio de importancia de las notas podría implementarse utilizando el mismo principio, haciendo una llamada asíncrona al servidor y luego enviando una acción apropiada.
-```js
-import { useReducer } from 'react'
+El estado actual del código para la aplicación se puede encontrar en [GitHub](https://github.com/fullstack-hy2020/redux-notes/tree/part6-4) en la rama
part6-4 .
-import Button from './components/Button'
-import Display from './components/Display'
-import CounterContext from './CounterContext' // highlight-line
+
-// ...
+
-const App = () => {
- const [counter, counterDispatch] = useReducer(counterReducer, 0)
+### Ejercicios 6.14.-6.15.
- return (
-
// highlight-line
- // highlight-line
-
- // highlight-start
-
-
-
- // highlight-end
-
- // highlight-line
- )
-}
-```
+#### 6.14 Anécdotas y el Backend, paso 1
-Como se puede ver, proveer el contexto se realiza envolviendo los componentes hijos dentro del componente
CounterContext.Provider y estableciendo un valor adecuado para el contexto.
+Cuando la aplicación se inicie, obtén las anécdotas del backend implementado usando json-server. Usa la Fetch API para hacer la petición HTTP.
-El valor del contexto ahora es un objeto con los atributos
counter y
counterDispatch . El campo
counter contiene el valor del contador y
counterDispatch la función
dispatch utilizada para cambiar el valor.
+Como datos de backend iniciales, puedes usar, por ejemplo, [esto](https://github.com/fullstack-hy2020/misc/blob/master/anecdotes.json).
-Otros componentes ahora pueden acceder al contexto utilizando el hook [useContext](https://react.dev/reference/react/useContext). El componente
Display cambia de la siguiente manera:
+#### 6.15 Anécdotas y el Backend, paso 2
-```js
-import { useContext } from 'react' // highlight-line
-import CounterContext from './CounterContext' // highlight-line
+Modifica la creación de nuevas anécdotas, de forma que las anécdotas se almacenen en el backend. Utiliza la Fetch API en tu implementación una vez más.
-const Display = () => { // highlight-line
- const { counter } = useContext(CounterContext) // highlight-line
+
- return
{counter}
-}
-```
+
-Por lo tanto, el componente
Display ya no necesita props; obtiene el valor del contador llamando al hook
useContext con el objeto
CounterContext como argumento.
+### Acciones asíncronas y Redux Thunk
-De manera similar, el componente
Button se convierte en:
+Nuestro enfoque es bastante bueno, pero no es muy bueno que la comunicación con el servidor suceda dentro de las funciones de los componentes. Sería mejor si la comunicación pudiera abstraerse de los componentes para que no tengan que hacer nada más que llamar al action creator apropiado. Como ejemplo,
App inicializaría el estado de la aplicación de la siguiente manera:
```js
-import { useContext } from 'react' // highlight-line
-import CounterContext from './CounterContext' // highlight-line
-
-const Button = ({ type, label }) => { // highlight-line
- const { counterDispatch } = useContext(CounterContext) // highlight-line
+const App = () => {
+ const dispatch = useDispatch()
- return (
-
counterDispatch({ type })}> // highlight-line
- {label}
-
- )
+ useEffect(() => {
+ dispatch(initializeNotes())
+ }, [dispatch])
+
+ // ...
}
```
-Por lo tanto, los componentes reciben el valor proporcionado por el proveedor de contexto. En este caso, el contexto es un objeto con un campo
counter que representa el valor del contador y un campo
counterDispatch que es la función dispatch utilizada para cambiar el estado del contador.
-
-Los componentes acceden a los atributos que necesitan usando la sintaxis de desestructuración de JavaScript:
+y
NoteForm crearía una nueva nota de la siguiente manera:
```js
-const { counter } = useContext(CounterContext)
+const NoteForm = () => {
+ const dispatch = useDispatch()
+
+ const addNote = async (event) => {
+ event.preventDefault()
+ const content = event.target.note.value
+ event.target.note.value = ''
+ dispatch(createNote(content))
+ }
+
+ // ...
+}
```
-El código actual de la aplicación se encuentra en [GitHub](https://github.com/fullstack-hy2020/hook-counter/tree/part6-2) en la rama
part6-2 .
+En esta implementación, ambos componentes enviarían una acción sin necesidad de saber sobre la comunicación con el servidor que sucede detrás de escena. Estos tipos de
acciones asíncronas se pueden implementar utilizando la librería [Redux Thunk](https://github.com/reduxjs/redux-thunk). El uso de la librería no requiere ninguna configuración adicional o incluso instalación cuando el store de Redux se ha creado utilizando la función
configureStore del kit de herramientas de Redux (Redux Toolkit).
-### Definiendo el contexto del contador en un archivo separado
+Con Redux Thunk, es posible implementar
action creators que devuelven una función en lugar de un objeto. La función recibe los métodos
dispatch y
getState del store de Redux como parámetros. Esto permite, por ejemplo, implementaciones de action creators asíncronos, que primero esperan la finalización de una cierta operación asíncrona y luego despachan alguna acción, que cambia el estado del store.
-Nuestra aplicación tiene una característica molesta, que la funcionalidad de la gestión del estado del contador está parcialmente definida en el componente
App . Ahora vamos a mover todo lo relacionado con el contador al archivo
CounterContext.jsx :
+Podemos definir un action creator llamado
initializeNotes en el archivo
noteReducer.js , que obtiene las notas iniciales del servidor, de la siguiente manera:
```js
-import { createContext, useReducer } from 'react'
+import { createSlice } from '@reduxjs/toolkit'
+import noteService from '../services/notes' // highlight-line
+
+const noteSlice = createSlice({
+ name: 'notes',
+ initialState: [],
+ reducers: {
+ createNote(state, action) {
+ state.push(action.payload)
+ },
+ toggleImportanceOf(state, action) {
+ const id = action.payload
+ const noteToChange = state.find((n) => n.id === id)
+ const changedNote = {
+ ...noteToChange,
+ important: !noteToChange.important,
+ }
+ return state.map((note) => (note.id !== id ? note : changedNote))
+ },
+ setNotes(state, action) {
+ return action.payload
+ },
+ },
+})
-const counterReducer = (state, action) => {
- switch (action.type) {
- case 'INC':
- return state + 1
- case 'DEC':
- return state - 1
- case 'ZERO':
- return 0
- default:
- return state
+const { setNotes } = noteSlice.actions // highlight-line
+
+// highlight-start
+export const initializeNotes = () => {
+ return async (dispatch) => {
+ const notes = await noteService.getAll()
+ dispatch(setNotes(notes))
}
}
+// highlight-end
-const CounterContext = createContext()
-
-export const CounterContextProvider = (props) => {
- const [counter, counterDispatch] = useReducer(counterReducer, 0)
-
- return (
-
- {props.children}
-
- )
-}
+export const { createNote, toggleImportanceOf } = noteSlice.actions // highlight-line
-export default CounterContext
+export default noteSlice.reducer
```
-El archivo ahora exporta, además del objeto
CounterContext correspondiente al contexto, el componente
CounterContextProvider , que es prácticamente un proveedor de contexto cuyo valor es un contador y un despachador utilizado para su gestión de estado.
-
-Habilitemos el proveedor de contexto haciendo un cambio en
main.jsx :
+En su función interna, es decir, en la
acción asíncrona , la operación primero obtiene todas las notas del servidor y luego
despacha la acción para agregarlas al store. Es importante destacar que Redux pasa automáticamente una referencia al método _dispatch_ como argumento a la función, por lo que el action creator _initializeNotes_ no requiere ningún parámetro.
-```js
-import { StrictMode } from 'react'
-import { createRoot } from 'react-dom/client'
+El action creator _setNotes_ ya no se exporta fuera del módulo, ya que el estado inicial de las notas ahora se establecerá usando el action creator asíncrono _initializeNotes_ que creamos. Sin embargo, todavía usamos el action creator _setNotes_ dentro del módulo.
-import App from './App'
-import { CounterContextProvider } from './CounterContext' // highlight-line
-
-createRoot(document.getElementById('root')).render(
-
- // highlight-line
-
- // highlight-line
-
-)
+El componente
App ahora puede definirse de la siguiente manera:
-```
+```js
+import { useEffect } from 'react'
+import { useDispatch } from 'react-redux'
-Ahora el contexto que define el valor y la funcionalidad del contador está disponible para
todos los componentes de la aplicación.
+import NoteForm from './components/NoteForm'
+import Notes from './components/Notes'
+import VisibilityFilter from './components/VisibilityFilter'
+import { initializeNotes } from './reducers/noteReducer' // highlight-line
-El componente
App se simplifica a la siguiente forma:
+const App = () => {
+ const dispatch = useDispatch()
-```js
-import Button from './components/Button'
-import Display from './components/Display'
+ useEffect(() => {
+ dispatch(initializeNotes()) // highlight-line
+ }, [dispatch])
-const App = () => {
return (
)
}
@@ -837,79 +2618,127 @@ const App = () => {
export default App
```
-El contexto todavía se usa de la misma manera, y no se necesitan cambios en los otros componentes. Por ejemplo, el componente
Button se define de la siguiente manera:
+La solución es bastante elegante. La lógica de inicialización de las notas se ha separado completamente del componente React.
+
+A continuación, creemos un action creator asíncrono llamado _appendNote_:
```js
-import { useContext } from 'react'
-import CounterContext from '../CounterContext'
+import { createSlice } from '@reduxjs/toolkit'
+import noteService from '../services/notes'
+
+const noteSlice = createSlice({
+ name: 'notes',
+ initialState: [],
+ reducers: {
+ createNote(state, action) {
+ state.push(action.payload)
+ },
+ toggleImportanceOf(state, action) {
+ const id = action.payload
+ const noteToChange = state.find((n) => n.id === id)
+ const changedNote = {
+ ...noteToChange,
+ important: !noteToChange.important,
+ }
+ return state.map((note) => (note.id !== id ? note : changedNote))
+ },
+ setNotes(state, action) {
+ return action.payload
+ },
+ },
+})
-const Button = ({ type, label }) => {
- const { counterDispatch } = useContext(CounterContext)
+const { createNote, setNotes } = noteSlice.actions // highlight-line
- return (
-
counterDispatch({ type })}>
- {label}
-
- )
+export const initializeNotes = () => {
+ return async (dispatch) => {
+ const notes = await noteService.getAll()
+ dispatch(setNotes(notes))
+ }
}
-export default Button
-```
-
-La solución es bastante elegante. Todo el estado de la aplicación, es decir, el valor del contador y el código para gestionarlo, ahora está aislado en el archivo
CounterContext . Los componentes acceden a la parte del contexto que necesitan usando el hook
useContext y la sintaxis de desestructuración de JavaScript.
-
-El código final de la aplicación se encuentra en [GitHub](https://github.com/fullstack-hy2020/hook-counter/tree/part6-3) en la rama
part6-3 .
+// highlight-start
+export const appendNote = (content) => {
+ return async (dispatch) => {
+ const newNote = await noteService.createNew(content)
+ dispatch(createNote(newNote))
+ }
+}
+// highlight-end
-
+export const { toggleImportanceOf } = noteSlice.actions // highlight-line
-
+export default noteSlice.reducer
+```
-### Ejercicios 6.23.-6.24.
+El principio es el mismo una vez más. Primero se realiza una operación asíncrona y, una vez completada, se despacha una acción que actualiza el estado del store. El action creator _createNote_ ya no se exporta fuera del archivo; solo se usa internamente en la implementación de la función _appendNote_.
-#### Ejercicio 6.23.
+El componente NoteForm cambia de la siguiente manera:
-La aplicación tiene un componente Notification para mostrar notificaciones al usuario.
+```js
+import { useDispatch } from 'react-redux'
+import { appendNote } from '../reducers/noteReducer' // highlight-line
-Implementa la gestión del estado de las notificaciones de la aplicación utilizando el hook useReducer y el contexto. La notificación debe informar al usuario cuando se crea una nueva anécdota o cuando se vota por ella:
+const NoteForm = () => {
+ const dispatch = useDispatch()
-
+ const addNote = async (event) => {
+ event.preventDefault()
+ const content = event.target.note.value
+ event.target.note.value = ''
+ dispatch(appendNote(content)) // highlight-line
+ }
-La notificación se muestra durante cinco segundos.
+ return (
+
+ )
+}
+```
-#### Ejercicio 6.24.
+El estado actual del código de la aplicación se puede encontrar en [GitHub](https://github.com/fullstack-hy2020/redux-notes/tree/part6-5) en la rama part6-5 .
-Como se indicó en el ejercicio 6.21, el servidor requiere que el contenido de la anécdota a agregar tenga al menos 5 caracteres de longitud. Ahora implementa el manejo de errores para la inserción. En la práctica, es suficiente mostrar una notificación al usuario en caso de una solicitud POST fallida:
+Redux Toolkit ofrece una gran cantidad de herramientas para simplificar la administración de estado asíncrono. Las herramientas adecuadas para este caso de uso son, por ejemplo, la función [createAsyncThunk](https://redux-toolkit.js.org/api/createAsyncThunk) y la API [RTK Query](https://redux-toolkit.js.org/rtk-query/overview).
-
+
-La condición de error debe manejarse en la función de callback registrada para ello, consulta [aquí](https://tanstack.com/query/latest/docs/react/reference/useMutation) cómo registrar una función.
+
-Este fue el último ejercicio para esta parte del curso y es hora de enviar tu código a GitHub y marcar todos tus ejercicios completados en el [sistema de envío de ejercicios](https://studies.cs.helsinki.fi/stats/courses/fullstackopen).
+### Ejercicios 6.16.-6.19.
-
+#### 6.16 Anécdotas y el Backend, paso 3
-
+Modifica la inicialización de la store de Redux para que suceda utilizando action creators asíncronos, los cuales son posibles gracias a la librería Redux Thunk.
-### ¿Qué solución de gestión de estado elegir?
+#### 6.17 Anécdotas y el Backend, paso 4
-En los capítulos 1-5, toda la gestión de estado de la aplicación se realizó utilizando el hook de React useState . Las llamadas asíncronas al backend requerían el uso del hook useEffect en algunas situaciones. En principio, no se necesita nada más.
+También modifica la creación de una nueva anécdota para que suceda usando action creators asíncronos, hecho posible por la librería Redux Thunk.
-Un problema sutil con una solución basada en un estado creado con el hook useState es que si alguna parte del estado de la aplicación se necesita en varios componentes de la aplicación, el estado y las funciones para manipularlo deben pasarse via props a todos los componentes que manejan el estado. A veces, las props deben pasar por varios componentes, y los componentes a lo largo del camino pueden ni siquiera estar interesados en el estado de ninguna manera. Este fenómeno algo desagradable se llama prop drilling .
+#### 6.18 Anécdotas y el Backend, paso 5
-A lo largo de los años, se han desarrollado varias soluciones alternativas para la gestión de estado de aplicaciones React, que se pueden usar para aliviar situaciones problemáticas (por ejemplo, prop drilling). Sin embargo, ninguna solución ha sido "final", todas tienen sus propias ventajas y desventajas, y se están desarrollando nuevas soluciones todo el tiempo.
+La votación aún no guarda los cambios en el backend. Arregla la situación con la ayuda de la librería Redux Thunk y la Fetch API.
-La situación puede confundir a un principiante e incluso a un desarrollador web experimentado. ¿Qué solución se debe usar?
+#### 6.19 Anécdotas y el Backend, paso 6
-Para una aplicación simple, useState es sin duda un buen punto de partida. Si la aplicación está comunicándose con el servidor, la comunicación se puede manejar de la misma manera que en los capítulos 1-5, utilizando el estado de la aplicación misma. Sin embargo, recientemente se ha vuelto más común mover la comunicación y la gestión asociada del estado al menos parcialmente bajo el control de React Query (o alguna otra librería similar). Si estás preocupado por useState y el prop drilling que conlleva, usar context puede ser una buena opción. También hay situaciones donde puede tener sentido manejar parte del estado con useState y parte con contextos.
+La creación de notificaciones sigue siendo un poco tediosa, ya que hay que realizar dos acciones y utilizar la función _setTimeout_:
-La solución de gestión de estado más completa y robusta es Redux, que es una forma de implementar la llamada arquitectura [Flux](https://facebookarchive.github.io/flux/docs/in-depth-overview/). Redux es ligeramente más antigua que las soluciones presentadas en esta sección. La rigidez de Redux ha sido la motivación para muchas nuevas soluciones de gestión de estado, como el useReducer de React. Algunas de las críticas a la rigidez de Redux ya se han vuelto obsoletas gracias al [Redux Toolkit](https://redux-toolkit.js.org/).
+```js
+dispatch(setNotification(`new anecdote '${content}'`))
+setTimeout(() => {
+ dispatch(clearNotification())
+}, 5000)
+```
-A lo largo de los años, también se han desarrollado otras librerías de gestión de estado que son similares a Redux, como el recién llegado [Recoil](https://recoiljs.org/) y el ligeramente más antiguo [MobX](https://mobx.js.org/). Sin embargo, según [Npm trends](https://npmtrends.com/mobx-vs-recoil-vs-redux), Redux todavía domina claramente, y de hecho parece estar aumentando su ventaja:
+Crea un action creator, que te permita proveer la notificación de la siguiente manera:
-
+```js
+dispatch(setNotification(`you voted '${anecdote.content}'`, 10))
+```
-También, Redux no tiene que ser usado en su totalidad en una aplicación. Puede tener sentido, por ejemplo, gestionar el estado de los formularios fuera de Redux, especialmente en situaciones donde el estado de un formulario no afecta al resto de la aplicación. También es perfectamente posible usar Redux y React Query juntos en la misma aplicación.
+El primer parámetro es el texto que sera renderizado y el segundo parámetro es el tiempo durante el cual se mostrara la notificación en segundos.
-La pregunta de qué solución de gestión de estado se debe usar no es para nada sencilla. Es imposible dar una sola respuesta correcta. También es probable que la solución de gestión de estado seleccionada pueda resultar ser subóptima a medida que la aplicación crece hasta tal punto que la solución tenga que cambiarse incluso si la aplicación ya ha sido puesta en uso de producción.
+Implementa el uso de esta notificación mejorada en tu aplicación.
diff --git a/src/content/7/es/part7.md b/src/content/7/es/part7.md
index ab2440269a3..966f80ce72b 100644
--- a/src/content/7/es/part7.md
+++ b/src/content/7/es/part7.md
@@ -6,10 +6,12 @@ lang: es
-La séptima parte del curso cubre varios temas. Empezamos viendo cómo definir funciones hook personalizadas. Después, exploramos cómo funciona el bundling en aplicaciones React: nos familiarizamos con esbuild como bundler de bajo nivel y vemos cómo puede configurarse Vite para distintos escenarios. Hacia el final de la parte, cubrimos brevemente los componentes de clase y otros temas del desarrollo con React, como la organización del código y la gestión del estado.
+La séptima parte del curso cubre varios temas. Empezamos viendo cómo definir funciones hook personalizadas. Después, exploramos cómo funciona el bundling en aplicaciones React: nos familiarizamos con esbuild como bundler de bajo nivel y vemos cómo puede configurarse Vite para distintos escenarios. Hacia el final de la parte, cubrimos brevemente los componentes de clase y otros temas del desarrollo con React, como la organización del código y los límites de errores.
-Parte actualizada el 6 de abril de 2026
+Parte actualizada el 7 de abril de 2026
- React Router, las librerías de UI y Styled Components se trasladaron a la parte 5
- Webpack fue reemplazado por esbuild
+- Se añadieron los límites de errores y el mantenimiento del frontend y el backend en un mismo repositorio
+- Algunos ejercicios cambiaron y se añadieron un par de ejercicios nuevos
diff --git a/src/content/7/es/part7a.md b/src/content/7/es/part7a.md
index 29479e8dedb..791ac3a5fd0 100644
--- a/src/content/7/es/part7a.md
+++ b/src/content/7/es/part7a.md
@@ -7,10 +7,799 @@ lang: es
-Los ejercicios de esta séptima parte del curso difieren un poco de los anteriores. En este capítulo y en el siguiente, como es habitual, hay [ejercicios relacionados con la teoría del capítulo](/es/part7/react_router#ejercicios-7-1-7-3).
+Los ejercicios de esta parte del curso difieren un poco de los anteriores. Como de costumbre, hay algunos ejercicios relacionados con la teoría de este capítulo. Los demás capítulos de esta parte no tienen ejercicios separados.
-### React Router
+Además, esta parte contiene una serie más amplia de ejercicios que amplía la aplicación BlogList creada en las partes 4 y 5. Puedes encontrar esos ejercicios [aquí](/es/part7/ejercicios_ampliar_la_lista_de_blogs).
-Esta sección se ha trasladado a la [parte 5](/es/part5).
+### Hooks de React
+
+React ofrece 18 [hooks incorporados](https://es.react.dev/reference/react/hooks) diferentes, de los cuales los más populares son [useState](https://es.react.dev/reference/react/useState) y [useEffect](https://es.react.dev/reference/react/useEffect), que ya hemos utilizado extensivamente.
+
+En la [parte 5](/es/part5/props_children_y_la_ref_del_componente#referencias-a-componentes-con-ref) usamos [useRef](https://es.react.dev/reference/react/useRef) y [useImperativeHandle](https://es.react.dev/reference/react/useImperativeHandle), que permitieron que un componente proporcionara acceso a sus funciones a otros componentes. En la [parte 6](/es/part6/react_query_y_context_api) utilizamos [useContext](https://es.react.dev/reference/react/useContext) para implementar un estado global.
+
+En los últimos años, los hooks se han convertido en la forma estándar en que las librerías exponen sus APIs. A lo largo del curso ya hemos visto varios ejemplos: [Zustand](https://zustand-demo.pmnd.rs/) proporciona
useStore para acceder al estado global, [React Router](https://reactrouter.com/) expone
useNavigate y
useParams para la navegación programática y el acceso a parámetros de la URL, y [React Query](https://tanstack.com/query/latest) ofrece
useQuery y
useMutation para gestionar el estado del servidor.
+
+Como se mencionó en la [parte 1](/es/part1/un_estado_mas_complejo_depurando_aplicaciones_react#reglas-de-los-hooks), los hooks no son funciones normales y cuando los usamos tenemos que cumplir con ciertas [reglas o limitaciones](https://es.react.dev/warnings/invalid-hook-call-warning#breaking-rules-of-hooks). Recapitulemos las reglas del uso de hooks, copiadas literalmente de la documentación oficial de React:
+
+**Evita utilizar Hooks dentro de loops, condicionales o funciones anidadas.** En su lugar, utiliza los Hooks únicamente en el nivel superior de tu función de React.
+
+**Los Hooks sólo deben ser utilizados durante la renderización de un componente de función en React:**
+
+- Utilízalos en el nivel superior del cuerpo de un componente de función.
+- Utilízalos en el nivel superior del cuerpo de un Hook personalizado.
+
+Existe un [plugin de ESLint](https://www.npmjs.com/package/eslint-plugin-react-hooks) que se puede usar para verificar que la aplicación utiliza los hooks correctamente:
+
+
+
+Además de los hooks que ya hemos utilizado, React proporciona varios hooks incorporados que merece la pena conocer. En esta sección veremos dos de ellos,
useMemo y
useCallback , ambos relacionados con la optimización del rendimiento. Después pasaremos a los hooks personalizados, que permiten empaquetar cualquier combinación de hooks en una función reutilizable propia.
+
+### useMemo
+
+Cada vez que un componente de React se vuelve a renderizar, se ejecuta de nuevo todo el cuerpo de la función. Para la mayoría de los componentes esto no supone ningún problema, pero en ocasiones un componente realiza un cálculo costoso —como filtrar una lista grande, ordenar datos u obtener un valor complejo— y repetirlo en cada renderizado desperdicia tiempo.
+
+[useMemo](https://es.react.dev/reference/react/useMemo) permite almacenar en caché el resultado de un cálculo entre renderizados. Recibe una función que realiza el cálculo y un array de dependencias. React solo vuelve a ejecutar la función cuando cambia alguna de las dependencias; en caso contrario, devuelve el resultado almacenado anteriormente.
+
+Consideremos un componente que renderiza una larga lista de elementos filtrados por un término de búsqueda:
+
+```js
+import { useState } from 'react'
+
+const expensiveCalculation = () => {
+ let sum = 0
+ for (let i = 0; i < 100000; i++) sum += i
+ return sum
+}
+
+const ITEMS = Array.from({ length: 10000 }, (_, i) => `item ${i + 1}`)
+
+const FilteredList = () => {
+ const [filter, setFilter] = useState('')
+ const [darkMode, setDarkMode] = useState(false)
+
+ console.log('filtering...')
+ const filtered = ITEMS.filter(item => {
+ expensiveCalculation()
+ return item.includes(filter)
+ })
+
+ return (
+
+
setFilter(e.target.value)}
+ placeholder="filter items"
+ />
+
setDarkMode(!darkMode)}>toggle dark mode
+
+ {filtered.map(item => {item} )}
+
+
+ )
+}
+
+export default FilteredList
+```
+
+Filtrar la lista lleva ahora tiempo, en parte gracias a la ralentización artificial que hemos introducido.
+
+El problema del componente es que al hacer clic en el botón del modo oscuro se vuelven a filtrar los 10 000 elementos aunque el texto del filtro no haya cambiado.
+
+Podemos solucionarlo con
useMemo :
+
+```js
+import { useState, useMemo } from 'react' // highlight-line
+
+const FilteredList = () => {
+ const [filter, setFilter] = useState('')
+ const [darkMode, setDarkMode] = useState(false)
+
+ const filtered = useMemo(() => { // highlight-line
+ console.log('filtering...')
+ return ITEMS.filter(item => {
+ expensiveCalculation()
+ return item.includes(filter)
+ })
+ }, [filter]) // highlight-line
+
+ return (
+
+ //...
+
+ )
+}
+```
+
+Con
useMemo , el filtrado costoso solo se ejecuta cuando cambia
filter . Alternar el modo oscuro únicamente actualiza el color de fondo y la lista filtrada almacenada en caché se devuelve de inmediato.
+
+El array de dependencias funciona exactamente igual que el de
useEffect : React compara cada valor con el del renderizado anterior. Si todos los valores son idénticos, se reutiliza el valor memorizado. Si alguno difiere, la función se ejecuta de nuevo y el resultado se almacena para el siguiente renderizado.
+
+
useMemo también puede utilizarse para memorizar objetos y arrays que se pasan como props, evitando renderizados innecesarios de componentes hijos que utilizan igualdad por referencia. Por ejemplo:
+
+```js
+const App = () => {
+ const [filter, setFilter] = useState('')
+
+ // Without useMemo, 'options' is a new object on every render even if filter hasn't changed
+ const options = useMemo(() => ({ caseSensitive: false, filter }), [filter]) // highlight-line
+
+ return
+}
+```
+
+
useMemo es una optimización del rendimiento; no deberías utilizarlo por defecto. La [memorización prematura](https://wiki.c2.com/?PrematureOptimization) añade complejidad sin aportar beneficios cuando el cálculo es rápido. Mide primero y añade
useMemo solo cuando hayas confirmado que un cálculo concreto constituye un cuello de botella.
+
+### React.memo
+
+Mientras que
useMemo almacena en caché el resultado de un cálculo dentro de un componente, [React.memo](https://es.react.dev/reference/react/memo) adopta un enfoque diferente: almacena la salida renderizada de un componente completo.
React.memo no es un hook, sino un componente de orden superior, y lo tratamos aquí porque complementa bien a
useMemo . Cuando un componente está envuelto en
React.memo , React omite su nuevo renderizado si sus props no han cambiado desde el renderizado anterior.
+
+```js
+const MyComponent = React.memo(({ value }) => {
+ console.log('rendered')
+ return
{value}
+})
+```
+
+Sin
React.memo ,
MyComponent se vuelve a renderizar cada vez que lo hace su componente padre, aunque
value sea el mismo. Con él, React compara las props anteriores y nuevas mediante una igualdad superficial y solo vuelve a renderizar cuando algo ha cambiado realmente.
+
+Ten en cuenta que
React.memo solo comprueba las props. Si el componente utiliza un valor del contexto o su propio estado, seguirá renderizándose de nuevo cuando estos cambien.
+
+
React.memo se combina de forma natural con
useMemo :
useMemo evita que se repitan cálculos costosos, mientras que
React.memo impide que el propio componente vuelva a renderizarse.
+
+Si un componente memorizado recibe una nueva referencia a una función u objeto en cada renderizado, la memorización queda anulada. Aquí es donde entra en juego
useCallback .
+
+### useCallback
+
+Las funciones definidas dentro de un componente se vuelven a crear como objetos nuevos en cada renderizado. Normalmente esto es inofensivo, pero se convierte en un problema en dos situaciones concretas:
+
+- Un componente hijo envuelto en [React.memo](https://es.react.dev/reference/react/memo) recibe la función como prop. Como la función es un objeto nuevo cada vez, el hijo siempre detecta una prop distinta y se vuelve a renderizar, anulando el propósito de la memorización.
+- Una función aparece como dependencia de
useEffect o
useMemo . Si se crea una función nueva en cada renderizado, el efecto o el valor memorizado se vuelven a ejecutar en cada renderizado.
+
+[useCallback](https://es.react.dev/reference/react/useCallback) resuelve este problema almacenando en caché la propia función entre renderizados y devolviendo el mismo objeto función mientras no cambien sus dependencias. Recibe una función y un array de dependencias, con una estructura idéntica a la de
useMemo .
+
+Veamos un ejemplo concreto. Tenemos un componente
NoteList costoso de renderizar, así que lo envolvemos en
React.memo :
+
+```js
+// React.memo makes this component skip re-rendering if its props haven't changed
+const NoteList = memo(({ onDelete, notes }) => {
+ console.log('NoteList rendered')
+ return (
+
+ {notes.map(note => (
+
+ {note.content}
+ onDelete(note.id)}>delete
+
+ ))}
+
+ )
+})
+
+const App = () => {
+ const [notes, setNotes] = useState([
+ { id: 1, content: 'Learn React' },
+ { id: 2, content: 'Learn hooks' },
+ { id: 3, content: 'Learn useMemo' },
+ { id: 4, content: 'Learn useCallback' },
+ { id: 5, content: 'Build something cool' },
+ ])
+ const [newNote, setNewNote] = useState('')
+
+ const handleDelete = (id) => {
+ setNotes(notes => notes.filter(note => note.id !== id))
+ }
+
+ const handleAdd = () => {
+ setNotes(notes => [...notes, { id: Date.now(), content: newNote }])
+ setNewNote('')
+ }
+
+ return (
+
+ setNewNote(e.target.value)} />
+ add
+
+
+ )
+}
+```
+
+El problema es que
handleDelete está definida como una función normal dentro de
App . Cada vez que
App se vuelve a renderizar —lo que ocurre con cada pulsación en el campo de la nueva nota— se crea un objeto función completamente nuevo y se pasa a
NoteList como prop
onDelete .
+
+Desde la perspectiva de
React.memo , la prop ha cambiado, por lo que
NoteList vuelve a renderizarse aunque la lista no haya cambiado:
+
+
+
+Podemos solucionarlo con
useCallback , que devuelve el mismo objeto función entre renderizados mientras sus dependencias no cambien:
+
+```js
+import { useState, useCallback, memo } from 'react'
+
+
+const App = () => {
+ const [notes, setNotes] = useState([])
+ const [newNote, setNewNote] = useState('')
+
+// highlight-start
+ const handleDelete = useCallback((id) => { // highlight-line
+ setNotes(notes => notes.filter(note => note.id !== id))
+ }, []) // no external dependencies: this function never needs to change
+// highlight-end
+
+ // ...
+ return (
+ // ...
+ )
+}
+```
+
+Ahora
handleDelete es estable: React devuelve exactamente el mismo objeto función en cada renderizado, por lo que
React.memo no detecta ningún cambio en la prop
onDelete y omite por completo el nuevo renderizado de
NoteList .
+
+Al igual que
useMemo , utiliza
useCallback solo cuando tengas un problema concreto, como un componente hijo memorizado que se renderiza innecesariamente o un
useEffect que se ejecuta con demasiada frecuencia debido a una dependencia de función. Añadirlo en todas partes hace que el código sea más difícil de leer sin aportar ninguna mejora de rendimiento.
+
+### Hooks personalizados
+
+React ofrece la opción de crear nuestros propios hooks [personalizados](https://es.react.dev/learn/reusing-logic-with-custom-hooks). Según React, el propósito principal de los hooks personalizados es facilitar la reutilización de la lógica utilizada en los componentes.
+
+>
Crear tus propios Hooks te permite extraer la lógica de los componentes en funciones reutilizables.
+
+Los hooks personalizados son funciones normales de JavaScript que pueden utilizar cualquier otro hook, siempre que respeten las [reglas de los hooks](/es/part1/un_estado_mas_complejo_depurando_aplicaciones_react#reglas-de-los-hooks). Además, el nombre de los hooks personalizados debe comenzar con la palabra
use .
+
+La idea clave es que cualquier lógica con estado que te encuentres duplicando entre componentes es candidata a extraerse a un hook personalizado. Cada llamada al mismo hook crea una porción de estado independiente. Esto es lo que distingue un hook personalizado de una función de utilidad corriente.
+
+Ya hemos implementado varios hooks personalizados en la parte 6. Los hooks
useNotes y
useNoteActions se crearon en la sección sobre [Zustand](/es/part6/arquitectura_flux_y_zustand#notas-de-zustand), y
useCounter se definió en la sección sobre [React Query y Context](/es/part6/react_query_y_context_api#definir-el-contexto-del-contador-en-su-propio-archivo).
+
+#### Hook de contador
+
+Implementamos una aplicación de contador en la [parte 1](/es/part1/estado_del_componente_controladores_de_eventos#control-de-eventos), que puede tener su valor incrementado, reducido o reiniciado. El código de la aplicación es el siguiente:
+
+```js
+import { useState } from 'react'
+
+const App = () => {
+ const [counter, setCounter] = useState(0)
+
+ return (
+
+
{counter}
+
setCounter(counter + 1)}>
+ plus
+
+
setCounter(counter - 1)}>
+ minus
+
+
setCounter(0)}>
+ zero
+
+
+ )
+}
+```
+
+Extraigamos la lógica del contador en su propio hook personalizado. El código del hook es el siguiente:
+
+```js
+const useCounter = () => {
+ const [value, setValue] = useState(0)
+
+ const increase = () => {
+ setValue(value + 1)
+ }
+
+ const decrease = () => {
+ setValue(value - 1)
+ }
+
+ const zero = () => {
+ setValue(0)
+ }
+
+ return {
+ value,
+ increase,
+ decrease,
+ zero
+ }
+}
+```
+
+Nuestro hook personalizado utiliza el hook _useState_ internamente para crear su propio estado. El hook devuelve un objeto, cuyas propiedades incluyen el valor del contador, así como funciones para manipular el valor.
+
+Los componentes de React pueden utilizar el hook como se muestra a continuación:
+
+```js
+const App = () => {
+ const counter = useCounter()
+
+ return (
+
+
{counter.value}
+
+ plus
+
+
+ minus
+
+
+ zero
+
+
+ )
+}
+```
+
+Al hacer esto, podemos extraer el estado del componente _App_ y su manipulación por completo en el hook _useCounter_. La gestión del estado y la lógica del contador ahora es responsabilidad del hook personalizado.
+
+El mismo hook podría
reutilizarse en la aplicación que realizaba un seguimiento de la cantidad de clics realizados en los botones izquierdo y derecho:
+
+```js
+
+const App = () => {
+ const left = useCounter()
+ const right = useCounter()
+
+ return (
+
+ {left.value}
+
+ left
+
+
+ right
+
+ {right.value}
+
+ )
+}
+```
+
+La aplicación crea
dos contadores completamente separados. El primero se asigna a la variable
left y el otro a la variable
right . Cada llamada a
useCounter crea su propia porción de estado independiente.
+
+#### Hooks personalizados y nuevos renderizados de componentes
+
+Una pregunta natural en este punto es: ¿cuándo se vuelve a renderizar realmente un componente que utiliza un hook personalizado?
+
+La respuesta es sencilla cuando se entiende qué es en realidad un hook personalizado. Desde la perspectiva del componente, un hook personalizado no es una entidad separada. Es simplemente una parte de la lógica del propio componente que se ha trasladado a una función independiente. Esto significa que todo el estado y los efectos definidos dentro del hook pertenecen al componente que lo llama, no al propio hook.
+
+En consecuencia, las reglas de renderizado son exactamente las mismas que con los hooks incorporados. El componente vuelve a renderizarse cuando cambia un estado gestionado dentro del hook, cuando cambia un valor de contexto al que el hook está suscrito o cuando cualquier hook al que el hook personalizado llama internamente provoca un nuevo renderizado.
+
+En cambio, acciones como reasignar variables normales dentro del hook o que cambien por sí solos los argumentos pasados al hook no provocan un nuevo renderizado.
+
+Los argumentos merecen un análisis más detallado. Pasar un nuevo valor a un hook no programa por sí mismo un nuevo renderizado, pero si el hook utiliza ese argumento como dependencia de un
useEffect o un
useMemo , el cambio hará que el efecto o el valor memorizado vuelvan a ejecutarse. Si esto, a su vez, llama a una función actualizadora del estado, el componente se renderizará de nuevo.
+
+Una forma útil de entenderlo es imaginar que copias y pegas todo el código del hook personalizado directamente dentro del componente. El comportamiento de renderizado sería idéntico. El hook solo sirve para organizar ese código; no es un límite que React trate de una manera especial.
+
+```js
+const useCounter = () => {
+ const [count, setCount] = useState(0) // this state belongs to the calling component
+ return { count, increment: () => setCount(c => c + 1) }
+}
+
+const MyComponent = () => {
+ const { count, increment } = useCounter()
+ // re-renders whenever the count state inside the hook is updated
+}
+```
+
+#### Hook para campos de formulario
+
+Tratar con formularios en React es algo complicado. La siguiente aplicación presenta al usuario un formulario que le solicita que ingrese su nombre, fecha de nacimiento y altura:
+
+```js
+const App = () => {
+ const [name, setName] = useState('')
+ const [born, setBorn] = useState('')
+ const [height, setHeight] = useState('')
+
+ return (
+
+
+
+ {name} {born} {height}
+
+
+ )
+}
+```
+
+Cada campo del formulario tiene su propio estado. Para mantener el estado del formulario sincronizado con los datos proporcionados por el usuario, tenemos que registrar un controlador
onChange apropiado para cada uno de los elementos
input . El patrón es idéntico para todos los campos; solo cambia el nombre de la variable de estado. Este es exactamente el tipo de repetición que los hooks personalizados están diseñados para eliminar.
+
+Definamos nuestro propio hook personalizado _useField_, que simplifica la gestión del estado del formulario:
+
+```js
+const useField = (type) => {
+ const [value, setValue] = useState('')
+
+ const onChange = (event) => {
+ setValue(event.target.value)
+ }
+
+ return {
+ type,
+ value,
+ onChange
+ }
+}
+```
+
+La función de hook recibe el tipo de campo de entrada como parámetro. Devuelve todos los atributos requeridos por el
input : su tipo, valor y el controlador onChange.
+
+El hook se puede utilizar de la siguiente manera:
+
+```js
+const App = () => {
+ const name = useField('text')
+ // ...
+
+ return (
+
+
+// ...
+
+ {name.value} {born} {height} // highlight-line
+
+
+ )
+}
+```
+
+### Propagación de atributos con spread
+
+Podríamos simplificar un poco más las cosas. Dado que el objeto _name_ tiene exactamente todos los atributos que el elemento
input espera recibir como props, podemos pasar los props al elemento usando la [sintaxis spread](https://developer.mozilla.org/es/docs/Web/JavaScript/Reference/Operators/Spread_syntax)(spread syntax) de la siguiente manera:
+
+```js
+
+```
+
+Como indica el [ejemplo](https://es.react.dev/learn/updating-objects-in-state#copying-objects-with-the-spread-syntax) en la documentación de React, las siguientes dos formas de pasar props a un componente logran exactamente el mismo resultado:
+
+```js
+
+
+const person = {
+ firstName: 'Arto',
+ lastName: 'Hellas'
+}
+
+
+```
+
+La aplicación se simplifica en el siguiente formato:
+
+```js
+const App = () => {
+ const name = useField('text')
+ const born = useField('date')
+ const height = useField('number')
+
+ return (
+
+
+
+ {name.value} {born.value} {height.value}
+
+
+ )
+}
+```
+
+Tratar con formularios se simplifica enormemente cuando los desagradables detalles esenciales relacionados con la sincronización del estado del formulario se encapsulan dentro de nuestro hook personalizado.
+
+#### Persistencia del estado con un hook personalizado
+
+Los hooks personalizados pueden combinar varios hooks incorporados para encapsular comportamientos más complejos. Una funcionalidad que se necesita habitualmente es persistir el estado en
localStorage para que sobreviva a una recarga de la página. El siguiente hook
useLocalStorage envuelve
useState y mantiene el valor sincronizado con localStorage:
+
+```js
+import { useState } from 'react'
+
+const useLocalStorage = (key, initialValue) => {
+ const [storedValue, setStoredValue] = useState(() => {
+ try {
+ const item = window.localStorage.getItem(key)
+ return item ? JSON.parse(item) : initialValue
+ } catch (error) {
+ return initialValue
+ }
+ })
+
+ const setValue = (value) => {
+ try {
+ setStoredValue(value)
+ window.localStorage.setItem(key, JSON.stringify(value))
+ } catch (error) {
+ console.error(error)
+ }
+ }
+
+ return [storedValue, setValue]
+}
+```
+
+El hook recibe una clave de almacenamiento y un valor inicial. En el primer renderizado lee el valor de localStorage y recurre a
initialValue si todavía no hay nada almacenado. La función actualizadora que devuelve modifica al mismo tiempo tanto el estado de React como localStorage.
+
+Un componente que lo utiliza tiene exactamente el mismo aspecto que uno que emplea
useState directamente:
+
+```js
+const App = () => {
+ const [name, setName] = useLocalStorage('name', '')
+
+ return (
+
+
setName(e.target.value)} />
+
Hello, {name}! (your name is stored in localStorage)
+
+ )
+}
+```
+
+El componente no sabe que localStorage está implicado. Esa responsabilidad queda completamente oculta dentro del hook.
+
+### Más sobre hooks
+
+Los hooks personalizados no son solo una herramienta para reutilizar código; también proporcionan una forma mejor de dividirlo en partes modulares más pequeñas.
+
+Internet está comenzando a llenarse con más y más material útil relacionado con los hooks. Vale la pena consultar las siguientes fuentes:
+
+- [Awesome React Hooks Resources](https://github.com/rehooks/awesome-react-hooks)
+- [Easy to understand React Hook recipes by Gabe Ragland](https://usehooks.com/)
+
+
+
+
+
+### Ejercicios 7.1.-7.6.
+
+Volvamos una vez más a trabajar con anécdotas. Utiliza como punto de partida para los ejercicios la aplicación del repositorio https://github.com/fullstack-hy2020/routed-anecdotes.
+
+Si clonas el proyecto dentro de un repositorio de Git existente, recuerda eliminar la configuración de Git de la aplicación clonada:
+
+```bash
+cd routed-anecdotes // go first to directory of the cloned repository
+rm -rf .git
+```
+
+La aplicación se inicia de la forma habitual, pero antes debes instalar sus dependencias:
+
+```bash
+npm install
+npm run dev
+```
+
+#### 7.1: Hook useField
+
+Copia el hook personalizado
useField en el archivo
src/hooks/index.js . El hook debe gestionar el estado de un único campo de formulario y devolver un objeto con las siguientes propiedades:
type ,
value y
onChange .
+
+Si utilizas la [exportación nombrada](https://developer.mozilla.org/es/docs/Web/JavaScript/Reference/Statements/export#descripci%C3%B3n) en lugar de la exportación predeterminada:
+
+```js
+import { useState } from 'react'
+
+export const useField = (type) => { // highlight-line
+ const [value, setValue] = useState('')
+
+ const onChange = (event) => {
+ setValue(event.target.value)
+ }
+
+ return {
+ type,
+ value,
+ onChange
+ }
+}
+
+// modules can have several named exports
+export const useAnotherHook = () => { // highlight-line
+ // ...
+}
+```
+
+Luego, la [importación](https://developer.mozilla.org/es/docs/Web/JavaScript/Reference/Statements/import) ocurre de la siguiente manera:
+
+```js
+import { useField } from './hooks'
+
+const App = () => {
+ // ...
+ const username = useField('text')
+ // ...
+}
+```
+
+Utiliza el hook en el formulario de creación de anécdotas.
+
+#### 7.2: useField con reset
+
+Añade al formulario un botón que borre todos los campos de entrada:
+
+
+
+Amplía el hook
useField para que exponga una función
reset que borre el valor del campo.
+
+Dependiendo de tu solución, es posible que veas la siguiente advertencia en tu consola:
+
+
+
+Volveremos a esta advertencia en el próximo ejercicio.
+
+#### 7.3: Corrección del problema con spread
+
+Si tu solución no provocó que apareciera una advertencia en la consola, ya has terminado este ejercicio.
+
+Si ves la advertencia
Invalid value for prop \`reset\` on \ tag en la consola, realiza los cambios necesarios para eliminarla.
+
+El motivo de esta advertencia es que después de realizar los cambios en tu aplicación, la siguiente expresión:
+
+```js
+
+```
+
+Esencialmente, es lo mismo que esto:
+
+```js
+
+```
+
+El elemento
input no debe recibir un atributo
reset .
+
+Una solución simple sería no usar la sintaxis de spread y escribir todos los formularios de esta manera:
+
+```js
+
+```
+
+Si hiciéramos esto, perderíamos gran parte del beneficio proporcionado por el hook
useField . En su lugar, busca una solución al problema, pero que aún sea fácil de usar con la sintaxis de spread.
+
+#### 7.4: useAnecdotes, paso 1
+
+El proyecto ya tiene configurado un servidor JSON. Puedes iniciarlo con:
+
+```bash
+npm run server
+```
+
+Esto inicia un backend de JSON Server que expone la colección de anécdotas como un recurso REST en
http://localhost:3001/anecdotes .
+
+El archivo existente
services/anecdotes.js contiene las funciones necesarias para comunicarse con el backend, excepto la del último ejercicio. Ten en cuenta que el servicio utiliza la [Fetch API](https://developer.mozilla.org/es/docs/Web/API/Fetch_API) en vez de Axios para las peticiones HTTP. Si no conoces Fetch, consulta la [parte 6](/es/part6/estado_complejo_fetch_y_pruebas#fetch-api) antes de continuar.
+
+El patrón habitual para obtener datos de un servidor en React tiene este aspecto:
+
+```js
+import { useState, useEffect } from 'react'
+import anecdoteService from './services/anecdotes'
+
+const App = () => {
+ const [anecdotes, setAnecdotes] = useState([])
+
+ useEffect(() => {
+ anecdoteService.getAll().then(data => setAnecdotes(data))
+ }, [])
+
+ // ...
+}
+```
+
+Implementa un hook personalizado
useAnecdotes que encapsule esta comunicación con el servidor. Para este ejercicio basta con que el hook obtenga todas las anécdotas. La creación de nuevas anécdotas puede abordarse en el siguiente ejercicio.
+
+El hook debe utilizarse de esta forma:
+
+```js
+// ...
+import { useAnecdotes } from './hooks' // highlight-line
+
+const App = () => {
+ const { anecdotes } = useAnecdotes() // highlight-line
+
+ const addAnecdote = () => {} // a dummy function to keep code from breaking
+
+ return (
+
+
+
Software anecdotes
+
+
+ } />
+ } />
+ } />
+
+
+
+
+ )
+}
+
+export default App
+```
+
+**Una pista:** antes se mencionó lo siguiente:
+
+> Una forma útil de entenderlo —es decir, de entender cómo funciona un hook— es imaginar que copias y pegas todo el código del hook personalizado directamente dentro del componente.
+
+Ahora debes hacer, en cierto modo, lo contrario: copiar y pegar el código pertinente del componente en el hook. Esto incluye tanto
useState como
useEffect .
+
+#### 7.5: useAnecdotes, paso 2
+
+Amplía el hook
useAnecdotes para que también permita crear nuevas anécdotas. El hook debe exponer una función
addAnecdote que envíe la nueva anécdota al servidor y actualice el estado local.
+
+Ahora el hook debe poder utilizarse así:
+
+```js
+const { anecdotes, addAnecdote } = useAnecdotes()
+```
+
+Actualiza el componente
App para pasar
addAnecdote al componente
CreateNew en lugar de la función provisional.
+
+#### 7.6: useAnecdotes, paso 3
+
+Amplía el hook
useAnecdotes con una función
deleteAnecdote que elimine una anécdota del servidor y actualice el estado local. Añade un botón para eliminar junto a cada anécdota de la lista.
+
+Además, refactoriza la aplicación para que ni los datos de las anécdotas ni las funciones del hook se pasen como props. En su lugar, los componentes que los necesiten deben llamar directamente a
useAnecdotes . Esto significa que
App ya no tiene que actuar como intermediario pasando datos y callbacks por el árbol de componentes.
+
+Tras la refactorización,
App debe tener este aspecto:
+
+```js
+const App = () => {
+ return (
+
+
+
Software anecdotes
+
+
+ } />
+ } />
+ } />
+
+
+
+
+ )
+}
+```
diff --git a/src/content/7/es/part7b.md b/src/content/7/es/part7b.md
index 5739cd882a0..fa80678bd32 100644
--- a/src/content/7/es/part7b.md
+++ b/src/content/7/es/part7b.md
@@ -7,493 +7,435 @@ lang: es
-### Hooks
+En los primeros tiempos, React era algo famoso por ser muy difícil de configurar con las herramientas necesarias para el desarrollo de aplicaciones. Para facilitar la situación, se desarrolló [Create React App](https://github.com/facebookincubator/create-react-app), que eliminó los problemas relacionados con la configuración. [Vite](https://vitejs.dev/), que se utiliza a lo largo de este curso, ha reemplazado desde entonces a Create React App como el estándar para nuevas aplicaciones React.
-React ofrece 15 [hooks incorporados](https://es.react.dev/reference/react/hooks) diferentes, de los cuales los más populares son [useState](https://es.react.dev/reference/react/useState) y [useEffect](https://es.react.dev/reference/react/useEffect), a los cuales ya hemos estado utilizando extensivamente.
+Tanto Vite como Create React App utilizan
bundlers para hacer el trabajo real. En esta sección veremos más de cerca qué hacen realmente los bundlers, cómo funciona Vite por debajo y cómo configurarlo para distintos escenarios. También examinaremos brevemente [esbuild](https://esbuild.github.io/), un bundler de bajo nivel que el propio Vite usa internamente. Entender esbuild ayuda a aclarar qué significa, en el fondo, el bundling.
-En la [parte 5](/es/part5/props_children_y_proptypes#referencias-a-componentes-con-ref) usamos el hook [useImperativeHandle](https://es.react.dev/reference/react/useImperativeHandle) que permite que los componentes proporcionen sus funciones a otros componentes. En la [parte 6](/es/part6/react_query_use_reducer_y_el_contexto) utilizamos [useReducer](https://es.react.dev/reference/react/useReducer) y [useContext](https://es.react.dev/reference/react/useContext) para implementar una gestión de estado similar a Redux.
+> #### ¿Y Webpack?
+>
+>Webpack fue el bundler dominante durante la mayor parte de la década de 2010 y todavía aparece en bases de código antiguas y empresariales. Este curso también cubrió Webpack hasta la primavera de 2026.
+>
+> Si trabajas en un proyecto legacy, es útil saber que Webpack existe y que utiliza los mismos conceptos básicos (entry points, loaders/plugins, output). Sin embargo, en 2026 no se recomienda configurar un proyecto nuevo con Webpack. Su configuración es compleja y las herramientas modernas como Vite proporcionan una experiencia de desarrollo mucho mejor. No cubriremos la configuración de Webpack en este curso.
-En los últimos años, muchas librerías de React han comenzado a ofrecer APIs basadas en hooks. En la [parte 6](/es/part6/flux_architecture_y_redux) usamos los hooks [useSelector](https://react-redux.js.org/api/hooks#useselector) y [useDispatch](https://react-redux.js.org/api/hooks#usedispatch) de la librería react-redux para compartir nuestra redux-store y la función dispatch con nuestros componentes.
+### Bundling
-La API de [React Router](https://reactrouter.com/en/main/start/tutorial) que presentamos en la [parte anterior](/es/part7/react_router) también se basa parcialmente en hooks. Sus hooks se pueden usar para acceder a los parámetros de la URL y al objeto
navigation , lo que permite manipular la URL del navegador programáticamente.
+Hemos implementado nuestras aplicaciones dividiendo nuestro código en módulos separados que han sido
importados en los lugares que los necesitan. Aunque los módulos ES6 están definidos en el estándar ECMAScript, no todos los entornos de ejecución manejan automáticamente el código basado en módulos. Incluso los navegadores modernos se benefician de que las dependencias se preprocesen y optimicen antes de ser entregadas.
-Como se mencionó en la [parte 1](/es/part1/un_estado_mas_complejo_depurando_aplicaciones_react#reglas-de-los-hooks), los hooks no son funciones normales y cuando los usamos tenemos que cumplir con ciertas [reglas o limitaciones](https://es.react.dev/warnings/invalid-hook-call-warning#breaking-rules-of-hooks). Recapitulemos las reglas del uso de hooks, copiadas literalmente de la documentación oficial de React:
+Por esta razón, el código dividido en módulos se
empaqueta para producción, es decir, los archivos de código fuente se transforman y combinan en un conjunto optimizado de archivos que el navegador puede cargar eficientemente. Cuando ejecutamos
npm run build en partes anteriores del curso, Vite realizó este bundling. La salida aparece en el directorio
dist :
-**Evita utilizar Hooks dentro de loops, condicionales o funciones anidadas.** En su lugar, utiliza los Hooks únicamente en el nivel superior de tu función de React.
+```
+├── assets
+│ ├── index-d526a0c5.css
+│ ├── index-e92ae01e.js
+│ └── react-35ef61ed.svg
+├── index.html
+└── vite.svg
+```
-**Los Hooks sólo deben ser utilizados durante la renderización de un componente de función en React:**
+El archivo
index.html de la raíz carga el JavaScript empaquetado con una etiqueta
script :
+
+```html
+
+
+
+
+
+
+
Vite + React
+ // highlight-line
+
// highlight-line
+
+
+
+
+
+```
-- Utilízalos en el nivel superior del cuerpo de un componente de función.
-- Utilízalos en el nivel superior del cuerpo de un Hook personalizado.
+El CSS también se empaqueta en un único archivo.
-Existe un plugin de [ESlint](https://www.npmjs.com/package/eslint-plugin-react-hooks) que se puede usar para verificar que la aplicación usa los hooks correctamente.
+En la práctica, el bundling parte de un entry point, que normalmente es
main.jsx . Vite incluye no solo el código del entry point, sino también todo lo que este importa, recursivamente, hasta que se resuelve el grafo completo de dependencias.
-
+Dado que parte de los archivos importados son paquetes como React, Redux y Axios, el archivo JavaScript empaquetado también contendrá el contenido de cada una de esas librerías.
-### Hooks personalizados
+> Antes de que existieran los bundlers, el enfoque antiguo se basaba en que el archivo index.html cargaba todos los archivos JavaScript separados de la aplicación con ayuda de etiquetas script. Esto provocaba un peor rendimiento, ya que la carga de cada archivo separado genera cierta sobrecarga. Por eso, hoy en día el método preferido es agrupar el código en un solo archivo. El bundling también permite optimizaciones como la minificación y el
tree-shaking (eliminación de código no utilizado).
-React ofrece la opción de crear nuestros propios hooks [personalizados](https://es.react.dev/learn/reusing-logic-with-custom-hooks). Según React, el propósito principal de los hooks personalizados es facilitar la reutilización de la lógica utilizada en los componentes.
+### Cómo funciona Vite
->
Crear tus propios Hooks te permite extraer la lógica de los componentes en funciones reutilizables.
+Vite tiene dos modos de funcionamiento claramente distintos, y funcionan de forma bastante diferente.
-Los hooks personalizados son funciones regulares de JavaScript que pueden utilizar cualquier otro hook, siempre que se adhieran a las [reglas de los hooks](/es/part1/un_estado_mas_complejo_depurando_aplicaciones_react#reglas-de-los-hooks). Además, el nombre de los hooks personalizados debe comenzar con la palabra _use_.
+**Modo de desarrollo** (
npm run dev ) no empaqueta tu código en absoluto. En su lugar, Vite arranca un servidor de desarrollo que sirve los archivos fuente como módulos ES nativos, dejando que el navegador resuelva los imports directamente. Por eso el arranque es casi instantáneo independientemente del tamaño del proyecto.
+Hay una excepción: las dependencias de terceros de node_modules se preempaquetan con esbuild antes de que arranque el servidor. Esto resuelve dos problemas: muchos paquetes de npm siguen estando en formato CommonJS (que los navegadores no pueden consumir nativamente) y algunas librerías consisten en cientos de pequeños archivos internos que, de otro modo, provocarían cientos de peticiones separadas. esbuild convierte y consolida esas dependencias, cachea el resultado en disco, y los siguientes arranques son casi instantáneos.
-Implementamos una aplicación de contador en la [parte 1](/es/part1/estado_del_componente_controladores_de_eventos#control-de-eventos), que puede tener su valor incrementado, reducido o reiniciado. El código de la aplicación es el siguiente:
+**Modo de producción** (
npm run build ) utiliza [Rollup](https://rollupjs.org/) para el bundling, con esbuild encargándose todavía de otras tareas como la transpilación (JSX, TypeScript) y la minificación. Rollup fue diseñado desde el principio para módulos ES, lo que lo hace especialmente bueno en
tree-shaking , una técnica que analiza estáticamente qué exports de cada módulo se usan realmente y elimina el resto del bundle final. Por ejemplo, si importas una sola función de utilidad de una librería grande, el tree-shaking garantiza que el resto del código de esa librería no se incluya en el bundle. Esto puede reducir significativamente el tamaño final.
-```js
-import { useState } from 'react'
-const App = () => {
- const [counter, setCounter] = useState(0)
+La división del trabajo, esbuild para la velocidad y Rollup para la calidad del bundle, es central en el diseño de Vite.
- return (
-
-
{counter}
-
setCounter(counter + 1)}>
- plus
-
-
setCounter(counter - 1)}>
- minus
-
-
setCounter(0)}>
- zero
-
-
- )
-}
-```
+> Puede que te preguntes por qué Vite no usa simplemente esbuild también para el bundling en producción, dado lo rápido que es. La razón es que la salida de bundling de esbuild, aunque correcta, produce resultados menos optimizados en escenarios avanzados: tiene soporte limitado para code splitting, no genera el mismo nivel de optimización de chunks y su ecosistema de plugins para transformaciones a nivel de bundle todavía está madurando. La salida de Rollup es más predecible y está mejor ajustada para los grafos de dependencias complejos que producen las aplicaciones reales. Los autores de Vite [han afirmado](https://vitejs.dev/guide/why.html#why-not-bundle-with-esbuild) que su intención es cambiar a esbuild para el bundling de producción cuando sus capacidades cierren esa brecha.
-Extraigamos la lógica del contador en su propio hook personalizado. El código del hook es el siguiente:
+### Entendiendo esbuild
-```js
-const useCounter = () => {
- const [value, setValue] = useState(0)
+Para entender qué implica realmente el bundling, es útil trabajar directamente con [esbuild](https://esbuild.github.io/), sin la capa de abstracción que Vite añade encima. Construyamos un entorno React mínimo desde cero.
- const increase = () => {
- setValue(value + 1)
- }
+Vamos a crear una app React sencilla con la siguiente estructura de directorios:
- const decrease = () => {
- setValue(value - 1)
- }
+```
+├── dist
+│ └── index.html
+├── src
+│ ├── main.jsx
+│ └── App.jsx
+└── package.json
+```
- const zero = () => {
- setValue(0)
- }
+Empezamos instalando React y react-dom:
- return {
- value,
- increase,
- decrease,
- zero
- }
-}
+```bash
+npm install react react-dom
```
-Nuestro hook personalizado utiliza el hook _useState_ internamente para crear su propio estado. El hook devuelve un objeto, cuyas propiedades incluyen el valor del contador, así como funciones para manipular el valor.
+También necesitamos instalar esbuild:
-Los componentes de React pueden utilizar el hook como se muestra a continuación:
+```bash
+npm install --save-dev esbuild
+```
-```js
-const App = () => {
- const counter = useCounter()
+Al principio añadimos dos scripts al archivo
package.json :
- return (
-
-
{counter.value}
-
- plus
-
-
- minus
-
-
- zero
-
-
- )
+```json
+{
+ "scripts": {
+ "build": "esbuild src/main.jsx --bundle --outfile=dist/main.js --jsx=automatic",
+ "serve": "npx serve dist"
+ },
+ // ...
}
```
-Al hacer esto, podemos extraer el estado del componente _App_ y su manipulación por completo en el hook _useCounter_. La gestión del estado y la lógica del contador ahora es responsabilidad del hook personalizado.
-
-El mismo hook podría
reutilizarse en la aplicación que realizaba un seguimiento de la cantidad de clics realizados en los botones izquierdo y derecho:
+Para la app necesitamos el archivo
dist/index.html , que carga el bundle JavaScript:
+
+```html
+
+
+
+
+
esbuild app
+
+
+
+
+
+
+```
-```js
+El entry point
src/main.jsx es el típico:
-const App = () => {
- const left = useCounter()
- const right = useCounter()
+```jsx
+import React from 'react'
+import ReactDOM from 'react-dom/client'
+import App from './App'
- return (
-
- {left.value}
-
- left
-
-
- right
-
- {right.value}
-
- )
-}
+ReactDOM.createRoot(document.getElementById('root')).render(
)
```
-La aplicación crea
dos contadores completamente separados. El primero se asigna a la variable _left_ y el otro a la variable _right_.
+El componente simple de la aplicación
src/App.jsx es el siguiente:
-Tratar con formularios en React es algo complicado. La siguiente aplicación presenta al usuario un formulario que le solicita que ingrese su nombre, fecha de nacimiento y altura:
+```jsx
+import React, { useState } from 'react'
-```js
const App = () => {
- const [name, setName] = useState('')
- const [born, setBorn] = useState('')
- const [height, setHeight] = useState('')
+ const [counter, setCounter] = useState(0)
return (
-
-
- {name} {born} {height}
-
+
count: {counter}
+
setCounter(counter + 1)}>increment button>
)
}
-```
-Cada campo del formulario tiene su propio estado. Para mantener el estado del formulario sincronizado con los datos proporcionados por el usuario, tenemos que registrar un controlador
onChange apropiado para cada uno de los elementos
input .
+export default App
+```
-Definamos nuestro propio hook personalizado _useField_, que simplifica la gestión del estado del formulario:
+Ahora podemos empaquetar la app:
-```js
-const useField = (type) => {
- const [value, setValue] = useState('')
+```bash
+npm run build
+```
- const onChange = (event) => {
- setValue(event.target.value)
- }
+La salida es un único
dist/main.js que contiene el código de tu aplicación junto con la librería React empaquetada.
- return {
- type,
- value,
- onChange
- }
-}
-```
+Ahora podemos ejecutar la app empaquetada con
npm run serve . Esto usa el paquete [serve](https://www.npmjs.com/package/serve) para iniciar un servidor estático local para el directorio
dist , dejando la aplicación disponible en
http://localhost:3000 :
-La función de hook recibe el tipo de campo de entrada como parámetro. Devuelve todos los atributos requeridos por el
input : su tipo, valor y el controlador onChange.
+
-El hook se puede utilizar de la siguiente manera:
+esbuild también soporta [minificación](https://en.wikipedia.org/wiki/Minification_(programming)) mediante flags de línea de comandos. La minificación elimina espacios en blanco y comentarios, acorta nombres de variables y aplica otras optimizaciones de tamaño. El bundle será notablemente grande porque incluye la librería React completa. La minificación reduce mucho su tamaño.
-```js
-const App = () => {
- const name = useField('text')
- // ...
+Activemos ahora la minificación:
- return (
-
-
-
- )
+```json
+{
+ "scripts": {
+ "build": "esbuild src/main.jsx --bundle --minify --outfile=dist/main.js --jsx=automatic", // highlight-line
+ "serve": "npx serve dist"
+ }
}
```
-### Propagando atributos con Spread
+La minificación reduce el tamaño del bundle de alrededor de 1,1 MB a unos 190 KB, una reducción importante.
-Podríamos simplificar un poco más las cosas. Dado que el objeto _name_ tiene exactamente todos los atributos que el elemento
input espera recibir como props, podemos pasar los props al elemento usando la [sintaxis spread](https://developer.mozilla.org/es/docs/Web/JavaScript/Reference/Operators/Spread_syntax)(spread syntax) de la siguiente manera:
+La minificación tiene una contrapartida: si la aplicación lanza un error en tiempo de ejecución, las herramientas de desarrollo del navegador señalarán una línea del archivo minificado
main.js , que es prácticamente imposible de leer:
-```js
-
-```
+
-Como indica el [ejemplo](https://es.react.dev/learn/updating-objects-in-state#copying-objects-with-the-spread-syntax) en la documentación de React, las siguientes dos formas de pasar props a un componente logran exactamente el mismo resultado:
+La solución es un [source map](https://developer.mozilla.org/en-US/docs/Glossary/Source_map): un archivo complementario (
dist/main.js.map ) que registra cómo cada línea del bundle minificado corresponde al código fuente original. Con él activado, un stack trace apunta a la línea exacta de
App.jsx o
main.jsx en lugar de hacerlo a un muro ilegible de código minificado.
-```js
-
+Podemos activar los source maps añadiendo el flag
--sourcemap :
-const person = {
- firstName: 'Arto',
- lastName: 'Hellas'
+```json
+{
+ "scripts": {
+ "build": "esbuild src/main.jsx --bundle --minify --sourcemap --outfile=dist/main.js --jsx=automatic", // highlight-line
+ "serve": "npx serve dist"
+ }
}
-
-
```
-La aplicación se simplifica en el siguiente formato:
-
-```js
-const App = () => {
- const name = useField('text')
- const born = useField('date')
- const height = useField('number')
+Ahora el error tiene sentido:
- return (
-
-
-
- {name.value} {born.value} {height.value}
-
-
- )
-}
-```
+
-Tratar con formularios se simplifica enormemente cuando los desagradables detalles esenciales relacionados con la sincronización del estado del formulario se encapsulan dentro de nuestro hook personalizado.
+Ten en cuenta que los source maps son muy valiosos durante el desarrollo y la depuración, pero puede que quieras dejarlos fuera de un build público de producción. Como un source map contiene tu código fuente original, cualquiera que abra las herramientas de desarrollo del navegador puede leer la lógica sin minificar de tu aplicación. Si eso te preocupa, simplemente omite el flag
--sourcemap en el comando de build de producción.
-Los hooks personalizados no son solo una herramienta para reutilizar código; sino que también brindan una mejor manera de dividirlo en partes modulares más pequeñas.
+### Transpilación
-### Más sobre hooks
+Junto con el bundling, esbuild realiza otra tarea esencial: la
transpilación . Transpilar significa convertir código fuente escrito en una forma de JavaScript a otra forma, normalmente de sintaxis moderna o extendida a JavaScript plano que el navegador pueda ejecutar.
-Internet está comenzando a llenarse con más y más material útil relacionado con los hooks. Vale la pena consultar las siguientes fuentes:
+Los navegadores entienden JavaScript estándar, pero JSX no es JavaScript válido y ningún navegador puede parsearlo directamente. Cuando escribimos:
-* [Awesome React Hooks Resources](https://github.com/rehooks/awesome-react-hooks)
-* [Easy to understand React Hook recipes by Gabe Ragland](https://usehooks.com/)
-* [Why Do React Hooks Rely on Call Order?](https://overreacted.io/why-do-hooks-rely-on-call-order/)
+```jsx
+const element =
+```
-
+hay que transpilarlo a algo que el navegador sí pueda ejecutar:
-
+```js
+const element = React.createElement(App, null)
+```
-### Ejercicios 7.1.-7.5.
+Por eso la transpilación es un paso obligatorio en cualquier proyecto React, no una optimización opcional. esbuild la realiza automáticamente durante el bundling. Con el flag
--jsx=automatic , esbuild maneja JSX sin ninguna herramienta externa. En el flujo antiguo basado en Webpack había que instalar y configurar [Babel](https://babeljs.io/) y paquetes relacionados para transpilar JSX para el navegador. Con esbuild, los archivos que terminan en
.jsx se transpilan directamente.
-Continuaremos con la aplicación de los [ejercicios](/es/part7/react_router#ejercicios-7-1-7-3) del capítulo de [react router](/es/part7/react_router).
+### Entorno de desarrollo
-#### 7.1: Anécdotas y Hooks paso 1
+Hasta ahora, cada cambio requiere ejecutar
npm run build y refrescar manualmente el navegador, un ciclo lento que rápidamente se vuelve tedioso. El [servidor de desarrollo](https://esbuild.github.io/api/#serve) integrado de esbuild resuelve esto. Añade un script
dev a
package.json :
-Simplifica el formulario de creación de anécdotas de tu aplicación con el hook personalizado _useField_ que definimos anteriormente.
+```json
+{
+ "scripts": {
+ "build": "esbuild src/main.jsx --bundle --minify --sourcemap --outfile=dist/main.js --jsx=automatic",
+ "serve": "npx serve dist",
+ "dev": "esbuild src/main.jsx --bundle --outfile=dist/main.js --jsx=automatic --servedir=./dist --watch" // highlight-line
+ }
+}
+```
-Un lugar natural para guardar los hooks personalizados en tu aplicación es el archivo
/src/hooks/index.js .
+Ejecutar
npm run dev hace dos cosas a la vez. En primer lugar, [--watch](https://esbuild.github.io/api/#watch) le dice a esbuild que observe todos los archivos fuente importados y reconstruya el bundle automáticamente siempre que alguno se guarde. En segundo lugar, [--servedir](https://esbuild.github.io/api/#serve) inicia un servidor HTTP ligero que sirve el contenido del directorio
dist , tu
index.html y el
main.js recién generado en
http://localhost:8000 .
-Si utilizas la [exportación nombrada](https://developer.mozilla.org/es/docs/Web/JavaScript/Reference/Statements/export#descripci%C3%B3n) en lugar de la exportación predeterminada:
+El flag
--servedir es lo que hace que ambas piezas funcionen juntas: sin él, esbuild solo reconstruiría en modo watch pero no serviría nada. Con él, el servidor siempre entrega el último bundle, así que solo necesitas refrescar el navegador después de guardar un archivo.
-```js
-import { useState } from 'react'
+Ten en cuenta que, a diferencia del servidor de desarrollo de Vite, esbuild no soporta hot module replacement. Los cambios en el código fuente requieren un refresco manual del navegador para surtir efecto.
-export const useField = (type) => { // highlight-line
- const [value, setValue] = useState('')
+La claridad de la interfaz de esbuild ilustra lo que hace fundamentalmente un bundler: toma un entry point, sigue todos los imports y produce una salida optimizada. Vite construye encima de esta base y añade la capa de experiencia de desarrollo: un servidor dev, hot module replacement y valores por defecto sensatos para proyectos React.
- const onChange = (event) => {
- setValue(event.target.value)
- }
+Ahora que tenemos una imagen más clara de lo que implican realmente el bundling y la transpilación, volvamos a Vite y veamos cómo puede configurarse.
- return {
- type,
- value,
- onChange
- }
-}
+### Configuración de Vite
-// modules can have several named exports
-export const useAnotherHook = () => { // highlight-line
- // ...
-}
-```
+Para la mayoría de los proyectos React, Vite funciona sin ninguna configuración. Sin embargo, cuando sí necesitas personalizar su comportamiento, editas
vite.config.js (o
vite.config.ts ).
-Luego, la [importación](https://developer.mozilla.org/es/docs/Web/JavaScript/Reference/Statements/import) ocurre de la siguiente manera:
+Una configuración mínima de Vite para un proyecto React se ve así:
```js
-import { useField } from './hooks'
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
-const App = () => {
- // ...
- const username = useField('text')
- // ...
-}
+export default defineConfig({
+ plugins: [react()],
+})
```
-#### 7.2: Anécdotas y Hooks paso 2
+El plugin
@vitejs/plugin-react habilita la transformación de JSX, fast refresh (hot module replacement que conserva el estado del componente) y otras características específicas de React.
-Agrega un botón al formulario que puedas usar para borrar todos los campos de entrada:
+#### Configuración del servidor de desarrollo
-
+Puedes configurar el puerto del servidor de desarrollo y otros ajustes bajo la clave
server :
-Amplia la funcionalidad del hook
useField para que ofrezca una nueva operación
reset para limpiar el campo.
+```js
+export default defineConfig({
+ plugins: [react()],
+ server: {
+ port: 3000,
+ open: true, // open browser automatically
+ },
+})
+```
-Dependiendo de tu solución, es posible que veas la siguiente advertencia en tu consola:
+#### Proxy de peticiones API
-
+Cuando desarrollas en local, tu app React normalmente se ejecuta en un puerto (por ejemplo, 3000) mientras que tu backend corre en otro (por ejemplo, 3001). La política same-origin del navegador normalmente bloquearía las peticiones entre ambos. La configuración de proxy de Vite resuelve esto sin requerir configuración CORS en el backend:
-Volveremos a esta advertencia en el próximo ejercicio.
+```js
+export default defineConfig({
+ plugins: [react()],
+ server: {
+ port: 3000,
+ proxy: {
+ '/api': {
+ target: 'http://localhost:3001',
+ changeOrigin: true,
+ },
+ },
+ },
+})
+```
-#### 7.3: Anécdotas y Hooks paso 3
+Con esta configuración, cualquier petición que tu app React haga a
/api/notes se reenviará automáticamente a
http://localhost:3001/api/notes a través del servidor de desarrollo de Vite. Tu código frontend nunca necesita incluir
localhost:3001 en sus URLs durante el desarrollo.
-Si tu solución no provocó que apareciera una advertencia en la consola, ya has terminado este ejercicio.
+#### Variables de entorno
-Si ves la advertencia _Invalid value for prop \`reset\` on \
tag_ en la consola, realiza los cambios necesarios para deshacerte de ella.
+Vite incluye soporte integrado para variables de entorno mediante archivos
.env . Este es el reemplazo moderno de inyectar constantes manualmente en el bundle.
-El motivo de esta advertencia es que después de realizar los cambios en tu aplicación, la siguiente expresión:
+Crea un archivo
.env en la raíz del proyecto:
-```js
-
+```
+VITE_BACKEND_URL=http://localhost:3001/api/notes
```
-Esencialmente, es lo mismo que esto:
+Y un archivo
.env.production para los valores de producción:
-```js
-
+```
+VITE_BACKEND_URL=https://myapp.fly.dev/api/notes
```
-El elemento
input no debe recibir un atributo
reset .
+**Importante:** todas las variables de entorno expuestas al navegador deben empezar con el prefijo
VITE_ . Las variables sin este prefijo permanecen solo del lado del servidor y no se incluyen en el bundle. Es una medida de seguridad deliberada para evitar filtrar secretos accidentalmente.
-Una solución simple sería no usar la sintaxis de spread y escribir todos los formularios de esta manera:
+Accede a la variable en el código de tu aplicación a través de
import.meta.env :
```js
-
+const App = () => {
+ const notes = useNotes(import.meta.env.VITE_BACKEND_URL)
+
+ return (
+
+ {notes.length} notes on server {import.meta.env.VITE_BACKEND_URL}
+
+ )
+}
```
-Si hiciéramos esto, perderíamos gran parte del beneficio proporcionado por el hook
useField . En su lugar, busca una solución al problema, pero que aún sea fácil de usar con la sintaxis de spread.
+Vite selecciona automáticamente el archivo
.env correcto según el modo:
+
+-
npm run dev usa
.env y
.env.development
+-
npm run build usa
.env y
.env.production
-#### 7.4: Hook de País
+Añade
.env.production a
.gitignore si contiene valores sensibles, y utiliza
.env.example para documentar qué variables son necesarias.
-Volvamos a los ejercicios [2.18-20](/es/part2/agregar_estilos_a_la_aplicacion_react#ejercicios-2-18-2-20).
+#### Transpilación
-Utilza el código de
como punto de partida.
+Vite gestiona la transpilación del código automáticamente. Durante el desarrollo, esbuild transpila tu TypeScript y JSX bajo demanda. Es lo bastante rápido como para hacerlo archivo por archivo sin retraso apreciable. Durante los builds de producción, Rollup se encarga del bundling mientras que esbuild realiza la transpilación.
-La aplicación se puede utilizar para buscar detalles de países desde la interfaz https://studies.cs.helsinki.fi/restcountries/. Si se encuentra a un país, se muestran sus detalles:
+El objetivo de transpilación por defecto en Vite son navegadores modernos que soportan módulos ES nativos (Chrome 87+, Firefox 78+, Safari 14+, Edge 88+). Si necesitas soportar navegadores más antiguos, puedes configurar el target explícitamente y añadir el plugin @vitejs/plugin-legacy :
-
+```bash
+npm install --save-dev @vitejs/plugin-legacy
+```
-Si no se encuentra ningún país, se le muestra un mensaje al usuario
+```js
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+import legacy from '@vitejs/plugin-legacy'
+
+export default defineConfig({
+ plugins: [
+ react(),
+ legacy({
+ targets: ['defaults', 'not IE 11'],
+ }),
+ ],
+})
+```
-
+El plugin legacy genera automáticamente un bundle separado para navegadores antiguos usando Babel.
-Por lo demás, la aplicación está completa, pero en este ejercicio debes implementar un hook personalizado _useCountry_, que se pueda utilizar para buscar los detalles del país dado al hook como parámetro.
+#### CSS
-Usa el endpoint [name](https://studies.cs.helsinki.fi/restcountries/) de la API para obtener los detalles del país en un hook _useEffect_ dentro de su hook personalizado.
+Vite maneja CSS sin ninguna configuración. Simplemente importa un archivo CSS desde tu JavaScript:
-Ten en cuenta que en este ejercicio es esencial utilizar el array del [segundo parámetro](https://react.dev/reference/react/useEffect#parameters) de useEffect para controlar cuándo se ejecuta la función de efecto. Consulta la [parte 2](/es/part2/agregar_estilos_a_la_aplicacion_react#un-par-de-observaciones-importantes) del curso para más información sobre cómo puede usarse el segundo parámetro.
+```js
+import './index.css'
+```
-#### 7.5: Hooks Definitivos
+Vite lo procesará y lo incluirá en el build. En producción, el CSS se extrae a un archivo separado. Durante el desarrollo, se inyecta mediante etiquetas <style> con soporte de hot reload.
-El código de la aplicación responsable de comunicarse con el backend de la aplicación de notas de las partes anteriores se ve así:
+Vite también soporta de forma nativa [CSS Modules](https://github.com/css-modules/css-modules) para estilos con alcance local. Cualquier archivo que termine en .module.css se trata como un CSS Module:
```js
-import axios from 'axios'
-const baseUrl = '/api/notes'
+import styles from './App.module.css'
-let token = null
+const App = () => (
+
+ hello vite
+
+)
+```
-const setToken = newToken => {
- token = `bearer ${newToken}`
-}
+Los preprocessors de CSS como [Sass](https://sass-lang.com/) pueden añadirse simplemente instalando el preprocessor, sin plugin ni configuración adicional:
-const getAll = async () => {
- const response = await axios.get(baseUrl)
- return response.data
-}
+```bash
+npm install --save-dev sass
+```
-const create = async newObject => {
- const config = {
- headers: { Authorization: token },
- }
+Después de eso, los archivos .scss funcionan automáticamente.
- const response = await axios.post(baseUrl, newObject, config)
- return response.data
-}
+#### Minificación
-const update = async (id, newObject) => {
- const response = await axios.put(`${ baseUrl }/${id}`, newObject)
- return response.data
-}
+Al ejecutar npm run build , Vite minifica la salida. La minificación elimina espacios en blanco y comentarios, acorta nombres de variables y aplica otras optimizaciones de tamaño. El resultado es un archivo mucho más pequeño que carga más rápido en el navegador.
-export default { getAll, create, update, setToken }
-```
+Vite usa esbuild para la minificación de JavaScript y un minificador integrado de CSS para las hojas de estilo.
-Notamos que el código de ninguna manera es especifico al hecho de que nuestra aplicación gestiona notas. Excluyendo el valor de la variable _baseUrl_, el mismo código podría reutilizarse en la aplicación de publicación de blogs para tratar la comunicación con el backend.
+#### Source maps
-Extrae el código para comunicarse con el backend en su propio hook _useResource_. Es suficiente implementar la búsqueda de todos los recursos y la creación de un nuevo recurso.
+Los source maps permiten que las herramientas de desarrollo del navegador mapeen errores y breakpoints a tu código fuente original en lugar de al bundle minificado. Sin ellos, un stack trace que apunta a la línea 1 de main.js sirve de muy poco para depurar.
-Puedes hacer el ejercicio en el proyecto que se encuentra en el repositorio https://github.com/fullstack-hy2020/ultimate-hooks. El componente App del proyecto es el siguiente:
+En desarrollo, Vite genera source maps automáticamente. Para builds de producción, puedes habilitarlos explícitamente:
```js
-const App = () => {
- const content = useField('text')
- const name = useField('text')
- const number = useField('text')
+export default defineConfig({
+ plugins: [react()],
+ build: {
+ sourcemap: true,
+ },
+})
+```
- const [notes, noteService] = useResource('http://localhost:3005/notes')
- const [persons, personService] = useResource('http://localhost:3005/persons')
+Ten en cuenta que los source maps de producción aumentan el tiempo de build y exponen tu código fuente a cualquiera que mire la pestaña de red. En muchos casos, es mejor subir los source maps a un servicio de monitorización de errores (como Sentry) y no publicarlos en el servidor.
- const handleNoteSubmit = (event) => {
- event.preventDefault()
- noteService.create({ content: content.value })
- }
-
- const handlePersonSubmit = (event) => {
- event.preventDefault()
- personService.create({ name: name.value, number: number.value})
- }
+#### Plugins
- return (
-
-
notes
-
- {notes.map(n =>
{n.content}
)}
-
-
persons
-
- {persons.map(n =>
{n.name} {n.number}
)}
-
- )
-}
-```
+La funcionalidad de Vite se amplía mediante [plugins](https://vite.dev/plugins/). El ecosistema de plugins ha crecido rápidamente y cubre la mayoría de las necesidades comunes. Algunos plugins ampliamente utilizados incluyen:
+
+- @vitejs/plugin-react — soporte para React (JSX, fast refresh)
+- @vitejs/plugin-legacy — soporte para navegadores antiguos
+- vite-plugin-svgr — importar archivos SVG como componentes React
+- rollup-plugin-visualizer — análisis del tamaño del bundle
+
+Los plugins se especifican en el array plugins dentro de vite.config.js . Siguen la misma interfaz que los plugins de Rollup, por lo que muchos plugins de Rollup también funcionan con Vite.
+
+#### Polyfills
-El hook personalizado _useResource_ devuelve un array de dos elementos al igual que los hooks de estado. El primer elemento del array contiene todos los recursos individuales y el segundo elemento del array es un objeto que se puede usar para manipular la colección de recursos y crear nuevos.
+Un polyfill es código que implementa una funcionalidad para navegadores que no la soportan de forma nativa. La transpilación por sí sola no basta para características que son sintácticamente válidas pero no están implementadas. Por ejemplo, un navegador puede parsear Promise correctamente pero no tener implementación de esa API.
-Si implementas el hook correctamente, se puede usar tanto para notas como para números de teléfono (inicia el servidor con el comando _npm run server_ en el puerto 3005).
+Con Vite, los polyfills se gestionan mediante el plugin @vitejs/plugin-legacy , que incluye automáticamente los polyfills necesarios según tus targets de navegador. Si necesitas un polyfill específico sin usar el plugin legacy, puedes instalarlo directamente e importarlo al principio de tu archivo de entrada.
-
+Puedes consultar la compatibilidad de APIs concretas en [https://caniuse.com](https://caniuse.com) o en la [documentación MDN de Mozilla](https://developer.mozilla.org/).
diff --git a/src/content/7/es/part7c.md b/src/content/7/es/part7c.md
index 059c3af25ab..b1f53a177c3 100644
--- a/src/content/7/es/part7c.md
+++ b/src/content/7/es/part7c.md
@@ -7,8 +7,686 @@ lang: es
-### Más sobre cómo dar estilo a la aplicación
+### Componentes de clase
-Esta sección se ha trasladado a la [parte 5](/es/part5).
+Durante el curso solo hemos utilizado componentes de React que se han definido como funciones de Javascript. Esto no fue posible sin la funcionalidad de [hook](https://reactjs.org/docs/hooks-intro.html) que llegó con la versión 16.8 de React. Antes, al definir un componente que usa estado, había que hacerlo usando la sintaxis de [Class](https://reactjs.org/docs/state-and-lifecycle.html#converting-a-function-to-a-class) de Javascript.
+
+Es beneficioso al menos estar familiarizado con los componentes de clase hasta cierto punto, ya que el mundo contiene una gran cantidad de código React antiguo, que probablemente nunca se reescribirá por completo con la sintaxis actualizada.
+
+Conozcamos las principales características de los componentes de clase produciendo otra aplicación de anécdotas, muy familiar para nosotros. Almacenamos las anécdotas en el archivo
db.json usando
json-server . El contenido del archivo se toma de [aquí](https://github.com/fullstack-hy/misc/blob/master/anecdotes.json).
+
+La versión inicial del componente de clase se ve así
+
+```js
+import React from 'react'
+
+class App extends React.Component {
+ constructor(props) {
+ super(props)
+ }
+
+ render() {
+ return (
+
+
anecdote of the day
+
+ )
+ }
+}
+
+export default App
+```
+
+El componente ahora tiene un [constructor](https://react.dev/reference/react/Component#constructor), en el que no sucede nada en este momento, y contiene el método [render](https://react.dev/reference/react/Component#render). Como se puede suponer, render define cómo y qué se renderiza en la pantalla.
+
+Definamos un estado para la lista de anécdotas y la anécdota actualmente visible. A diferencia de cuando se usa el hook [useState](https://react.dev/reference/react/useState), los componentes de clase solo contienen un estado. Por tanto, si el estado se compone de varias "partes", deben almacenarse como propiedades del estado. El estado se inicializa en el constructor:
+
+```js
+class App extends React.Component {
+ constructor(props) {
+ super(props)
+
+ // highlight-start
+ this.state = {
+ anecdotes: [],
+ current: 0
+ }
+ // highlight-end
+ }
+
+ render() {
+ // highlight-start
+ if (this.state.anecdotes.length === 0) {
+ return
no anecdotes...
+ }
+ // highlight-end
+
+ return (
+
+
anecdote of the day
+ // highlight-start
+
+ {this.state.anecdotes[this.state.current].content}
+
+
next
+ // highlight-end
+
+ )
+ }
+}
+```
+
+El estado del componente está en la variable de instancia _this.state_. El estado es un objeto que tiene dos propiedades.
this.state.anecdotes es la lista de anécdotas y
this.state.current es el índice de la anécdota que se muestra actualmente.
+
+En componentes funcionales, el lugar adecuado para obtener datos de un servidor es dentro de un [effect hook](https://react.dev/reference/react/useEffect), que se ejecuta cuando un componente se renderiza o con menos frecuencia si es necesario, por ejemplo, solo en combinación con el primer renderizado.
+
+Los [métodos de ciclo de vida](https://react.dev/reference/react/Component#adding-lifecycle-methods-to-a-class-component) de los componentes de clase ofrecen la funcionalidad correspondiente. El lugar correcto para desencadenar la obtención de datos de un servidor es dentro del método de ciclo de vida [componentDidMount](https://react.dev/reference/react/Component#componentdidmount), que se ejecuta una vez justo después de que un componente se renderiza por primera vez:
+
+```js
+class App extends React.Component {
+ constructor(props) {
+ super(props)
+
+ this.state = {
+ anecdotes: [],
+ current: 0
+ }
+ }
+
+ // highlight-start
+ componentDidMount = () => {
+ axios.get('http://localhost:3001/anecdotes').then(response => {
+ this.setState({ anecdotes: response.data })
+ })
+ }
+ // highlight-end
+
+ // ...
+}
+```
+
+La función callback de la solicitud HTTP actualiza el estado del componente mediante el método [setState](https://react.dev/reference/react/Component#setstate). El método solo toca las keys que se han definido en el objeto pasado al método como argumento. El valor de la key
current permanece sin cambios.
+
+Llamar al método setState siempre desencadena la re-renderización del componente de clase, es decir que realiza nuevamente un llamado al método _render_.
+
+Terminaremos el componente con la posibilidad de cambiar la anécdota mostrada. El siguiente es el código para todo el componente con la adición resaltada:
+
+```js
+class App extends React.Component {
+ constructor(props) {
+ super(props)
+
+ this.state = {
+ anecdotes: [],
+ current: 0
+ }
+ }
+
+ componentDidMount = () => {
+ axios.get('http://localhost:3001/anecdotes').then(response => {
+ this.setState({ anecdotes: response.data })
+ })
+ }
+
+ // highlight-start
+ handleClick = () => {
+ const current = Math.floor(
+ Math.random() * this.state.anecdotes.length
+ )
+ this.setState({ current })
+ }
+ // highlight-end
+
+ render() {
+ if (this.state.anecdotes.length === 0 ) {
+ return
no anecdotes...
+ }
+
+ return (
+
+
anecdote of the day
+
{this.state.anecdotes[this.state.current].content}
+
next // highlight-line
+
+ )
+ }
+}
+```
+
+A modo de comparación, aquí está la misma aplicación como un componente funcional:
+
+```js
+const App = () => {
+ const [anecdotes, setAnecdotes] = useState([])
+ const [current, setCurrent] = useState(0)
+
+ useEffect(() =>{
+ axios.get('http://localhost:3001/anecdotes').then(response => {
+ setAnecdotes(response.data)
+ })
+ },[])
+
+ const handleClick = () => {
+ setCurrent(Math.round(Math.random() * (anecdotes.length - 1)))
+ }
+
+ if (anecdotes.length === 0) {
+ return
no anecdotes...
+ }
+
+ return (
+
+
anecdote of the day
+
{anecdotes[current].content}
+
next
+
+ )
+}
+```
+
+En el caso de nuestro ejemplo, las diferencias fueron menores. La mayor diferencia entre los componentes funcionales y los componentes de clase es principalmente que el estado de un componente de clase es un solo objeto y que el estado se actualiza utilizando el método _setState_, mientras que en los componentes funcionales el estado puede constar de múltiples variables diferentes, con todas ellos tienen su propia función de actualización.
+
+En 2026, los componentes de clase son en gran medida un artefacto histórico. Todo el desarrollo moderno con React utiliza componentes funcionales con hooks y no hay ninguna razón racional para recurrir a un componente de clase al escribir código nuevo. La propia documentación de React trata los componentes de clase como una API heredada.
+
+### Límite de errores
+
+Aunque los componentes de clase están prácticamente obsoletos, todavía hay una situación en la que no pueden evitarse: los [límites de errores](https://es.react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary). Un límite de errores es un componente que captura errores de JavaScript en cualquier lugar de su árbol de componentes hijo y muestra una interfaz alternativa en vez de bloquear toda la aplicación. En 2026, React aún no ha introducido una alternativa basada en hooks, por lo que los límites de errores siguen teniendo que implementarse como componentes de clase.
+
+Un límite de errores tiene este aspecto:
+
+```js
+import React from 'react'
+
+class ErrorBoundary extends React.Component {
+ constructor(props) {
+ super(props)
+ this.state = { hasError: false, error: null }
+ }
+
+ static getDerivedStateFromError(error) {
+ return { hasError: true, error }
+ }
+
+ componentDidCatch(error, info) {
+ console.error('ErrorBoundary caught an error', error, info)
+ }
+
+ render() {
+ if (this.state.hasError) {
+ return (
+
+
Something went wrong.
+
{this.state.error.message}
+
this.setState({ hasError: false, error: null })}>
+ try again
+
+
+ )
+ }
+
+ return this.props.children
+ }
+}
+
+export default ErrorBoundary
+```
+
+Los dos métodos clave del ciclo de vida son
getDerivedStateFromError , que actualiza el estado para que el siguiente renderizado muestre la interfaz alternativa, y
componentDidCatch , que es un buen lugar para registrar el error en un servicio de notificación de errores.
+
+Puedes envolver cualquier parte del árbol de componentes con un límite de errores para contener los fallos dentro de ese subárbol:
+
+```js
+const App = () => {
+ return (
+
+ )
+}
+```
+
+Si
Notes lanza un error, solo esa sección muestra la interfaz alternativa.
Persons continúa funcionando con normalidad.
+
+Como este es el único caso de uso que queda para los componentes de clase, muchos proyectos utilizan la librería [react-error-boundary](https://github.com/bvaughn/react-error-boundary), que oculta el mecanismo basado en clases tras una cómoda API de componentes funcionales para que nunca tengas que escribir tú mismo un componente de clase.
+
+### Frontend y backend en el mismo repositorio
+
+Durante el curso, hemos creado el frontend y el backend en repositorios separados. Este es un enfoque muy típico. Sin embargo, hicimos el despliegue [copiando](/es/part3/despliegue_de_la_aplicacion_a_internet#sirviendo-archivos-estaticos-desde-el-backend) el código de frontend incluido en el repositorio de backend. Un enfoque posiblemente mejor habría sido desplegar el código del frontend por separado.
+
+A veces, toda la aplicación se coloca en un único repositorio. Una forma habitual y limpia de hacerlo con un stack moderno consiste en mantener el frontend de Vite en un directorio
client y el backend de Express en un directorio
server , cada uno con su propio
package.json . La raíz del repositorio recibe un tercer
package.json que actúa como envoltorio práctico con scripts para ejecutar ambos conjuntamente.
+
+Una estructura mínima de un [repositorio](https://github.com/fullstack-hy2020/monorepo) de este tipo tiene el siguiente aspecto:
+
+```
+app/
+ package.json (root, scripts only)
+ client/
+ package.json (Vite + React)
+ vite.config.js
+ src/
+ App.jsx
+ server/
+ package.json (Express)
+ index.js
+```
+
+El servidor Express de
server/index.js proporciona la API y, en producción, también sirve el frontend compilado desde el directorio
client/dist :
+
+```js
+const express = require('express')
+const path = require('path')
+
+const app = express()
+
+app.use(express.json())
+
+app.get('/api/ping', (req, res) => {
+ res.json({ message: 'pong', time: new Date().toISOString() })
+})
+
+// serve the built Vite frontend in production
+if (process.env.NODE_ENV === 'production') {
+ app.use(express.static(path.join(__dirname, '../client/dist')))
+ app.get('/*splat', (req, res) => {
+ res.sendFile(path.join(__dirname, '../client/dist/index.html'))
+ })
+}
+
+const PORT = process.env.PORT || 3001
+app.listen(PORT, () => console.log(`server running on port ${PORT}`))
+```
+
+Durante el desarrollo, el servidor de desarrollo de Vite se ejecuta en su propio puerto y necesita reenviar las peticiones de la API a Express. Esto se configura en
client/vite.config.js :
+
+```js
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+
+export default defineConfig({
+ plugins: [react()],
+ server: {
+ proxy: {
+ '/api': 'http://localhost:3001',
+ },
+ },
+})
+```
+
+Con el proxy configurado, una llamada
fetch a
/api/ping desde el frontend se reenvía automáticamente al servidor Express durante el desarrollo, por lo que nunca es necesario fijar en el código la URL del backend.
+
+El
package.json de la raíz conecta todo mediante un par de scripts:
+
+```json
+{
+ "scripts": {
+ "dev": "concurrently \"npm run dev --prefix server\" \"npm run dev --prefix client\"",
+ "build": "npm run build --prefix client",
+ "start": "NODE_ENV=production npm start --prefix server"
+ },
+ "devDependencies": {
+ "concurrently": "^8.0.0"
+ }
+}
+```
+
+Aquí hay un par de aspectos interesantes.
+
+El script
dev utiliza [concurrently](https://github.com/open-cli-tools/concurrently), una pequeña utilidad que ejecuta varios comandos al mismo tiempo y combina su salida en un único flujo del terminal. Sin ella habría que abrir dos terminales independientes, uno para el backend y otro para el frontend.
+
+La opción
--prefix indica a npm qué subdirectorio debe tratar como directorio de trabajo del comando, por lo que
npm run dev --prefix server equivale a
cd server && npm run dev .
+
+Por tanto, ejecutar
npm run dev desde la raíz inicia en paralelo el servidor de desarrollo de Vite y Express con un solo comando. En este modo, Vite sirve el frontend con sustitución de módulos en caliente: al editar un componente de React, el navegador se actualiza de inmediato sin recargar toda la página. El servidor Express se ejecuta por separado y el proxy de Vite le reenvía las peticiones a
/api .
+
+Ejecutar
npm run build compila el frontend en el directorio
client/dist . Después,
npm start establece
NODE_ENV=production e inicia Express, que recoge los archivos estáticos de
client/dist y sirve tanto la API como el frontend desde un único puerto. Esta es la configuración que se utilizaría al desplegar en un servidor.
+
+Como cada parte del proyecto tiene su propio
package.json , debes indicar de forma explícita a cuál te diriges al instalar nuevos paquetes. La misma opción
--prefix también funciona con
npm install :
+
+```bash
+npm install axios --prefix client # add to the frontend
+npm install mongoose --prefix server # add to the backend
+```
+
+Como alternativa, puedes entrar con
cd en el directorio correspondiente y ejecutar allí
npm install de la forma habitual.
+
+### Organización del código en una aplicación React
+
+En la mayoría de las aplicaciones del curso hemos seguido la convención de colocar los componentes en un directorio
components , los hooks en
hooks y el código de comunicación con el servidor en
services . Para la aplicación BlogList, esto podría tener el siguiente aspecto:
+
+```
+src/
+ App.jsx
+ components/
+ Blog.jsx
+ BlogList.jsx
+ LoginForm.jsx
+ Notification.jsx
+ hooks/
+ useField.js
+ services/
+ blogs.js
+ users.js
+ stores/
+ blogStore.js
+ notificationStore.js
+```
+
+Esta agrupación plana por tipo funciona bien para aplicaciones pequeñas.
+
+Cuando la aplicación utiliza enrutamiento, es habitual añadir un directorio
pages —a veces llamado
views — para los componentes de nivel superior correspondientes a las rutas, manteniendo los componentes de interfaz reutilizables en
components . Esta convención se utiliza en frameworks como [Next.js](https://nextjs.org/docs/pages/building-your-application/routing) y se describe en las [preguntas frecuentes de React sobre la estructura de archivos](https://legacy.reactjs.org/docs/faq-structure.html):
+
+```
+src/
+ App.jsx
+ pages/
+ HomePage.jsx
+ BlogPage.jsx
+ UserPage.jsx
+ components/
+ Blog.jsx
+ BlogList.jsx
+ LoginForm.jsx
+ Notification.jsx
+ hooks/
+ useField.js
+ services/
+ blogs.js
+ users.js
+ stores/
+ blogStore.js
+ notificationStore.js
+```
+
+Sin embargo, cuando el código sigue creciendo, un cambio en una única funcionalidad puede seguir afectando a archivos dispersos por todos los directorios, y tanto
components como
pages pueden resultar difíciles de recorrer.
+
+Una respuesta habitual consiste en agrupar los archivos por
funcionalidad . La metodología [Feature-Sliced Design](https://feature-sliced.design/) formaliza este enfoque, y el proyecto [bulletproof-react](https://github.com/alan2207/bulletproof-react) es un ejemplo ampliamente citado de su aplicación práctica:
+
+```
+src/
+ App.jsx
+ features/
+ blogs/
+ Blog.jsx
+ BlogList.jsx
+ blogService.js
+ blogStore.js
+ users/
+ UserList.jsx
+ userService.js
+ notifications/
+ Notification.jsx
+ notificationStore.js
+ hooks/
+ useField.js
+```
+
+Todo lo relacionado con los blogs se encuentra en un mismo lugar, por lo que añadir o modificar una funcionalidad implica trabajar en un solo sitio en vez de en varios. No existe una única forma correcta de organizar un proyecto grande; la elección adecuada depende del tamaño y la naturaleza de la aplicación.
+
+### Cambios en el servidor
+
+Las aplicaciones que creamos durante este curso obtienen datos del servidor cuando se carga la página y después de las acciones del usuario, pero no tienen forma de enterarse de los cambios realizados por otros usuarios. Si otro usuario añade una nueva entrada de blog, nuestro frontend sencillamente no lo sabe hasta que se actualiza la página. ¿Cómo podemos mantener la interfaz sincronizada con un servidor que cambia de forma independiente?
+
+El enfoque más sencillo es el [polling](
): el frontend solicita repetidamente al servidor datos actualizados a intervalos fijos, por ejemplo mediante [setInterval](https://developer.mozilla.org/es/docs/Web/API/setInterval). El polling es fácil de implementar, pero derrochador, porque la mayoría de las peticiones no devuelven nada nuevo.
+
+Una alternativa más limpia son los [WebSockets](https://developer.mozilla.org/es/docs/Web/API/WebSockets_API), que abren una conexión bidireccional persistente entre el navegador y el servidor. Así, el servidor puede enviar actualizaciones a los clientes conectados en cuanto cambia algo, sin que el cliente tenga que preguntar. Todos los navegadores modernos son compatibles con WebSockets.
+
+Trabajar directamente con la API de WebSocket puede resultar engorroso. La librería [Socket.io](https://socket.io/) la envuelve con una API de más alto nivel y añade reconexión automática y otras facilidades.
+
+En la [parte 8](/es/part8) veremos GraphQL, que incluye un mecanismo de suscripciones que permite al servidor notificar a los clientes los cambios en los datos de forma estructurada.
+
+### Seguridad en aplicaciones React/node
+
+Hasta ahora, durante el curso, no hemos abordado en absoluto la seguridad de la información. Tampoco tenemos mucho tiempo ahora, pero afortunadamente la Universidad de Helsinki cuenta con un curso MOOC [Securing Software](https://cybersecuritybase.mooc.fi/module-2.1) que cubre este tema tan importante.
+
+Sin embargo, echaremos un vistazo a algunas cosas específicas de este curso.
+
+El Open Web Application Security Project (proyecto abierto de seguridad de aplicaciones web), también conocido como [OWASP](https://www.owasp.org), publica una lista anual de los riesgos de seguridad más comunes en las aplicaciones web. La lista más reciente se puede encontrar [aquí](https://owasp.org/Top10/). Los mismos riesgos se pueden encontrar de un año a otro.
+
+En la parte superior de la lista encontramos la inyección , lo que significa que, por ejemplo, el texto enviado mediante un formulario en una aplicación se interpreta de forma completamente diferente de lo que pretendía el desarrollador del software. El tipo de inyección más famoso es probablemente la [inyección SQL](https://stackoverflow.com/questions/332365/how-does-the-sql-injection-from-the-bobby-tables-xkcd-comic-work).
+
+Por ejemplo, si la siguiente consulta SQL se ejecuta en una aplicación vulnerable:
+
+```js
+let query = "SELECT * FROM Users WHERE name = '" + userName + "';"
+```
+
+Ahora supongamos que un usuario malintencionado Arto Hellas definiría su nombre como
+
+```
+Arto Hell-as'; DROP TABLE Users; --
+```
+
+para que el nombre contenga una comilla simple ', que es el carácter inicial y final de un string SQL. Como resultado de esto, se ejecutarían dos operaciones SQL, la segunda de las cuales destruiría la tabla Users de la base de datos.
+
+```sql
+SELECT * FROM Users WHERE name = 'Arto Hell-as'; DROP TABLE Users; --'
+```
+
+Las inyecciones de SQL se previenen utilizando [queries parametrizadas](https://security.stackexchange.com/questions/230211/why-are-stored-procedures-and-prepared-statements-the-preferred-modern-methods-f). Con ellas, el input del usuario no se mezcla con la SQL query, en cambio, la base de datos inserta los valores del input en los placeholders de la query (normalmente ?):
+
+```js
+execute("SELECT * FROM Users WHERE name = ?", [userName])
+```
+
+Los ataques de inyección también son posibles en bases de datos NoSQL. Sin embargo, mongoose los previene [desinfectando](https://zanon.io/posts/nosql-injection-in-mongodb) las consultas. Puedes encontrar más información sobre el tema, por ejemplo, [aquí](https://web.archive.org/web/20220901024441/https://blog.websecurify.com/2014/08/hacking-nodejs-and-mongodb.html).
+
+Cross-site scripting (XSS) es un ataque en el que es posible inyectar código JavaScript malicioso en una aplicación web legítima. Luego, el código malicioso se ejecutaría en el navegador de la víctima. Si intentamos inyectar lo siguiente en, por ejemplo, la aplicación de notas:
+
+```html
+
+```
+
+el código no se ejecuta, sino que solo se renderiza como 'texto' en la página:
+
+
+
+ya que React [se encarga de desinfectar los datos en variables](https://es.legacy.reactjs.org/docs/introducing-jsx.html#jsx-prevents-injection-attacks). Algunas versiones de React [han sido vulnerables](https://medium.com/dailyjs/exploiting-script-injection-flaws-in-reactjs-883fb1fe36c1) a los ataques XSS. Los agujeros de seguridad, por supuesto, han sido reparados, pero no hay garantía de que no vuelvan a suceder.
+
+Es necesario permanecer alerta cuando se utilizan librerías; si hay actualizaciones de seguridad para esas librerías, se recomienda actualizarlas en nuestras aplicaciones. Las actualizaciones de seguridad para Express se encuentran en la [documentación de la librería](https://expressjs.com/en/advanced/security-updates.html) y las de Node se encuentran en [este blog](https://nodejs.org/en/blog/vulnerability/).
+
+Puedes verificar qué tan actualizadas están tus dependencias usando el comando
+
+```bash
+npm outdated --depth 0
+```
+
+El proyecto del año pasado que es utilizado en la [parte 9](/es/part9) ya tiene bastantes dependencias desactualizadas:
+
+
+
+Las dependencias se pueden actualizar modificando el archivo package.json . La mejor forma de hacerlo es utilizar una herramienta llamada npm-check-updates . Puede instalarse globalmente ejecutando el comando:
+
+```bash
+npm install -g npm-check-updates
+```
+
+Usando esta herramienta, la actualización de las dependencias se verifica de la siguiente manera:
+
+```bash
+$ npm-check-updates
+Checking ...\my-app\package.json
+[====================] 11/11 100%
+
+ @testing-library/react ^14.0.0 → ^15.0.0
+ @testing-library/user-event ^14.4.3 → ^14.5.2
+ react ^18.2.0 → ^19.0.0
+ vite ^5.0.0 → ^6.0.0
+
+Run ncu -u to upgrade package.json
+```
+
+El archivo package.json se actualiza ejecutando el comando ncu -u .
+
+```bash
+$ ncu -u
+Upgrading ...\my-app\package.json
+[====================] 11/11 100%
+
+ @testing-library/react ^14.0.0 → ^15.0.0
+ @testing-library/user-event ^14.4.3 → ^14.5.2
+ react ^18.2.0 → ^19.0.0
+ vite ^5.0.0 → ^6.0.0
+
+Run npm install to install new versions.
+```
+
+Ahora es el momento de actualizar las dependencias ejecutando el comando npm install . Sin embargo, las versiones antiguas de las dependencias no son necesariamente un riesgo de seguridad.
+
+El comando npm [audit](https://docs.npmjs.com/cli/audit) se puede utilizar para verificar la seguridad de las dependencias. Compara los números de versión de las dependencias de tu aplicación con una lista de los números de versión de las dependencias que contienen amenazas de seguridad conocidas en una base de datos de errores centralizada.
+
+Al ejecutar npm audit en el mismo proyecto, se imprime una larga lista de quejas y correcciones sugeridas.
+A continuación se muestra una parte del informe:
+
+```js
+$ patientor npm audit
+
+... many lines removed ...
+
+url-parse <1.5.2
+Severity: moderate
+Open redirect in url-parse - https://github.com/advisories/GHSA-hh27-ffr2-f2jc
+fix available via `npm audit fix`
+node_modules/url-parse
+
+ws 6.0.0 - 6.2.1 || 7.0.0 - 7.4.5
+Severity: moderate
+ReDoS in Sec-Websocket-Protocol header - https://github.com/advisories/GHSA-6fc8-4gx4-v693
+ReDoS in Sec-Websocket-Protocol header - https://github.com/advisories/GHSA-6fc8-4gx4-v693
+fix available via `npm audit fix`
+node_modules/webpack-dev-server/node_modules/ws
+node_modules/ws
+
+120 vulnerabilities (102 moderate, 16 high, 2 critical)
+
+To address issues that do not require attention, run:
+ npm audit fix
+
+To address all issues (including breaking changes), run:
+ npm audit fix --force
+```
+
+Después de solo un año, el código está lleno de pequeñas amenazas de seguridad. Afortunadamente, solo hay 2 amenazas críticas. Ejecutemos npm audit fix como sugiere el informe:
+
+```js
+$ npm audit fix
+
++ mongoose@5.9.1
+added 19 packages from 8 contributors, removed 8 packages and updated 15 packages in 7.325s
+fixed 354 of 416 vulnerabilities in 20047 scanned packages
+ 1 package update for 62 vulns involved breaking changes
+ (use `npm audit fix --force` to install breaking changes; or refer to `npm audit` for steps to fix these manually)
+```
+
+Quedan 62 amenazas porque, por defecto, audit fix no actualiza las dependencias si su número de versión major ha aumentado. Actualizar estas dependencias podría hacer que toda la aplicación dejara de funcionar.
+
+El origen del error crítico es la librería [immer](https://github.com/immerjs/immer)
+
+```js
+immer <9.0.6
+Severity: critical
+Prototype Pollution in immer - https://github.com/advisories/GHSA-33f9-j839-rf8h
+fix available via `npm audit fix --force`
+Will install react-scripts@5.0.0, which is a breaking change
+```
+
+Ejecutar npm audit fix --force actualizaría la versión de la librería, pero también actualizaría react-scripts , lo que podría hacer que el entorno de desarrollo dejara de funcionar. Así que dejaremos las actualizaciones de las librerías para más adelante...
+
+Una de las amenazas mencionadas en la lista de OWASP es Broken Authentication y Broken Access Control . La autenticación basada en tokens que hemos estado usando es bastante sólida si la aplicación se usa con el protocolo HTTPS de cifrado de tráfico. Al implementar el control de acceso, por ejemplo, se debe recordar no solo verificar la identidad de un usuario en el navegador, sino también en el servidor. Mala seguridad sería evitar que se tomen algunas acciones solo ocultando las opciones de ejecución en el código del navegador.
+
+En MDN de Mozilla hay una muy buena [guía de seguridad de sitios web](https://developer.mozilla.org/es/docs/Learn/Server-side/First_steps/Website_security), que trae a colación este tema tan importante:
+
+
+
+La documentación de Express incluye una sección sobre seguridad: [Mejores prácticas de producción: seguridad](https://expressjs.com/es/advanced/best-practice-security.html), que vale la pena leer. También se recomienda agregar una librería llamada [Helmet](https://helmetjs.github.io/) al backend. Incluye un conjunto de middlewares que eliminan algunas vulnerabilidades de seguridad en aplicaciones Express.
+
+También vale la pena usar el [plugin de seguridad](https://github.com/nodesecurity/eslint-plugin-security) de ESlint.
+
+### Tendencias actuales
+
+Finalmente, echemos un vistazo a la tecnología del mañana (o en realidad ya de hoy), y las direcciones en las que se dirige el desarrollo web.
+
+#### Versiones tipadas de JavaScript
+
+El [tipado dinámico](https://developer.mozilla.org/es/docs/Glossary/Dynamic_typing) de JavaScript puede producir errores sutiles que solo se descubren durante la ejecución. En la parte 5 mencionamos [PropTypes](/es/part5/props_children_y_la_ref_del_componente#prop-types) como forma de añadir comprobaciones de tipos en tiempo de ejecución a las props de los componentes, pero PropTypes ha caído en gran medida en desuso a medida que el ecosistema ha adoptado la [comprobación estática de tipos](https://en.wikipedia.org/wiki/Type_system#Static_type_checking).
+
+[TypeScript](https://www.typescriptlang.org/), desarrollado por Microsoft, se ha convertido en el estándar de facto para JavaScript tipado. Detecta errores de tipo en tiempo de compilación en vez de durante la ejecución, ofrece herramientas excelentes para el editor y ya se utiliza en la mayoría de los proyectos nuevos de React. TypeScript se cubre en la [parte 9](/es/part9).
+
+#### Renderizado en el servidor y React Server Components
+
+Los componentes de React no tienen por qué ejecutarse en el navegador. También pueden renderizarse en el [servidor](https://es.react.dev/reference/react-dom/server), que envía HTML ya preparado al cliente en vez de una página vacía que JavaScript deba rellenar. Este server-side rendering (SSR) mejora el tiempo de carga percibido y es importante para la optimización para motores de búsqueda (SEO), ya que los rastreadores ven el contenido completamente renderizado sin tener que ejecutar JavaScript.
+
+El desarrollo más reciente y significativo son los [React Server Components](https://react.dev/blog/2023/03/22/react-labs-what-we-have-been-working-on-march-2023#react-server-components) (RSC), introducidos en React 18 y ahora parte fundamental de la arquitectura de React. Un componente de servidor se ejecuta exclusivamente en el servidor y nunca se envía al navegador como JavaScript. Puede leer directamente de una base de datos o del sistema de archivos, mantener secretos fuera del bundle del cliente y transmitir su salida al navegador. El navegador recibe estos componentes como datos renderizados, no como código ejecutable. Los componentes de cliente , marcados con 'use client', siguen ejecutándose en el navegador y gestionan la interactividad como antes. En una aplicación RSC, la mayoría de los componentes son componentes de servidor por defecto y solo se utilizan componentes de cliente donde se necesita interacción con el usuario.
+
+[Next.js](https://nextjs.org/) se ha convertido en el framework estándar para crear aplicaciones React que necesitan comportamiento del lado del servidor. Su App Router —introducido en Next.js 13— se basa en React Server Components y proporciona enrutamiento basado en archivos, layouts anidados, acciones de servidor para modificar datos y soporte incorporado para la generación estática y la regeneración estática incremental. En 2026, Next.js es la primera opción para cualquier proyecto React en el que importen el SSR, el SEO o las capacidades full stack.
+
+#### Arquitectura de microservicios
+
+Durante este curso solo hemos arañado la superficie del lado del servidor. En nuestras aplicaciones teníamos un backend monolítico , es decir, una aplicación que formaba un todo y se ejecutaba en un solo servidor, sirviendo solo unos pocos endpoints de API.
+
+A medida que la aplicación crece, el enfoque de backend monolítico comienza a tornarse problemático tanto en términos de rendimiento como de mantenibilidad.
+
+Una [arquitectura de microservicios](https://martinfowler.com/articles/microservices.html) es una forma de componer el backend de una aplicación a partir de muchos servicios independientes que se comunican entre sí a través de la red. El propósito de un microservicio individual es encargarse de un todo funcional lógico particular. En una arquitectura de microservicios pura, los servicios no utilizan una base de datos compartida.
+
+Por ejemplo, la aplicación de lista de blogs podría constar de dos servicios: uno que maneja al usuario y otro que se ocupa de los blogs. La responsabilidad del servicio de usuario sería el registro y la autenticación del usuario, mientras que el servicio de blogs se haría cargo de las operaciones relacionadas con los blogs.
+
+La siguiente imagen muestra la diferencia entre la estructura de una aplicación basada en una arquitectura de microservicios y una basada en una estructura monolítica más tradicional:
+
+
+
+El papel del frontend (encerrado por un cuadrado en la imagen) no difiere mucho entre los dos modelos. A menudo existe una [API gateway](http://microservices.io/patterns/apigateway) (puerta de enlace API) entre los microservicios y el frontend, que proporciona la ilusión de una API más tradicional de "todo en el mismo servidor". [Netflix](https://medium.com/netflix-techblog/optimizing-the-netflix-api-5c9ac715cf19), entre otros, utiliza este tipo de enfoque.
+
+Las arquitecturas de microservicios surgieron y evolucionaron para las necesidades de las grandes aplicaciones a gran escala de Internet. Amazon marcó la tendencia mucho antes de la aparición del término microservicio. El punto de partida crítico fue un correo electrónico enviado a todos los empleados en 2002 por el CEO de Amazon, Jeff Bezos:
+
+> De ahora en adelante, todos los equipos expondrán sus datos y funcionalidad a través de interfaces de servicio.
+>
+> Los equipos deben comunicarse entre sí a través de estas interfaces.
+>
+> No se permitirá ninguna otra forma de comunicación entre procesos: ningún enlace directo, ninguna lectura directa del almacén de datos de otro equipo, ningún modelo de memoria compartida, ninguna puerta trasera. La única comunicación permitida es a través de llamadas de interfaz de servicio a través de la red.
+>
+> No importa qué tecnología uses.
+>
+> Todas las interfaces de servicio, sin excepción, deben diseñarse desde cero para ser externalizables. Es decir, el equipo debe planificar y diseñar para poder exponer la interfaz a los desarrolladores del mundo exterior.
+>
+> Sin excepciones.
+>
+> Cualquiera que no haga esto será despedido. Gracias; ¡que tengas un buen día!
+
+Hoy en día, uno de los mayores precursores en el uso de microservicios es [Netflix](https://www.infoq.com/presentations/netflix-chaos-microservices).
+
+El uso de microservicios ha ido ganando popularidad hasta convertirse en una especie de [bala de plata](https://es.wikipedia.org/wiki/No_hay_balas_de_plata) de hoy en día, que se ofrece como una solución a casi todo tipo de problemas. Sin embargo, hay una serie de desafíos cuando se trata de aplicar una arquitectura de microservicios, y podría tener sentido ir [primero al monolito](https://martinfowler.com/bliki/MonolithFirst.html) al hacer inicialmente un backend tradicional que lo abarque todo. O quizás [no](https://martinfowler.com/articles/dont-start-monolith.html). Hay un montón de opiniones diferentes sobre el tema. Ambos enlaces conducen al sitio de Martin Fowler; como podemos ver, incluso los sabios no están completamente seguros de cuál de las formas correctas es la más correcta.
+
+Desafortunadamente, no podemos profundizar en este importante tema durante este curso. Incluso una mirada superficial al tema requeriría al menos 5 semanas más.
+
+#### Serverless (Sin servidor)
+
+Después del lanzamiento del servicio [Lambda](https://aws.amazon.com/lambda/) de Amazon a fines de 2014, comenzó a surgir una nueva tendencia en el desarrollo de aplicaciones web: [sin servidor](https://serverless.com/).
+
+Lo principal acerca de Lambda, y hoy en día también de Google [Cloud functions](https://cloud.google.com/functions/), así como de una [funcionalidad similar en Azure](https://azure.microsoft.com/en-us/services/functions/), es que permite la ejecución de funciones individuales en la nube. Antes, la unidad ejecutable más pequeña en la nube era un proceso único , por ejemplo, un entorno de ejecución que ejecuta un backend de Node.
+
+Utilizando la [API gateway](https://aws.amazon.com/es/api-gateway/) de Amazon es posible crear aplicaciones sin servidor donde las solicitudes a la API HTTP definida obtienen respuestas directamente de las funciones de la nube. Por lo general, las funciones ya operan utilizando datos almacenados en las bases de datos del servicio en la nube.
+
+Serverless no se trata de que no haya un servidor en las aplicaciones, sino de cómo se define el servidor. El desarrollador de software puede cambiar sus esfuerzos de programación a un mayor nivel de abstracción, pues ya no es necesario definir mediante programación el enrutamiento de solicitudes HTTP, relaciones de bases de datos, etc., debido a que la infraestructura de la nube proporciona todo esto. Las funciones en la nube también se prestan para crear un buen sistema de escalado, por ejemplo, Lambda de Amazon puede ejecutar una gran cantidad de funciones en la nube por segundo. Todo esto ocurre automáticamente a través de la infraestructura y no es necesario iniciar nuevos servidores, etc.
+
+### Librerías útiles y lecturas adicionales
+
+La comunidad de desarrolladores de JavaScript ha creado una gran variedad de librerías útiles. Antes de escribir algo desde cero, siempre merece la pena comprobar si ya existe una solución bien mantenida.
+
+Puedes aprovechar tus conocimientos de React para desarrollar aplicaciones móviles con [React Native](https://reactnative.dev/), tema que se trata en la [parte 10](/es/part10) del curso.
+
+El curso continúa más allá de la parte 7: la [parte 8](/es/part8) cubre GraphQL; la [parte 9](/es/part9), TypeScript; la [parte 10](/es/part10), React Native; la [parte 11](/es/part11), CI/CD; y la [parte 12](/es/part12), contenedores. El contenido completo se encuentra en la [página del curso](/es/#contenido-del-curso).
+
+Los siguientes recursos externos son buenos lugares para profundizar en patrones de React, calidad del código y el ecosistema en general:
+
+- [Patterns.dev](https://www.patterns.dev/) trata en profundidad patrones modernos de React y JavaScript. Como colección seleccionada de técnicas específicas de React, [React bits](https://vasanthk.gitbooks.io/react-bits/) es un complemento útil.
+- [Overreacted](https://overreacted.io/) es el blog de Dan Abramov, uno de los miembros originales del equipo principal de React. Sus artículos profundizan en las decisiones de diseño y los modelos mentales de React y merece la pena leerlos aunque tengan ya algunos años.
+- [Kent C. Dodds](https://kentcdodds.com/blog) escribe extensamente sobre buenas prácticas de React, pruebas y diseño de componentes. En particular, sus artículos sobre la filosofía de las pruebas han influido en la forma en que la comunidad concibe las pruebas del frontend.
+- [Tao of React](https://alexkondov.com/tao-of-react/) es una guía breve y deliberadamente subjetiva para estructurar aplicaciones React que trata los componentes, el estado, las props y la organización de proyectos de forma pragmática.
+- [Reactiflux](https://www.reactiflux.com/) es una gran comunidad de desarrolladores de React en Discord y un buen lugar para plantear preguntas después de terminar el curso. Muchas librerías de código abierto mantienen allí sus propios canales.
diff --git a/src/content/7/es/part7d.md b/src/content/7/es/part7d.md
index 4c35d1f9195..2812f0c428e 100644
--- a/src/content/7/es/part7d.md
+++ b/src/content/7/es/part7d.md
@@ -7,435 +7,194 @@ lang: es
-En los primeros tiempos, React era algo famoso por ser muy difícil de configurar con las herramientas necesarias para el desarrollo de aplicaciones. Para facilitar la situación, se desarrolló [Create React App](https://github.com/facebookincubator/create-react-app), que eliminó los problemas relacionados con la configuración. [Vite](https://vitejs.dev/), que se utiliza a lo largo de este curso, ha reemplazado desde entonces a Create React App como el estándar para nuevas aplicaciones React.
+Además de los seis ejercicios de las secciones sobre [hooks de React](/es/part7/mas_sobre_los_hooks_de_react) de esta parte del material, hay 14 ejercicios que continúan nuestro trabajo con la aplicación BlogList de las partes cuatro y cinco. Algunos de los siguientes ejercicios son funcionalidades independientes entre sí, lo que significa que no es necesario completarlos en un orden concreto. Puedes saltarte una parte de los ejercicios si así lo deseas. Muchos de ellos consisten en aplicar las técnicas avanzadas de gestión del estado —Zustand, React Query y context— tratadas en la [parte 6](/es/part6).
-Tanto Vite como Create React App utilizan
bundlers para hacer el trabajo real. En esta sección veremos más de cerca qué hacen realmente los bundlers, cómo funciona Vite por debajo y cómo configurarlo para distintos escenarios. También examinaremos brevemente [esbuild](https://esbuild.github.io/), un bundler de bajo nivel que el propio Vite usa internamente. Entender esbuild ayuda a aclarar qué significa, en el fondo, el bundling.
+Si no quieres utilizar tu propia aplicación BlogList, puedes usar el código de la solución modelo como punto de partida para estos ejercicios.
-> #### ¿Y Webpack?
->
->Webpack fue el bundler dominante durante la mayor parte de la década de 2010 y todavía aparece en bases de código antiguas y empresariales. Este curso también cubrió Webpack hasta la primavera de 2026.
->
-> Si trabajas en un proyecto legacy, es útil saber que Webpack existe y que utiliza los mismos conceptos básicos (entry points, loaders/plugins, output). Sin embargo, en 2026 no se recomienda configurar un proyecto nuevo con Webpack. Su configuración es compleja y las herramientas modernas como Vite proporcionan una experiencia de desarrollo mucho mejor. No cubriremos la configuración de Webpack en este curso.
+Muchos de los ejercicios de esta parte del material requerirán [refactorizar](https://es.wikipedia.org/wiki/Refactorizaci%C3%B3n) código existente. Esta es una realidad habitual al ampliar aplicaciones existentes, por lo que la refactorización es una habilidad importante y necesaria aunque a veces pueda resultar difícil y desagradable.
-### Bundling
+Un buen consejo tanto para refactorizar como para escribir código nuevo es avanzar a
pasos pequeños . Perder la cordura está casi garantizado si dejas la aplicación en un estado completamente roto durante periodos prolongados mientras refactorizas.
-Hemos implementado nuestras aplicaciones dividiendo nuestro código en módulos separados que han sido
importados en los lugares que los necesitan. Aunque los módulos ES6 están definidos en el estándar ECMAScript, no todos los entornos de ejecución manejan automáticamente el código basado en módulos. Incluso los navegadores modernos se benefician de que las dependencias se preprocesen y optimicen antes de ser entregadas.
+**Estos ejercicios presuponen que ya has completado los ejercicios [5.24-5.28](/es/part5/react_router_librerias_de_ui#ejercicios-524–528). Si no lo has hecho, complétalos primero.**
-Por esta razón, el código dividido en módulos se
empaqueta para producción, es decir, los archivos de código fuente se transforman y combinan en un conjunto optimizado de archivos que el navegador puede cargar eficientemente. Cuando ejecutamos
npm run build en partes anteriores del curso, Vite realizó este bundling. La salida aparece en el directorio
dist :
-
-```
-├── assets
-│ ├── index-d526a0c5.css
-│ ├── index-e92ae01e.js
-│ └── react-35ef61ed.svg
-├── index.html
-└── vite.svg
-```
-
-El archivo
index.html de la raíz carga el JavaScript empaquetado con una etiqueta
script :
-
-```html
-
-
-
-
-
-
-
Vite + React
- // highlight-line
-
// highlight-line
-
-
-
-
-
-```
-
-El CSS también se empaqueta en un único archivo.
-
-En la práctica, el bundling parte de un entry point, que normalmente es
main.jsx . Vite incluye no solo el código del entry point, sino también todo lo que este importa, recursivamente, hasta que se resuelve el grafo completo de dependencias.
-
-Dado que parte de los archivos importados son paquetes como React, Redux y Axios, el archivo JavaScript empaquetado también contendrá el contenido de cada una de esas librerías.
-
-> Antes de que existieran los bundlers, el enfoque antiguo se basaba en que el archivo index.html cargaba todos los archivos JavaScript separados de la aplicación con ayuda de etiquetas script. Esto provocaba un peor rendimiento, ya que la carga de cada archivo separado genera cierta sobrecarga. Por eso, hoy en día el método preferido es agrupar el código en un solo archivo. El bundling también permite optimizaciones como la minificación y el
tree-shaking (eliminación de código no utilizado).
-
-### Cómo funciona Vite
-
-Vite tiene dos modos de funcionamiento claramente distintos, y funcionan de forma bastante diferente.
-
-**Modo de desarrollo** (
npm run dev ) no empaqueta tu código en absoluto. En su lugar, Vite arranca un servidor de desarrollo que sirve los archivos fuente como módulos ES nativos, dejando que el navegador resuelva los imports directamente. Por eso el arranque es casi instantáneo independientemente del tamaño del proyecto.
-Hay una excepción: las dependencias de terceros de node_modules se preempaquetan con esbuild antes de que arranque el servidor. Esto resuelve dos problemas: muchos paquetes de npm siguen estando en formato CommonJS (que los navegadores no pueden consumir nativamente) y algunas librerías consisten en cientos de pequeños archivos internos que, de otro modo, provocarían cientos de peticiones separadas. esbuild convierte y consolida esas dependencias, cachea el resultado en disco, y los siguientes arranques son casi instantáneos.
-
-**Modo de producción** (
npm run build ) utiliza [Rollup](https://rollupjs.org/) para el bundling, con esbuild encargándose todavía de otras tareas como la transpilación (JSX, TypeScript) y la minificación. Rollup fue diseñado desde el principio para módulos ES, lo que lo hace especialmente bueno en
tree-shaking , una técnica que analiza estáticamente qué exports de cada módulo se usan realmente y elimina el resto del bundle final. Por ejemplo, si importas una sola función de utilidad de una librería grande, el tree-shaking garantiza que el resto del código de esa librería no se incluya en el bundle. Esto puede reducir significativamente el tamaño final.
+
-La división del trabajo, esbuild para la velocidad y Rollup para la calidad del bundle, es central en el diseño de Vite.
+
-> Puede que te preguntes por qué Vite no usa simplemente esbuild también para el bundling en producción, dado lo rápido que es. La razón es que la salida de bundling de esbuild, aunque correcta, produce resultados menos optimizados en escenarios avanzados: tiene soporte limitado para code splitting, no genera el mismo nivel de optimización de chunks y su ecosistema de plugins para transformaciones a nivel de bundle todavía está madurando. La salida de Rollup es más predecible y está mejor ajustada para los grafos de dependencias complejos que producen las aplicaciones reales. Los autores de Vite [han afirmado](https://vitejs.dev/guide/why.html#why-not-bundle-with-esbuild) que su intención es cambiar a esbuild para el bundling de producción cuando sus capacidades cierren esa brecha.
+### Ejercicios 7.7.-7.20.
-### Entendiendo esbuild
+Estos ejercicios presuponen que ya has completado los ejercicios [5.24-5.28](/es/part5/react_router_librerias_de_ui#ejercicios-524–528). Si no lo has hecho, complétalos primero.
-Para entender qué implica realmente el bundling, es útil trabajar directamente con [esbuild](https://esbuild.github.io/), sin la capa de abstracción que Vite añade encima. Construyamos un entorno React mínimo desde cero.
+#### 7.7: Frontend y backend en el mismo repositorio
-Vamos a crear una app React sencilla con la siguiente estructura de directorios:
+Durante el curso, el frontend y el backend de la aplicación BlogList han estado en repositorios separados. Una práctica habitual en proyectos reales consiste en colocar ambos en un único repositorio, lo que simplifica el despliegue y facilita compartir código entre ellos.
-```
-├── dist
-│ └── index.html
-├── src
-│ ├── index.jsx
-│ └── App.jsx
-└── package.json
-```
+Lee la sección [Frontend y backend en el mismo repositorio](/es/part7/miscelanea#frontend-y-backend-en-el-mismo-repositorio) del material y reestructura tu aplicación en consecuencia. Coloca el código fuente del frontend y del backend en el mismo repositorio, manteniendo separados sus archivos
package.json .
-Empezamos instalando React y react-dom:
+Asegúrate de que el flujo de desarrollo sigue funcionando: ejecutar
npm run dev en el directorio del frontend debe iniciar el servidor de desarrollo de Vite con recarga en caliente, igual que antes. Comprueba también que el build de producción funciona: el backend debe poder servir el frontend compilado como sitio estático mediante un comando como
npm run build && npm start —o scripts equivalentes que definas—.
-```bash
-npm install react react-dom
-```
+
Nota: si después de reorganizar el repositorio aparecen errores extraños de dependencias, la solución más segura suele ser eliminar todos los directorios
node_modules y volver a ejecutar
npm install desde cero en cada directorio pertinente.
-También necesitamos instalar esbuild:
+#### 7.8: Límite de errores
-```bash
-npm install --save-dev esbuild
-```
+Los errores de una aplicación React que no se capturen en ningún lugar producen una página en blanco. Esto no ofrece una buena experiencia de usuario. La solución estándar de React es el concepto de límite de errores: un componente que envuelve una parte del árbol de componentes, captura los errores de renderizado que se producen en su interior y muestra una interfaz alternativa en vez de bloquear toda la página.
-Al principio añadimos dos scripts al archivo
package.json :
+Lee la sección [Límite de errores](/es/part7/miscelanea#limite-de-errores) del material. Después, añade a tu aplicación un componente de límite de errores que capture los errores de renderizado y muestre un mensaje comprensible en vez de una página en blanco.
-```json
-{
- "scripts": {
- "build": "esbuild src/main.jsx --bundle --outfile=dist/main.js --jsx=automatic",
- "serve": "npx serve dist"
- },
- // ...
-}
-```
+El límite de errores debe añadirse de manera que la barra de navegación quede fuera de él. Si se produce un error de renderizado en cualquier lugar del resto de la aplicación, el límite lo captura y muestra un mensaje fácil de entender como este:
-Para la app necesitamos el archivo
dist/index.html , que carga el bundle JavaScript:
-
-```html
-
-
-
-
-
esbuild app
-
-
-
-
-
-
-```
+
-El entry point
src/main.jsx es el típico:
-
-```jsx
-import React from 'react'
-import ReactDOM from 'react-dom/client'
-import App from './App'
-
-ReactDOM.createRoot(document.getElementById('root')).render(
)
-```
-
-El componente simple de la aplicación
src/App.jsx es el siguiente:
-
-```jsx
-import React, { useState } from 'react'
-
-const App = () => {
- const [counter, setCounter] = useState(0)
+Puedes simular un error de renderizado lanzando temporalmente una excepción dentro de uno de tus componentes, por ejemplo:
+```js
+const BlogList = ({ blogs }) => {
+ throw new Error('simulated error') // highlight line
return (
-
-
count: {counter}
-
setCounter(counter + 1)}>increment button>
-
+ // ...
)
}
-
-export default App
-```
-
-Ahora podemos empaquetar la app:
-
-```bash
-npm run build
```
-La salida es un único
dist/main.js que contiene el código de tu aplicación junto con la librería React empaquetada.
-
-Ahora podemos ejecutar la app empaquetada con
npm run serve . Esto usa el paquete [serve](https://www.npmjs.com/package/serve) para iniciar un servidor estático local para el directorio
dist , dejando la aplicación disponible en
http://localhost:3000 :
-
-
-
-esbuild también soporta [minificación](https://en.wikipedia.org/wiki/Minification_(programming)) mediante flags de línea de comandos. La minificación elimina espacios en blanco y comentarios, acorta nombres de variables y aplica otras optimizaciones de tamaño. El bundle será notablemente grande porque incluye la librería React completa. La minificación reduce mucho su tamaño.
-
-Activemos ahora la minificación:
+#### 7.9: Rutas inexistentes
-```json
-{
- "scripts": {
- "build": "esbuild src/main.jsx --bundle --minify --outfile=dist/main.js --jsx=automatic", // highlight-line
- "serve": "npx serve dist"
- }
-}
-```
+La aplicación también presenta otro tipo de error. Si el usuario intenta navegar a una ruta que no existe, como
-La minificación reduce el tamaño del bundle de alrededor de 1,1 MB a unos 190 KB, una reducción importante.
+
-La minificación tiene una contrapartida: si la aplicación lanza un error en tiempo de ejecución, las herramientas de desarrollo del navegador señalarán una línea del archivo minificado
main.js , que es prácticamente imposible de leer:
+o
-
+
-La solución es un [source map](https://developer.mozilla.org/en-US/docs/Glossary/Source_map): un archivo complementario (
dist/main.js.map ) que registra cómo cada línea del bundle minificado corresponde al código fuente original. Con él activado, un stack trace apunta a la línea exacta de
App.jsx o
main.jsx en lugar de hacerlo a un muro ilegible de código minificado.
+el resultado es una página en blanco. Corrige el enrutamiento para que al navegar a una ruta inexistente se muestre un mensaje apropiado de «Página no encontrada». La [ruta comodín](https://reactrouter.com/start/framework/routing#splats) de React Router (
path="*" ) es la herramienta adecuada: coincide con cualquier ruta que no cubra ninguna otra. El resultado debe tener este aspecto:
-Podemos activar los source maps añadiendo el flag
--sourcemap :
+
-```json
-{
- "scripts": {
- "build": "esbuild src/main.jsx --bundle --minify --sourcemap --outfile=dist/main.js --jsx=automatic", // highlight-line
- "serve": "npx serve dist"
- }
-}
-```
+#### 7.10: Formateo automático del código
-Ahora el error tiene sentido:
+En las partes anteriores utilizamos ESLint para garantizar que el código siguiera las convenciones definidas. [Prettier](https://prettier.io/) es otro enfoque para el mismo problema. Según la documentación, Prettier es
an opinionated code formatter , es decir, no solo controla el estilo del código, sino que también lo formatea según su definición.
-
+Prettier es fácil de integrar en el editor de código para que el archivo se formatee automáticamente al guardarlo.
-Ten en cuenta que los source maps son muy valiosos durante el desarrollo y la depuración, pero puede que quieras dejarlos fuera de un build público de producción. Como un source map contiene tu código fuente original, cualquiera que abra las herramientas de desarrollo del navegador puede leer la lógica sin minificar de tu aplicación. Si eso te preocupa, simplemente omite el flag
--sourcemap en el comando de build de producción.
+Incorpora Prettier a tu aplicación y configúralo para que funcione con tu editor.
-### Transpilación
+### Gestión del estado: Zustand
-Junto con el bundling, esbuild realiza otra tarea esencial: la
transpilación . Transpilar significa convertir código fuente escrito en una forma de JavaScript a otra forma, normalmente de sintaxis moderna o extendida a JavaScript plano que el navegador pueda ejecutar.
+
Hay dos versiones alternativas entre las que elegir para los ejercicios 7.11-7.14: puedes gestionar el estado de la aplicación con Zustand o con React Query y Context . Si quieres maximizar tu aprendizaje, ¡deberías hacer ambas versiones!
-Los navegadores entienden JavaScript estándar, pero JSX no es JavaScript válido y ningún navegador puede parsearlo directamente. Cuando escribimos:
+Nota: si completaste la parte 6 utilizando Redux, por supuesto puedes usar Redux en vez de Zustand en esta serie de ejercicios.
-```jsx
-const element =
-```
+#### 7.11: Zustand, paso 1
-hay que transpilarlo a algo que el navegador sí pueda ejecutar:
+Refactoriza la aplicación para utilizar Zustand en la gestión de los datos de las notificaciones.
-```js
-const element = React.createElement(App, null)
-```
+#### 7.12: Zustand, paso 2
-Por eso la transpilación es un paso obligatorio en cualquier proyecto React, no una optimización opcional. esbuild la realiza automáticamente durante el bundling. Con el flag
--jsx=automatic , esbuild maneja JSX sin ninguna herramienta externa. En el flujo antiguo basado en Webpack había que instalar y configurar [Babel](https://babeljs.io/) y paquetes relacionados para transpilar JSX para el navegador. Con esbuild, los archivos que terminan en
.jsx se transpilan directamente.
+
Ten en cuenta que este ejercicio y los dos siguientes son bastante laboriosos, pero increíblemente educativos.
-### Entorno de desarrollo
+Almacena la información de las entradas de blog en el store de Zustand. En este ejercicio basta con que puedas ver los blogs del backend y crear un nuevo blog.
-Hasta ahora, cada cambio requiere ejecutar
npm run build y refrescar manualmente el navegador, un ciclo lento que rápidamente se vuelve tedioso. El [servidor de desarrollo](https://esbuild.github.io/api/#serve) integrado de esbuild resuelve esto. Añade un script
dev a
package.json :
+Puedes gestionar el estado del inicio de sesión y de la creación de nuevas entradas de blog mediante el estado interno de los componentes de React.
-```json
-{
- "scripts": {
- "build": "esbuild src/main.jsx --bundle --minify --sourcemap --outfile=dist/main.js --jsx=automatic",
- "serve": "npx serve dist",
- "dev": "esbuild src/main.jsx --bundle --outfile=dist/main.js --jsx=automatic --servedir=./dist --watch" // highlight-line
- }
-}
-```
+#### 7.13: Zustand, paso 3
-Ejecutar
npm run dev hace dos cosas a la vez. En primer lugar, [--watch](https://esbuild.github.io/api/#watch) le dice a esbuild que observe todos los archivos fuente importados y reconstruya el bundle automáticamente siempre que alguno se guarde. En segundo lugar, [--servedir](https://esbuild.github.io/api/#serve) inicia un servidor HTTP ligero que sirve el contenido del directorio
dist , tu
index.html y el
main.js recién generado en
http://localhost:8000 .
+Amplía tu solución para que vuelva a ser posible dar «me gusta» a un blog y eliminarlo.
-El flag
--servedir es lo que hace que ambas piezas funcionen juntas: sin él, esbuild solo reconstruiría en modo watch pero no serviría nada. Con él, el servidor siempre entrega el último bundle, así que solo necesitas refrescar el navegador después de guardar un archivo.
+#### 7.14: Zustand, paso 4
-Ten en cuenta que, a diferencia del servidor de desarrollo de Vite, esbuild no soporta hot module replacement. Los cambios en el código fuente requieren un refresco manual del navegador para surtir efecto.
+Almacena la información del usuario que ha iniciado sesión en el store de Zustand.
-La claridad de la interfaz de esbuild ilustra lo que hace fundamentalmente un bundler: toma un entry point, sigue todos los imports y produce una salida optimizada. Vite construye encima de esta base y añade la capa de experiencia de desarrollo: un servidor dev, hot module replacement y valores por defecto sensatos para proyectos React.
+### Gestión del estado: React Query y Context
-Ahora que tenemos una imagen más clara de lo que implican realmente el bundling y la transpilación, volvamos a Vite y veamos cómo puede configurarse.
+
Hay dos versiones alternativas entre las que elegir para los ejercicios 7.11-7.14: puedes gestionar el estado de la aplicación con Zustand o con React Query y Context . Si quieres maximizar tu aprendizaje, ¡deberías hacer ambas versiones!
-### Configuración de Vite
+#### 7.11: React Query y Context, paso 1
-Para la mayoría de los proyectos React, Vite funciona sin ninguna configuración. Sin embargo, cuando sí necesitas personalizar su comportamiento, editas
vite.config.js (o
vite.config.ts ).
+Refactoriza la aplicación para utilizar el hook useReducer y el contexto en la gestión de los datos de las notificaciones.
-Una configuración mínima de Vite para un proyecto React se ve así:
+#### 7.12: React Query y Context, paso 2
-```js
-import { defineConfig } from 'vite'
-import react from '@vitejs/plugin-react'
+Utiliza React Query para gestionar el estado de las entradas de blog. Para este ejercicio basta con que la aplicación muestre los blogs existentes y permita crear correctamente un nuevo blog.
-export default defineConfig({
- plugins: [react()],
-})
-```
+Puedes gestionar el estado del inicio de sesión y de la creación de nuevas entradas de blog mediante el estado interno de los componentes de React.
-El plugin
@vitejs/plugin-react habilita la transformación de JSX, fast refresh (hot module replacement que conserva el estado del componente) y otras características específicas de React.
+#### 7.13: React Query y Context, paso 3
-#### Configuración del servidor de desarrollo
+Amplía tu solución para que vuelva a ser posible dar «me gusta» a un blog y eliminarlo.
-Puedes configurar el puerto del servidor de desarrollo y otros ajustes bajo la clave
server :
+#### 7.14: React Query y Context, paso 4
-```js
-export default defineConfig({
- plugins: [react()],
- server: {
- port: 3000,
- open: true, // open browser automatically
- },
-})
-```
+Utiliza la Context API para gestionar los datos del usuario que ha iniciado sesión.
-#### Proxy de peticiones API
+#### 7.15: Limpieza del código
-Cuando desarrollas en local, tu app React normalmente se ejecuta en un puerto (por ejemplo, 3000) mientras que tu backend corre en otro (por ejemplo, 3001). La política same-origin del navegador normalmente bloquearía las peticiones entre ambos. La configuración de proxy de Vite resuelve esto sin requerir configuración CORS en el backend:
+Lo más probable es que tu aplicación contenga código que gestiona el usuario que ha iniciado sesión mediante
localStorage en varios lugares:
```js
-export default defineConfig({
- plugins: [react()],
- server: {
- port: 3000,
- proxy: {
- '/api': {
- target: 'http://localhost:3001',
- changeOrigin: true,
- },
- },
- },
-})
-```
+const userJSON = window.localStorage.getItem('loggedBlogappUser')
-Con esta configuración, cualquier petición que tu app React haga a
/api/notes se reenviará automáticamente a
http://localhost:3001/api/notes a través del servidor de desarrollo de Vite. Tu código frontend nunca necesita incluir
localhost:3001 en sus URLs durante el desarrollo.
+// ...
-#### Variables de entorno
+window.localStorage.setItem('loggedBlogappUser', JSON.stringify(user))
-Vite incluye soporte integrado para variables de entorno mediante archivos
.env . Este es el reemplazo moderno de inyectar constantes manualmente en el bundle.
+// ...
-Crea un archivo
.env en la raíz del proyecto:
-
-```
-VITE_BACKEND_URL=http://localhost:3001/api/notes
+window.localStorage.removeItem('loggedBlogappUser')
```
-Y un archivo
.env.production para los valores de producción:
-
-```
-VITE_BACKEND_URL=https://myapp.fly.dev/api/notes
-```
-
-**Importante:** todas las variables de entorno expuestas al navegador deben empezar con el prefijo
VITE_ . Las variables sin este prefijo permanecen solo del lado del servidor y no se incluyen en el bundle. Es una medida de seguridad deliberada para evitar filtrar secretos accidentalmente.
-
-Accede a la variable en el código de tu aplicación a través de
import.meta.env :
+Extrae esta lógica a un módulo de servicio específico,
src/services/persistentUser.js , que exporte las siguientes funciones:
```js
-const App = () => {
- const notes = useNotes(import.meta.env.VITE_BACKEND_URL)
-
- return (
-
- {notes.length} notes on server {import.meta.env.VITE_BACKEND_URL}
-
- )
-}
+const getUser = () => { ... }
+const saveUser = (user) => { ... }
+const removeUser = () => { ... }
```
-Vite selecciona automáticamente el archivo
.env correcto según el modo:
+Sustituye todos los accesos directos a
localStorage de la aplicación por llamadas a estas funciones.
--
npm run dev usa
.env y
.env.development
--
npm run build usa
.env y
.env.production
+Utiliza también en los formularios el hook [useField](/es/part7/mas_sobre_los_hooks_de_react) presentado anteriormente en esta parte.
-Añade
.env.production a
.gitignore si contiene valores sensibles, y utiliza
.env.example para documentar qué variables son necesarias.
+El resto de las tareas son comunes a las versiones con Zustand y con React Query.
-#### Transpilación
+#### 7.16: Vista de usuarios
-Vite gestiona la transpilación del código automáticamente. Durante el desarrollo, esbuild transpila tu TypeScript y JSX bajo demanda. Es lo bastante rápido como para hacerlo archivo por archivo sin retraso apreciable. Durante los builds de producción, Rollup se encarga del bundling mientras que esbuild realiza la transpilación.
+Implementa una vista en la aplicación que muestre toda la información básica relacionada con los usuarios:
-El objetivo de transpilación por defecto en Vite son navegadores modernos que soportan módulos ES nativos (Chrome 87+, Firefox 78+, Safari 14+, Edge 88+). Si necesitas soportar navegadores más antiguos, puedes configurar el target explícitamente y añadir el plugin
@vitejs/plugin-legacy :
+
-```bash
-npm install --save-dev @vitejs/plugin-legacy
-```
+#### 7.17: Vista de un usuario
-```js
-import { defineConfig } from 'vite'
-import react from '@vitejs/plugin-react'
-import legacy from '@vitejs/plugin-legacy'
-
-export default defineConfig({
- plugins: [
- react(),
- legacy({
- targets: ['defaults', 'not IE 11'],
- }),
- ],
-})
-```
-
-El plugin legacy genera automáticamente un bundle separado para navegadores antiguos usando Babel.
-
-#### CSS
-
-Vite maneja CSS sin ninguna configuración. Simplemente importa un archivo CSS desde tu JavaScript:
-
-```js
-import './index.css'
-```
-
-Vite lo procesará y lo incluirá en el build. En producción, el CSS se extrae a un archivo separado. Durante el desarrollo, se inyecta mediante etiquetas
<style> con soporte de hot reload.
-
-Vite también soporta de forma nativa [CSS Modules](https://github.com/css-modules/css-modules) para estilos con alcance local. Cualquier archivo que termine en
.module.css se trata como un CSS Module:
+Implementa una vista para cada usuario que muestre todas las entradas de blog añadidas por él:
-```js
-import styles from './App.module.css'
-
-const App = () => (
-
- hello vite
-
-)
-```
-
-Los preprocessors de CSS como [Sass](https://sass-lang.com/) pueden añadirse simplemente instalando el preprocessor, sin plugin ni configuración adicional:
-
-```bash
-npm install --save-dev sass
-```
-
-Después de eso, los archivos
.scss funcionan automáticamente.
-
-#### Minificación
+
-Al ejecutar
npm run build , Vite minifica la salida. La minificación elimina espacios en blanco y comentarios, acorta nombres de variables y aplica otras optimizaciones de tamaño. El resultado es un archivo mucho más pequeño que carga más rápido en el navegador.
+Puedes acceder a esta vista haciendo clic en el nombre del usuario en la vista que enumera todos los usuarios:
-Vite usa esbuild para la minificación de JavaScript y un minificador integrado de CSS para las hojas de estilo.
+
-#### Source maps
-Los source maps permiten que las herramientas de desarrollo del navegador mapeen errores y breakpoints a tu código fuente original en lugar de al bundle minificado. Sin ellos, un stack trace que apunta a la línea 1 de
main.js sirve de muy poco para depurar.
+#### 7.18: Comentarios, paso 1
-En desarrollo, Vite genera source maps automáticamente. Para builds de producción, puedes habilitarlos explícitamente:
+Implementa la funcionalidad para comentar las entradas de blog:
-```js
-export default defineConfig({
- plugins: [react()],
- build: {
- sourcemap: true,
- },
-})
-```
+
-Ten en cuenta que los source maps de producción aumentan el tiempo de build y exponen tu código fuente a cualquiera que mire la pestaña de red. En muchos casos, es mejor subir los source maps a un servicio de monitorización de errores (como Sentry) y no publicarlos en el servidor.
+Los comentarios deben ser anónimos, es decir, no deben estar asociados al usuario que los dejó.
-#### Plugins
+En este ejercicio basta con que el frontend muestre los comentarios que la aplicación recibe del backend.
-La funcionalidad de Vite se amplía mediante [plugins](https://vite.dev/plugins/). El ecosistema de plugins ha crecido rápidamente y cubre la mayoría de las necesidades comunes. Algunos plugins ampliamente utilizados incluyen:
+Un mecanismo apropiado para añadir comentarios a una entrada de blog sería una petición HTTP POST al endpoint
api/blogs/:id/comments .
--
@vitejs/plugin-react — soporte para React (JSX, fast refresh)
--
@vitejs/plugin-legacy — soporte para navegadores antiguos
--
vite-plugin-svgr — importar archivos SVG como componentes React
--
rollup-plugin-visualizer — análisis del tamaño del bundle
+#### 7.19: Comentarios, paso 2
-Los plugins se especifican en el array
plugins dentro de
vite.config.js . Siguen la misma interfaz que los plugins de Rollup, por lo que muchos plugins de Rollup también funcionan con Vite.
+Amplía tu aplicación para que los usuarios puedan añadir comentarios a las entradas de blog desde el frontend:
-#### Polyfills
+
-Un
polyfill es código que implementa una funcionalidad para navegadores que no la soportan de forma nativa. La transpilación por sí sola no basta para características que son sintácticamente válidas pero no están implementadas. Por ejemplo, un navegador puede parsear
Promise correctamente pero no tener implementación de esa API.
+#### 7.20: Estilos
-Con Vite, los polyfills se gestionan mediante el plugin
@vitejs/plugin-legacy , que incluye automáticamente los polyfills necesarios según tus targets de navegador. Si necesitas un polyfill específico sin usar el plugin legacy, puedes instalarlo directamente e importarlo al principio de tu archivo de entrada.
+Mejora la apariencia visual de las nuevas funcionalidades de tu aplicación utilizando las técnicas tratadas en la [parte 5](/es/part5/react_router_librerias_de_ui).
-Puedes consultar la compatibilidad de APIs concretas en [https://caniuse.com](https://caniuse.com) o en la [documentación MDN de Mozilla](https://developer.mozilla.org/).
+Este era el último ejercicio de esta parte del curso. Es hora de subir tu código a GitHub y marcar todos los ejercicios que hayas completado en el [sistema de entrega de ejercicios](https://studies.cs.helsinki.fi/stats/courses/fullstackopen).
diff --git a/src/content/7/es/part7e.md b/src/content/7/es/part7e.md
deleted file mode 100644
index 9d8d1fbc350..00000000000
--- a/src/content/7/es/part7e.md
+++ /dev/null
@@ -1,534 +0,0 @@
----
-mainImage: ../../../images/part-7.svg
-part: 7
-letter: e
-lang: es
----
-
-
-
-### Componentes de clase
-
-Durante el curso solo hemos utilizado componentes de React que se han definido como funciones de Javascript. Esto no fue posible sin la funcionalidad de [hook](https://reactjs.org/docs/hooks-intro.html) que llegó con la versión 16.8 de React. Antes, al definir un componente que usa estado, había que hacerlo usando la sintaxis de [Class](https://reactjs.org/docs/state-and-lifecycle.html#converting-a-function-to-a-class) de Javascript.
-
-Es beneficioso al menos estar familiarizado con los componentes de clase hasta cierto punto, ya que el mundo contiene una gran cantidad de código React antiguo, que probablemente nunca se reescribirá por completo con la sintaxis actualizada.
-
-Conozcamos las principales características de los componentes de clase produciendo otra aplicación de anécdotas, muy familiar para nosotros. Almacenamos las anécdotas en el archivo
db.json usando
json-server . El contenido del archivo se toma de [aquí](https://github.com/fullstack-hy/misc/blob/master/anecdotes.json).
-
-La versión inicial del componente de clase se ve así
-
-```js
-import React from 'react'
-
-class App extends React.Component {
- constructor(props) {
- super(props)
- }
-
- render() {
- return (
-
-
anecdote of the day
-
- )
- }
-}
-
-export default App
-```
-
-El componente ahora tiene un [constructor](https://react.dev/reference/react/Component#constructor), en el que no sucede nada en este momento, y contiene el método [render](https://react.dev/reference/react/Component#render). Como se puede suponer, render define cómo y qué se renderiza en la pantalla.
-
-Definamos un estado para la lista de anécdotas y la anécdota actualmente visible. A diferencia de cuando se usa el hook [useState](https://react.dev/reference/react/useState), los componentes de clase solo contienen un estado. Por tanto, si el estado se compone de varias "partes", deben almacenarse como propiedades del estado. El estado se inicializa en el constructor:
-
-```js
-class App extends React.Component {
- constructor(props) {
- super(props)
-
- // highlight-start
- this.state = {
- anecdotes: [],
- current: 0
- }
- // highlight-end
- }
-
- render() {
- // highlight-start
- if (this.state.anecdotes.length === 0) {
- return
no anecdotes...
- }
- // highlight-end
-
- return (
-
-
anecdote of the day
- // highlight-start
-
- {this.state.anecdotes[this.state.current].content}
-
-
next
- // highlight-end
-
- )
- }
-}
-```
-
-El estado del componente está en la variable de instancia _this.state_. El estado es un objeto que tiene dos propiedades.
this.state.anecdotes es la lista de anécdotas y
this.state.current es el índice de la anécdota que se muestra actualmente.
-
-En componentes funcionales, el lugar adecuado para obtener datos de un servidor es dentro de un [effect hook](https://react.dev/reference/react/useEffect), que se ejecuta cuando un componente se renderiza o con menos frecuencia si es necesario, por ejemplo, solo en combinación con el primer renderizado.
-
-Los [métodos de ciclo de vida](https://react.dev/reference/react/Component#adding-lifecycle-methods-to-a-class-component) de los componentes de clase ofrecen la funcionalidad correspondiente. El lugar correcto para desencadenar la obtención de datos de un servidor es dentro del método de ciclo de vida [componentDidMount](https://react.dev/reference/react/Component#componentdidmount), que se ejecuta una vez justo después de que un componente se renderiza por primera vez:
-
-```js
-class App extends React.Component {
- constructor(props) {
- super(props)
-
- this.state = {
- anecdotes: [],
- current: 0
- }
- }
-
- // highlight-start
- componentDidMount = () => {
- axios.get('http://localhost:3001/anecdotes').then(response => {
- this.setState({ anecdotes: response.data })
- })
- }
- // highlight-end
-
- // ...
-}
-```
-
-La función callback de la solicitud HTTP actualiza el estado del componente mediante el método [setState](https://react.dev/reference/react/Component#setstate). El método solo toca las keys que se han definido en el objeto pasado al método como argumento. El valor de la key
current permanece sin cambios.
-
-Llamar al método setState siempre desencadena la re-renderización del componente de clase, es decir que realiza nuevamente un llamado al método _render_.
-
-Terminaremos el componente con la posibilidad de cambiar la anécdota mostrada. El siguiente es el código para todo el componente con la adición resaltada:
-
-```js
-class App extends React.Component {
- constructor(props) {
- super(props)
-
- this.state = {
- anecdotes: [],
- current: 0
- }
- }
-
- componentDidMount = () => {
- axios.get('http://localhost:3001/anecdotes').then(response => {
- this.setState({ anecdotes: response.data })
- })
- }
-
- // highlight-start
- handleClick = () => {
- const current = Math.floor(
- Math.random() * this.state.anecdotes.length
- )
- this.setState({ current })
- }
- // highlight-end
-
- render() {
- if (this.state.anecdotes.length === 0 ) {
- return
no anecdotes...
- }
-
- return (
-
-
anecdote of the day
-
{this.state.anecdotes[this.state.current].content}
-
next // highlight-line
-
- )
- }
-}
-```
-
-A modo de comparación, aquí está la misma aplicación como un componente funcional:
-
-```js
-const App = () => {
- const [anecdotes, setAnecdotes] = useState([])
- const [current, setCurrent] = useState(0)
-
- useEffect(() =>{
- axios.get('http://localhost:3001/anecdotes').then(response => {
- setAnecdotes(response.data)
- })
- },[])
-
- const handleClick = () => {
- setCurrent(Math.round(Math.random() * (anecdotes.length - 1)))
- }
-
- if (anecdotes.length === 0) {
- return
no anecdotes...
- }
-
- return (
-
-
anecdote of the day
-
{anecdotes[current].content}
-
next
-
- )
-}
-```
-
-En el caso de nuestro ejemplo, las diferencias fueron menores. La mayor diferencia entre los componentes funcionales y los componentes de clase es principalmente que el estado de un componente de clase es un solo objeto y que el estado se actualiza utilizando el método _setState_, mientras que en los componentes funcionales el estado puede constar de múltiples variables diferentes, con todas ellos tienen su propia función de actualización.
-
-En algunos casos de uso más avanzados, el hook effect ofrece un mecanismo considerablemente mejor para controlar los efectos secundarios en comparación con los métodos de ciclo de vida de los componentes de clase.
-
-Un beneficio notable de usar componentes funcionales es no tener que lidiar con la auto-referencia _this_ de la clase Javascript.
-
-En mi opinión, y la opinión de muchos otros, los componentes de clase básicamente no ofrecen beneficios sobre los componentes funcionales mejorados con hooks, con la excepción del llamado mecanismo de [error boundary](https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary), que actualmente (15 de febrero de 2021) aún no está en uso por componentes funcionales.
-
-Al escribir código nuevo, [no hay ninguna razón racional para usar componentes de clase](https://reactjs.org/docs/hooks-faq.html#should-i-use-hooks-classes-or-a-mix-of-both) si el proyecto usa React con una versión 16.8 o superior. Por otro lado, [actualmente no hay necesidad de reescribir todo el código React antiguo](https://reactjs.org/docs/hooks-faq.html#do-i-need-to-rewrite-all-my-class-components) como componentes funcionales.
-
-### Organización del código en la aplicación de React
-
-En la mayoría de las aplicaciones seguimos el principio según el cual los componentes se colocaban en el directorio
components , los reducers se colocan en el directorio
reducers y el código responsable de comunicarse con el servidor se colocaba en el directorio
services . Esta forma de organización se adapta perfectamente a una aplicación pequeña, pero a medida que aumenta la cantidad de componentes, se necesitan mejores soluciones. No existe una forma correcta de organizar un proyecto. El artículo [La forma 100% correcta de estructurar una aplicación React (o por qué no existe tal cosa)](https://medium.com/hackernoon/the-100-correct-way-to-structure-a-react-app-or-why-theres-no-such-thing-3ede534ef1ed) proporciona una perspectiva sobre el problema.
-
-### Frontend y backend en el mismo repositorio
-
-Durante el curso, hemos creado el frontend y el backend en repositorios separados. Este es un enfoque muy típico. Sin embargo, hicimos el despliegue [copiando](/es/part3/despliegue_de_la_aplicacion_a_internet#sirviendo-archivos-estaticos-desde-el-backend) el código de frontend incluido en el repositorio de backend. Un enfoque posiblemente mejor habría sido desplegar el código del frontend por separado.
-
-A veces, puede haber una situación en la que la aplicación completa se coloque en un solo repositorio. En este caso, un enfoque común es colocar
package.json y
webpack.config.js en el directorio raíz, así como colocar el código de frontend y backend en sus propios directorios, por ejemplo,
client y
server .
-
-### Cambios en el servidor
-
-Si hay cambios en el estado del servidor, por ejemplo, cuando otros usuarios agregan nuevos blogs al servicio de Blog List el frontend de React que implementamos durante este curso no notará estos cambios hasta que la página se vuelva a cargar. Una situación similar surge cuando el frontend desencadena un cálculo lento en el backend. ¿Cómo reflejamos los resultados del cálculo en el frontend?
-
-Una forma es ejecutar [polling](
) en el frontend, es decir, solicitudes repetidas a la API de backend, por ejemplo, utilizando el comando [setInterval](https://developer.mozilla.org/es/docs/Web/API/setInterval).
-
-Una forma más sofisticada es utilizar [WebSockets](https://developer.mozilla.org/es/docs/Web/API/WebSockets_API), mediante los cuales es posible establecer un canal de comunicación bidireccional entre el navegador y el servidor. En este caso, el navegador no necesita sondear el backend y, en su lugar, solo tienes que definir funciones callbacks para situaciones en las que el servidor envía datos sobre la actualización del estado mediante un WebSocket.
-
-Los WebSockets son una API proporcionada por el navegador, que aún no es totalmente compatible con todos los navegadores:
-
-
-
-En lugar de utilizar directamente la API de WebSocket, es aconsejable utilizar la librería [Socket.io](https://socket.io/), que proporciona varias opciones de fallback en caso de que el navegador no tenga el soporte completo para WebSockets.
-
-En la [parte 8](/es/part8), nuestro tema es GraphQL, que proporciona un buen mecanismo para notificar a los clientes cuando hay cambios en los datos del backend.
-
-### Virtual DOM
-
-El concepto de virtual DOM a menudo surge cuando se habla de React. ¿Que es todo esto? Como se mencionó en la [parte 0](/es/part0/fundamentos_de_las_aplicaciones_web#modelo-de-objetos-del-documento-o-dom), los navegadores proporcionan una [API DOM](https://developer.mozilla.org/es/docs/Web/API/Document_Object_Model), mediante la cual el JavaScript que se ejecuta en el navegador puede modificar los elementos que definen la apariencia de la página.
-
-Cuando un desarrollador de software usa React, rara vez o nunca manipula directamente el DOM. La función que define el componente React devuelve un conjunto de [elementos React](https://reactjs.org/docs/glossary.html#elements). Aunque algunos de los elementos parecen elementos HTML normales
-
-```js
-const element = Hello, world
-```
-
-internamente también son elementos de React basados en JavaScript.
-
-Los elementos React que definen la apariencia de los componentes de la aplicación conforman el [Virtual DOM](https://reactjs.org/docs/faq-internals.html#what-is-the-virtual-dom), que se almacena en la memoria del sistema durante el tiempo de ejecución.
-
-Con la ayuda de la librería [ReactDOM](https://react.dev/reference/react-dom), el DOM virtual definido por los componentes se renderiza en un DOM real que el navegador puede mostrar utilizando la API DOM:
-
-```js
-ReactDOM.createRoot(document.getElementById('root')).render( )
-```
-
-Cuando el estado de la aplicación cambia, los componentes definen un nuevo virtual DOM . React tiene la versión anterior del virtual DOM en la memoria y en lugar de renderizar directamente el nuevo virtual DOM usando la API DOM, React calcula la forma óptima de actualizar el DOM (eliminar, agregar o modificar elementos en el DOM) de modo que el DOM refleje el nuevo virtual DOM.
-
-### El papel de React en las aplicaciones
-
-Es posible que en el material no hayamos puesto suficiente énfasis en el hecho de que React es principalmente una librería para administrar la creación de vistas para una aplicación. Si nos fijamos en el patrón de [Modelo Vista Controlador](https://es.wikipedia.org/wiki/Modelo%E2%80%93vista%E2%80%93controlador) tradicional, entonces el dominio de React sería la Vista . React tiene un área de aplicación más estrecha que, por ejemplo, [Angular](https://angular.io/), que es un framework de Frontend MVC que lo abarca todo. Por lo tanto, React no es un framework , sino que es una librería .
-
-En aplicaciones pequeñas, los datos manejados por la aplicación se almacenan en el estado de los componentes de React, por lo que en este escenario el estado de los componentes se puede considerar como modelos de una arquitectura MVC.
-
-### Seguridad en aplicaciones React/node
-
-Hasta ahora, durante el curso, no hemos abordado en absoluto la seguridad de la información. Tampoco tenemos mucho tiempo ahora, pero afortunadamente la Universidad de Helsinki cuenta con un curso MOOC [Securing Software](https://cybersecuritybase.mooc.fi/module-2.1) que cubre este tema tan importante.
-
-Sin embargo, echaremos un vistazo a algunas cosas específicas de este curso.
-
-El Open Web Application Security Project (proyecto abierto de seguridad de aplicaciones web), también conocido como [OWASP](https://www.owasp.org), publica una lista anual de los riesgos de seguridad más comunes en las aplicaciones web. La lista más reciente se puede encontrar [aquí](https://owasp.org/Top10/). Los mismos riesgos se pueden encontrar de un año a otro.
-
-En la parte superior de la lista encontramos la inyección , lo que significa que, por ejemplo, el texto enviado mediante un formulario en una aplicación se interpreta de forma completamente diferente de lo que pretendía el desarrollador del software. El tipo de inyección más famoso es probablemente la [inyección SQL](https://stackoverflow.com/questions/332365/how-does-the-sql-injection-from-the-bobby-tables-xkcd-comic-work).
-
-Por ejemplo, si la siguiente consulta SQL se ejecuta en una aplicación vulnerable:
-
-```js
-let query = "SELECT * FROM Users WHERE name = '" + userName + "';"
-```
-
-Ahora supongamos que un usuario malintencionado Arto Hellas definiría su nombre como
-
-```
-Arto Hell-as'; DROP TABLE Users; --
-```
-
-para que el nombre contenga una comilla simple ', que es el carácter inicial y final de un string SQL. Como resultado de esto, se ejecutarían dos operaciones SQL, la segunda de las cuales destruiría la tabla Users de la base de datos.
-
-```sql
-SELECT * FROM Users WHERE name = 'Arto Hell-as'; DROP TABLE Users; --'
-```
-
-Las inyecciones de SQL se previenen utilizando [queries parametrizadas](https://security.stackexchange.com/questions/230211/why-are-stored-procedures-and-prepared-statements-the-preferred-modern-methods-f). Con ellas, el input del usuario no se mezcla con la SQL query, en cambio, la base de datos inserta los valores del input en los placeholders de la query (normalmente ?):
-
-```js
-execute("SELECT * FROM Users WHERE name = ?", [userName])
-```
-
-Los ataques de inyección también son posibles en bases de datos NoSQL. Sin embargo, mongoose los previene [desinfectando](https://zanon.io/posts/nosql-injection-in-mongodb) las consultas. Puedes encontrar más información sobre el tema, por ejemplo, [aquí](https://web.archive.org/web/20220901024441/https://blog.websecurify.com/2014/08/hacking-nodejs-and-mongodb.html).
-
-Cross-site scripting (XSS) es un ataque en el que es posible inyectar código JavaScript malicioso en una aplicación web legítima. Luego, el código malicioso se ejecutaría en el navegador de la víctima. Si intentamos inyectar lo siguiente en, por ejemplo, la aplicación de notas:
-
-```html
-
-```
-
-el código no se ejecuta, sino que solo se renderiza como 'texto' en la página:
-
-
-
-ya que React [se encarga de desinfectar los datos en variables](https://es.legacy.reactjs.org/docs/introducing-jsx.html#jsx-prevents-injection-attacks). Algunas versiones de React [han sido vulnerables](https://medium.com/dailyjs/exploiting-script-injection-flaws-in-reactjs-883fb1fe36c1) a los ataques XSS. Los agujeros de seguridad, por supuesto, han sido reparados, pero no hay garantía de que no vuelvan a suceder.
-
-Es necesario permanecer alerta cuando se utilizan librerías; si hay actualizaciones de seguridad para esas librerías, se recomienda actualizarlas en nuestras aplicaciones. Las actualizaciones de seguridad para Express se encuentran en la [documentación de la librería](https://expressjs.com/en/advanced/security-updates.html) y las de Node se encuentran en [este blog](https://nodejs.org/en/blog/vulnerability/).
-
-Puedes verificar qué tan actualizadas están tus dependencias usando el comando
-
-```bash
-npm outdated --depth 0
-```
-
-El proyecto del año pasado que es utilizado en la [parte 9](/es/part9) ya tiene bastantes dependencias desactualizadas:
-
-
-
-Las dependencias se pueden actualizar al actualizar el archivo package.json . La mejor manera de hacerlo es utilizando una herramienta llamada _npm-check-updates_. Puede instalarse globalmente ejecutando el comando:
-
-```bash
-npm install -g npm-check-updates
-```
-
-Usando esta herramienta, la actualización de las dependencias se verifica de la siguiente manera:
-
-```bash
-$ npm-check-updates
-Checking ...\ultimate-hooks\package.json
-[====================] 9/9 100%
-
- @testing-library/react ^13.0.0 → ^13.1.1
- @testing-library/user-event ^14.0.4 → ^14.1.1
- react-scripts 5.0.0 → 5.0.1
-
-Run ncu -u to upgrade package.json
-```
-
-El archivo package.json se actualiza ejecutando el comando _ncu -u_.
-
-```bash
-$ ncu -u
-Upgrading ...\ultimate-hooks\package.json
-[====================] 9/9 100%
-
- @testing-library/react ^13.0.0 → ^13.1.1
- @testing-library/user-event ^14.0.4 → ^14.1.1
- react-scripts 5.0.0 → 5.0.1
-
-Run npm install to install new versions.
-```
-
-Ahora es el momento de actualizar las dependencias ejecutando el comando _npm install_. Sin embargo, las versiones antiguas de las dependencias no necesariamente son un riesgo de seguridad.
-
-El comando npm [audit](https://docs.npmjs.com/cli/audit) se puede utilizar para verificar la seguridad de las dependencias. Compara los números de versión de las dependencias de tu aplicación con una lista de los números de versión de las dependencias que contienen amenazas de seguridad conocidas en una base de datos de errores centralizada.
-
-Ejecutando _npm audit_ en el mismo proyecto, imprime una larga lista de quejas y sugerencias de arreglos.
-A continuación se muestra una parte del informe:
-
-```js
-$ patientor npm audit
-
-... many lines removed ...
-
-url-parse <1.5.2
-Severity: moderate
-Open redirect in url-parse - https://github.com/advisories/GHSA-hh27-ffr2-f2jc
-fix available via `npm audit fix`
-node_modules/url-parse
-
-ws 6.0.0 - 6.2.1 || 7.0.0 - 7.4.5
-Severity: moderate
-ReDoS in Sec-Websocket-Protocol header - https://github.com/advisories/GHSA-6fc8-4gx4-v693
-ReDoS in Sec-Websocket-Protocol header - https://github.com/advisories/GHSA-6fc8-4gx4-v693
-fix available via `npm audit fix`
-node_modules/webpack-dev-server/node_modules/ws
-node_modules/ws
-
-120 vulnerabilities (102 moderate, 16 high, 2 critical)
-
-To address issues that do not require attention, run:
- npm audit fix
-
-To address all issues (including breaking changes), run:
- npm audit fix --force
-```
-
-Después de solo un año, el código está lleno de pequeñas amenazas de seguridad. Afortunadamente, solo hay 2 amenazas críticas. Ejecutemos _npm audit fix_ como sugiere el informe:
-
-```js
-$ npm audit fix
-
-+ mongoose@5.9.1
-added 19 packages from 8 contributors, removed 8 packages and updated 15 packages in 7.325s
-fixed 354 of 416 vulnerabilities in 20047 scanned packages
- 1 package update for 62 vulns involved breaking changes
- (use `npm audit fix --force` to install breaking changes; or refer to `npm audit` for steps to fix these manually)
-```
-
-62 amenazas permanecen porque, por defecto, _audit fix_ no actualiza las dependencias si su número de versión major ha aumentado. Actualizar estas dependencias podría conducir al colapso de toda la aplicación.
-
-El origen del error crítico es la librería [immer](https://github.com/immerjs/immer)
-
-```js
-immer <9.0.6
-Severity: critical
-Prototype Pollution in immer - https://github.com/advisories/GHSA-33f9-j839-rf8h
-fix available via `npm audit fix --force`
-Will install react-scripts@5.0.0, which is a breaking change
-```
-
-Ejecutando _npm audit fix --force_ actualizaría la versión de la librería, pero también actualizaría la librería _react-scripts_ y eso podría potencialmente colapsar el entorno de desarrollo. Así que dejaremos las actualizaciones de librerías para más tarde...
-
-Una de las amenazas mencionadas en la lista de OWASP es Broken Authentication y Broken Access Control . La autenticación basada en tokens que hemos estado usando es bastante sólida si la aplicación se usa con el protocolo HTTPS de cifrado de tráfico. Al implementar el control de acceso, por ejemplo, se debe recordar no solo verificar la identidad de un usuario en el navegador, sino también en el servidor. Mala seguridad sería evitar que se tomen algunas acciones solo ocultando las opciones de ejecución en el código del navegador.
-
-En MDN de Mozilla hay una muy buena [guía de seguridad de sitios web](https://developer.mozilla.org/es/docs/Learn/Server-side/First_steps/Website_security), que trae a colación este tema tan importante:
-
-
-
-La documentación de Express incluye una sección sobre seguridad: [Mejores prácticas de producción: seguridad](https://expressjs.com/es/advanced/best-practice-security.html), que vale la pena leer. También se recomienda agregar una librería llamada [Helmet](https://helmetjs.github.io/) al backend. Incluye un conjunto de middlewares que eliminan algunas vulnerabilidades de seguridad en aplicaciones Express.
-
-También vale la pena usar el [plugin de seguridad](https://github.com/nodesecurity/eslint-plugin-security) de ESlint.
-
-### Tendencias actuales
-
-Finalmente, echemos un vistazo a la tecnología del mañana (o en realidad ya de hoy), y las direcciones en las que se dirige el desarrollo web.
-
-#### Versiones tipadas de JavaScript
-
-A veces, el [tipado dinámico](https://developer.mozilla.org/es/docs/Glossary/Dynamic_typing) de variables de JavaScript crea errores molestos. En la parte 5 hablamos brevemente sobre [PropTypes](/es/part5/props_children_y_proptypes#prop-types): un mecanismo que permite hacer cumplir la verificación de tipos para los props pasados a los componentes de React.
-
-Últimamente ha habido un aumento notable en el interés por la [verificación de tipos estáticos](https://en.wikipedia.org/wiki/Type_system#Static_type_checking). Por el momento, la versión tipada más popular de Javascript es [Typescript](https://www.typescriptlang.org/), que ha sido desarrollado por Microsoft. Typescript se cubre en la [parte 9](/es/part9).
-
-#### Renderización del lado del servidor, aplicaciones isomórficas y código universal
-
-El navegador no es el único dominio donde se pueden renderizar los componentes definidos usando React. La renderización también se puede realizar en el [servidor](https://es.react.dev/reference/react-dom/server). Este tipo de enfoque se utiliza cada vez más, de modo que cuando se accede a la aplicación por primera vez, el servidor muestra una página renderizada previamente creada con React. A partir de aquí, el funcionamiento de la aplicación continúa como de costumbre, es decir, el navegador ejecuta React, que manipula el DOM que muestra el navegador. La renderización que se realiza en el servidor se conoce con el nombre de server-side rendering .
-
-Una motivación para la renderización del lado del servidor es la optimización de motores de búsqueda (SEO). Los motores de búsqueda tradicionalmente han sido muy malos para reconocer el contenido renderizado en JavaScript, sin embargo, la marea podría estar cambiando, por ejemplo, échale un vistazo a [esto](https://www.javascriptstuff.com/react-seo/) y [esto](https://medium.freecodecamp.org/seo-vs-react-is-it-neccessary-to-render-react-pages-in-the-backend-74ce5015c0c9).
-
-Por supuesto, la renderización del lado del servidor no es algo específico de React o incluso de JavaScript. Usar el mismo lenguaje de programación en todo el stack en teoría simplifica la ejecución del concepto, porque el mismo código se puede ejecutar tanto en el frontend como en el backend.
-
-Junto con la renderización del lado del servidor se ha hablado de las llamadas aplicaciones isomórficas y de código universal , aunque ha habido cierto debate sobre sus definiciones. Según algunas [definiciones](https://medium.com/@ghengeveld/isomorphism-vs-universal-javascript-4b47fb481beb), una aplicación web isomórfica es aquella que realiza la renderización tanto en el frontend como en el backend. Por otro lado, el código universal es código que se puede ejecutar en la mayoría de los entornos, es decir, tanto el frontend como el backend.
-
-React y Node proporcionan una opción deseable para implementar tanto una aplicación isomorfa como código universal.
-
-Escribir código universal directamente usando React todavía es bastante engorroso. Últimamente, una librería llamada [Next.js](https://github.com/vercel/next.js), que se implementa sobre React, ha atraído mucha atención y es una buena opción para hacer aplicaciones universales.
-
-#### Aplicaciones web progresivas
-
-Recientemente, la gente ha comenzado a usar el término [aplicación web progresiva](https://developers.google.com/web/progressive-web-apps/) (PWA) lanzado por Google.
-
-En resumen, estamos hablando de aplicaciones web funcionando lo mejor posible en todas las plataformas, aprovechando las mejores partes de esas plataformas. La pantalla más pequeña de los dispositivos móviles no debe obstaculizar la usabilidad de la aplicación. Las PWA también deberían funcionar sin problemas sin Internet o con una conexión a Internet lenta. En dispositivos móviles, deben ser instalables como cualquier otra aplicación. Todo el tráfico de red en una PWA debe estar cifrado.
-
-#### Arquitectura de microservicios
-
-Durante este curso solo hemos arañado la superficie del lado del servidor. En nuestras aplicaciones teníamos un backend monolítico , es decir, una aplicación que formaba un todo y se ejecutaba en un solo servidor, sirviendo solo unos pocos endpoints de API.
-
-A medida que la aplicación crece, el enfoque de backend monolítico comienza a tornarse problemático tanto en términos de rendimiento como de mantenibilidad.
-
-Una [arquitectura de microservicios](https://martinfowler.com/articles/microservices.html) es una forma de componer el backend de una aplicación a partir de muchos servicios independientes que se comunican entre sí a través de la red. El propósito de un microservicio individual es encargarse de un todo funcional lógico particular. En una arquitectura de microservicios pura, los servicios no utilizan una base de datos compartida.
-
-Por ejemplo, la aplicación de lista de blogs podría constar de dos servicios: uno que maneja al usuario y otro que se ocupa de los blogs. La responsabilidad del servicio de usuario sería el registro y la autenticación del usuario, mientras que el servicio de blogs se haría cargo de las operaciones relacionadas con los blogs.
-
-La siguiente imagen muestra la diferencia entre la estructura de una aplicación basada en una arquitectura de microservicios y una basada en una estructura monolítica más tradicional:
-
-
-
-El papel del frontend (encerrado por un cuadrado en la imagen) no difiere mucho entre los dos modelos. A menudo existe una [API gateway](http://microservices.io/patterns/apigateway) (puerta de enlace API) entre los microservicios y el frontend, que proporciona la ilusión de una API más tradicional de "todo en el mismo servidor". [Netflix](https://medium.com/netflix-techblog/optimizing-the-netflix-api-5c9ac715cf19), entre otros, utiliza este tipo de enfoque.
-
-Las arquitecturas de microservicios surgieron y evolucionaron para las necesidades de las grandes aplicaciones a gran escala de Internet. Amazon marcó la tendencia mucho antes de la aparición del término microservicio. El punto de partida crítico fue un correo electrónico enviado a todos los empleados en 2002 por el CEO de Amazon, Jeff Bezos:
-
-> De ahora en adelante, todos los equipos expondrán sus datos y funcionalidad a través de interfaces de servicio.
->
-> Los equipos deben comunicarse entre sí a través de estas interfaces.
->
-> No se permitirá ninguna otra forma de comunicación entre procesos: ningún enlace directo, ninguna lectura directa del almacén de datos de otro equipo, ningún modelo de memoria compartida, ninguna puerta trasera. La única comunicación permitida es a través de llamadas de interfaz de servicio a través de la red.
->
-> No importa qué tecnología uses.
->
-> Todas las interfaces de servicio, sin excepción, deben diseñarse desde cero para ser externalizables. Es decir, el equipo debe planificar y diseñar para poder exponer la interfaz a los desarrolladores del mundo exterior.
->
-> Sin excepciones.
->
-> Cualquiera que no haga esto será despedido. Gracias; ¡que tengas un buen día!
-
-Hoy en día, uno de los mayores precursores en el uso de microservicios es [Netflix](https://www.infoq.com/presentations/netflix-chaos-microservices).
-
-El uso de microservicios ha ido ganando popularidad hasta convertirse en una especie de [bala de plata](https://es.wikipedia.org/wiki/No_hay_balas_de_plata) de hoy en día, que se ofrece como una solución a casi todo tipo de problemas. Sin embargo, hay una serie de desafíos cuando se trata de aplicar una arquitectura de microservicios, y podría tener sentido ir [primero al monolito](https://martinfowler.com/bliki/MonolithFirst.html) al hacer inicialmente un backend tradicional que lo abarque todo. O quizás [no](https://martinfowler.com/articles/dont-start-monolith.html). Hay un montón de opiniones diferentes sobre el tema. Ambos enlaces conducen al sitio de Martin Fowler; como podemos ver, incluso los sabios no están completamente seguros de cuál de las formas correctas es la más correcta.
-
-Desafortunadamente, no podemos profundizar en este importante tema durante este curso. Incluso una mirada superficial al tema requeriría al menos 5 semanas más.
-
-#### Serverless (Sin servidor)
-
-Después del lanzamiento del servicio [Lambda](https://aws.amazon.com/lambda/) de Amazon a fines de 2014, comenzó a surgir una nueva tendencia en el desarrollo de aplicaciones web: [sin servidor](https://serverless.com/).
-
-Lo principal acerca de Lambda, y hoy en día también de Google [Cloud functions](https://cloud.google.com/functions/), así como de una [funcionalidad similar en Azure](https://azure.microsoft.com/en-us/services/functions/), es que permite la ejecución de funciones individuales en la nube. Antes, la unidad ejecutable más pequeña en la nube era un proceso único , por ejemplo, un entorno de ejecución que ejecuta un backend de Node.
-
-Utilizando la [API gateway](https://aws.amazon.com/es/api-gateway/) de Amazon es posible crear aplicaciones sin servidor donde las solicitudes a la API HTTP definida obtienen respuestas directamente de las funciones de la nube. Por lo general, las funciones ya operan utilizando datos almacenados en las bases de datos del servicio en la nube.
-
-Serverless no se trata de que no haya un servidor en las aplicaciones, sino de cómo se define el servidor. El desarrollador de software puede cambiar sus esfuerzos de programación a un mayor nivel de abstracción, pues ya no es necesario definir mediante programación el enrutamiento de solicitudes HTTP, relaciones de bases de datos, etc., debido a que la infraestructura de la nube proporciona todo esto. Las funciones en la nube también se prestan para crear un buen sistema de escalado, por ejemplo, Lambda de Amazon puede ejecutar una gran cantidad de funciones en la nube por segundo. Todo esto ocurre automáticamente a través de la infraestructura y no es necesario iniciar nuevos servidores, etc.
-
-### Librerías útiles y enlaces interesantes
-
-La comunidad de desarrolladores de JavaScript ha producido una gran variedad de librerías útiles. Si estás desarrollando algo más sustancial, vale la pena comprobar si las soluciones existentes ya están disponibles.
-A continuación se enumeran algunas librerías recomendadas por partes confiables.
-
-Si tu aplicación tiene que manejar datos complicados, [lodash](https://www.npmjs.com/package/lodash), que recomendamos en la [parte 4](/es/part4/estructura_de_la_aplicacion_backend_introduccion_a_las_pruebas#ejercicios-4-3-4-7), es una buena librería para usar. Si prefieres un estilo de programación funcional, podrías considerar usar [ramda](https://ramdajs.com/).
-
-Si maneja horas y fechas, [date-fns](https://github.com/date-fns/date-fns) ofrece buenas herramientas para eso.
-
-Si tienes formularios complejos en tus aplicaciones, puede que [React Hook Form](https://react-hook-form.com/) sea una buena opción.
-
-Si tu aplicación muestra gráficos, hay varias opciones para elegir. Se recomiendan tanto [recharts](https://recharts.org/en-US/) como [highcharts](https://github.com/highcharts/highcharts-react) .
-
-La librería [Immer](https://github.com/mweststrate/immer) provee implementaciones inmutables de algunas estructuras de datos. La librería podría ser útil cuando se usa Redux, ya que como recordamos de la [parte 6](/es/part6/flux_architecture_y_redux#funciones-puras-inmutables), los reducers deben ser funciones puras, lo que significa que no deben modificar el estado del store, sino que deben reemplazarlo por uno nuevo cuando se produce un cambio.
-
-[Redux-saga](https://redux-saga.js.org/) proporciona una forma alternativa de hacer las acciones asíncronas para [redux thunk](/es/part6/comunicarse_con_el_servidor_en_una_aplicacion_redux#acciones-asincronas-y-redux-thunk) que vimos en la parte 6. Algunos abrazan el hype y les gusta. Yo no.
-
-Para las aplicaciones de una sola página, la recopilación de datos analíticos sobre la interacción entre los usuarios y la página es [más desafiante](https://developers.google.com/analytics/devguides/collection/ga4/single-page-applications?implementation=browser-history&hl=es) que para las aplicaciones web tradicionales donde se carga toda la página. La librería [React Google Analytics 4](https://github.com/codler/react-ga4) ofrece una solución.
-
-Puedes aprovechar tu conocimiento de React al desarrollar aplicaciones móviles utilizando la extremadamente popular librería [React Native](https://facebook.github.io/react-native/) de Facebook, la cual es el tema de la [parte 10](/es/part10) del curso.
-
-En lo que respecta a las herramientas utilizadas para la gestión y bundling de proyectos de JavaScript, la comunidad ha sido muy voluble. Las mejores prácticas han cambiado rápidamente (los años son aproximaciones, nadie recuerda bien tan atrás en el tiempo):
-
-- 2011 [Bower](https://www.npmjs.com/package/bower)
-- 2012 [Grunt](https://www.npmjs.com/package/grunt)
-- 2013-14 [Gulp](https://www.npmjs.com/package/gulp)
-- 2012-14 [Browserify](https://www.npmjs.com/package/browserify)
-- 2015-2023 [Webpack](https://www.npmjs.com/package/webpack)
-- 2023- [esbuild](https://esbuild.github.io/)
-
-Los hipsters parecían haber perdido su interés en el desarrollo de herramientas después de que webpack comenzara a dominar los mercados. Hace unos años, [Parcel](https://parceljs.org) comenzó a hacer marketing, vendiéndose a sí mismo como más simple (cosa que Webpack no es) y más rápido que Webpack. Sin embargo, después de un comienzo prometedor, Parcel no ha cobrado fuerza. Pero recientemente, [esbuild](https://esbuild.github.io/) ha ganado mucha popularidad y ya está reemplazando a Webpack.
-
-El sitio ofrece una lista concisa de las mejores prácticas para React, algunas de las cuales ya son familiares en este curso. Otra lista similar es [react bits](https://vasanthk.gitbooks.io/react-bits/).
-
-[Reactiflux](https://www.reactiflux.com/) es una gran comunidad de chat de desarrolladores de React en Discord. Podría ser un buen lugar para obtener ayuda después de que el curso haya concluido. Numerosas librerías tienen sus propios canales.
-
-Si conoces algunos enlaces o librerías recomendables, ¡haz un pull request!
-
-
diff --git a/src/content/7/es/part7f.md b/src/content/7/es/part7f.md
deleted file mode 100644
index 3ae8f24eea8..00000000000
--- a/src/content/7/es/part7f.md
+++ /dev/null
@@ -1,171 +0,0 @@
----
-mainImage: ../../../images/part-7.svg
-part: 7
-letter: f
-lang: es
----
-
-
-
-Además de los ocho ejercicios de las secciones [React router](/es/part7/react_router) y [hooks personalizados](/es/part7/hooks_personalizados) de esta séptima parte del material del curso, hay 13 ejercicios que continúan nuestro trabajo sobre la aplicación BlogList en la que trabajamos en las partes cuatro y cinco del material del curso. Algunos de los siguientes ejercicios son "features" independientes entre sí, lo que significa que no es necesario terminarlos en ningún orden concreto. Eres libre de saltarte una parte de los ejercicios si así lo deseas. Bastantes de ellos tratan sobre aplicar la técnica avanzada de gestión del estado (Zustand, React Query y context) que cubrimos en la [parte 6](/es/part6).
-
-Si no quieres usar tu propia aplicación BlogList, puedes utilizar el código de la solución modelo como punto de partida para estos ejercicios.
-
-Muchos de los ejercicios de esta parte del material del curso requerirán refactorizar código existente. Esto es una realidad común al extender aplicaciones existentes, lo que significa que la refactorización es una habilidad importante y necesaria aunque a veces pueda resultar difícil y desagradable.
-
-Un buen consejo tanto para refactorizar como para escribir código nuevo es avanzar a pasos pequeños . Perder la cordura está casi garantizado si dejas la aplicación en un estado completamente roto durante largos periodos mientras refactorizas.
-
-
-
-
-
-### Ejercicios 7.6.-7.18.
-
-#### 7.6: Formateo Automático de Código
-
-En las partes anteriores usamos ESLint para asegurarnos de que el código sigue las convenciones definidas. [Prettier](https://prettier.io/) es otra aproximación al mismo problema. Según su documentación, Prettier es
an opinionated code formatter , es decir, no solo controla el estilo del código sino que además lo formatea de acuerdo con esa definición.
-
-Prettier es fácil de integrar en el editor de código para que el archivo se formatee automáticamente al guardarlo.
-
-Integra Prettier en tu aplicación y configúralo para que funcione con tu editor.
-
-### Gestión del Estado: Zustand
-
-
Hay dos versiones alternativas para elegir para los ejercicios 7.7-7.10: puedes hacer la gestión del estado de la aplicación usando Zustand o React Query y Context . Si quieres maximizar tu aprendizaje, deberías hacer ambas versiones.
-
-Nota: si completaste la parte 6 usando Redux, por supuesto puedes usar Redux en lugar de Zustand en esta serie de ejercicios.
-
-#### 7.7: Zustand, paso 1
-
-Refactoriza la aplicación para usar Zustand para gestionar los datos de la notificación.
-
-#### 7.8: Zustand, paso 2
-
-
Ten en cuenta que este ejercicio y los dos siguientes son bastante laboriosos, pero increíblemente educativos.
-
-Guarda la información sobre las publicaciones del blog en el store de Zustand. En este ejercicio es suficiente con que puedas ver los blogs en el backend y crear un nuevo blog.
-
-Eres libre de gestionar el estado del inicio de sesión y la creación de nuevas publicaciones usando el estado interno de los componentes React.
-
-#### 7.9: Zustand, paso 3
-
-Amplía tu solución para que vuelva a ser posible dar like y eliminar un blog.
-
-#### 7.10: Zustand, paso 4
-
-Guarda la información sobre el usuario autenticado en el store de Zustand.
-
-### Gestión del Estado: React Query y Context
-
-
Hay dos versiones alternativas para elegir para los ejercicios 7.7-7.10: puedes hacer la gestión del estado de la aplicación usando Zustand o React Query y Context . Si quieres maximizar tu aprendizaje, deberías hacer ambas versiones.
-
-#### 7.7: React Query y Context, paso 1
-
-Refactoriza la aplicación para usar el hook useReducer y context para gestionar los datos de la notificación.
-
-#### 7.8: React Query y Context, paso 2
-
-Usa React Query para gestionar el estado de las publicaciones del blog. Para este ejercicio, basta con que la aplicación muestre los blogs existentes y que la creación de un nuevo blog funcione correctamente.
-
-Eres libre de gestionar el estado del inicio de sesión y la creación de nuevas publicaciones usando el estado interno de los componentes React.
-
-#### 7.9: React Query y Context, paso 3
-
-Amplía tu solución para que vuelva a ser posible dar like y eliminar un blog.
-
-#### 7.10: React Query y Context, paso 4
-
-Usa el hook useReducer y context para gestionar los datos del usuario autenticado.
-
-### Vistas
-
-El resto de tareas son comunes tanto para la versión con Zustand como para la versión con React Query.
-
-#### 7.11: Vista de usuarios
-
-Implementa en la aplicación una vista que muestre toda la información básica relacionada con los usuarios:
-
-
-
-#### 7.12: Vista individual de usuario
-
-Implementa una vista para usuarios individuales que muestre todas las publicaciones de blog añadidas por ese usuario:
-
-
-
-Puedes acceder a esta vista haciendo clic en el nombre del usuario en la vista que enumera a todos los usuarios:
-
-
-
-
**NB:** es casi seguro que te encontrarás con el siguiente mensaje de error durante este ejercicio:
-
-
-
-El mensaje aparecerá si recargas la página de un usuario individual.
-
-La causa del problema es que, cuando navegamos directamente a la página de un usuario concreto, la aplicación React todavía no ha recibido los datos del backend. Una solución a este problema es usar renderizado condicional:
-
-```js
-const User = () => {
- const user = ...
- // highlight-start
- if (!user) {
- return null
- }
- // highlight-end
-
- return (
-
- // ...
-
- )
-}
-```
-
-#### 7.13: Vista de blog
-
-Implementa una vista separada para las publicaciones de blog. Puedes modelar el diseño de tu vista a partir del siguiente ejemplo:
-
-
-
-Los usuarios deben poder acceder a esta vista haciendo clic en el nombre de la publicación en la vista que lista todas las publicaciones.
-
-
-
-Cuando hayas terminado este ejercicio, la funcionalidad implementada en el ejercicio 5.7 ya no será necesaria. Hacer clic en una publicación ya no tiene que expandir el elemento de la lista y mostrar sus detalles.
-
-#### 7.14: Navegación
-
-Implementa un menú de navegación para la aplicación:
-
-
-
-#### 7.15: Comentarios, paso 1
-
-Implementa la funcionalidad para comentar publicaciones de blog:
-
-
-
-Los comentarios deben ser anónimos, es decir, no deben estar asociados al usuario que los dejó.
-
-En este ejercicio, basta con que el frontend muestre los comentarios que la aplicación recibe del backend.
-
-Un mecanismo apropiado para añadir comentarios a una publicación sería una petición HTTP POST al endpoint
api/blogs/:id/comments .
-
-#### 7.16: Comentarios, paso 2
-
-Amplía tu aplicación para que los usuarios puedan añadir comentarios a las publicaciones desde el frontend:
-
-
-
-#### 7.17: Estilos, paso 1
-
-Mejora el aspecto de tu aplicación aplicando alguno de los métodos mostrados en el material del curso.
-
-#### 7.18: Estilos, paso 2
-
-Puedes marcar este ejercicio como terminado si dedicas una hora o más a dar estilo a tu aplicación.
-
-Este fue el último ejercicio de esta parte del curso y ya es momento de subir tu código a GitHub y marcar todos tus ejercicios terminados en el [sistema de envío de ejercicios](https://studies.cs.helsinki.fi/stats/courses/fullstackopen).
-
-
diff --git a/src/content/partnavigation/partnavigation.js b/src/content/partnavigation/partnavigation.js
index ec922ed862a..702f6410964 100644
--- a/src/content/partnavigation/partnavigation.js
+++ b/src/content/partnavigation/partnavigation.js
@@ -231,21 +231,19 @@ module.exports = {
b: 'props.children y la ref del componente',
c: 'Probando aplicaciones React',
d: 'Pruebas de extremo a extremo: Playwright',
- e: 'Pruebas de extremo a extremo: Cypress',
+ e: 'React Router, librerías de UI',
},
6: {
- a: 'Flux-architecture y Redux',
- b: 'Muchos reducers',
- c: 'Comunicarse con el servidor en una aplicación redux',
- d: 'React Query, useReducer y el contexto',
+ a: 'Arquitectura Flux y Zustand',
+ b: 'Estado complejo, fetch y pruebas',
+ c: 'React Query y Context API',
+ d: 'Redux (heredado)',
},
7: {
- a: 'React-router',
- b: 'Hooks personalizados',
- c: 'Más sobre estilos',
- d: 'Webpack',
- e: 'Componentes de clase, misceláneos',
- f: 'Ejercicios: ampliar la lista de blogs',
+ a: 'Más sobre los hooks de React',
+ b: 'Funcionamiento interno de Vite y esbuild',
+ c: 'Miscelánea',
+ d: 'Ejercicios: ampliar la lista de blogs',
},
8: {
a: 'Servidor GraphQL',