`state.matches`: preguntar por un prefijo, no por una hoja
Si el estado activo es una ruta, la pregunta correcta no es de igualdad sino de pertenencia: ¿pasa mi camino actual por este punto del árbol? Eso es exactamente lo que resuelve `state.matches`, y de ahí nace su propiedad más útil e incomprendida, el emparejamiento parcial. Esta lección compara las dos notaciones equivalentes —la de punto y la de objeto—, formaliza qué significa que un patrón sea un prefijo del camino activo, explica por qué preguntar por un compuesto devuelve verdadero para todos sus hijos, y establece la disciplina de preguntar al nivel de abstracción exacto que necesita cada consumidor, ni más profundo ni más superficial.
La lección anterior dejó una deuda concreta: si el valor del snapshot es un objeto anidado, comparar con una cadena deja de funcionar y hace falta otra operación. Esa operación es state.matches, y entenderla como un simple azucarillo para comparar objetos es perder su idea central. matches no comprueba igualdad, comprueba prefijo: pregunta si el patrón que le das es un tramo inicial del camino activo. De esa definición se derivan todas sus propiedades, incluida la que más sorprende y más rendimiento da, el emparejamiento parcial, que permite a una vista preguntar solo por el tramo de jerarquía que le importa y quedar inmune a los subestados que se añadan por debajo mañana.
- Formular
state.matchescomo una comprobación de prefijo sobre el camino activo. - Alternar entre la notación de punto y la de objeto sabiendo cuándo cada una es obligatoria.
- Explotar el emparejamiento parcial para desacoplar la vista de la profundidad de la máquina.
- Elegir el nivel de granularidad correcto en cada consumidor y detectar cuándo se pregunta de más.
Dos notaciones para el mismo camino
XState acepta el patrón de dos formas equivalentes. La notación de punto encadena los segmentos en una cadena legible; la de objeto reconstruye literalmente la forma anidada del valor. Producen el mismo resultado y la elección es de gusto, salvo en un caso que veremos en la última sección.
const s = actor.getSnapshot()
s.matches('pago') // prefijo de un segmento: parcial
s.matches('pago.validando') // notacion de punto: camino completo
s.matches({ pago: 'validando' }) // notacion de objeto: identico al anterior
// Con tres niveles, ambas notaciones escalan igual:
s.matches('pago.tarjeta.3ds')
s.matches({ pago: { tarjeta: '3ds' } })
La regla es que cada segmento del patrón debe coincidir con el nodo activo en ese nivel, empezando por la raíz. Si el patrón se agota antes que el camino, la comprobación es verdadera de todos modos: eso es el emparejamiento parcial. Si el patrón menciona un nodo que no está activo en su nivel, es falsa. Y si el patrón nombra un estado que no existe en la definición de la máquina, XState v5 lo señala como error de tipos en cuanto usaste setup con tipos, que es una de las mejores razones para haber pasado por el nivel anterior.
En una máquina construida con setup, el argumento de matches está tipado con las rutas realmente existentes. Escribir pago.validado en lugar de pago.validando ya no produce un falso silencioso que se manifiesta como una vista que nunca se pinta, sino un error rojo en el editor. Este es el punto donde la inversión en tipos del nivel once se cobra sola: sin ella, cada comprobación de estado es una cadena mágica que envejece sin avisar cada vez que alguien renombra un subestado.
El emparejamiento parcial es la característica, no el accidente
Que s.matches('pago') devuelva verdadero estando en pago.rechazado no es una tolerancia benévola de la implementación: es el comportamiento que hace útil a la jerarquía desde fuera de la máquina. Permite que cada consumidor pregunte al nivel de abstracción que le corresponde.
| Patrón | Camino activo pago.validando |
Camino activo pago.rechazado |
|---|---|---|
pago |
verdadero | verdadero |
pago.validando |
verdadero | falso |
carrito |
falso | falso |
pago.validando o pago.rechazado con || |
verdadero | verdadero |
La segunda columna y la tercera describen el mismo instante del sistema desde dos profundidades distintas de observación, y por eso la tabla se lee mejor por filas que por columnas: cada fila responde a la pregunta de si esa condición sobrevive a un cambio de hoja dentro del mismo compuesto. Las filas que dan el mismo resultado en ambas columnas son condiciones estables; las que difieren son condiciones sensibles al detalle, y solo deberían aparecer en los lugares donde ese detalle importa.
Lee la tabla con atención a la primera fila, porque contiene toda la doctrina. Una barra de navegación que solo necesita saber si hay un pago en curso pregunta por pago y nunca vuelve a tocarse: da igual que mañana el equipo divida validando en validando y verificandoTresDominios, que añada reintentando o que renombre rechazado. La barra sigue correcta porque preguntó por el tramo que le importaba y no por el detalle que no le incumbe. En cambio, el panel que debe mostrar el mensaje concreto de rechazo pregunta por el camino completo, porque su corrección sí depende del detalle.
El olor a diagnosticar es una vista que enumera hojas donde le bastaba un prefijo: una condición con tres o cuatro rutas completas unidas por barras que, leídas juntas, no dicen otra cosa que estamos dentro del compuesto. Cada hoja enumerada es una arista de acoplamiento entre la vista y la estructura interna del estado; cuando la máquina gane un subestado, alguien tendrá que acordarse de añadirlo a esa lista, y no se acordará. Sustituir la enumeración por el prefijo hace que la vista siga siendo correcta por construcción ante subdivisiones futuras.
Hay un caso, sin embargo, en el que ni el prefijo ni la hoja sirven: cuando la condición que necesita la vista agrupa estados que no son hermanos ni comparten ancestro. Pensemos en la pregunta de si hay una operación de red en curso, cierta en pago.validando y también en carrito.sincronizando, dos nodos de ramas distintas. Ningún prefijo los cubre a los dos, y enumerar ambas rutas devuelve el acoplamiento que acabamos de desterrar. La respuesta idiomática de XState v5 es la etiqueta, un metadato que se declara en el estado y se consulta con state.hasTag.
// Se declaran en la definicion, junto al estado, y son acumulativas.
validando: { tags: ['red', 'bloqueante'] },
sincronizando: { tags: ['red'] },
// La vista pregunta por la propiedad, no por la posicion en el arbol:
s.hasTag('red') // true en validando Y en sincronizando
s.matches('pago') // solo cubre una de las dos ramas
La diferencia es conceptual y no meramente práctica. matches pregunta por posición estructural, es decir por dónde estamos; hasTag pregunta por una propiedad transversal, es decir por cómo es lo que estamos haciendo. Cuando la agrupación que necesita el consumidor coincide con una rama del árbol, el prefijo es la herramienta correcta y la etiqueta sobra; cuando la agrupación atraviesa ramas, la etiqueta es la herramienta correcta y forzar la jerarquía para que encaje sería deformar el modelo por conveniencia de la vista. Una regla práctica: si te sorprendes moviendo estados de sitio en la máquina para que un prefijo cubra lo que la interfaz necesita, lo que buscabas era una etiqueta.
La forma sana de decidir el nivel es preguntarse de qué depende realmente la corrección de este consumidor. Si el fragmento de interfaz se ve idéntico para todos los hijos de un compuesto, su condición debe ser el compuesto. Si cambia de un hijo a otro, su condición debe ser el hijo. Esa regla, aplicada sin excepciones, produce un código en el que la profundidad de cada comprobación es un documento de qué distinciones importan de verdad en cada lugar.
flowchart TD
A[camino activo pago punto validando] --> B{el patron es prefijo?}
B -->|patron pago| C[verdadero: emparejamiento parcial]
B -->|patron pago punto validando| D[verdadero: camino completo]
B -->|patron pago punto rechazado| E[falso: difiere en el ultimo nivel]
B -->|patron carrito| F[falso: difiere en la raiz]
style C fill:#a6e3a1,color:#11111b
style D fill:#89b4fa,color:#11111b
style E fill:#fab387,color:#11111b
style F fill:#f38ba8,color:#11111bCuándo la notación de objeto deja de ser opcional
Las dos notaciones son intercambiables mientras el camino sea una sola rama. Dejan de serlo en cuanto hay regiones paralelas, porque entonces el camino activo no es una rama sino un abanico, y una cadena de puntos solo puede describir una rama a la vez. Para exigir simultaneidad en varias regiones necesitas la forma de objeto.
// Con un nodo paralelo llamado editor y dos regiones, formato y guardado:
s.matches({ editor: { formato: 'negrita' } }) // una region
s.matches({ editor: { formato: 'negrita', guardado: 'sucio' } }) // AMBAS a la vez
// La notacion de punto no puede expresar la conjuncion anterior:
s.matches('editor.formato.negrita') && s.matches('editor.guardado.sucio')
Las dos últimas líneas son equivalentes en resultado, pero la de objeto expresa la conjunción como un único patrón y la de punto la simula con dos comprobaciones unidas a mano. La primera declara la condición; la segunda la calcula. Cuando el número de regiones crece, la diferencia deja de ser estética y se vuelve legibilidad pura.
Un matiz sobre el paralelismo que conviene fijar aquí y que la lección tercera desarrollará: mencionar solo algunas regiones en el patrón sigue siendo emparejamiento parcial, ahora en anchura además de en profundidad. Preguntar por la región formato sin decir nada de guardado es afirmar una condición sobre una dimensión y desentenderse del resto, lo cual es casi siempre lo que quieres. Exigir todas las regiones a la vez solo tiene sentido cuando la corrección del consumidor depende de la conjunción exacta, y esos casos son minoritarios.
Merece también un aviso el uso de matches dentro de la propia máquina. La tentación de escribir un guard que consulte el estado activo indica casi siempre que la condición debía expresarse como estructura y no como comprobación: si una transición solo es válida estando dentro de cierto compuesto, declárala en ese compuesto y el intérprete garantizará la condición sin que nadie la escriba. matches es una API para los observadores de la máquina, no para su interior.
Hay un beneficio adicional del prefijo que solo se aprecia al conectar la máquina a una interfaz reactiva. Un selector que devuelve un booleano derivado de matches cambia mucho menos a menudo que uno que devuelve el valor entero del estado, y por tanto provoca muchos menos renderizados. Cuanto más alto sea el prefijo por el que preguntas, más estable es el booleano y menos trabajo hace la vista.
// Se vuelve a renderizar en CADA cambio de subestado:
const valor = useSelector(actor, (s) => s.value)
// Se vuelve a renderizar solo al entrar o salir del compuesto:
const enPago = useSelector(actor, (s) => s.matches('pago'))
El contraste es exacto: el primer selector devuelve un objeto nuevo en cada transición interna del pago, mientras que el segundo devuelve el mismo booleano durante toda la estancia. La granularidad de la pregunta no solo determina el acoplamiento conceptual de la lección anterior, también determina literalmente cuántas veces se vuelve a pintar el componente. Preguntar al nivel justo es, a la vez, una decisión de diseño y una optimización.
Prefijo por defecto
Pregunta por el nodo más alto cuya distinción te importe. Es la opción que menos se rompe cuando la máquina gana profundidad.
Camino completo cuando distingue
Si tu interfaz cambia entre hermanos, la comprobación debe llegar hasta la hoja. La profundidad de la pregunta documenta la distinción real.
Objeto para conjunciones
Con regiones paralelas, solo la forma de objeto expresa que varias regiones deben estar en cierto estado a la vez.
Tipado sobre cadenas mágicas
Con setup, las rutas están tipadas. Un renombrado rompe la compilación en lugar de producir una condición que nunca se cumple.
La forma madura de leer esta API es entender que state.matches no pertenece a la familia de la igualdad sino a la de las consultas estructurales, y que su pariente conceptual no es el operador de comparación sino el emparejamiento de patrones con comodín implícito al final. Cuando escribes el patrón pago estás diciendo, literalmente, que te interesa cualquier camino que empiece por ese segmento, sin importar cómo continúe; el comodín no se escribe porque está siempre implícito en la cola. Esa asimetría deliberada —el patrón puede ser más corto que el camino, nunca más largo— es lo que convierte a la jerarquía en un mecanismo de abstracción y no en una mera reorganización visual. Sin emparejamiento parcial, anidar sería contraproducente: cada nivel nuevo obligaría a reescribir todas las condiciones externas, y la jerarquía sería un impuesto en vez de una herramienta. Con él, la profundidad se vuelve un detalle interno que la máquina puede refinar sin romper a sus consumidores, y cada comprobación en tu código pasa a ser una declaración explícita de cuánto detalle necesita ese lugar concreto para ser correcto. Ahí está la disciplina de diseño que esta lección quiere dejarte: la profundidad de tus patrones no es un accidente de escritura, es la superficie de acoplamiento entre tu máquina y todo lo que la observa. Preguntar más hondo de lo necesario es firmar un contrato con detalles que no te incumben y que cambiarán; preguntar menos hondo de lo necesario es fingir que dos situaciones distintas son la misma. El nivel exacto de cada pregunta es una decisión de arquitectura disfrazada de detalle de sintaxis.
- Sobre la máquina de compra, comprueba en consola que
matchescon el patrónpagoes verdadero tanto envalidandocomo enrechazado, y falso encarrito. - Escribe la misma comprobación con las dos notaciones y verifica que producen resultados idénticos mientras no haya regiones paralelas.
- Introduce un nuevo subestado dentro de
pagoy comprueba que las condiciones escritas con prefijo siguen siendo correctas sin tocarlas, mientras que las escritas con hojas enumeradas quedan incompletas. - Busca en tu código real una condición que una tres o más rutas completas con barras y sustitúyela por el prefijo común; justifica por qué el resultado es equivalente hoy y más robusto mañana.
- Añade un nodo paralelo y escribe una condición que exija dos regiones a la vez usando la forma de objeto; después reescríbela con dos comprobaciones unidas a mano y compara la legibilidad.
- Renombra un subestado en la definición y confirma que, gracias a
setup, las comprobaciones desactualizadas fallan al compilar en vez de fallar en silencio.