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

# Pagination

> How to page through large result sets efficiently

## Why Cursor-Based Pagination

Traditional offset pagination (`page=2&limit=20`) breaks down at scale. When a guest is browsing page 5 and a new dish gets added, suddenly they see duplicates or miss items entirely. For a dining app where menu availability changes in real-time, this creates a frustrating experience.

EveryBite uses **cursor-based pagination**—the same approach used by Twitter, Facebook, and Slack. Instead of "give me page 2", you say "give me the next 20 items after this cursor." The cursor is an opaque marker that points to a specific position in the result set, ensuring stable pagination even as data changes.

## How It Works

<Steps>
  <Step title="First Request">
    Request the first page of results:

    ```graphql theme={null}
    search(pagination: { first: 20 })
    ```

    Response includes dishes 1-20, plus `endCursor: "abc123"` and `hasNextPage: true`
  </Step>

  <Step title="Next Page">
    User scrolls down. Request the next page using the cursor:

    ```graphql theme={null}
    search(pagination: { first: 20, after: "abc123" })
    ```

    Response includes dishes 21-40, plus `endCursor: "def456"` and `hasNextPage: true`
  </Step>

  <Step title="Final Page">
    User scrolls again. Request the next page:

    ```graphql theme={null}
    search(pagination: { first: 20, after: "def456" })
    ```

    Response includes dishes 41-52 and `hasNextPage: false` — no more results.
  </Step>
</Steps>

Each response includes:

* **Results** — The dishes for this page
* **endCursor** — Pass this as `after` to get the next page
* **hasNextPage** — Whether more results exist

## Pagination Parameters

```graphql theme={null}
input PaginationArgs {
  first: Int      # Number of results to return when paginating forwards (default: 25, max: 100)
  after: String   # Cursor for next page (forward pagination)
  last: Int       # Number of results to return when paginating backwards
  before: String  # Cursor for previous page (backward pagination)
}
```

| Parameter | Type   | Default | Description                                                     |
| --------- | ------ | ------- | --------------------------------------------------------------- |
| `first`   | Int    | 25      | Number of results per page when paginating forwards (max 100)   |
| `after`   | String | —       | Cursor marking where to continue from when paginating forwards  |
| `last`    | Int    | —       | Number of results per page when paginating backwards            |
| `before`  | String | —       | Cursor marking where to continue from when paginating backwards |

## Page Info Response

Every paginated response includes a `pageInfo` object:

```graphql theme={null}
type PageInfo {
  hasNextPage: Boolean!      # More results available after endCursor
  hasPreviousPage: Boolean!  # Results exist before startCursor
  startCursor: String        # Cursor of first item in this page
  endCursor: String          # Cursor of last item—pass as `after` for next page
}
```

## Example: Infinite Scroll

The most common pattern—load more results as the user scrolls:

```typescript theme={null}
const [dishes, setDishes] = useState([]);
const [cursor, setCursor] = useState(null);
const [hasMore, setHasMore] = useState(true);

async function loadMore() {
  const { data } = await client.query({
    query: SMART_MENU_SEARCH,
    variables: {
      preferences: userPreferences,
      pagination: { first: 20, after: cursor }
    }
  });

  const { matches, pageInfo } = data.search;

  setDishes(prev => [...prev, ...matches]);
  setCursor(pageInfo.endCursor);
  setHasMore(pageInfo.hasNextPage);
}

// Trigger loadMore when user scrolls near bottom
```

## Example: Load More Button

For explicit pagination with a "Load More" button:

```graphql theme={null}
query SearchWithPagination($cursor: String) {
  search(
    preferences: { diets: [VEGETARIAN] }
    pagination: { first: 20, after: $cursor }
  ) {
    matches {
      dish { id, name, nutrition { calories } }
    }
    pageInfo {
      hasNextPage
      endCursor
    }
    counts { total }
  }
}
```

<Accordion title="Example Response">
  ```json theme={null}
  {
    "data": {
      "search": {
        "matches": [
          { "dish": { "id": "dish_021", "name": "Veggie Wrap", "nutrition": { "calories": 380 } } },
          { "dish": { "id": "dish_022", "name": "Garden Bowl", "nutrition": { "calories": 420 } } }
        ],
        "pageInfo": {
          "hasNextPage": true,
          "endCursor": "eyJpZCI6NDAsInMiOjEuMH0="
        },
        "counts": { "total": 52 }
      }
    }
  }
  ```
</Accordion>

## Pagination with Match Groups

When paginating search results, pagination applies across all match groups:

```graphql theme={null}
query PaginatedSearch($cursor: String) {
  search(
    preferences: { diets: [VEGETARIAN] }
    pagination: { first: 20, after: $cursor }
  ) {
    matches {
      dish { id, name }
      matchStatus
    }
    almostMatches {
      dish { id, name }
      matchReasons
    }
    pageInfo {
      hasNextPage
      endCursor
    }
  }
}
```

<Info>
  Results are returned in match quality order: all `matches` first, then `almostMatches`, then `notMatches`. Pagination respects this ordering.
</Info>

## Cursor Stability

Cursors remain valid for the duration of a session. However:

* **Cursors are opaque** — Don't parse or construct them; treat as strings
* **Cursors are session-scoped** — A cursor from one session won't work in another
* **Cursors expire** — After session timeout (30 min inactivity), start fresh

## Best Practices

<CardGroup cols={2}>
  <Card title="Request Reasonable Pages" icon="layer-group">
    20-50 items per page balances performance and UX. Avoid requesting 100 items if you only show 10.
  </Card>

  <Card title="Don't Store Cursors" icon="database">
    Cursors are ephemeral. Don't persist them to local storage or databases.
  </Card>

  <Card title="Handle Empty Pages" icon="rectangle-xmark">
    If `hasNextPage` is false and results are empty, show an appropriate empty state.
  </Card>

  <Card title="Show Total Count" icon="calculator">
    Use `counts.total` to show "Showing 20 of 52 dishes" for better UX.
  </Card>
</CardGroup>

## Why Not Offset Pagination?

| Issue           | Offset Pagination          | Cursor Pagination    |
| --------------- | -------------------------- | -------------------- |
| New items added | Duplicates or missed items | Stable results       |
| Items removed   | Skipped items              | Stable results       |
| Deep pages      | Slow (must skip N items)   | Fast (direct lookup) |
| Real-time data  | Inconsistent               | Consistent           |

For a menu that can change in real-time (dishes sold out, specials added), cursor pagination ensures guests see a consistent, complete list.
