El mòdul anterior va tancar el recorregut tècnic i va acabar amb una promesa: construir CicloUrbano sencer, de principi a fi, amb Vite i React Router. Aquesta lliçó és el primer pas, i és el que més gent es salta: abans d'escriure una línia de JSX cal saber què es construeix, per a qui, amb quines dades i amb quines decisions tècniques. Un projecte que comença per npm create vite sense haver respost aquestes preguntes acaba, sense excepció, refactoritzant-se a mig camí.
Faràs tres coses, en aquest ordre. Primer, definir el producte: l'abast en una frase, les dues persones usuàries, les històries amb els seus criteris d'acceptació i —tan important com l'anterior— la llista explícita del que no entra a la primera versió. Segon, fixar les decisions d'arquitectura en una acta que justifiqui cada elecció i anomeni l'alternativa descartada, perquè d'aquí a tres mesos ningú no hagi de reconstruir el raonament. I tercer, muntar el bastiment real: Vite, dependències, scripts, linter, formatador, variables d'entorn, estructura de carpetes, json-server sembrat i un primer commit que ja arrenca.
En acabar tindràs un repositori que es clona, s'instal·la i s'executa, amb l'API de desenvolupament responent i sense ni una sola pantalla escrita. Les pantalles són la lliçó següent.
Contingut
- El producte en una frase
- Persones usuàries i els seus objectius
- Històries d'usuari i criteris d'acceptació
- El que queda fora de la primera versió, i per què
- Model de dades definitiu
- El
db.jsoninicial complet - Mapa de pantalles i arbre de rutes
- L'acta de decisions d'arquitectura
- Crear el projecte amb Vite
- Dependències: què s'instal·la i per què
- El
package.jsoncomplet - Qualitat del codi: ESLint, Prettier i
.editorconfig - Variables d'entorn amb
import.meta.env - Estructura de carpetes: per tipus o per funcionalitat
- L'API de desenvolupament:
json-serveren marxa - Convencions de l'equip
- El pla de treball: increments verticals
- El primer commit
- El producte en una frase
Si no pots descriure el que construeixes en una frase, encara no saps el que construeixes. La d'aquest projecte:
CicloUrbano és una aplicació web privada que permet a una persona client reservar bicicletes d'una xarxa urbana per estacions, i a una persona operària mantenir l'estat de la flota.
Aquesta frase carrega més informació de la que sembla:
| Fragment | Què decideix |
|---|---|
| «aplicació web» | No és mòbil (encara que 10-05 va deixar la porta oberta) |
| «privada» | Tot el que és interessant passa després d'identificar-se: no hi ha contingut públic per indexar |
| «reservar bicicletes» | El camí dels diners del producte. Tota la resta el serveix |
| «per estacions» | Les estacions són un eix de navegació, no un adorn |
| «mantenir l'estat de la flota» | Hi ha un segon rol amb permisos diferents |
La paraula «privada» és la que decideix l'arquitectura de renderitzat, i ho fa abans que es discuteixi cap eina. Hi tornarem a l'apartat 8.
- Persones usuàries i els seus objectius
Dues persones, ni una més. Un producte amb cinc perfils a la primera versió no en té cap ben resolt.
| Client | Operari | |
|---|---|---|
| Exemple del domini | Ana Ribera (usr-01) |
Marc Solé (usr-02) |
| Objectiu principal | Aconseguir una bicicleta quan la necessita | Que la flota estigui operativa |
| Freqüència d'ús | Ràfegues curtes, diverses vegades per setmana | Sessions llargues, cada dia |
| Context | Mòbil, al carrer, amb pressa | Escriptori, al taller |
| Què el frustra | No saber si la bicicleta estarà disponible en arribar | No saber quina bicicleta porta dies aturada |
| Què necessita veure primer | Bicicletes disponibles a prop | Bicicletes en manteniment |
| Permisos | Veure el catàleg, reservar, cancel·lar el que és seu | Tot l'anterior més canviar l'estat d'una bicicleta |
D'aquest quadre en surten dues conseqüències de disseny que ja no es discuteixen després:
- La pantalla d'inici és el catàleg, no un tauler de control. El client és qui més hi entra, i hi entra amb pressa.
- El taller és una pantalla a part amb accés per rol, no un mode ocult del catàleg. Barrejar els dos usos en una pantalla obligaria a amagar controls per permís a cada targeta, que és exactament el tipus de complexitat que es paga durant anys.
- Històries d'usuari i criteris d'acceptació
Una història d'usuari té una forma fixa —com a X vull Y per a Z— i només serveix si porta criteris d'acceptació comprovables. «Que el catàleg funcioni bé» no és un criteri; «en filtrar per elèctrica només apareixen les de tipus electrica» sí que ho és. A 11-04 cadascun d'aquests criteris es convertirà en una prova, i per això estan redactats així des d'avui.
Imprescindibles
| ID | Història | Criteris d'acceptació |
|---|---|---|
| H1 | Com a client vull veure el catàleg de bicicletes per saber què hi ha disponible | Es llisten les 5 bicicletes amb model, tipus, estat, estació i preu/hora · Mentre carreguen es veu un esquelet, no un text · Si l'API falla, hi ha missatge d'error i botó de reintent |
| H2 | Com a client vull filtrar per tipus i cercar per model per trobar ràpid el que necessito | El filtre viu a la URL (?tipo=electrica) i és compartible per enllaç · La cerca per text és local i amb retard · Filtre i cerca es combinen |
| H3 | Com a client vull veure la fitxa d'una bicicleta per decidir si la reservo | Ruta /bicicletas/:bicicletaId · Mostra estació, preu i estat · Un identificador inexistent dona un 404 propi, no una pantalla en blanc |
| H4 | Com a persona usuària vull identificar-me per accedir al que és meu | Formulari amb validació accessible · En entrar es torna a la pantalla d'origen, no a l'inici · La sessió sobreviu a una recàrrega |
| H5 | Com a client vull reservar una bicicleta disponible per usar-la | Formulari amb bicicleta, inici, hores i condicions · No es pot reservar una que no estigui disponible · En crear-la, avís d'èxit i redirecció a /reservas · El catàleg reflecteix el canvi |
| H6 | Com a client vull veure i cancel·lar les meves reserves per gestionar els meus plans | /reservas llista només les de l'usuari identificat · Cancel·lar demana confirmació · El canvi es veu a l'instant i es confirma contra el servidor |
| H7 | Com a operari vull canviar l'estat d'una bicicleta per retirar-la o tornar-la al servei | /taller només accessible amb rol operario · Un client que hi entri per URL veu «sense permisos» · El canvi es reflecteix al catàleg públic |
| H8 | Com a persona usuària vull veure les estacions i la seva flota per orientar-me per la ciutat | /estaciones amb les 3 estacions · /estaciones/:estacionId amb pestanyes de flota i incidències · La pestanya activa és a la URL |
Desitjables
| ID | Història | Per què és desitjable i no imprescindible |
|---|---|---|
| D1 | Tema clar/fosc amb preferència recordada | Millora real, però ningú deixa de reservar per no tenir-lo |
| D2 | Avisos temporals d'èxit i error a tota l'aplicació | Es pot començar amb missatges en línia; els avisos globals poleixen l'experiència |
| D3 | Avís en perdre la connexió | Només importa al mòbil al carrer; el cas es degrada de manera acceptable sense ell |
| D4 | Resum de flota per estat al catàleg | Informatiu. Útil per a l'operari, prescindible per al client |
La distinció no és cosmètica: si el termini s'escurça, es talla per D1-D4 sense tocar H1-H8. Tenir aquesta línia traçada abans de començar és el que impedeix que un projecte entregui deu coses a mitges en lloc de vuit acabades.
- El que queda fora de la primera versió, i per què
Aquesta llista és tan important com l'anterior, i convé escriure-la i compartir-la, perquè el que no està escrit s'acaba demanant a mig projecte.
| Fora d'abast | Motiu |
|---|---|
| Pagaments reals | Requereix passarel·la, compliment normatiu i auditoria. El preu es mostra i es calcula; no es cobra |
| Registre d'usuaris i recuperació de contrasenya | Els usuaris es sembren a db.json. La identificació real és del servidor (11-05) |
| Mapa geogràfic d'estacions | Afegeix una biblioteca pesant i una clau de servei. Es llisten per barri |
| Notificacions push | Exigeix servidor, permisos i service worker. Fora |
| Tauler d'estadístiques | Cap de les dues persones usuàries l'ha demanat per al seu objectiu principal |
| Internacionalització | L'aplicació és d'una ciutat i un idioma. S'anota com a full de ruta a 11-05 |
| Mode sense connexió | Complexitat alta, valor incert fins a tenir usuaris reals |
| Aparador públic amb SEO | És contingut públic: si algun dia es fa, es fa amb Next.js (10-02), com a projecte a part |
Fixa't en l'última fila. Decidir que alguna cosa està fora no és decidir que mai es farà: és decidir que no competeix pel temps d'aquesta versió, i de passada deixar anotat com es faria si arriba el moment.
- Model de dades definitiu
Quatre entitats. Les coneixes de tot el curs; aquí queden fixades amb les seves relacions.
erDiagram
ESTACIO ||--o{ BICICLETA : "allotja"
BICICLETA ||--o{ RESERVA : "es objecte de"
USUARI ||--o{ RESERVA : "realitza"
ESTACIO {
string id PK "est-01"
string nom "Plaça Major"
string barri "Centre"
number places "20"
}
BICICLETA {
string id PK "bici-001"
string model "Urbana Clàssica"
string tipus "urbana | electrica | carga"
string estat "disponible | alquilada | mantenimiento"
string estacioId FK "est-01"
number preuHora "2.5"
}
USUARI {
string id PK "usr-01"
string nom "Ana Ribera"
string email "[email protected]"
string rol "cliente | operario"
}
RESERVA {
string id PK "res-01"
string bicicletaId FK "bici-002"
string usuari FK "usr-01"
string dataInici "2026-05-04T09:00"
number hores "2"
string estat "activa | confirmada | cancelada"
}
Tres decisions del model que convé entendre abans de codificar:
- Les relacions es guarden per identificador, mai imbricant l'objecte sencer. Una reserva guarda
bicicletaId, no una còpia de la bicicleta. Si s'imbriqués, el preu de la reserva quedaria congelat en el moment de crear-la i qualsevol canvi a la bicicleta deixaria còpies desincronitzades per tota la base de dades. És la mateixa normalització que es va aplicar a l'estat de Redux a 07-04, i pel mateix motiu. - L'estat és una cadena d'un conjunt tancat, no un booleà.
disponible | alquilada | mantenimientoadmet un quart estat el dia que calgui;disponible: true/falseobliga a afegir un altre booleà i a raonar sobre combinacions impossibles. - El camp de la reserva es diu
usuarii conté un identificador. És una incoherència de nom respecte abicicletaId, i es conserva perquè així es va fixar a tot el curs: canviar-ho ara trencariavalidarReserva, les proves i els gestors de MSW. S'anota com a deute tècnic menor a l'acta, que és exactament el que es fa en un projecte real amb una incoherència innòcua ja estesa.
- El
db.json inicial complet
db.json inicial completAquest fitxer és la base de dades de desenvolupament i, alhora, la llavor que 11-04 restaurarà abans de cada prova d'extrem a extrem. Va a l'arrel del repositori.
{
"bicicletas": [
{ "id": "bici-001", "model": "Urbana Clàssica", "tipus": "urbana", "estat": "disponible", "estacioId": "est-01", "preuHora": 2.5 },
{ "id": "bici-002", "model": "Elèctrica Pro", "tipus": "electrica", "estat": "alquilada", "estacioId": "est-01", "preuHora": 4.0 },
{ "id": "bici-003", "model": "Càrrega Max", "tipus": "carga", "estat": "mantenimiento", "estacioId": "est-02", "preuHora": 5.5 },
{ "id": "bici-004", "model": "Urbana Clàssica", "tipus": "urbana", "estat": "disponible", "estacioId": "est-03", "preuHora": 2.5 },
{ "id": "bici-005", "model": "Elèctrica Pro", "tipus": "electrica", "estat": "disponible", "estacioId": "est-02", "preuHora": 4.0 }
],
"estaciones": [
{ "id": "est-01", "nom": "Plaça Major", "barri": "Centre", "places": 20 },
{ "id": "est-02", "nom": "Parc Nord", "barri": "Nord", "places": 15 },
{ "id": "est-03", "nom": "Estació Central", "barri": "Eixample", "places": 30 }
],
"usuarios": [
{ "id": "usr-01", "nom": "Ana Ribera", "email": "[email protected]", "rol": "cliente" },
{ "id": "usr-02", "nom": "Marc Solé", "email": "[email protected]", "rol": "operario" }
],
"reservas": [
{ "id": "res-01", "bicicletaId": "bici-002", "usuari": "usr-01", "dataInici": "2026-05-04T09:00", "hores": 2, "estat": "activa" }
]
}El conjunt està triat perquè cada estat i cada cas límit tingui almenys un representant, que és la propietat que ha de complir qualsevol joc de dades de desenvolupament:
| Cas que cal poder provar | Dada que el cobreix |
|---|---|
| Bicicleta reservable | bici-001, bici-004, bici-005 |
| Bicicleta no reservable per estar llogada | bici-002 |
| Bicicleta no reservable per manteniment | bici-003 |
| Dues bicicletes amb el mateix model (claus de llista) | bici-001 i bici-004 |
| Estació amb diverses bicicletes | est-01 i est-02 |
| Estació sense bicicletes disponibles | est-01 té una llogada; est-02, una en manteniment |
| Usuari amb reserves | usr-01 |
| Usuari sense cap reserva (estat buit) | usr-02 |
| Els tres tipus de bicicleta | urbana, elèctrica i de càrrega |
Aquest penúltim cas és el que més s'oblida i el que més errors produeix: si mai no veus la llista buida en desenvolupament, no la dissenyes, i l'usuari que estrena l'aplicació es troba un buit en blanc.
- Mapa de pantalles i arbre de rutes
Les rutes van quedar fixades al mòdul 6. Aquí es comprova que cadascuna serveix una història i que cap història es queda sense pantalla.
flowchart TD
RAIZ["/ · Disseny (Outlet)"]
RAIZ --> IDX["index · PaginaCataleg · H1 H2"]
RAIZ --> BICI["bicicletas/:bicicletaId · PaginaFitxaBicicleta · H3"]
RAIZ --> EST["estaciones"]
EST --> ESTI["index · PaginaEstacions · H8"]
EST --> DET[":estacionId · PaginaDetallEstacio · H8"]
DET --> FLO["index · PestanyaFlota"]
DET --> INC["incidencias · PestanyaIncidencies"]
RAIZ --> ACC["acceso · PaginaAcces · H4"]
RAIZ --> PROT["🔒 RutaProtegida (sense path)"]
PROT --> RES["reservas"]
RES --> RESI["index · PaginaReserves · H6"]
RES --> NUE["nueva · PaginaNovaReserva · H5"]
PROT --> ROL["🔒 RequereixRol operario (sense path)"]
ROL --> TAL["taller · PaginaTaller · H7"]
RAIZ --> NF["* · PaginaNoTrobada"]
style PROT fill:#fde68a
style ROL fill:#fed7aa
style TAL fill:#fecaca
I la taula de cobertura, que és la que es revisa per detectar buits:
| Ruta | Pantalla | Història | Accés |
|---|---|---|---|
/ |
PaginaCataleg |
H1, H2 | Públic |
/bicicletas/:bicicletaId |
PaginaFitxaBicicleta |
H3 | Públic |
/estaciones |
PaginaEstacions |
H8 | Públic |
/estaciones/:estacionId |
PaginaDetallEstacio (+ pestanyes) |
H8 | Públic |
/acceso |
PaginaAcces |
H4 | Públic |
/reservas |
PaginaReserves |
H6 | Identificat |
/reservas/nueva |
PaginaNovaReserva |
H5 | Identificat |
/taller |
PaginaTaller |
H7 | Rol operario |
* |
PaginaNoTrobada |
— | Públic |
| (error de ruta) | PaginaErrorRuta via errorElement |
— | — |
| (sense permisos) | PaginaSensePermisos |
H7 | — |
Un detall que canvia respecte al mòdul 6: /reservas entra dins de la branca protegida. En el seu moment es va deixar pública per no complicar els exemples; al projecte real, veure reserves exigeix sessió, perquè són dades personals. És el tipus d'ajust que apareix just en fer aquesta taula, i per això es fa la taula.
- L'acta de decisions d'arquitectura
Aquest és el document més valuós de la lliçó. Cada fila registra què es decideix, per què, i què es descarta. Es desa al repositori com a DECISIONS.md i s'actualitza quan alguna cosa canvia; mai no s'esborra una fila, s'afegeix la nova amb la seva data.
| # | Decisió | Per què | Alternativa descartada i motiu |
|---|---|---|---|
| A1 | Vite + React Router, aplicació de client | L'aplicació és privada, interactiva i després d'identificar-se: no hi ha SEO a guanyar i el primer pintat no és un factor de negoci. Arrencada instantània del servidor de desenvolupament i desplegament com a estàtics | Next.js: aporta SSR/SSG que aquí no s'aprofiten, i afegeix un servidor a mantenir. Criteri aplicat a 10-02 |
| A2 | React Router v7 en mode de dades (createBrowserRouter + RouterProvider) |
Rutes imbricades, errorElement per branca i càrrega mandrosa declarativa |
BrowserRouter amb <Routes>: sense errorElement ni API de dades. Descartat a 06-01 |
| A3 | TanStack Query per a l'estat del servidor | Memòria cau, deduplicació, revalidació, estats de càrrega i error i invalidació després de mutar: tot el que caldria escriure a mà | Redux per a tot: obliga a reimplementar memòria cau i cicle de vida (07-06). useEffect + fetch: sense memòria cau ni deduplicació |
| A4 | Redux Toolkit per a l'estat del client compartit (sessió, catàleg) | Selecció granular, DevTools amb viatge en el temps, reductors purs fàcils de provar | Context per a tot: repinta tots els consumidors davant de qualsevol canvi (07-02) |
| A5 | Context per a tema i avisos | Canvien poc i els necessita tot l'arbre. És exactament el seu cas d'ús | Redux: vàlid, però afegeix cerimònia a dues dades trivials |
| A6 | La URL per al filtre per tipus | Un catàleg filtrat ha de poder-se compartir per enllaç i sobreviure a una recàrrega | Estat local o Redux: es perd en recarregar i no és compartible |
| A7 | CSS Modules | Àmbit local sense dependències, sense cost en execució, suportat per Vite de sèrie | Tailwind: excel·lent, però afegeix configuració i una corba pròpia. CSS-in-JS: cost en execució i fricció amb RSC (10-03) |
| A8 | Vitest + Testing Library + MSW + Cypress | Vitest comparteix configuració amb Vite; el trofeu de proves de 09-01 aplicat tal qual | Jest: exigeix transformador propi i duplicar la configuració. Sense e2e: deixa fora enrutament real, CSS i persistència |
| A9 | JavaScript, amb migració a TypeScript prevista | L'equip el domina i la primera versió s'ha d'entregar. L'estructura ja és compatible: tipus en un fitxer, validació centralitzada | TypeScript des del dia u: millor a mitjà termini, però frena l'arrencada. Es planifica a 11-05 amb el que s'ha vist a 10-04 |
| A10 | json-server només en desenvolupament |
Dona una API REST completa sobre un JSON en un minut | API pròpia: fora d'abast. Es documenta a 11-05 què caldria de veritat |
I l'assignació d'estat, que és l'aplicació literal de la taxonomia de 07-01 a aquest projecte:
| Tipus d'estat | Exemple a CicloUrbano | Eina | Per què |
|---|---|---|---|
| Del servidor | Bicicletes, estacions, reserves, usuaris | TanStack Query | És una còpia local d'alguna cosa remota; necessita memòria cau i revalidació |
| De client compartit | Usuari identificat, terme de cerca, ordre | Redux Toolkit | Diversos components llunyans el llegeixen i l'escriuen |
| D'interfície global | Tema, avisos | Context | Poc canvi, molts consumidors |
| D'URL | ?tipo=electrica, pestanya activa |
React Router | Ha de ser compartible i navegable |
| Local | Modal obert, esborrany del formulari | useState |
Ningú més ho necessita |
- Crear el projecte amb Vite
Amb l'acta signada, el bastiment és mecànic.
Què ha fet cada línia:
npm create vite@latestdescarrega i executa el generador oficial. El--separa els arguments denpmdels del generador; sense ell,npmintentaria interpretar--templatecom a seu.--template reacttria la plantilla de React amb JavaScript. Existeixreact-tsper a TypeScript ireact-swcper usar SWC en lloc de Babel; amb React 19 i el complement oficial, la diferència de velocitat en un projecte d'aquesta mida és irrellevant.npm run devaixeca el servidor de desenvolupament ahttp://localhost:5173.
Comprova la versió de React abans de seguir, perquè el projecte assumeix React 19:
npm ls react
# [email protected]
# └── [email protected]
- Dependències: què s'instal·la i per què
Res no s'instal·la «perquè sí». Cada paquet respon a una fila de l'acta.
# Producció
npm install react-router @reduxjs/toolkit react-redux @tanstack/react-query
# Desenvolupament
npm install -D @tanstack/react-query-devtools
npm install -D vitest jsdom @testing-library/react @testing-library/jest-dom @testing-library/user-event
npm install -D msw cypress start-server-and-test
npm install -D eslint-plugin-jsx-a11y prettier eslint-config-prettier
npm install -D json-server npm-run-all| Paquet | On | Per a què | Decisió |
|---|---|---|---|
react-router |
Producció | Enrutament al client (v7, paquet únic) | A2 |
@reduxjs/toolkit |
Producció | createSlice, configureStore, Immer inclòs |
A4 |
react-redux |
Producció | useSelector, useDispatch, Provider |
A4 |
@tanstack/react-query |
Producció | Estat del servidor amb memòria cau | A3 |
@tanstack/react-query-devtools |
Desenvolupament | Inspector de la memòria cau. No entra al paquet de producció | A3 |
vitest + jsdom |
Desenvolupament | Executor de proves i DOM simulat | A8 |
@testing-library/* |
Desenvolupament | Renderitzat, asercions i interacció realista | A8 |
msw |
Desenvolupament | Simulació de la xarxa a nivell de petició | A8 |
cypress |
Desenvolupament | Proves d'extrem a extrem en navegador real | A8 |
start-server-and-test |
Desenvolupament | Espera que Vite i l'API responguin abans de llançar Cypress | A8 |
eslint-plugin-jsx-a11y |
Desenvolupament | Regles d'accessibilitat al linter | Accessibilitat com a requisit |
prettier + eslint-config-prettier |
Desenvolupament | Formatat automàtic sense barallar-se amb ESLint | Convencions |
json-server |
Desenvolupament | API REST de desenvolupament | A10 |
npm-run-all |
Desenvolupament | Executar Vite i l'API en paral·lel amb una sola comanda | Comoditat |
La distinció entre dependencies i devDependencies no és burocràcia: el que hi ha a dependencies pot acabar al paquet que es descarrega l'usuari. Posar json-server o Cypress allà no trencaria el build de Vite, però sí que engreixa qualsevol instal·lació de producció i dona un senyal fals sobre el que l'aplicació necessita per funcionar.
- El
package.json complet
package.json complet{
"name": "ciclourbano",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"api": "json-server --watch db.json --port 3001",
"dev:todo": "npm-run-all --parallel dev api",
"lint": "eslint . --max-warnings 0",
"formato": "prettier --write \"src/**/*.{js,jsx,css,json}\"",
"formato:comprobar": "prettier --check \"src/**/*.{js,jsx,css,json}\"",
"test": "vitest",
"test:ejecutar": "vitest run",
"cobertura": "vitest run --coverage",
"cy:abrir": "cypress open",
"cy:ejecutar": "cypress run",
"e2e": "start-server-and-test dev:todo \"http://localhost:5173|http://localhost:3001/bicicletas\" cy:ejecutar",
"pruebas:todas": "npm-run-all lint test:ejecutar e2e"
},
"dependencies": {
"@reduxjs/toolkit": "^2.5.0",
"@tanstack/react-query": "^5.62.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-redux": "^9.2.0",
"react-router": "^7.1.0"
},
"devDependencies": {
"@tanstack/react-query-devtools": "^5.62.0",
"@testing-library/jest-dom": "^6.6.0",
"@testing-library/react": "^16.1.0",
"@testing-library/user-event": "^14.5.0",
"@vitejs/plugin-react": "^4.3.0",
"@vitest/coverage-v8": "^2.1.0",
"cypress": "^13.17.0",
"eslint": "^9.17.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-jsx-a11y": "^6.10.0",
"eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-react-refresh": "^0.4.0",
"jsdom": "^25.0.0",
"json-server": "^0.17.4",
"msw": "^2.7.0",
"npm-run-all": "^4.1.5",
"prettier": "^3.4.0",
"start-server-and-test": "^2.0.0",
"vite": "^6.0.0",
"vitest": "^2.1.0"
}
}Els scripts es mereixen un comentari, perquè són la interfície del projecte per a qualsevol que arribi nou:
| Script | Què fa | Quan s'usa |
|---|---|---|
dev |
Servidor de Vite al 5173 | Cada dia |
api |
json-server al 3001 vigilant db.json |
Cada dia, en un altre terminal |
dev:todo |
Els dos en paral·lel | La comanda de treball habitual |
build |
Construeix dist/ per a producció |
Abans de desplegar (11-05) |
preview |
Serveix dist/ per comprovar-la |
Després de build |
lint |
ESLint amb zero avisos tolerats | A cada commit i a CI |
formato |
Prettier reescriu els fitxers | En desar o abans de commit |
formato:comprobar |
Prettier només comprova, sense escriure | A CI |
test |
Vitest en mode vigilància | Mentre es programa |
test:ejecutar |
Vitest una vegada i surt | A CI |
cobertura |
Informe de cobertura | Revisions periòdiques |
e2e |
Aixeca tot, espera i llança Cypress | A CI i abans d'una entrega |
pruebas:todas |
Linter + unitàries + e2e, en aquest ordre | La porta de qualitat completa |
El --max-warnings 0 de lint és deliberat: un avís que no trenca res s'acumula fins que n'hi ha cent vint i ningú els mira. O importen i fallen, o es desactiva la regla.
- Qualitat del codi: ESLint, Prettier i
.editorconfig
.editorconfigVite genera un eslint.config.js bàsic. Aquest és el del projecte, amb les dues addicions que exigeix l'acta: accessibilitat i regles de hooks.
// eslint.config.js
import js from '@eslint/js';
import globals from 'globals';
import reactHooks from 'eslint-plugin-react-hooks';
import reactRefresh from 'eslint-plugin-react-refresh';
import jsxA11y from 'eslint-plugin-jsx-a11y';
import prettier from 'eslint-config-prettier';
export default [
{ ignores: ['dist', 'coverage', 'cypress/videos', 'cypress/screenshots'] },
{
files: ['**/*.{js,jsx}'],
languageOptions: {
ecmaVersion: 2022,
globals: globals.browser,
parserOptions: {
ecmaFeatures: { jsx: true },
sourceType: 'module'
}
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
'jsx-a11y': jsxA11y
},
rules: {
...js.configs.recommended.rules,
...reactHooks.configs.recommended.rules,
...jsxA11y.configs.recommended.rules,
// Un import no utilitzat és soroll; una variable d'error sí que pot quedar-se
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
// Vite necessita que cada mòdul exporti només components per al refresc ràpid
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
// Elevades a error: són les errades que el mòdul 5 i el 3 van costar més d'explicar
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'error',
'jsx-a11y/label-has-associated-control': 'error',
'jsx-a11y/no-autofocus': 'warn'
}
},
prettier // SEMPRE l'últim: desactiva les regles d'estil que xoquen amb Prettier
];Tres punts que cal entendre, no copiar:
react-hooks/exhaustive-depscom aerror, no com awarn. És la decisió més discutida de qualsevol configuració de React i aquí es pren a consciència: a 05-02 es va veure que una dependència omesa produeix dades obsoletes que no fallen en desenvolupament i sí en producció. Com a avís, s'ignora; com a error, obliga a resoldre'l o a justificar l'excepció amb un comentarieslint-disable-next-lineque queda visible a la revisió de codi.jsx-a11yen mode recomanat. Caça a l'editor la imatge sensealt, l'onClicksobre undivsense rol ni teclat, i l'etiqueta sense control associat. No substitueix la revisió manual de 03-06, però elimina el 80 % dels errors habituals abans que existeixin.prettierva l'últim de l'array.eslint-config-prettierno afegeix regles: desactiva les d'ESLint que xoquen amb el formatat. Si es posés abans, les regles posteriors les tornarien a activar i tindries el linter i el formatador barallant-se a cada desada.
// .prettierrc
{
"semi": true,
"singleQuote": true,
"printWidth": 100,
"trailingComma": "none",
"arrowParens": "always"
}# .editorconfig
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false.editorconfig és el que evita el clàssic «el fitxer sencer apareix modificat al diff» quan algú treballa amb Windows: fixa els finals de línia i la indentació a nivell d'editor, abans que Prettier hi intervingui.
# .gitignore
node_modules
dist
dist-ssr
coverage
*.local
# Entorn: s'ignoren els .env reals, es versiona l'exemple
.env
.env.*
!.env.example
# Cypress
cypress/videos
cypress/screenshots
cypress/downloads
# Editor i sistema
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.log
- Variables d'entorn amb
import.meta.env
import.meta.envLa URL de l'API no pot estar escrita a mà en quinze fitxers. Vite exposa les variables d'entorn a través d'import.meta.env, amb una regla estricta: només les que comencen per VITE_ arriben al codi del client.
# .env.example (SÍ es versiona: és la plantilla)
# Copia aquest fitxer a .env i ajusta els valors.
# URL base de l'API REST de desenvolupament (json-server)
VITE_URL_API=http://localhost:3001
# Nom visible de l'aplicació
VITE_NOM_APP=CicloUrbano// src/configuracio.js
export const URL_API = import.meta.env.VITE_URL_API ?? 'http://localhost:3001';
export const NOM_APP = import.meta.env.VITE_NOM_APP ?? 'CicloUrbano';
export const ES_DESENVOLUPAMENT = import.meta.env.DEV; // booleà que Vite injecta semprePer què un mòdul configuracio.js en lloc de llegir import.meta.env allà on calgui:
- Un únic punt de veritat. El dia que la variable canviï de nom, es toca un fitxer.
- Valors per defecte en un sol lloc, amb
??, perquè clonar el repositori i oblidar-se del.envno trenqui l'arrencada. - Es pot substituir a les proves amb un
vi.mockd'un mòdul propi; ambimport.meta.envescampat, no.
I l'avís que es repetirà a 11-05 amb totes les lletres: import.meta.env és text que s'incrusta al JavaScript que es descarrega el navegador. Qualsevol persona pot llegir-lo amb dos clics. Una clau d'API privada, una contrasenya o un testimoni d'administració no hi van mai, ni tan sols «temporalment mentre provem».
- Estructura de carpetes: per tipus o per funcionalitat
Hi ha dues escoles, i totes dues tenen raó en el seu context:
Per tipus (components/, hooks/, pagines/) |
Per funcionalitat (reserves/, cataleg/, sessio/) |
|
|---|---|---|
| Trobar un component pel seu nom | Immediat | Cal saber a quina funcionalitat pertany |
| Treballar en una funcionalitat completa | Saltes entre quatre carpetes | Tot junt |
| Esborrar una funcionalitat sencera | Cal caçar fitxers per tot arreu | S'esborra la carpeta |
| Projecte petit (< 30 fitxers) | Còmode | Sobredimensionat |
| Projecte gran o amb diversos equips | Carpetes de 60 fitxers | Escala millor |
| Risc típic | Un components/ ingovernable |
Discutir a quina funcionalitat pertany cada cosa |
CicloUrbano adopta un híbrid, que és el que fa la majoria de projectes reals d'aquesta mida: per tipus el que és compartit, per funcionalitat l'estat de domini. Aquesta última part ja venia decidida des de 07-05.
ciclourbano/ ├── cypress/ │ ├── e2e/ # acces.cy.js, reservar.cy.js, cancellar.cy.js │ └── support/ # comandos.js: cy.perTestId, cy.sembrarDades, cy.accedirCom ├── public/ # fitxers servits tal com són (favicon, robots.txt) ├── src/ │ ├── api/ # client.js i funcions per recurs. NO sap res de React │ ├── magatzem/ # magatzem.js: configureStore i la seva composició │ ├── components/ # reutilitzables, sense ruta pròpia │ │ └── base/ # sistema de disseny: Boto, Camp, Panell, Etiqueta, Modal │ ├── consultes/ # clientConsultes.js, claus.js i els hooks de TanStack Query │ ├── contextos/ # ProveidorTema, contextos d'avisos, Proveidors.jsx │ ├── dades/ # domini.js: dades d'exemple mentre no hi ha xarxa (11-02) │ ├── funcionalitats/ # estat de client organitzat per domini │ │ ├── cataleg/ # sliceCataleg.js i els seus selectors │ │ ├── reserves/ # sliceReserves.js i els seus selectors │ │ └── sessio/ # sliceSessio.js i els seus selectors │ ├── hooks/ # useAlternar, useMagatzemLocal, useDebounce, useEsdevenimentTeclat… │ ├── pagines/ # una per ruta: PaginaCataleg, PaginaReserves… │ ├── proves/ # configuracio.js, utilitats.jsx, gestors.js, servidor.js │ ├── utilitats/ # classes.js, validarReserva.js, monitoritzacio.js, disponibilitat.js │ ├── configuracio.js # lectura única d'import.meta.env │ ├── rutes.jsx # createBrowserRouter: el mapa complet │ ├── index.css # reinici + variables :root + tema fosc │ └── main.jsx # composició de proveïdors ├── .editorconfig ├── .env.example ├── .gitignore ├── .prettierrc ├── DECISIONS.md # l'acta de l'apartat 8 ├── db.json # base de dades de desenvolupament i llavor de les proves ├── eslint.config.js ├── package.json ├── README.md └── vite.config.js
Dues regles de col·locació que eviten la majoria de les discussions:
- Un fitxer de prova viu al costat del codi que prova (
TargetaBicicleta.jsxiTargetaBicicleta.test.jsxa la mateixa carpeta). Només la infraestructura de proves és asrc/proves/. Així, en esborrar un component, la seva prova se'n va amb ell. - Un component puja a
components/quan l'usa la segona pantalla, no abans. Fins llavors viu al costat de la seva pàgina. Generalitzar amb un sol cas d'ús produeix abstraccions equivocades.
- L'API de desenvolupament:
json-server en marxa
json-server en marxa \{^_^}/ hi!
Loading db.json
Done
Resources
http://localhost:3001/bicicletas
http://localhost:3001/estaciones
http://localhost:3001/usuarios
http://localhost:3001/reservasComprovació obligatòria abans de seguir. Si això no respon, cap pantalla d'11-03 funcionarà i perdràs una hora buscant l'error a React:
# Llista completa
curl http://localhost:3001/bicicletas
# Filtre per camp: json-server el dona gratis
curl "http://localhost:3001/bicicletas?tipo=electrica"
# Un recurs concret
curl http://localhost:3001/bicicletas/bici-003
# Crear (el que farà H5)
curl -X POST http://localhost:3001/reservas \
-H "Content-Type: application/json" \
-d '{"id":"res-99","bicicletaId":"bici-001","usuari":"usr-01","dataInici":"2026-06-01T10:00","hores":2,"estat":"activa"}'
# I desfer l'experiment
curl -X DELETE http://localhost:3001/reservas/res-99El que json-server dona de sèrie i el projecte aprofitarà:
| Capacitat | Exemple | S'usa a |
|---|---|---|
| Llistat | GET /bicicletas |
H1 |
| Detall | GET /bicicletas/bici-001 |
H3 |
| Filtre per camp | GET /bicicletas?tipo=urbana |
H2 |
| Filtre per relació | GET /reservas?usuari=usr-01 |
H6 |
| Creació | POST /reservas |
H5 |
| Modificació parcial | PATCH /bicicletas/bici-001 |
H7 |
| 404 real | GET /bicicletas/no-existeix |
H3 |
I el que no dona, i cal tenir present des d'ara: no valida res, no autentica ningú i no comprova permisos. Un PATCH des de la consola del navegador canvia l'estat de qualsevol bicicleta sense sessió. És acceptable en desenvolupament i és la raó per la qual 11-05 insisteix que l'autorització de veritat es comprova sempre al servidor.
- Convencions de l'equip
Escrites al README.md, perquè una convenció que només és al cap de qui la va inventar no és una convenció.
Noms
| Element | Convenció | Exemple |
|---|---|---|
| Component | PascalCase, fitxer .jsx igual que el component |
TargetaBicicleta.jsx |
| Pàgina | Prefix Pagina |
PaginaNovaReserva.jsx |
| Hook | use + camelCase |
useMagatzemLocal.js |
| Utilitat | camelCase, exportació amb nom |
validarReserva.js |
| Estils | Component.module.css al costat del component |
TargetaBicicleta.module.css |
| Gestor intern | gestionarX |
gestionarEnviar |
| Prop de callback | alX |
alSeleccionar |
| Acció de Redux | Substantiu en passat | reservaConfirmada |
| Selector | seleccionarX |
seleccionarUsuari |
| Fitxers | Sense accents ni ç | Capcalera.jsx, no Capçalera.jsx |
Exportacions: export default per a components i pàgines; exportació amb nom per a hooks, utilitats, slices i selectors. Barrejar els dos criteris en el mateix tipus de fitxer és el que produeix importacions inconsistents.
Ordre d'importacions, sempre el mateix, amb línia en blanc entre grups:
// 1. React i biblioteques externes
import { useState } from 'react';
import { useNavigate } from 'react-router';
import { useSelector } from 'react-redux';
// 2. Mòduls propis, del més general al més concret
import { useCrearReserva } from '../consultes/reservas.js';
import { validarReserva } from '../utilitats/validarReserva.js';
import FormulariReserva from '../components/FormulariReserva.jsx';
// 3. Estils, sempre al final
import estils from './PaginaNovaReserva.module.css';Missatges de commit, en format convencional, en català i en imperatiu:
feat(cataleg): filtrar bicicletes per tipus des de la URL fix(reserves): impedir reservar una bicicleta en manteniment test(formulari): cobrir els missatges de validació accessibles refactor(api): extreure l'embolcall de fetch a src/api/client.js docs(readme): documentar l'arrencada amb dev:todo chore(deps): actualitzar vitest a la 2.1
El prefix no és decoració: permet generar el registre de canvis automàticament i, sobretot, obliga que un commit faci una sola cosa. Si dubtes entre feat i fix, probablement el commit contingui dos canvis i calgui partir-lo.
- El pla de treball: increments verticals
Aquí hi ha la decisió de mètode que més influeix en com se sent el projecte mentre es construeix.
| Per capes (horitzontal) | Per increments verticals | |
|---|---|---|
| Ordre de treball | Tots els components → tota l'API → totes les proves | Una història completa de punta a punta, després la següent |
| Primera demo possible | Al final | En acabar la primera història |
| Risc detectat | Tard, quan tot està escrit | Aviat, a la primera integració |
| Sensació d'avenç | Nul·la durant setmanes | Contínua |
| Risc real | Descobrir a l'última setmana que el model no encaixa | Refactoritzar alguna cosa del que s'ha fet en arribar la tercera història |
Es tria vertical, amb una salvetat honesta: aquest mòdul està organitzat per capes —interfície, estat, proves, desplegament— perquè explicar requereix agrupar conceptes, mentre que construir requereix entregar valor aviat. És una diferència important i convé tenir-la clara: l'ordre del llibre no és l'ordre del taller.
Tot i així, dins de cada lliçó es treballa per històries completes. I si estiguessis fent aquest projecte en un equip real, l'ordre seria aquest:
flowchart LR
subgraph L1["11-01 · Bastiment"]
A1["Producte i acta"] --> A2["Vite, linter, entorn"] --> A3["db.json + API viva"]
end
subgraph L2["11-02 · Interfície"]
B1["Sistema de disseny"] --> B2["Disseny + rutes"] --> B3["Pantalles amb dades estàtiques"]
end
subgraph L3["11-03 · Estat i API"]
C1["Capa de dades"] --> C2["Query + mutacions"] --> C3["Redux, context, URL"]
end
subgraph L4["11-04 · Proves"]
D1["Unitàries"] --> D2["Components"] --> D3["Integració"] --> D4["E2E + CI"]
end
subgraph L5["11-05 · Producció"]
E1["build i entorns"] --> E2["Desplegament"] --> E3["Monitorització"]
end
L1 --> L2 --> L3 --> L4 --> L5
I l'ordre de les històries, prioritzat per risc primer:
- H4 (accés) abans que res: és la porta de tota la resta i toca sessió, formulari, redirecció i persistència. Si alguna cosa ha de sortir malament en l'arquitectura, surt malament aquí.
- H1 + H2 (catàleg i filtre): la pantalla més visitada, i la que valida la capa de dades.
- H5 (reservar): el camí dels diners. Toca mutació, validació, invalidació i navegació.
- H6 (les meves reserves i cancel·lar): reutilitza gairebé tot l'anterior.
- H3, H8 (fitxa i estacions): lectura pura, poc risc.
- H7 (taller): el rol es prova amb tota la resta ja en peu.
- D1-D4 si queda temps.
La regla que governa la llista: el que pot enfonsar el projecte va primer. Deixar la identificació per al final és l'error clàssic, perquè és justament la funcionalitat que travessa totes les capes i obliga a refer el que ja s'havia donat per bo.
- El primer commit
Un commit inicial ha de complir una condició: qui el cloni, el pot arrencar.
git init
git add .
git commit -m "chore: bastiment inicial de CicloUrbano amb Vite, linter i API de desenvolupament"I el README.md que l'acompanya, que és el primer que llegeix qualsevol:
# CicloUrbano Aplicació web de lloguer de bicicletes urbanes per estacions. ## Requisits - Node.js 20 o superior ## Arrencada
npm install cp .env.example .env npm run dev:todo # Vite a :5173 i json-server a :3001
## Scripts | Comanda | Què fa | |---|---| | `npm run dev:todo` | Aplicació i API en paral·lel | | `npm run lint` | ESLint sense tolerància a avisos | | `npm test` | Vitest en mode vigilància | | `npm run e2e` | Cypress sobre l'aplicació aixecada | | `npm run build` | Construcció de producció a `dist/` | ## Documentació - `DECISIONS.md`: acta de decisions d'arquitectura - `db.json`: dades de desenvolupament i llavor de les proves
La llista de comprovació abans de donar el bastiment per acabat:
- [ ]
npm installfunciona en un clon net - [ ]
npm run devserveix l'aplicació al 5173 - [ ]
npm run apirespon al 3001 amb els quatre recursos - [ ]
npm run lintacaba sense errors ni avisos - [ ]
.envestà ignorat i.env.exampleversionat - [ ]
DECISIONS.mdté les deu files de l'acta - [ ] El
README.mdpermet arrencar sense preguntar res a ningú
Errors Comuns i Consells
- Començar pel codi i planificar sobre la marxa. És l'error de fons d'aquesta lliçó. Sense històries amb criteris d'acceptació no saps quan has acabat, i sense acta de decisions cada discussió tècnica es repeteix cada dues setmanes. Mitja jornada de planificació estalvia setmanes.
- Històries sense criteris comprovables. «El catàleg ha de ser ràpid» no es pot verificar ni convertir en prova. «El catàleg mostra l'esquelet mentre carrega i la llista tan bon punt arriben les dades» sí.
- No escriure el que queda fora. L'abast que no es nega explícitament s'assumeix inclòs. La taula de l'apartat 4 és la que protegeix l'entrega.
- Imbricar objectes en el model de dades. Guardar la bicicleta sencera dins de la reserva sembla còmode el primer dia i produeix dades desincronitzades el segon. Identificadors, sempre.
- Un joc de dades de desenvolupament sense casos límit. Si totes les bicicletes estan disponibles i tots els usuaris tenen reserves, mai no veuràs l'estat buit ni el botó deshabilitat, i arribaran trencats a producció.
- Posar eines de desenvolupament a
dependencies. Cypress ijson-serveren producció són un símptoma que ningú no ha mirat elpackage.jsondes que es va generar. - Deixar
exhaustive-depscom a avís. Amb el temps se n'acumulen desenes i deixen de llegir-se. Com a error, es resol o es justifica amb un comentari visible. - Posar
eslint-config-prettieren qualsevol posició menys l'última. Deixa de fer efecte i acabes amb el linter i el formatador en conflicte a cada desada. - Secrets en variables
VITE_. Tot el que comença perVITE_viatja al navegador en text pla. No hi ha excepció, ni «només mentre provem». - Consell: crea
DECISIONS.mddes del primer dia i afegeix una fila cada vegada que discuteixis una elecció tècnica. El valor no és al document, és a no tornar a tenir la mateixa conversa. - Consell: verifica l'API amb
curlabans d'escriure el primerfetch. Descartar la meitat del sistema en trenta segons estalvia depuracions llarguíssimes.
Exercicis
Exercici 1. El client demana una història nova: «com a client vull rebre un avís 10 minuts abans que acabi la meva reserva». Escriu-la amb el format de l'apartat 3, amb criteris d'acceptació comprovables. Després decideix si és imprescindible o desitjable, i si entra o no a la primera versió, justificant-ho amb l'abast de l'apartat 1 i la taula de l'apartat 4. Si decideixes deixar-la fora, redacta la fila corresponent.
Exercici 2. L'equip proposa afegir una entitat Incidencia per a la pestanya d'incidències de /estaciones/:estacionId, amb la informació d'una avaria reportada. Defineix els seus camps i les seves relacions, afegeix-la al diagrama d'entitat-relació i al db.json amb dos registres d'exemple coherents amb el cànon, i explica quins recursos nous apareixerien a json-server. Indica a més quina clau de TanStack Query li correspondria segons la fàbrica del projecte.
Exercici 3. Un company proposa tres canvis sobre l'acta: (a) usar BrowserRouter amb <Routes> perquè «és més senzill», (b) guardar la llista de bicicletes a Redux «per tenir-ho tot en un lloc», i (c) posar la clau d'un servei de mapes a VITE_CLAU_MAPES per usar-la des del client. Respon a cadascun amb l'argument tècnic corresponent i digues quina fila de l'acta ho cobreix. Per al tercer, explica a més què caldria per usar aquest servei sense exposar la clau.
Solucions
Solució 1.
| ID | Història | Criteris d'acceptació |
|---|---|---|
| D5 | Com a client vull rebre un avís 10 minuts abans que acabi la meva reserva per tornar la bicicleta a temps | Amb una reserva activa o confirmada l'hora de fi de la qual estigui a menys de 10 minuts, es mostra un avís persistent amb el temps restant i un enllaç a la reserva · L'avís desapareix en finalitzar la reserva o en cancel·lar-la · Si hi ha diverses reserves properes a vèncer, es mostra la més propera · El càlcul usa dataInici + hores i s'actualitza cada minut |
Classificació: desitjable (D5), fora de la primera versió. Els tres arguments:
- No bloqueja l'objectiu principal de cap de les dues persones. Ana pot reservar i usar la bicicleta sense l'avís; Marc no el necessita en absolut.
- L'avís realment útil és el que arriba amb l'aplicació tancada, i això són notificacions push, que estan explícitament fora d'abast per exigir servidor, permisos i service worker. Un avís que només es veu amb la pestanya oberta resol una fracció petita del problema real i pot donar una falsa sensació de cobertura.
- Requereix un temporitzador global que reavaluï cada minut i una font de veritat de l'hora actual, cosa que complica les proves (cal fixar el rellotge) per a un benefici marginal en aquesta versió.
Fila per a la taula de l'apartat 4:
| Fora d'abast | Motiu |
|---|---|
| Avisos de fi de reserva | La versió valuosa exigeix notificacions push, que ja estan fora d'abast. La variant dins de la pestanya cobreix pocs casos reals i afegeix un temporitzador global difícil de provar. Es reconsidera quan hi hagi servidor propi (11-05) |
I l'anotació honesta: quan arribi l'API real, aquesta història puja a imprescindible, perquè el negoci penalitza la devolució tardana i avisar és més barat que cobrar recàrrecs.
Solució 2.
Camps i relacions. Una incidència pertany a una bicicleta (que al seu torn és en una estació) i la reporta un usuari:
erDiagram
BICICLETA ||--o{ INCIDENCIA : "acumula"
USUARI ||--o{ INCIDENCIA : "reporta"
INCIDENCIA {
string id PK "inc-01"
string bicicletaId FK "bici-003"
string reportadaPer FK "usr-01"
string data "2026-05-02T18:30"
string tipus "frens | roda | bateria | altre"
string descripcio "El fre del darrere patina"
string estat "oberta | en_curs | resolta"
}
Registres per a db.json, coherents amb el cànon —bici-003 és en manteniment, així que és la candidata natural a tenir una incidència oberta, i bici-002 pot tenir-ne una ja resolta:
{
"incidencias": [
{
"id": "inc-01",
"bicicletaId": "bici-003",
"reportadaPer": "usr-01",
"data": "2026-05-02T18:30",
"tipus": "frens",
"descripcio": "El fre del darrere patina amb càrrega.",
"estat": "oberta"
},
{
"id": "inc-02",
"bicicletaId": "bici-002",
"reportadaPer": "usr-02",
"data": "2026-04-28T09:15",
"tipus": "bateria",
"descripcio": "La bateria no arribava al 60 % d'autonomia anunciada.",
"estat": "resolta"
}
]
}Decisió de modelatge important: la incidència s'associa a la bicicleta, no a l'estació, encara que la pestanya que la mostra sigui la d'una estació. El motiu és que una avaria viatja amb la bicicleta: si bici-003 es trasllada a est-03, el seu historial ha d'anar amb ella. La pestanya d'incidències d'una estació s'obté llavors derivant: les bicicletes d'aquesta estació, i les incidències d'aquestes bicicletes. Modelar la relació al revés obligaria a reescriure l'estacionId de cada incidència a cada trasllat.
Recursos que apareixen a json-server:
| Petició | Ús |
|---|---|
GET /incidencias |
Totes |
GET /incidencias?bicicletaId=bici-003 |
Historial d'una bicicleta |
GET /incidencias?estat=oberta |
Cua de treball del taller (H7) |
POST /incidencias |
Reportar-ne una de nova |
PATCH /incidencias/inc-01 |
Canviar-ne l'estat |
I la clau de consulta, aprofitant que la fàbrica del projecte ja l'havia previst:
// src/consultes/claus.js
estaciones: {
totes: () => ['estaciones'],
detall: (id) => ['estaciones', id],
incidencias: (id) => ['estaciones', id, 'incidencias'] // ja existia
},
incidencias: {
totes: () => ['incidencias'],
deBicicleta: (bicicletaId) => ['incidencias', { bicicleta: bicicletaId }]
}La jerarquia importa: invalidar ['estaciones', 'est-02'] invalida també ['estaciones', 'est-02', 'incidencias'], perquè TanStack Query compara les claus per prefix. És justament el comportament que es vol en resoldre una incidència.
Solució 3.
(a) BrowserRouter amb <Routes>. Cobert per la fila A2. És cert que és més senzill d'escriure, però el projecte necessita tres coses que aquest mode no dona:
errorElementper branca: sense ell, un error en carregar la fitxa d'una bicicleta enfonsa tota l'aplicació en lloc de deixar la capçalera i el menú drets. És la diferència entre una pantalla en blanc i un error contingut.lazya nivell de ruta amb l'enrutador gestionant la càrrega, que és el que fa possible la divisió de codi de 08-04 sense embolicar cada pantalla a mà.- L'API de dades (
useNavigation,useRouteError,handleper a les molles de pa), que ja s'usa aMollesDePa.
I l'argument decisiu: migrar després costa més que començar bé, perquè implica reescriure l'arbre de rutes sencer quan ja hi ha pantalles al damunt.
(b) La llista de bicicletes a Redux. Cobert per A3 i per la taula d'assignació d'estat. «Tenir-ho tot en un lloc» sona a ordre, però barreja dues coses de naturalesa diferent: les bicicletes són estat del servidor, una còpia local d'una dada que viu en una altra màquina i que pot canviar sense que l'aplicació se n'assabenti. Ficar-les a Redux obliga a escriure a mà, i a mantenir, tot això:
| Necessitat | Amb TanStack Query | Amb Redux a mà |
|---|---|---|
| Memòria cau per clau | Inclosa | slice amb estructura pròpia |
| Deduplicació de peticions simultànies | Inclosa | condition al thunk |
| Estats de càrrega i error | isPending, isError |
Tres camps per recurs a extraReducers |
| Revalidació en tornar a la pestanya | refetchOnWindowFocus |
Efecte propi amb visibilitychange |
| Dades obsoletes i refresc en segon pla | staleTime |
No existeix: o hi ha dada o no n'hi ha |
| Invalidar després d'una mutació | invalidateQueries |
Despatxar i recarregar a mà |
És exactament la feina que 07-06 va mostrar que no val la pena reescriure. Redux es queda amb el que sí que és seu: sessió, terme de cerca i ordre.
(c) La clau del servei de mapes a VITE_CLAU_MAPES. És el més greu dels tres. Tot el que comença per VITE_ se substitueix literalment al codi durant la construcció i acaba en un fitxer de dist/ que qualsevol pot obrir:
No hi ha ofuscació que ho arregli: el navegador ha de poder llegir-la per usar-la, per tant l'usuari també. Les conseqüències són consum facturat al teu compte i, segons el servei, accés a dades que no haurien de sortir.
Què fer en el seu lloc, per ordre de preferència:
- Que la petició al servei surti del servidor. El client crida la teva API, la teva API crida el servei amb la clau i retorna el resultat. La clau mai no surt de la màquina.
- Si el servei està pensat per al client —molts proveïdors de mapes ofereixen claus públiques—, usar aquest tipus de clau i restringir-la al tauler del proveïdor per domini d'origen, per quota i per permisos mínims. Continua sent visible, però només funciona des del teu domini i amb un sostre de despesa.
- Mai usar una clau amb permisos d'escriptura o de facturació des del client, en cap circumstància.
I com que l'exercici parteix d'una història que està fora d'abast —el mapa geogràfic—, la resposta completa inclou recordar-ho: no cal resoldre el problema de la clau encara, perquè la funcionalitat no entra en aquesta versió.
Conclusió
Aquesta lliçó ha convertit una idea en un projecte que es pot començar a construir, i ho ha fet en l'ordre correcte: primer el producte, després les decisions, i només al final les eines.
Del producte queda fixat l'essencial: CicloUrbano en una frase que ja decideix l'arquitectura en declarar-se privada, dues persones usuàries amb objectius i contextos diferents que justifiquen per què l'inici és el catàleg i el taller una pantalla a part, vuit històries imprescindibles i quatre desitjables amb criteris d'acceptació comprovables —que a 11-04 es convertiran literalment en proves—, i una llista explícita del que queda fora amb el seu motiu, que és la que protegeix l'entrega quan algú proposi afegir un mapa a mig camí.
Del domini queda el model definitiu amb les seves quatre entitats i tres regles que es respecten sense excepció: relacions per identificador i mai objectes imbricats, estats com a cadena d'un conjunt tancat en lloc de booleans, i les incoherències heretades anotades com a deute en comptes d'arreglades a destemps. El db.json no és un munt de dades de farciment: cada estat, cada cas límit i l'estat buit tenen el seu representant, perquè el que no es veu en desenvolupament no es dissenya.
Del rumb tècnic queda l'acta de decisions, deu files amb la seva justificació i la seva alternativa descartada: Vite en lloc de Next.js perquè l'aplicació és privada i interactiva; React Router en mode de dades per errorElement, lazy i l'API de dades; TanStack Query per al servidor i Redux Toolkit per al client, amb context per a tema i avisos i la URL per al filtre; CSS Modules; el quartet Vitest, Testing Library, MSW i Cypress; i JavaScript ara amb la porta oberta a TypeScript. Junt amb l'acta, la taula d'assignació d'estat que respon per endavant a la pregunta que més vegades es repeteix en un projecte de React: on viu aquesta dada?
I del bastiment queda un repositori funcionant: Vite amb React 19, dependències triades una a una i ben separades entre producció i desenvolupament, un package.json amb catorze scripts que són la interfície del projecte, ESLint amb jsx-a11y i exhaustive-deps elevat a error, Prettier l'últim de la cadena, .editorconfig, .gitignore, variables d'entorn centralitzades a configuracio.js amb .env.example versionat i cap secret, una estructura de carpetes híbrida —per tipus el que és compartit, per funcionalitat l'estat de domini—, json-server sembrat i verificat amb curl, les convencions de l'equip escrites al README.md, i un pla de treball per increments verticals que ataca primer el que té més risc.
Curs de React
Mòdul 1: Introducció a React
- Què és React?
- Configuració de l'Entorn de Desenvolupament
- Hola Món amb React
- JSX: Extensió de Sintaxi de JavaScript
- Com Renderitza React: Virtual DOM i Reconciliació
Mòdul 2: Components de React
- Entendre els Components
- Components Funcionals vs de Classe
- Props: Passar Dades als Components
- State: Gestió de l'Estat del Component
- Estils en els Components: CSS, Mòduls i Utilitats
Mòdul 3: Treballar amb Esdeveniments
- Gestió d'Esdeveniments a React
- Renderitzat Condicional
- Llistes i Claus
- Formularis i Components Controlats
- Validació de Formularis i Components No Controlats
- Accessibilitat en Components Interactius
Mòdul 4: Conceptes Avançats de Components
- Elevar l'Estat
- Composició vs Herència
- Mètodes del Cicle de Vida de React
- Hooks: Introducció i Ús Bàsic
- Límits d'Error: Capturar Fallades a la Interfície
Mòdul 5: Hooks de React
- Hook useState
- Hook useEffect
- Hook useRef i Accés al DOM
- Hook useContext
- Hook useReducer
- Hooks Personalitzats
Mòdul 6: Enrutament a React
- Introducció a React Router
- Configuració de React Router
- Rutes Imbricades
- Navegació Programàtica
- Rutes Protegides i Control d'Accés
Mòdul 7: Gestió de l'Estat
- Introducció a la Gestió de l'Estat
- API de Context
- Redux: Introducció i Configuració
- Redux: Accions i Reductors
- Redux: Connectar-lo a React
- Estat del Servidor: Peticions, Memòria Cau i Sincronització
Mòdul 8: Optimització del Rendiment
- Tècniques d'Optimització del Rendiment a React
- Memoïtzació amb React.memo
- Hooks useMemo i useCallback
- Divisió de Codi i Càrrega Mandrosa
- Mesurar el Rendiment amb React DevTools Profiler
Mòdul 9: Proves a React
- Introducció a les Proves
- Proves Unitàries amb Jest
- Proves de Components amb React Testing Library
- Proves de Codi Asíncron i Simulació d'APIs
- Proves d'Extrem a Extrem amb Cypress
Mòdul 10: Temes Avançats
- Renderitzat al Servidor (SSR) amb Next.js
- Generació de Llocs Estàtics (SSG) amb Next.js
- Suspense i React Server Components
- TypeScript amb React
- React Native: Creació d'Aplicacions Mòbils
