Guides

List

Displaying data lists.

Displaying data in lists is a core feature of most applications. Ginjou provides useList and useInfiniteList to make this easy.

List

useList is the go-to composable for fetching lists of data.

Composition:

  • State Management: Manages currentPage, perPage, filters, and sorters.
  • Data Hook: Uses useGetList to fetch data based on the current state.
  • Router Sync: Uses useGo and useLocation to sync state with the URL (if enabled).
mermaid
graph TD
    A[useList] --> B[useResource]
    A --> C[State Management]
    C --> |filters, sorters, pagination| D[useGetList]
    D --> |data, total| A
    C --> |syncRoute| E[useGo / useLocation]
    B -- Resource & Fetcher Name --> D
<script setup lang="ts">
import { useList } from '@ginjou/vue'

const { records, isFetching } = useList({
    resource: 'posts',
})
</script>

<template>
    <div v-if="isFetching">
        Loading...
    </div>
    <ul v-else>
        <li v-for="record in records" :key="record.id">
            {{ record.id }} - {{ record.title }}
        </li>
    </ul>
</template>

Infinite List

For "Load More" or Infinite Scroll interfaces, use useInfiniteList.

Composition:

  • State Management: Manages perPage (or limit), filters, and sorters. Note it does NOT use standard page-based pagination state like useList in the same way.
  • Data Hook: Uses useGetInfiniteList.
mermaid
graph TD
    A[useInfiniteList] --> B[useResource]
    A --> C[State Management]
    C --> |filters, sorters, perPage| D[useGetInfiniteList]
    D --> |data, hasNextPage| A
    C --> |syncRoute| E[useGo / useLocation]
    B -- Resource & Fetcher Name --> D
<script setup lang="ts">
import { useInfiniteList } from '@ginjou/vue'

const {
    records, // Note: This is a nested array of pages -> records
    hasNextPage,
    fetchNextPage,
    isFetching,
} = useInfiniteList({
    resource: 'posts',
    pagination: {
        perPage: 10,
    },
})
</script>

<template>
    <div v-for="(page, i) in records" :key="i">
        <div v-for="item in page" :key="item.id">
            {{ item.title }}
        </div>
    </div>

    <button
        v-if="hasNextPage"
        :disabled="isFetching"
        @click="fetchNextPage()"
    >
        {{ isFetching ? 'Loading...' : 'Load More' }}
    </button>
</template>

Pagination

useList provides currentPage, perPage, and pageCount refs.

Modes

You can control where pagination happens using pagination.mode:

  • server (Default): Parameters are sent to the API.
  • client: All data is expected to be available (or fetched once), and Ginjou slices the array in the browser.
  • off: Pagination is disabled.
<script setup lang="ts">
import { useList } from '@ginjou/vue'

const {
    records,
    currentPage,
    perPage,
    pageCount,
    total,
} = useList({
    resource: 'posts',
    pagination: {
        current: 1,
        perPage: 10,
        mode: 'server', // or 'client', 'off'
    }
})
</script>

<template>
    <!-- List rendering... -->

    <div class="pagination">
        <button :disabled="currentPage === 1" @click="currentPage--">
            Prev
        </button>
        <span>{{ currentPage }} / {{ pageCount }}</span>
        <button :disabled="currentPage === pageCount" @click="currentPage++">
            Next
        </button>
    </div>
</template>

Filters

Updates to the filters array trigger data refetches (or client-side filtering).

Modes

Controlled by filters.mode:

  • server (Default): Filters are sent to the API.
  • off: Filters are ignored/disabled.

(Note: Client-side filtering logic for useList is typically handled by the developer or specific helpers if mode: 'client' logic is needed, but the primary supported modes for the prop are server/off for API interaction)

<script setup lang="ts">
import { FilterOperator } from '@ginjou/core'
import { useList } from '@ginjou/vue'
import { reactive, unref, watch } from 'vue'

const { records, filters } = useList({
    resource: 'posts',
    filters: {
        mode: 'server',
    }
})

// ... form logic to update filters ...
</script>

Permanent

Use permanent to enforce constraints that users cannot remove. Permanent filters are always applied to queries, ensuring certain conditions are always met.

<script setup lang="ts">
import { FilterOperator } from '@ginjou/core'
import { useList } from '@ginjou/vue'

const { records } = useList({
    resource: 'posts',
    filters: {
        permanent: [
            {
                field: 'status',
                operator: FilterOperator.eq,
                value: 'published',
            }
        ],
        value: []
    }
})
</script>

Permanent filters are useful for:

  • Restricting lists to a specific user's data
  • Filtering to only active/published items
  • Enforcing multi-tenancy constraints
  • Applying organizational or role-based filters that users cannot override

Sorters

Modify sorters to change order.

Modes

Controlled by sorters.mode:

  • server (Default): Sort params sent to API.
  • off: Sorting disabled.
