wandres.dev
CONTROL FLOW: SHOW Y SWITCH · condicionales reactivos

Show como guard: el accesor estrechado

La forma callback de Show entrega un Accessor de NonNullable: el valor ya no-nulo, con el tipo estrechado, sin dobles lecturas ni afirmaciones con exclamacion. Por que es un accesor vivo y no el valor directo, y como encadenar guards.

⏱ 13 min

La última cara de Show es la que más ergonomía regala y la que más se malentiende. Cuando when es un valor que puede ser nulo, la forma de hijo-función convierte a Show en un guard: dentro del callback recibes un accesor del valor ya estrechado —un Accessor<NonNullable<T>>— y TypeScript sabe que ahí dentro el valor no es nulo. Se acabaron la doble lectura y el !. Pero el detalle que separa a quien copia el patrón de quien lo entiende es por qué recibes un accesor vivo y no el valor a secas.

🎯 Al terminar esta lección sabrás
  • Reconocer el antipatrón de doble lectura con afirmación no-nula.
  • Usar el hijo-función de Show para obtener un Accessor<NonNullable<T>>.
  • Explicar por qué es un accesor —vivo y fino— y no el valor directo.
  • Encadenar varios Show como guards de valores anidados que pueden faltar.

El antipatrón: leer dos veces y afirmar

Sin la forma callback, guardar contra un nulo obliga a leer la señal dos veces —una para la condición, otra para el uso— y a afirmarle a TypeScript, con !, que el valor no es nulo:

// dos lecturas de usuario(), y una asercion no-nula fragil
<Show when={usuario()}>
  <Perfil nombre={usuario()!.nombre} />
</Show>

Hay dos problemas. El de tipos: el ! es una promesa que le haces al compilador y que él no verifica; si un día cambia la condición, el ! queda mintiendo. Y el conceptual: son dos lecturas de la misma fuente que, en principio, podrían ver valores distintos. Show resuelve ambos de un golpe con el hijo-función.

La forma callback: un accesor ya estrechado

Pasa una función como hijo. Show la invoca con un accesor que devuelve el valor de when con el tipo estrechado a NonNullable<T>:

// una lectura, tipo estrechado, sin exclamacion
<Show when={usuario()} fallback={<Login />}>
  {(usuario) => <Perfil nombre={usuario().nombre} />}
</Show>

Dentro del callback, usuario es Accessor<NonNullable<Usuario>>: lo invocas con usuario() y obtienes el usuario garantizado no-nulo. TypeScript lo sabe sin ayuda, así que .nombre es legal y ! desaparece. La firma oficial lo confirma: en la forma sin keyed, los hijos son JSX.Element | ((item: Accessor<NonNullable<T>>) => JSX.Element).

⚠️
No vuelvas a leer la señal externa dentro del callback

El error más común anula todo el beneficio: ignorar el parámetro y volver a leer la fuente original.

// MAL: pierde el estrechamiento y reintroduce el !
<Show when={usuario()}>
  {(u) => <Perfil nombre={usuario()!.nombre} />}
</Show>

Dentro del callback usa siempre el parámetro (u()), nunca usuario(). El parámetro es la lectura estrechada y coherente que Show te ofrece; volver a la señal externa te devuelve a T | null y a la doble lectura que veníamos a eliminar.

Por qué es un accesor y no el valor

Aquí está la profundidad. En la forma sin keyed, Show conserva el subárbol mientras when siga siendo verdadero (lección 15.1). Si el callback recibiera el valor directo, ese valor quedaría congelado en la primera invocación y Show tendría que recrear el subárbol para entregarte uno nuevo cada vez que cambiara. Eso es, precisamente, lo que hace keyed. Al entregarte un accesor, Show puede conservar el subárbol y, a la vez, mantener el valor vivo: cada lectura de usuario() devuelve el estado más reciente, y los bindings finos de dentro se actualizan sin recrear nada.

flowchart TD
W[when igual usuario no-nulo] --> S[Show memoiza la verdad y conserva el subarbol]
S --> CB[el hijo funcion recibe un accesor estrechado]
CB --> R[leer el accesor da NonNullable sin nulo]
W2[usuario pasa a nulo] --> D[Show desmonta el subarbol]
D --> G[el accesor deja de leerse y aparece el fallback]
style R fill:#a6e3a1,color:#11111b
style G fill:#fab387,color:#11111b

Dos garantías refuerzan el diseño. Primera: el accesor “solo puede leerse mientras la condición siga siendo verdadera”; como Show desmonta el subárbol en cuanto when pasa a falsy, nunca leerás un nulo a través de él —el estrechamiento es honesto, no una ilusión de tipos—. Segunda: el hijo-función se envuelve en untrack, así que la función que construye la vista no se resuscribe a when; el seguimiento reactivo vive en el accesor y en los bindings internos. Por eso el subárbol se crea una vez y se actualiza fino, en vez de reconstruirse.

