El FormulariReserva de la lliçó anterior funciona, però accepta qualsevol cosa: es pot enviar sense triar bicicleta, amb una data de l'any passat, amb zero hores o sense acceptar les condicions. Un formulari que no valida no està acabat. Aquesta lliçó completa l'altra meitat de la història amb dos temes que van agafats de la mà: els components no controlats, l'alternativa en què el valor el guarda el DOM i només es llegeix en enviar, i la validació, tant la que ofereix el navegador de sèrie com la que escrius tu en JavaScript. Aprendràs a decidir quan validar sense resultar molest —l'equilibri entre avisar aviat i no cridar a qui encara està escrivint—, a escriure la validació com una funció pura fàcil de provar i a mostrar els errors on importa: al costat del camp que els provoca.
Contingut
- Les dues maneres de guardar el valor d'un camp
- Components no controlats: el DOM mana
defaultValueidefaultChecked- Llegir els valors amb
FormData useRefcom a alternativa- Comparativa: controlat enfront de no controlat
- El cas especial de
<input type="file"> - Validació nativa del navegador
- Validació en JavaScript: una funció pura
- Quan validar i el concepte de camp «tocat»
- Mostrar els errors i bloquejar l'enviament
- CicloUrbano:
FormulariReservavalidat - Biblioteques de formularis i esquemes
- Les dues maneres de guardar el valor d'un camp
Només hi ha dues respostes possibles a la pregunta «on viu el que la persona usuària ha escrit?».
| Enfocament | Qui guarda el valor | Com es llegeix | Com es posa un valor inicial |
|---|---|---|---|
| Controlat | L'estat de React | De l'estat, sempre disponible | useState(valorInicial) |
| No controlat | El node del DOM | En llegir-lo explícitament (FormData o ref) |
defaultValue / defaultChecked |
Ja domines el primer. El segon no és «la forma antiga» ni una mala pràctica: és la forma nativa d'HTML, i en certs casos és l'elecció correcta. La clau és entendre què es guanya i què es perd amb cadascun.
flowchart TD
A["Algú escriu en un camp"] --> B{"Controlat?"}
B -- "Sí" --> C["onChange actualitza l'estat"] --> D["Render nou"] --> E["El camp mostra l'estat"]
B -- "No" --> F["El DOM guarda el valor<br/>React no se n'assabenta"] --> G["Es llegeix només en enviar"]
- Components no controlats: el DOM mana
Un camp no controlat és simplement un camp sense value ni onChange: es comporta com en l'HTML de tota la vida.
function FormulariSenzill() {
function gestionarEnviament(esdeveniment) {
esdeveniment.preventDefault();
// El valor es llegeix ARA, no abans
const dades = new FormData(esdeveniment.target);
console.log(dades.get('model'));
}
return (
<form onSubmit={gestionarEnviament}>
<label htmlFor="model">Model</label>
<input id="model" name="model" type="text" />
<button type="submit">Desar</button>
</form>
);
}Fixa't en el que no hi ha: ni useState, ni onChange, ni value. El component no es torna a renderitzar en escriure, perquè no canvia res a React.
I fixa't en el que sí que hi ha i ara és imprescindible: l'atribut name a cada camp. En un formulari controlat, el name era una comoditat per al gestor genèric; aquí és l'única manera d'identificar el camp en llegir-lo.
El que es guanya:
- Menys codi. Un formulari de deu camps no necessita deu entrades d'estat.
- Zero renders en escriure. En formularis enormes es pot notar.
- Interoperabilitat. Encaixa amb codi no-React i amb biblioteques de tercers que manipulen el DOM.
El que es perd:
- No pots reaccionar al que s'escriu. Res de validació en temps real, comptadors de caràcters, previsualitzacions ni transformacions al vol.
- No pots deshabilitar el botó fins que el formulari sigui vàlid, perquè no saps què conté.
- Reiniciar i emplenar de manera programàtica exigeix tocar el DOM.
defaultValue i defaultChecked
defaultValue i defaultCheckedUn camp no controlat també pot començar amb un valor. Però no s'utilitza value: això el convertiria en controlat (i provocaria l'avís del camp congelat que vas veure a 03-04).
{/* ✔ Valor inicial d'un camp NO controlat */}
<input name="hores" type="number" defaultValue={2} />
<textarea name="observacions" defaultValue="Sense observacions" />
<select name="tipus" defaultValue="urbana">
<option value="urbana">Urbana</option>
<option value="electrica">Elèctrica</option>
</select>
{/* Per a caselles i ràdios, defaultChecked */}
<input name="condicions" type="checkbox" defaultChecked />
<input name="estat" type="radio" value="disponible" defaultChecked />| Prop | Camp controlat | Camp no controlat |
|---|---|---|
| Text, àrea, desplegable | value (+ onChange) |
defaultValue |
| Casella i ràdio | checked (+ onChange) |
defaultChecked |
Hi ha un detall que sorprèn: defaultValue només s'aplica en el primer render. Si més endavant canvies aquesta prop, el camp no s'actualitza, perquè el seu valor ja el gestiona el DOM. Si necessites canviar el valor des de fora després de muntar el component, aquest camp ha de ser controlat.
- Llegir els valors amb
FormData
FormDataFormData és una API del navegador —no de React— que recull tots els camps d'un formulari a partir del seu atribut name. És la forma més neta de llegir un formulari no controlat.
// src/components/FormulariIncidencia.jsx
/**
* Part d'incidència d'una bicicleta, amb camps NO controlats.
* Props:
* - bicicletes (array, opcional, per defecte [])
* - alRegistrar (funció, opcional): rep l'objecte d'incidència
*/
function FormulariIncidencia({ bicicletes = [], alRegistrar }) {
function gestionarEnviament(esdeveniment) {
esdeveniment.preventDefault();
// esdeveniment.target és el <form>; FormData recull tots els seus camps amb name
const formulari = esdeveniment.target;
const dades = new FormData(formulari);
const incidencia = {
bicicletaId: dades.get('bicicletaId'),
descripcio: dades.get('descripcio'),
// get() SEMPRE retorna cadena (o null): cal convertir
urgencia: Number(dades.get('urgencia')),
// Una casella sense marcar NO apareix a FormData: get() retorna null
bloquejaUs: dades.get('bloquejaUs') === 'on'
};
if (alRegistrar) {
alRegistrar(incidencia);
}
formulari.reset(); // en un formulari NO controlat, reset() sí que és correcte
}
return (
<form onSubmit={gestionarEnviament}>
<label htmlFor="bicicletaId">Bicicleta</label>
<select id="bicicletaId" name="bicicletaId" defaultValue="">
<option value="">— Tria una bicicleta —</option>
{bicicletes.map((bicicleta) => (
<option key={bicicleta.id} value={bicicleta.id}>
{bicicleta.model} ({bicicleta.id})
</option>
))}
</select>
<label htmlFor="descripcio">Descripció</label>
<textarea id="descripcio" name="descripcio" rows={3} />
<label htmlFor="urgencia">Urgència (1-5)</label>
<input id="urgencia" name="urgencia" type="number" min={1} max={5} defaultValue={3} />
<label>
<input name="bloquejaUs" type="checkbox" /> Impedeix l'ús de la bicicleta
</label>
<button type="submit">Registrar incidència</button>
</form>
);
}
export default FormulariIncidencia;Tres detalls de FormData que causen errors si no es coneixen:
| Comportament | Conseqüència |
|---|---|
get() retorna cadena o null |
Els nombres cal convertir-los amb Number() |
| Una casella sense marcar no apareix | get('bloquejaUs') retorna null, no false |
Només recull camps amb name |
Un camp amb id però sense name es queda fora, en silenci |
I dues dreceres molt pràctiques:
// Convertir tot el formulari en un objecte pla d'una sola línia
const objecte = Object.fromEntries(new FormData(formulari));
// Recollir tots els valors d'un camp repetit (caselles amb el mateix name)
const tipus = new FormData(formulari).getAll('tipus'); // array de cadenesAquí sí que és correcte formulari.reset(): com que el valor el guarda el DOM, buidar-lo és exactament el que cal fer. En un formulari controlat seria inútil, perquè React tornaria a posar els valors de l'estat.
useRef com a alternativa
useRef com a alternativaHi ha una altra manera de llegir un camp no controlat: guardar una referència al node del DOM.
import { useRef } from 'react';
function CercadorRapid({ alBuscar }) {
const campCerca = useRef(null); // comença buit
function gestionarEnviament(esdeveniment) {
esdeveniment.preventDefault();
alBuscar(campCerca.current.value); // accés directe al node
}
return (
<form onSubmit={gestionarEnviament}>
<input ref={campCerca} type="text" name="cerca" />
<button type="submit">Cercar</button>
</form>
);
}useRef crea un objecte amb una propietat current; en passar-lo com a prop ref d'un element, React hi col·loca el node real del DOM. A partir d'aquest moment, campCerca.current és l'<input>, amb tots els seus mètodes i propietats.
Quan FormData i quan useRef?
| Necessitat | Eina |
|---|---|
| Llegir tots els camps en enviar | FormData: menys codi, sense referències que mantenir |
| Llegir un únic camp solt | Qualsevol de les dues |
| Donar el focus a un camp, seleccionar-ne el text, fer scroll fins a ell | useRef: són accions sobre el node, no lectures |
| Integrar una biblioteca externa que necessita el node | useRef |
useRef és molt més que una manera de llegir camps: és la via d'escapament controlada de React cap al DOM, i també serveix per guardar valors que persisteixen entre renders sense provocar renders. S'estudia a fons a Hook useRef i Accés al DOM; aquí n'hi ha prou que sàpigues que existeix i per a què es fa servir en formularis.
- Comparativa: controlat enfront de no controlat
| Criteri | Controlat | No controlat |
|---|---|---|
| On viu el valor | Estat de React | Node del DOM |
| Props del camp | value + onChange |
defaultValue (res més) |
| Com es llegeix | De l'estat, en qualsevol moment | FormData o ref, en llegir-lo |
| Renders en escriure | Un per pulsació | Cap |
| Validació en temps real | Sí | No |
| Deshabilitar l'enviament si no és vàlid | Sí | No (només amb validació nativa) |
| Transformar mentre s'escriu | Sí | No |
| Canviar el valor des de fora | Sí | No sense tocar el DOM |
| Quantitat de codi | Més | Menys |
| Facilitat per provar | Alta: n'hi ha prou amb l'estat | Mitjana: cal simular el DOM |
| Camps de fitxer | Impossible | Obligatori |
Criteris per triar, en ordre d'importància:
- Necessites reaccionar al que s'escriu? Validació al vol, comptador de caràcters, botó que s'habilita, previsualització, camps que depenen d'altres. → Controlat.
- És un camp de fitxer? → No controlat, obligatòriament (apartat 7).
- És un formulari gran i senzill, que només es llegeix en enviar? Una alta de vint camps sense interdependències. → No controlat és una opció raonable.
- S'integra amb codi extern que toca el DOM? → No controlat.
- En cas de dubte → Controlat. És el valor per defecte de React i el que deixa la porta oberta a afegir comportament més endavant.
A CicloUrbano seguirem amb controlats per al FormulariReserva, precisament perquè volem validar mentre s'emplena.
- El cas especial de
<input type="file">
<input type="file">Els camps de fitxer són l'única excepció absoluta: sempre són no controlats, sense alternativa.
function PujadaFoto({ alSeleccionarFoto }) {
function gestionarCanvi(esdeveniment) {
const fitxer = esdeveniment.target.files[0]; // FileList, no una cadena
if (!fitxer) {
return;
}
console.log(fitxer.name, fitxer.size, fitxer.type);
alSeleccionarFoto(fitxer);
}
return (
<label>
Foto del desperfecte:{' '}
<input type="file" accept="image/*" onChange={gestionarCanvi} />
</label>
);
}El motiu és de seguretat: si React pogués fixar el value d'un camp de fitxer, una pàgina maliciosa podria escriure-hi una ruta del disc i pujar un arxiu sense que ningú l'hagués triat. Per això el navegador prohibeix assignar aquest valor per codi: només la persona usuària el pot canviar triant un fitxer.
Les conseqüències pràctiques:
- No posis
valueen un<input type="file">. React avisarà. - El valor es llegeix de
esdeveniment.target.files, un objecteFileListsemblant a un array. Ambmultiple, conté diversos fitxers:Array.from(esdeveniment.target.files). - Sí que pots fer servir
onChangeper reaccionar a la selecció: això no el converteix en controlat, perquè no imposes el valor. - Per buidar-lo, la via habitual és reiniciar el formulari o canviar la
keyde l'element perquè React el recreï.
- Validació nativa del navegador
HTML porta validació de sèrie, sense una línia de JavaScript. És gratis i convé aprofitar-la.
<form onSubmit={gestionarEnviament}>
<input name="email" type="email" required />
<input name="hores" type="number" min={1} max={24} step={1} required />
<input name="codi" type="text" pattern="[A-Z]{3}-[0-9]{3}" title="Format: ABC-123" />
<input name="dataInici" type="datetime-local" required />
<button type="submit">Enviar</button>
</form>| Atribut | Què comprova |
|---|---|
required |
Que el camp no estigui buit (o marcat, en una casella) |
type="email" |
Que el text tingui forma d'adreça de correu |
type="url" |
Que el text tingui forma d'URL |
min / max |
Rang en nombres i dates |
step |
Increments vàlids: step={0.5}, step={1} |
minLength / maxLength |
Longitud del text |
pattern |
Expressió regular que ha de complir el valor |
title |
Text d'ajuda que el navegador mostra quan falla pattern |
Si algun camp no compleix, el navegador bloqueja l'enviament —l'onSubmit ni tan sols s'executa— i mostra un globus amb un missatge.
Els seus límits, que són reals
| Límit | Detall |
|---|---|
| Missatges incontrolables | El text i l'idioma els posa el navegador; l'estil del globus no es pot canviar |
| Aspecte inconsistent | Cada navegador el mostra de manera diferent |
| Només un error alhora | Mostra el primer camp no vàlid, no tots |
| No cobreix regles de negoci | «La data no pot ser anterior a ara», «aquesta bicicleta ja està llogada»: impossible |
| No hi ha validació creuada | «La devolució ha de ser posterior a la recollida» necessita JavaScript |
| Es pot esquivar | N'hi ha prou de manipular l'HTML des de les eines del navegador |
| Accessibilitat limitada | El globus no sempre s'anuncia bé als lectors de pantalla |
Aquest últim punt sobre esquivar el filtre té una conseqüència que no cal oblidar mai: la validació del client és per a la comoditat de qui emplena el formulari, no per a la seguretat. El servidor ha de tornar a validar tot el que rep, sempre.
noValidate: desactivar la nativa
Quan escrius la teva pròpia validació i vols controlar tots els missatges, es desactiva la del navegador amb l'atribut noValidate al <form>:
És l'habitual en aplicacions React amb validació pròpia: els atributs required, min i max es mantenen al marcatge —perquè comuniquen informació útil a les tecnologies d'assistència— però els missatges els poses tu.
- Validació en JavaScript: una funció pura
La millor manera de validar és una funció pura: rep les dades i retorna un objecte d'errors, sense tocar l'estat, sense llegir el DOM i sense efectes secundaris.
// src/utilitats/validarReserva.js
const MAX_HORES = 24;
/**
* Valida les dades d'una reserva de CicloUrbano.
*
* @param {Object} dades - { bicicletaId, dataInici, hores, condicions }
* @param {Array} bicicletes - catàleg, per comprovar la disponibilitat
* @returns {Object} objecte amb un missatge per cada camp amb error.
* Sense errors, retorna un objecte buit {}.
*/
export function validarReserva(dades, bicicletes = []) {
const errors = {};
// --- Bicicleta: obligatòria i disponible ---
if (!dades.bicicletaId) {
errors.bicicletaId = 'Tria una bicicleta.';
} else {
const bicicleta = bicicletes.find((b) => b.id === dades.bicicletaId);
if (!bicicleta) {
errors.bicicletaId = 'La bicicleta seleccionada no existeix.';
} else if (bicicleta.estat !== 'disponible') {
errors.bicicletaId = `${bicicleta.model} no està disponible ara mateix.`;
}
}
// --- Data d'inici: obligatòria i no anterior a aquest moment ---
if (!dades.dataInici) {
errors.dataInici = 'Indica quan comença la reserva.';
} else {
const inici = new Date(dades.dataInici);
if (Number.isNaN(inici.getTime())) {
errors.dataInici = 'La data no té un format vàlid.';
} else if (inici.getTime() < Date.now()) {
errors.dataInici = 'La reserva no pot començar en el passat.';
}
}
// --- Hores: enter entre 1 i 24 ---
const hores = Number(dades.hores);
if (dades.hores === '' || Number.isNaN(hores)) {
errors.hores = 'Indica quantes hores vols la bicicleta.';
} else if (!Number.isInteger(hores)) {
errors.hores = 'Les hores han de ser un nombre enter.';
} else if (hores < 1) {
errors.hores = 'La reserva mínima és d\'1 hora.';
} else if (hores > MAX_HORES) {
errors.hores = `La reserva màxima és de ${MAX_HORES} hores.`;
}
// --- Condicions: cal acceptar-les ---
if (!dades.condicions) {
errors.condicions = 'Has d\'acceptar les condicions d\'ús.';
}
return errors;
}Per què aquesta forma és la correcta:
- És pura. Amb les mateixes entrades sempre retorna la mateixa sortida. No hi ha estat, ni dates ocultes, ni sorpreses.
- Es prova sense React. És JavaScript normal: se li passen objectes i es comprova el resultat. Quan arribis al Mòdul 9 veuràs que aquest tipus de funció és la més fàcil de cobrir amb proves.
- Retorna un objecte per camp, no un booleà ni una llista solta. Així el formulari sap on posar cada missatge.
- Un sol error per camp, el primer que es detecta. Encadenar
else ifevita aclaparar amb tres missatges sobre el mateix camp. - Regles de negoci incloses. Comprovar que la bicicleta estigui
'disponible'és una cosa que cap atribut HTML pot fer. - Viu a
src/utilitats/, no dins del component: es pot reutilitzar en una altra pantalla i, arribat el cas, al servidor.
Comprovar si el formulari és vàlid es redueix llavors a una línia:
- Quan validar i el concepte de camp «tocat»
Validar és fàcil; validar en el moment adequat és el que distingeix un formulari agradable d'un insuportable.
Imagina un camp de correu amb validació a cada tecla. En escriure la primera lletra, «a», apareix en vermell: «El correu no és vàlid». És clar que no ho és: encara falten quinze caràcters. El missatge és correcte i l'experiència, pèssima.
| Moment | Avantatge | Inconvenient | Recomanat per a |
|---|---|---|---|
En escriure (onChange) |
Reacció immediata | Crida abans d'hora en camps buits o a mig omplir | Contrasenyes amb requisits, comptadors, camps ja corregits |
En perdre el focus (onBlur) |
La persona ha acabat amb aquest camp | L'error triga a aparèixer | El moment per defecte |
En enviar (onSubmit) |
Mai molesta durant l'emplenament | Tots els errors apareixen de cop al final | Última xarxa de seguretat, sempre obligatòria |
L'estratègia que fan servir les aplicacions ben fetes en combina les tres:
- En perdre el focus, es marca el camp com a «tocat» i es mostra el seu error si en té.
- En escriure, només s'actualitza l'error dels camps ja tocats. Així, quan algú està corregint un error, veu desaparèixer el missatge tan bon punt l'arregla.
- En enviar, es marquen tots els camps com a tocats i es mostren tots els errors pendents.
Els camps «tocats»
Un camp està tocat quan la persona hi ha interactuat i l'ha abandonat. Es guarda en un segon estat, paral·lel a les dades:
const [tocats, setTocats] = useState({}); // { bicicletaId: true, hores: true, … }
function gestionarBlur(esdeveniment) {
const { name } = esdeveniment.target;
setTocats((anterior) => ({ ...anterior, [name]: true }));
}I la regla per mostrar un missatge és la conjunció de dues condicions:
En enviar, es marquen tots de cop:
function marcarTotsTocats(dades) {
const tots = {};
Object.keys(dades).forEach((camp) => {
tots[camp] = true;
});
return tots;
}flowchart TD
A["La persona escriu"] --> B["S'actualitzen les dades"]
B --> C["Es recalculen els errors<br/>(valor derivat)"]
C --> D{"El camp està tocat?"}
D -- "No" --> E["No es mostra res encara"]
D -- "Sí" --> F["Es mostra el missatge del camp"]
G["La persona surt del camp (blur)"] --> H["El camp passa a tocat"] --> D
I["Enviament del formulari"] --> J["Tots els camps passen a tocats"] --> D
Un punt conceptual important: els errors no són estat. Es calculen amb validarReserva(dades, bicicletes) a cada render a partir de les dades. Guardar-los en un useState crearia una segona font de veritat que caldria recordar d'actualitzar a cada canvi. El que sí que és estat són les dades i els camps tocats, perquè no es poden derivar de res. És la distinció de la lliçó 02-04 aplicada a la validació.
- Mostrar els errors i bloquejar l'enviament
Tres decisions de disseny que convé prendre conscientment.
On va el missatge
Al costat del camp que el provoca, mai en una llista al principi ni al final. Qui llegeix el missatge necessita saber de seguida quin camp ha de corregir, sense haver de buscar.
<div className={estils.camp}>
<label htmlFor="hores">Durada (hores)</label>
<input id="hores" name="hores" type="number" value={dades.hores} onChange={gestionarCanvi} onBlur={gestionarBlur} />
{mostrarError('hores') && <p className={estils.error}>{errors.hores}</p>}
</div>Deshabilitar el botó d'enviament?
És temptador posar disabled={!esValid}, i cal pensar-s'ho bé:
| Enfocament | A favor | En contra |
|---|---|---|
| Botó deshabilitat | Impedeix l'enviament no vàlid de manera evident | No explica per què; qui el veu no sap què li falta. Els botons deshabilitats són problemàtics amb els lectors de pantalla |
| Botó actiu + validar en enviar | En prémer apareixen tots els errors explicats | Permet un intent fallit |
| Enfocament mixt | Botó actiu, i en enviar es marquen tots els camps i es mostren els errors | És el recomanat |
La recomanació pràctica: deixa el botó actiu i valida en l'enviament mostrant tots els missatges. Si el teu disseny exigeix deshabilitar-lo, afegeix sempre un text visible que expliqui què falta.
Estils i senyals
Un camp amb error s'ha de distingir per més d'un senyal: vora de color, icona i el text del missatge. El color tot sol no n'hi ha prou —una part de la població no distingeix el vermell del verd— i aquesta idea, juntament amb la forma correcta d'associar el missatge al camp perquè un lector de pantalla l'anunciï (aria-invalid, aria-describedby, role="alert"), es desenvolupa a la propera lliçó, Accessibilitat en Components Interactius. Aquí ens ocupem de la lògica; allà, que arribi a tothom.
- CicloUrbano:
FormulariReserva validat
FormulariReserva validatHo ajuntem tot sobre el formulari de la lliçó anterior.
// src/components/FormulariReserva.jsx
import { useState } from 'react';
import { classes } from '../utilitats/classes.js';
import { validarReserva } from '../utilitats/validarReserva.js';
import estils from './FormulariReserva.module.css';
const DADES_INICIALS = {
bicicletaId: '',
dataInici: '',
hores: 2,
condicions: false
};
/**
* Formulari de creació de reserves de CicloUrbano, amb validació.
* Props:
* - bicicletes (array, opcional, per defecte []): catàleg complet
* - usuariId (cadena, opcional, per defecte 'usr-01')
* - alCrearReserva (funció, opcional): rep l'objecte Reserva validat
*
* Estat: `dades` (el que s'escriu) i `tocats` (amb quins camps s'ha interactuat).
* Els ERRORS no són estat: es deriven de `dades` a cada render.
*/
function FormulariReserva({ bicicletes = [], usuariId = 'usr-01', alCrearReserva }) {
const [dades, setDades] = useState(DADES_INICIALS);
const [tocats, setTocats] = useState({});
// Valors derivats
const errors = validarReserva(dades, bicicletes);
const esValid = Object.keys(errors).length === 0;
const disponibles = bicicletes.filter((bicicleta) => bicicleta.estat === 'disponible');
const escollida = bicicletes.find((bicicleta) => bicicleta.id === dades.bicicletaId);
const total = escollida ? escollida.preuHora * Number(dades.hores || 0) : 0;
// Un error només s'ensenya si el camp ja ha estat tocat
function mostrarError(camp) {
return Boolean(tocats[camp] && errors[camp]);
}
function gestionarCanvi(esdeveniment) {
const { name, type, value, checked } = esdeveniment.target;
let valorFinal = value;
if (type === 'checkbox') {
valorFinal = checked;
} else if (type === 'number') {
valorFinal = value === '' ? '' : Number(value);
}
setDades((anterior) => ({ ...anterior, [name]: valorFinal }));
}
function gestionarBlur(esdeveniment) {
const { name } = esdeveniment.target;
setTocats((anterior) => ({ ...anterior, [name]: true }));
}
function gestionarEnviament(esdeveniment) {
esdeveniment.preventDefault();
// En enviar, tots els camps passen a tocats: es veuen tots els errors
const totsTocats = {};
Object.keys(DADES_INICIALS).forEach((camp) => {
totsTocats[camp] = true;
});
setTocats(totsTocats);
if (!esValid) {
return; // no s'envia res mentre hi hagi errors
}
const reserva = {
id: `res-${crypto.randomUUID().slice(0, 8)}`,
bicicletaId: dades.bicicletaId,
usuari: usuariId,
dataInici: dades.dataInici,
hores: Number(dades.hores),
estat: 'activa'
};
if (alCrearReserva) {
alCrearReserva(reserva);
}
setDades(DADES_INICIALS);
setTocats({});
}
return (
// noValidate: desactivem els globus del navegador i fem servir els nostres missatges
<form className={estils.formulari} onSubmit={gestionarEnviament} noValidate>
<h2>Nova reserva</h2>
<div className={estils.camp}>
<label htmlFor="bicicletaId">Bicicleta</label>
<select
id="bicicletaId"
name="bicicletaId"
required
value={dades.bicicletaId}
onChange={gestionarCanvi}
onBlur={gestionarBlur}
className={classes(estils.control, mostrarError('bicicletaId') && estils.invalid)}
>
<option value="">— Tria una bicicleta —</option>
{disponibles.map((bicicleta) => (
<option key={bicicleta.id} value={bicicleta.id}>
{bicicleta.model} · {bicicleta.preuHora.toFixed(2).replace('.', ',')} €/h
</option>
))}
</select>
{mostrarError('bicicletaId') && (
<p className={estils.error}>{errors.bicicletaId}</p>
)}
</div>
<div className={estils.camp}>
<label htmlFor="dataInici">Inici de la reserva</label>
<input
id="dataInici"
name="dataInici"
type="datetime-local"
required
value={dades.dataInici}
onChange={gestionarCanvi}
onBlur={gestionarBlur}
className={classes(estils.control, mostrarError('dataInici') && estils.invalid)}
/>
{mostrarError('dataInici') && (
<p className={estils.error}>{errors.dataInici}</p>
)}
</div>
<div className={estils.camp}>
<label htmlFor="hores">Durada (hores)</label>
<input
id="hores"
name="hores"
type="number"
min={1}
max={24}
step={1}
required
value={dades.hores}
onChange={gestionarCanvi}
onBlur={gestionarBlur}
className={classes(estils.control, mostrarError('hores') && estils.invalid)}
/>
{mostrarError('hores') && <p className={estils.error}>{errors.hores}</p>}
</div>
<div className={estils.campCasella}>
<label>
<input
name="condicions"
type="checkbox"
checked={dades.condicions}
onChange={gestionarCanvi}
onBlur={gestionarBlur}
/>{' '}
Accepto les condicions d'ús de CicloUrbano
</label>
{mostrarError('condicions') && (
<p className={estils.error}>{errors.condicions}</p>
)}
</div>
{escollida && esValid && (
<p className={estils.total}>
{escollida.model} · {dades.hores} h ·{' '}
<strong>{total.toFixed(2).replace('.', ',')} €</strong>
</p>
)}
<button type="submit" className={estils.enviar}>
Crear reserva
</button>
</form>
);
}
export default FormulariReserva;Comportament resultant, pas a pas:
| Acció | Què es veu |
|---|---|
| S'obre el formulari | Cap error: encara no hi ha res tocat |
| S'obre el desplegable i es tanca sense triar | En perdre el focus: «Tria una bicicleta.» |
| Es tria una bicicleta | El missatge desapareix tan bon punt la dada és vàlida |
S'escriu 0 a les hores i se surt del camp |
«La reserva mínima és d'1 hora.» |
Es corregeix a 2 |
El missatge desapareix en escriure, perquè el camp ja estava tocat |
| Es prem «Crear reserva» amb tot buit | Apareixen els quatre missatges alhora i no s'envia res |
| S'envia correctament | El pare rep la Reserva; el formulari i els tocats es reinicien |
I un detall que il·lustra el valor de les regles de negoci: el desplegable només llista bicicletes disponible, però validarReserva ho torna a comprovar. Si mentre s'emplena el formulari aquesta bicicleta passés a estar llogada, el missatge seria «Elèctrica Pro no està disponible ara mateix». Cap atribut HTML pot fer això.
- Biblioteques de formularis i esquemes
En projectes amb molts formularis, el codi d'aquesta lliçó es repeteix. L'ecosistema té dues famílies d'eines que convé conèixer de nom:
| Eina | Què aporta |
|---|---|
| React Hook Form | Gestiona valors, tocats i errors amb camps majoritàriament no controlats, reduint molt els renders i el codi repetitiu |
| Formik | L'opció clàssica, basada en components controlats; molt estesa en codi existent |
| Zod | Defineix un esquema de dades i hi valida en contra, generant els missatges; s'integra amb les anteriors |
| Yup | Alternativa a Zod, anterior i també molt usada |
No les desenvoluparem: totes es recolzen en els conceptes que acabes d'aprendre —controlat i no controlat, camps tocats, errors per camp, validació en l'enviament— i adoptar-les sense entendre el mecanisme porta a no saber depurar-les quan alguna cosa falla. Quan arribis a un projecte que faci servir React Hook Form amb Zod, reconeixeràs cada peça.
Errors Comuns i Consells
- Posar
valueen un camp no controlat. El converteix en controlat i el congela. Per a valors inicials,defaultValue/defaultChecked. - Oblidar l'atribut
nameen un formulari no controlat.FormDataignora els camps sensename: el valor no apareix i no hi ha cap error. - Esperar que
FormData.get()retorni nombres o booleans. Sempre retorna cadena onull. Converteix explícitament, i recorda que una casella sense marcar no apareix. - Posar
valueen un<input type="file">. És impossible per seguretat; el valor es llegeix deesdeveniment.target.files. - Guardar els errors a l'estat. Són un valor derivat de les dades. Guardar-los crea una segona font de veritat que es desincronitza.
- Validar a cada tecla des del primer caràcter. Molesta moltíssim. Mostra l'error només quan el camp estigui «tocat».
- Validar únicament en escriure i no en enviar. Qui no toca un camp mai no veu el seu error. L'enviament ha de marcar tots els camps com a tocats.
- Confiar només en la validació del client. Es pot esquivar des de les eines del navegador. El servidor ha de validar sempre.
- Deshabilitar el botó sense explicar per què. Deixa la persona sense pistes de què li falta. Prefereix validar en enviar i mostrar els missatges.
- Fer servir
patternamb expressions regulars complicades per al correu. Cap n'és completament correcta. Fes servirtype="email"i confirma amb un enviament real. - Posar el missatge d'error lluny del camp. Ha d'anar immediatament a sota o al costat.
- Barrejar la validació nativa i la pròpia sense
noValidate. El navegador bloqueja l'enviament abans que s'executi el teuonSubmiti els teus missatges no arriben a mostrar-se mai. - Consell: escriu la validació com una funció pura a
src/utilitats/. Es prova sense React, es reutilitza i documenta les regles de negoci en un sol lloc. - Consell: un missatge per camp i en to útil. «La reserva mínima és d'1 hora» explica què cal fer; «Valor no vàlid» no.
- Consell: mantén
required,minimaxal marcatge encara que facis servirnoValidate. Comuniquen informació a les tecnologies d'assistència i documenten el camp.
Exercicis
Exercici 1
Aquest formulari barreja els dos enfocaments i té quatre problemes. Identifica'ls, explica el símptoma de cadascun i decideix si convé fer-lo controlat o no controlat, justificant l'elecció.
function FormulariDevolucio({ estacions }) {
const [estacio, setEstacio] = useState('');
function gestionarEnviament(esdeveniment) {
esdeveniment.preventDefault();
const dades = new FormData(esdeveniment.target);
console.log({
estacio,
quilometres: dades.get('quilometres'),
incidencia: dades.get('incidencia'),
foto: dades.get('foto')
});
esdeveniment.target.reset();
}
return (
<form onSubmit={gestionarEnviament}>
<select value={estacio}>
{estacions.map((e) => <option key={e.id} value={e.id}>{e.nom}</option>)}
</select>
<input type="number" defaultValue={0} />
<input type="checkbox" name="incidencia" />
<input type="file" name="foto" value="" />
<button type="submit">Retornar</button>
</form>
);
}Exercici 2
Escriu la funció pura validarEstacio(dades) a src/utilitats/validarEstacio.js, que validi l'alta d'una estació de CicloUrbano. Ha de retornar un objecte d'errors per camp amb aquestes regles:
nom: obligatori, entre 3 i 40 caràcters, i no pot coincidir (ignorant majúscules i espais sobrants) amb el nom d'una estació existent, que arribarà com a segon paràmetre.barri: obligatori, i ha de ser un de'Centre','Nord'o'Eixample'.places: enter entre 5 i 60.enServei: si ésfalse, hi ha d'haver unmotiud'almenys 10 caràcters.
Escriu a més tres casos de prova a mà (entrada i sortida esperada) que demostrin que funciona.
Exercici 3
Converteix el FormulariIncidencia de l'apartat 4 —que és no controlat— en un formulari controlat amb validació, aplicant el patró complet de camps tocats. Les regles:
bicicletaId: obligatòria.descripcio: obligatòria, mínim 15 caràcters, màxim 500, amb comptador de caràcters restants.urgencia: enter entre 1 i 5.- Si
bloquejaUsestà marcat, la urgència ha de ser 4 o 5.
Explica per què aquesta última regla és impossible d'expressar amb validació nativa del navegador.
Solucions
Solució 1.
Els quatre problemes:
| Problema | Símptoma |
|---|---|
El <select> té value però no onChange |
El desplegable queda congelat en l'opció inicial i React avisa de camp de només lectura |
El camp numèric no té name |
FormData no el recull: dades.get('quilometres') retorna null sempre |
<input type="file" value=""> |
Prohibit per seguretat; React avisa. A més, FormData.get('foto') retorna un objecte File, no una cadena |
esdeveniment.target.reset() amb un camp controlat |
El select no es reinicia: React torna a posar el valor de l'estat al render següent |
Sobre l'enfocament: convé fer-ho tot no controlat llevat que calgui reaccionar al que s'escriu. És un part de devolució que es llegeix sencer en enviar, no té camps interdependents ni validació en temps real, i inclou un camp de fitxer, que obligatòriament és no controlat. Barrejar els dos enfocaments en el mateix formulari és el que ha produït tres de les quatre fallades.
// src/components/FormulariDevolucio.jsx
/**
* Part de devolució d'una bicicleta. Formulari NO controlat:
* els valors els guarda el DOM i es llegeixen amb FormData en enviar.
* Props:
* - estacions (array, opcional, per defecte [])
* - alRetornar (funció, opcional): rep el part de devolució
*/
function FormulariDevolucio({ estacions = [], alRetornar }) {
function gestionarEnviament(esdeveniment) {
esdeveniment.preventDefault();
const formulari = esdeveniment.target;
const dades = new FormData(formulari);
const part = {
estacioId: dades.get('estacioId'),
quilometres: Number(dades.get('quilometres')),
// Una casella sense marcar no apareix a FormData
incidencia: dades.get('incidencia') === 'on',
foto: dades.get('foto') // objecte File, o un File buit si no s'ha triat res
};
if (alRetornar) {
alRetornar(part);
}
formulari.reset(); // correcte: tots els camps són no controlats
}
return (
<form onSubmit={gestionarEnviament}>
<label htmlFor="estacioId">Estació de devolució</label>
<select id="estacioId" name="estacioId" defaultValue="" required>
<option value="">— Tria una estació —</option>
{estacions.map((estacio) => (
<option key={estacio.id} value={estacio.id}>
{estacio.nom} ({estacio.barri})
</option>
))}
</select>
<label htmlFor="quilometres">Quilòmetres recorreguts</label>
<input id="quilometres" name="quilometres" type="number" min={0} defaultValue={0} />
<label>
<input name="incidencia" type="checkbox" /> Hi ha una incidència a reportar
</label>
<label htmlFor="foto">Foto (opcional)</label>
<input id="foto" name="foto" type="file" accept="image/*" />
<button type="submit">Retornar</button>
</form>
);
}
export default FormulariDevolucio;Solució 2.
// src/utilitats/validarEstacio.js
const BARRIS_VALIDS = ['Centre', 'Nord', 'Eixample'];
const MIN_PLACES = 5;
const MAX_PLACES = 60;
function normalitzar(text) {
return String(text ?? '').trim().toLowerCase();
}
/**
* Valida l'alta d'una estació de CicloUrbano.
*
* @param {Object} dades - { nom, barri, places, enServei, motiu }
* @param {Array} existents - estacions ja registrades, per evitar duplicats
* @returns {Object} un missatge per camp amb error; {} si tot és vàlid
*/
export function validarEstacio(dades, existents = []) {
const errors = {};
// --- Nom ---
const nom = String(dades.nom ?? '').trim();
if (nom === '') {
errors.nom = 'El nom de l\'estació és obligatori.';
} else if (nom.length < 3) {
errors.nom = 'El nom ha de tenir com a mínim 3 caràcters.';
} else if (nom.length > 40) {
errors.nom = 'El nom no pot superar els 40 caràcters.';
} else if (existents.some((estacio) => normalitzar(estacio.nom) === normalitzar(nom))) {
errors.nom = `Ja existeix una estació anomenada «${nom}».`;
}
// --- Barri ---
if (!dades.barri) {
errors.barri = 'Tria un barri.';
} else if (!BARRIS_VALIDS.includes(dades.barri)) {
errors.barri = `El barri ha de ser un d'aquests: ${BARRIS_VALIDS.join(', ')}.`;
}
// --- Places ---
const places = Number(dades.places);
if (dades.places === '' || Number.isNaN(places)) {
errors.places = 'Indica el nombre de places.';
} else if (!Number.isInteger(places)) {
errors.places = 'Les places han de ser un nombre enter.';
} else if (places < MIN_PLACES || places > MAX_PLACES) {
errors.places = `Les places han d'estar entre ${MIN_PLACES} i ${MAX_PLACES}.`;
}
// --- Motiu, només si l'estació no entra en servei ---
if (dades.enServei === false) {
const motiu = String(dades.motiu ?? '').trim();
if (motiu.length < 10) {
errors.motiu = 'Explica en almenys 10 caràcters per què no entra en servei.';
}
}
return errors;
}Els tres casos de prova:
const existents = [
{ id: 'est-01', nom: 'Plaça Major', barri: 'Centre', places: 20 },
{ id: 'est-02', nom: 'Parc Nord', barri: 'Nord', places: 15 }
];
// Cas 1: tot correcte -> {}
validarEstacio(
{ nom: 'Mercat Vell', barri: 'Eixample', places: 25, enServei: true },
existents
);
// Cas 2: nom duplicat amb caixa diferent i espais, places fora de rang
validarEstacio(
{ nom: ' plaça major ', barri: 'Centre', places: 100, enServei: true },
existents
);
// -> {
// nom: 'Ja existeix una estació anomenada «plaça major».',
// places: 'Les places han d\'estar entre 5 i 60.'
// }
// Cas 3: fora de servei sense motiu suficient
validarEstacio(
{ nom: 'Pont Sud', barri: 'Eixample', places: 12, enServei: false, motiu: 'obres' },
existents
);
// -> { motiu: 'Explica en almenys 10 caràcters per què no entra en servei.' }La funció normalitzar centralitza la comparació —retallar espais i passar a minúscules— perquè la regla s'apliqui igual a tot arreu. I retornar {} quan tot és vàlid permet la comprovació d'una línia: Object.keys(errors).length === 0.
Solució 3.
// src/components/FormulariIncidencia.jsx
import { useState } from 'react';
const MAX_DESCRIPCIO = 500;
const MIN_DESCRIPCIO = 15;
const DADES_INICIALS = {
bicicletaId: '',
descripcio: '',
urgencia: 3,
bloquejaUs: false
};
function validarIncidencia(dades) {
const errors = {};
if (!dades.bicicletaId) {
errors.bicicletaId = 'Tria la bicicleta afectada.';
}
const descripcio = dades.descripcio.trim();
if (descripcio.length === 0) {
errors.descripcio = 'Descriu la incidència.';
} else if (descripcio.length < MIN_DESCRIPCIO) {
errors.descripcio = `Descriu la incidència amb almenys ${MIN_DESCRIPCIO} caràcters.`;
} else if (descripcio.length > MAX_DESCRIPCIO) {
errors.descripcio = `La descripció no pot superar els ${MAX_DESCRIPCIO} caràcters.`;
}
const urgencia = Number(dades.urgencia);
if (dades.urgencia === '' || Number.isNaN(urgencia)) {
errors.urgencia = 'Indica el nivell d\'urgència.';
} else if (!Number.isInteger(urgencia) || urgencia < 1 || urgencia > 5) {
errors.urgencia = 'La urgència ha de ser un enter entre 1 i 5.';
} else if (dades.bloquejaUs && urgencia < 4) {
// Validació CREUADA entre dos camps
errors.urgencia = 'Si la incidència impedeix l\'ús, la urgència ha de ser 4 o 5.';
}
return errors;
}
/**
* Part d'incidència d'una bicicleta, controlat i validat.
* Props:
* - bicicletes (array, opcional, per defecte [])
* - alRegistrar (funció, opcional): rep la incidència validada
*/
function FormulariIncidencia({ bicicletes = [], alRegistrar }) {
const [dades, setDades] = useState(DADES_INICIALS);
const [tocats, setTocats] = useState({});
const errors = validarIncidencia(dades);
const esValid = Object.keys(errors).length === 0;
const restants = MAX_DESCRIPCIO - dades.descripcio.length;
function mostrarError(camp) {
return Boolean(tocats[camp] && errors[camp]);
}
function gestionarCanvi(esdeveniment) {
const { name, type, value, checked } = esdeveniment.target;
let valorFinal = value;
if (type === 'checkbox') {
valorFinal = checked;
} else if (type === 'number') {
valorFinal = value === '' ? '' : Number(value);
}
setDades((anterior) => ({ ...anterior, [name]: valorFinal }));
}
function gestionarBlur(esdeveniment) {
const { name } = esdeveniment.target;
setTocats((anterior) => ({ ...anterior, [name]: true }));
}
function gestionarEnviament(esdeveniment) {
esdeveniment.preventDefault();
const tots = {};
Object.keys(DADES_INICIALS).forEach((camp) => {
tots[camp] = true;
});
setTocats(tots);
if (!esValid) {
return;
}
if (alRegistrar) {
alRegistrar({ ...dades, descripcio: dades.descripcio.trim() });
}
setDades(DADES_INICIALS);
setTocats({});
}
return (
<form onSubmit={gestionarEnviament} noValidate>
<div>
<label htmlFor="bicicletaId">Bicicleta</label>
<select
id="bicicletaId"
name="bicicletaId"
value={dades.bicicletaId}
onChange={gestionarCanvi}
onBlur={gestionarBlur}
>
<option value="">— Tria una bicicleta —</option>
{bicicletes.map((bicicleta) => (
<option key={bicicleta.id} value={bicicleta.id}>
{bicicleta.model} ({bicicleta.id})
</option>
))}
</select>
{mostrarError('bicicletaId') && <p className="error">{errors.bicicletaId}</p>}
</div>
<div>
<label htmlFor="descripcio">Descripció</label>
<textarea
id="descripcio"
name="descripcio"
rows={3}
maxLength={MAX_DESCRIPCIO}
value={dades.descripcio}
onChange={gestionarCanvi}
onBlur={gestionarBlur}
/>
<small>Queden {restants} caràcters.</small>
{mostrarError('descripcio') && <p className="error">{errors.descripcio}</p>}
</div>
<div>
<label htmlFor="urgencia">Urgència (1-5)</label>
<input
id="urgencia"
name="urgencia"
type="number"
min={1}
max={5}
value={dades.urgencia}
onChange={gestionarCanvi}
onBlur={gestionarBlur}
/>
{mostrarError('urgencia') && <p className="error">{errors.urgencia}</p>}
</div>
<label>
<input
name="bloquejaUs"
type="checkbox"
checked={dades.bloquejaUs}
onChange={gestionarCanvi}
onBlur={gestionarBlur}
/>{' '}
Impedeix l'ús de la bicicleta
</label>
<button type="submit">Registrar incidència</button>
</form>
);
}
export default FormulariIncidencia;Per què l'última regla és impossible de forma nativa: els atributs de validació d'HTML són locals a un camp. min={1} i max={5} només saben del valor d'aquest <input>; no existeix cap atribut que digui «el mínim d'aquest camp depèn de si aquella casella està marcada». És una validació creuada entre dos camps, és a dir, una regla de negoci, i per a això cal JavaScript. Fixa't a més que l'enfocament de funció pura l'absorbeix sense esforç: és un else if més dins de la validació d'urgencia.
Conclusió
Amb aquesta lliçó els formularis de React queden complets. Has vist l'alternativa als components controlats: els no controlats, en què el valor el guarda el DOM, es declara amb defaultValue o defaultChecked i es llegeix en enviar amb FormData —recordant que retorna cadenes, que les caselles sense marcar no apareixen i que només recull camps amb name— o amb useRef, la via d'accés directe al node que s'estudiarà a fons al Mòdul 5. Saps triar entre tots dos enfocaments amb criteri: controlat sempre que necessitis reaccionar al que s'escriu, no controlat per a formularis grans que només es llegeixen al final, i obligatòriament no controlat a l'<input type="file">, que per raons de seguretat no admet que el codi en fixi el valor.
Pel que fa a la validació, has après que el navegador ofereix gratis un primer filtre amb required, min, max, type="email" i pattern, però que els seus missatges no es poden controlar, només mostra un error alhora, no expressa regles de negoci ni validacions creuades i es pot esquivar —d'aquí que el servidor hagi de validar sempre—. Per això la validació seriosa s'escriu com una funció pura a src/utilitats/, que rep les dades i retorna un objecte amb un missatge per camp: es prova sense React, es reutilitza i concentra les regles del domini en un sol lloc. I sobretot has après quan validar: els errors es calculen sempre, però només es mostren quan el camp està tocat, amb l'enviament marcant tots els camps de cop. Els errors no són estat; les dades i els tocats sí.
CicloUrbano té ara un FormulariReserva que no deixa crear reserves en el passat, de zero hores, sobre bicicletes no disponibles ni sense acceptar les condicions, i que diu exactament què falla i on. Queda una pregunta pendent: aquests missatges es veuen, però se senten? Un lector de pantalla no relaciona automàticament un paràgraf vermell amb el camp de dalt, un <article> amb onClick no es pot activar amb el teclat i una etiqueta d'estat que només es distingeix pel color deixa fora qui no percep aquest color. Tot això —i com comprovar-ho— és el que tanca el mòdul a Accessibilitat en Components Interactius.
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
