---
name: use_infinite_scroll
kind: component
lang: ts
domain: core
version: "1.0.0"
purity: impure
signature: "useInfiniteScroll<T>(opts: UseInfiniteScrollOptions<T>): UseInfiniteScrollResult<T>"
description: "Hook de paginacion infinita cursor-based y offset-based. Concatena paginas en array plano. Expone load_more, has_more y reset. Zero dependencias externas."
tags: [infinite, scroll, pagination, cursor, hook, http, core, component]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: [react]
params:
  - name: fetch_fn
    desc: "Funcion que recibe el cursor actual (string, number o null) y retorna Promise<{items, next_cursor}>"
  - name: initial_cursor
    desc: "Cursor inicial, null para empezar desde el principio (default null)"
  - name: enabled
    desc: "Si hace fetch automatico al montar (default true)"
output: "Objeto con data (array concatenado), loading, error, has_more, load_more() y reset()"
tested: false
tests: []
test_file_path: ""
file_path: "frontend/functions/core/use_infinite_scroll.tsx"
props:
  - name: fetch_fn
    type: "(cursor: string | number | null) => Promise<InfiniteScrollPage<T>>"
    required: true
    description: "Funcion de fetch que recibe cursor y retorna pagina con items y next_cursor"
  - name: initial_cursor
    desc: "Cursor inicial (default null)"
    type: "string | number | null"
    required: false
    description: "Cursor/offset inicial"
  - name: enabled
    type: "boolean"
    required: false
    description: "Fetch automatico al montar (default true)"
emits: []
has_state: true
framework: react
variant: [default]
---

## Ejemplo cursor-based

```tsx
import { useInfiniteScroll } from '@/functions/core/use_infinite_scroll'
import { apiClient } from '@/functions/core/api_client'

const api = apiClient({ base_url: 'http://localhost:8484/api' })

function EventsList() {
  const { data, loading, has_more, load_more } = useInfiniteScroll<Event>({
    fetch_fn: async (cursor) => {
      const res = await api.get<{ items: Event[]; next_cursor: string | null }>(
        '/events',
        cursor ? { after: String(cursor) } : undefined,
      )
      return { items: res.items, next_cursor: res.next_cursor }
    },
  })

  return (
    <div>
      {data.map((e) => <div key={e.id}>{e.name}</div>)}
      {has_more && (
        <button disabled={loading} onClick={load_more}>
          {loading ? 'Cargando...' : 'Cargar mas'}
        </button>
      )}
    </div>
  )
}
```

## Ejemplo offset-based

```tsx
const { data, load_more, has_more } = useInfiniteScroll<Product>({
  fetch_fn: async (cursor) => {
    const page = typeof cursor === 'number' ? cursor : 0
    const items = await api.get<Product[]>('/products', {
      offset: String(page * 20),
      limit: '20',
    })
    return {
      items,
      next_cursor: items.length === 20 ? page + 1 : null,
    }
  },
  initial_cursor: 0,
})
```

## Notas

- `InfiniteScrollPage.next_cursor = null` indica que no hay mas paginas
- `reset()` vuelve al cursor inicial y refetchea desde el principio
- `load_more` es no-op si `loading` o `!has_more`