<script setup lang="ts">
import { useList } from '@ginjou/vue'

const { records, sorters } = useList({
    resource: 'posts',
    sorters: {
        mode: 'server',
        value: [
            { field: 'created_at', order: 'desc' },
        ],
    }
})
</script>

Permanent

Use permanent to enforce default sort orders that users cannot remove.

<script setup lang="ts">
import { useList } from '@ginjou/vue'

const { records, sorters } = useList({
    resource: 'posts',
    sorters: {
        permanent: [
            { field: 'created_at', order: 'desc' }
        ],
        value: []
    }
})
</script>

Permanent sorters are useful for:

  • Ensuring consistent default ordering (e.g., most recent first)
  • Combining multiple sort priorities
  • Maintaining required sort order while allowing users to add additional sorts

Sync With Route

syncRoute keeps your state in sync with the URL.

Enabling syncRoute is highly recommended for list pages, as it allows users to bookmark or share specific search results and pagination states.
<script setup lang="ts">
const { records } = useList({
    resource: 'posts',
    syncRoute: true,
})
// URL becomes: ?current=1&perPage=10...
</script>

Disable Fields

By default, all state fields (pagination, filters, and sorters) are synchronized with the URL. You can selectively disable synchronization for specific fields to keep them as local state.

Disable Pagination Sync:

const { records } = useList({
    resource: 'posts',
    syncRoute: {
        currentPage: false,
        perPage: false,
    }
})
// URL: /posts?filters=...&sorters=...

Disable Filters Sync:

const { records } = useList({
    resource: 'posts',
    syncRoute: {
        filters: false,
    }
})
// URL: /posts?current=1&perPage=10&sorters=...

Disable Sorters Sync:

const { records } = useList({
    resource: 'posts',
    syncRoute: {
        sorters: false,
    }
})
// URL: /posts?current=1&perPage=10&filters=...

Disable All Sync:

By default, syncRoute is disabled (no synchronization). You can explicitly enable or disable it using a boolean value as a quick toggle.

// Disable all sync (default behavior when syncRoute is not set)
const { records } = useList({
    resource: 'posts',
    syncRoute: false, // All state is local, no URL parameters
})
// URL: /posts
// Enable all sync with default field names
const { records } = useList({
    resource: 'posts',
    syncRoute: true, // Sync all state fields with default names
})
// URL: /posts?current=1&perPage=10&filters=...&sorters=...

Field Names

Change the query parameter names to customize URL appearance. This is useful for SEO, shorter URLs, or maintaining compatibility with existing URL schemes.

Customize Filter Parameter:

const { records } = useList({
    resource: 'posts',
    syncRoute: {
        filters: {
            field: 'q', // Changed from 'filters' to 'q'
        }
    }
})
// URL: /posts?current=1&perPage=10&q=...&sorters=...

Customize Pagination Parameter:

const { records } = useList({
    resource: 'posts',
    syncRoute: {
        currentPage: {
            field: 'page', // Changed from 'current'
        },
        perPage: {
            field: 'limit', // Changed from 'perPage'
        }
    }
})
// URL: /posts?page=1&limit=10&filters=...&sorters=...

Customize Sorter Parameter:

const { records } = useList({
    resource: 'posts',
    syncRoute: {
        sorters: {
            field: 'sort', // Changed from 'sorters' to 'sort'
        }
    }
})
// URL: /posts?current=1&perPage=10&filters=...&sort=...

Serialization/Deserialization

For complex filtering or sorting logic, you can customize how data is encoded to and decoded from URL query parameters using stringify and parse functions.

Custom Filters Serialization:

const { records } = useList({
    resource: 'posts',
    syncRoute: {
        filters: {
            field: 'search',
            stringify: (filters) => {
                // Convert filters to custom format: "status:published,author:john"
                return filters.map(f => `${f.field}:${f.value}`).join(',')
            },
            parse: (queryValue) => {
                // Parse custom format back to filter objects
                return queryValue.split(',').map((item) => {
                    const [field, value] = item.split(':')
                    return { field, operator: 'eq', value }
                })
            }
        }
    }
})
// URL: /posts?search=status:published,author:john

Custom Sorters Serialization:

const { records } = useList({
    resource: 'posts',
    syncRoute: {
        sorters: {
            field: 'sort',
            stringify: (sorters) => {
                // Convert sorters to format: "created_at-desc,title-asc"
                return sorters.map(s => `${s.field}-${s.order}`).join(',')
            },
            parse: (queryValue) => {
                // Parse back to sorter objects
                return queryValue.split(',').map((item) => {
                    const [field, order] = item.split('-')
                    return { field, order: order as 'asc' | 'desc' }
                })
            }
        }
    }
})
// URL: /posts?sort=created_at-desc,title-asc
Copyright © 2026