Ir al contenido

Composables: atoms y presets

La capa visual de Aurora es atómica — hlm-checkbox, hlm-input, hlm-button: cada pieza hace una sola cosa y las composiciones complejas se construyen pegándolas. La capa cerebro sigue el mismo principio: en lugar de un único composable que lo hace todo, la lógica se reparte en atoms de responsabilidad única, y los casos típicos se arman con presets opinados por encima. Esta página describe esa convención. El argumento de fondo (por qué composición y no herencia) vive en Composición vs herencia.

Un atom es un composable con exactamente un concern, sin composición interna de otros composables, sin imports de UI, y una forma retornable hecha de signals readonly más setters explícitos. Si un atom necesitaría importar otro atom para hacer su trabajo, no es atom — es preset.

Un preset es la composición opinada. Toma los atoms que necesita (o los instancia con defaults), conecta sus reglas transversales (por ejemplo, “cuando el atom de búsqueda cambia, resetea la paginación a la página 0 antes de refrescar”) y expone una API unificada adaptada a un caso típico.

Esta disciplina previene el pegamento prematuro. useTableSort no sabe nada de useTablePagination; solo guarda un signal SortingState. Que “cambiar el sort dispare un refetch y resetee el page index” es una propiedad del caso “lista paginada en servidor”, no de ninguno de los dos atoms — por eso vive en usePaginatedDataTable, el preset.

Los atoms viven bajo @aurora/composables/atoms/<subdominio>/:

  • atoms/data-table/ — primitivas de estado de tabla (búsqueda, sort, paginación, filtros, selección, visibilidad de columna, orden de columna, exportación, carga de datos).
  • atoms/graphql/ — un composable por operación GraphQL, cada uno delegando en un fetcher puro bajo @aurora/modules/graphql/fetchers/.

Los presets viven planos bajo @aurora/composables/presets/. Son menos numerosos y a menudo cruzan subdominios (un preset puede componer atoms de data-table y de graphql), así que una taxonomía más profunda añade fricción sin valor.

La cadena de barrels reexporta todo desde el alias raíz, así que los consumers escriben:

import { useTableSort, usePaginatedDataTable, useGraphqlList } from '@aurora';

sin tener que conocer la ruta interna.

useDataTable es el preset que conecta los atoms con TanStack. Acepta atoms (o los instancia con defaults) y produce un Table<T> cuyos state.sorting, state.pagination, state.columnFilters, state.rowSelection, state.columnVisibility y state.columnOrder están enlazados a las señales del atom. Los callbacks onXxxChange de TanStack delegan en los setters de los atoms. No hay signals propios dentro de useDataTable — el estado vive una sola vez, en los atoms, y el binding es solo cableado.

AtomResponsabilidad única
useTableSearchQuery de búsqueda libre más un signal derivado con debounce para refetch.
useTableSortSortingState compatible con TanStack.
useTablePagination{ pageIndex, pageSize } más accessors offset / limit.
useTableFiltersColumnFilter[] con la forma de Catalyst (consumido por buildFilterWhere).
useTableSelectionSet<string> de ids seleccionados; persiste a través de cambios de datos cuando se cablea getRowId.
useTableColumnVisibilityVisibilityState en memoria. La persistencia se delega a un wrapper aparte.
useTableColumnOrderColumnOrderState.
useTableExportHelpers puros — generan blobs CSV/XLS y disparan la descarga. Sin estado.
useTableData<T>Carga server-paginated vía un callback paginate; soporta seed() para prefetch desde resolver.

Cada atom delega en su fetcher bajo @aurora/modules/graphql/fetchers/. La responsabilidad del atom se limita al ciclo de vida del signal loading; ningún apollo.query / apollo.mutate vive dentro de un atom.

