> ## Documentation Index
> Fetch the complete documentation index at: https://firespark.cloud/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Mostrar menús

> Aplana un menú compuesto y mantén la navegación de categorías alineada mientras el cliente se desplaza.

Carga el [menú compuesto](https://firespark.cloud/docs/es/storefront-api/menus/get.md) de la tienda, el canal y el fulfillment que está usando el cliente. `@fire-spark/storefront` convierte ese árbol en secciones. `@fire-spark/rn-ui` deja la navegación de categorías en la sección que está en pantalla.

`@fire-spark/react-ui` (web) sigue en el modo por offset. Las listas virtualizadas son un tema de React Native.

## Aplana el catálogo

`flattenMenuCategories` recorre el menú en profundidad. El `instanceId` de cada sección es la ruta de ids de categoría (`food/mains`), así la misma categoría puede aparecer bajo dos padres.

```ts theme={null}
import { flattenMenuCategories } from "@fire-spark/storefront";

const sections = flattenMenuCategories(categories, categoriesById);
```

## Filas para una lista virtualizada

`buildMenuRows` convierte esas secciones en filas que una lista puede reciclar. No renderiza nada. El tipo del producto es el que guardes en el mapa.

```ts theme={null}
import { buildMenuRows } from "@fire-spark/storefront";

const productsById = new Map(menu.products.map(item => [item.id, item]));
const { rows, headerIndex } = buildMenuRows(sections, productsById);
```

Cada sección empieza con una fila `header` (`sectionId` es `instanceId`). Después van las filas de producto, y `index` vuelve a 0 en cada sección. Una fila `empty` se agrega solo cuando la sección no tiene productos ni ítems `CATEGORY`, así un padre que solo agrupa subcategorías no muestra un estado vacío.

Las claves siguen siendo únicas cuando un producto está en dos secciones:

| Fila | Clave |
| - | - |
| Header | `h:${sectionId}` |
| Producto | `p:${sectionId}:${index}:${productId}` |
| Vacía | `e:${sectionId}` |

`first` es verdadero solo en el primer header.

```ts theme={null}
import { menuRowKey, menuRowType, spanMenuRow } from "@fire-spark/storefront";

// FlashList v2: headers y filas vacías a ancho completo dentro de una grilla.
<FlashList
  data={rows}
  keyExtractor={menuRowKey}
  getItemType={menuRowType}
  overrideItemLayout={spanMenuRow}
/>
```

`spanMenuRow` pone `layout.span` en el número de columnas de la lista cuando la fila no es un producto. Las celdas de producto quedan con span uno.

## Scroll-spy en un scroll view simple

Pasa los ids de sección y la ref del scroll view. `registerCategory` guarda el `y` de cada sección. `onScroll` elige la última sección cuyo tope está en o sobre la línea del offset. `selectCategory` se desplaza hasta ahí.

```ts theme={null}
import { useActiveCategory } from "@fire-spark/rn-ui/use-active-category";

const scrollRef = useRef(null);
const categoryIds = sections.map(section => section.instanceId);
const active = useActiveCategory({ categoryIds, offset: 8, scrollRef });
```

Pon `active.registerCategory(section.instanceId)` en el `onLayout` de la sección, y `active.onScroll` en el scroll view.

## Scroll-spy en una lista virtualizada

Las filas fuera de pantalla no están montadas, así que faltan los offsets de layout. Pasa el modo por índice. El hook no importa un paquete de listas. Cualquier ref con `scrollToIndex` encaja, incluidas FlashList, FlatList y LegendList.

```ts theme={null}
import { useActiveCategory } from "@fire-spark/rn-ui/use-active-category";

const listRef = useRef(null);
const rowSectionIds = rows.map(row => row.sectionId);
const active = useActiveCategory({
  rowSectionIds,
  headerIndex,
  listRef,
  offset: 8,
  initialCategoryId
});
```

* **Spy.** `onScroll` lee `listRef.current.getFirstVisibleIndex()` y mapea esa fila a `rowSectionIds`. Puedes llamarlo sin evento. Para FlatList, pasa `active.onViewableItemsChanged`. Usa el índice visible más bajo.
* **Salto.** `selectCategory(id)` llama `scrollToIndex({ index: headerIndex.get(id), animated: true, viewOffset: offset })`. No esperes la promesa que devuelve FlashList.
* **Bloqueo.** Mientras el salto se anima, los eventos de scroll no mueven el resaltado. El bloqueo se suelta 140 ms después de que el scroll se detiene, o después de 1 s si la lista nunca se desplaza.
* **Deep link.** `initialCategoryId` es la sección activa antes del primer scroll. Si ese id sale de `headerIndex`, el id activo vuelve a la primera sección.

`registerCategory` no hace nada en este modo. Los primitivos `Category*` son vistas simples, así que se renderizan dentro de una celda sin otros cambios.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.