Compruébalo con un valor que cambia sin volverse nulo:

// el accesor entrega SIEMPRE el valor mas reciente sin recrear el subarbol
<Show when={sesion()} fallback={<Login />}>
  {(sesion) => (
    <header>
      Hola, {sesion().nombre}
      <Racha dias={sesion().rachaDias} />   {/* se refresca solo este binding */}
    </header>
  )}
</Show>

Si sesion() actualiza su rachaDias sin volverse nula, el header no se reconstruye: solo cambia el binding de la racha. Ese es el pago exacto del accesor —persistencia del subárbol y frescura del dato a la vez—, algo que un valor congelado, entregado una única vez, no podría ofrecer sin recrear.

💡
Accesor para persistir y estrechar; keyed para resetear

Elige por lo que quieras que pase cuando el valor cambie sin volverse nulo. ¿Conservar el estado del subárbol y solo refrescar los datos? Forma sin keyed: accesor vivo. ¿Reiniciar todo el subárbol porque es otra entidad y no debe arrastrar estado? keyed: valor directo y recreación. El estrechamiento de tipos lo tienes en ambas —Accessor<NonNullable<T>> sin keyed, NonNullable<T> con keyed—; lo que cambia es la política de vida.

Encadenar guards

Como cada Show-guard estrecha un nivel, encadenarlos estrecha una cadena de valores opcionales sin una sola afirmación no-nula, y cada nivel aporta su propio fallback:

<Show when={usuario()} fallback={<Login />}>
  {(usuario) => (
    <Show when={usuario().perfil} fallback={<CompletarPerfil />}>
      {(perfil) => <Cabecera avatar={perfil().avatar} />}
    </Show>
  )}
</Show>

Cada capa responde una pregunta —¿hay usuario?, ¿tiene perfil?— con su propia respuesta para el caso negativo. El resultado se lee como una secuencia de guardas de precondición, igual que los guard let encadenados de otros lenguajes, pero reactivo: si usuario() se vuelve nulo, toda la cadena interior se desmonta en cascada y aparece <Login />.

📝
Guard para presencia, Switch para estado

No confundas las dos herramientas. Encadenar Show-guards es lo correcto cuando cada nivel comprueba la presencia de un valor opcional distinto —hay usuario, tiene perfil, el perfil tiene avatar—: son preguntas independientes que se estrechan una tras otra. Si en cambio ramificas entre estados mutuamente excluyentes de una misma cosa —cargando, error, listo—, eso es un Switch, no una torre de guards. La señal para distinguirlos: si tus condiciones podrían ser todas verdaderas a la vez, son guards encadenados; si solo una puede serlo, es un Switch.

El guard convierte la ausencia en una frontera del grafo

Lo que de verdad hace la forma callback de Show es ascender el manejo de la ausencia —el nulo, el “todavía no hay datos”, el “aún no ha llegado”— desde el terreno de las comprobaciones defensivas al terreno de la estructura del grafo reactivo. En un modelo imperativo, un valor que puede faltar te persigue por todo el código: cada acceso exige un if, cada uso arrastra un ?. o un !, y el compilador nunca termina de creerte. La forma guard invierte eso: Show establece una frontera en el grafo tal que, dentro de ella, el valor existe —garantizado por el tipo y por el tiempo de vida a la vez—, y fuera de ella vive el fallback que cubre su ausencia. El accesor estrechado es el pasaporte que solo es válido dentro de esa frontera, y untrack más la memoización de la verdad se aseguran de que la frontera se dibuje una vez y se mueva sola cuando la realidad cambia. Deja de pensar “compruebo si es nulo antes de usarlo” y empieza a pensar “declaro la región donde no es nulo”: esa es la diferencia entre defenderte de la ausencia en cada línea y modelarla una vez como lo que es, una rama del grafo con su propio ciclo de vida. Cuando el guard sustituye al ! en tu código, no has ganado azúcar sintáctico; has movido la ausencia de los valores a la topología de tu interfaz.

⚔️ Del bang al guard
  1. Reescribe usuario() && <Perfil nombre={usuario()!.nombre} /> como Show con hijo-función y elimina el !.
  2. Comete el error a propósito: dentro del callback lee usuario() en vez del parámetro y observa cómo vuelve el tipo T | null.
  3. Encadena dos guards —usuario y su perfil— cada uno con su fallback, y comprueba que ninguno necesita afirmación no-nula.
  4. Cambia el valor de usuario() de un objeto a otro no-nulo y confirma que, sin keyed, el subárbol se conserva y solo se refrescan los datos.
  5. Añade keyed al guard exterior y explica en qué cambia el comportamiento y por qué el callback pasa a recibir el valor directo.