AtomFetcher en el que delegaQué hace
useGraphqlPaginatequeryPaginateLectura paginada de lista.
useGraphqlGetqueryGetLectura multi-registro sin paginar.
useGraphqlFindqueryFindLectura de un registro por criterio.
useGraphqlFindByIdqueryFindByIdLectura de un registro por id.
useGraphqlCreatemutateCreateCreación de una sola fila.
useGraphqlInsertmutateInsertCreación en bulk — diseñado para agregados pivot.
useGraphqlUpdatemutateUpdateActualización.
useGraphqlDeleteByIdmutateDeleteByIdBorrado por PK simple.
useGraphqlDeleteByKeysmutateDeleteByKeysBorrado por clave compuesta — agregados pivot con PK multi-columna.
useGraphqlDeletemutateDeleteBorrado en bulk basado en where.
PresetComponeÚsalo cuando
useDataTableLos atoms de data-table (o sus defaults) → Table<T> de TanStack.Quieres una tabla cuyo estado se gestiona vía atoms, sin escribir el cableado.
usePaginatedDataTableTodos los atoms de data-table + useTableData + useDataTable.Listas server-paginated. Sustituye el patrón inline useDataTable + signals + fetchData.
useStaticDataTableAtoms de data-table + useDataTable configurado para sort y paginación cliente-side.Tablas cliente-side con las filas ya en memoria. Sin fetch.
useGraphqlListuseGraphqlPaginate + useGraphqlDeleteById.Cableado graphql de una página de lista (paginar + borrar por id).
useGraphqlDetailuseGraphqlFindById + useGraphqlCreate + useGraphqlUpdate.Cableado graphql de una página de detalle (lectura + crear + actualizar).
useAggregateShelluseGraphqlList + useGraphqlDetail.Página completa de un agregado con vista de lista y detalle compartiendo shell.
usePivotMembershipuseGraphqlGet + useGraphqlInsert + useGraphqlDelete.La capa de membership de un many-to-many: set completo de ids, link(), unlink(), refresh().
useRelationshipPivotDos usePaginatedDataTable (linked + candidates) + un usePivotMembership.Pantalla de manager many-to-many: dos tablas coordinadas más mutaciones del pivot.

Tras un link() o unlink() exitoso, useRelationshipPivot invoca automáticamente linked.refetch(). La orquestación está en el preset; los atoms se mantienen limpios.

  • Estás escribiendo una nueva página de lista paginada en servidor. Empieza por usePaginatedDataTable + seed(). Los handlers (onSearch, onFiltersChange, …) se reducen a one-liners.
  • Estás escribiendo un manager many-to-many (los permisos de un rol, las tags de un usuario). Empieza por useRelationshipPivot — las dos tablas coordinadas, la membership del pivot y el auto-refetch ya están conectados.
  • Necesitas una composición personalizada que no encaja en ningún preset (kanban, virtual list, tree table). Coge los atoms directamente y escribe la orquestación inline; promuévela a preset solo cuando un segundo consumer necesite la misma orquestación.
  • Tienes una lista con el cableado inline useDataTable + sortSignal + paginationSignal + filtersSignal + fetchData(). Ese bloque es el smell — sustitúyelo por usePaginatedDataTable. Espera perder unas 25 líneas por archivo.
  • La atomización para en “aparece en dos presets”. Un concern que solo aflora en un preset se queda inline. Atomizar especulativamente contamina atoms/ con primitivas que nadie reusa.
  • La persistencia no es problema del atom. useTableColumnVisibility y useTableColumnOrder son in-memory. Si necesitas persistir el layout por gridId, envuelve el atom en un composable separado que hable con column-config-storage. El atom se mantiene puro.
  • useDataTable no es propietario de estado. Es puro cableado. Para leer sort, paginación o cualquier otro estado, hazlo desde el atom correspondiente — pasado por el caller o reexpuesto por el preset en su retorno.
  • grid-select-multiple-elements exige un cableado específico. Los barrels de @aurora mantienen los imports resolviendo de forma transparente, así que el resto de componentes no requieren atención. La excepción es este: es un manager many-to-many con outputs (linkRequested) / (unlinkRequested), y el template que lo monta debe cablear esos outputs. Solo emite la intención porque no sabe dónde persistir el vínculo: la pertenencia se muta al instante, fuera del ciclo create/update del formulario, así que el orquestador host hace la mutación y refresca los datos.