Guides

Data

How to fetch data?

Data is the most important part of any app. It lets users see and update information from a database or API.

Fetcher Provider

Use a Fetcher provider to communicate with your backend. Think of it as a translator between your app and your API.

The Fetcher interface defines the contract:

interface Fetcher {
    getList: (props: GetListProps) => Promise<GetListResult>
    getMany: (props: GetManyProps) => Promise<GetManyResult>
    getOne: (props: GetOneProps) => Promise<GetOneResult>
    createOne: (props: CreateOneProps) => Promise<CreateResult>
    createMany: (props: CreateManyProps) => Promise<CreateManyResult>
    updateOne: (props: UpdateOneProps) => Promise<UpdateResult>
    updateMany: (props: UpdateManyProps) => Promise<UpdateManyResult>
    deleteOne: (props: DeleteOneProps) => Promise<DeleteOneResult>
    deleteMany: (props: DeleteManyProps) => Promise<DeleteManyResult>
    custom: (props: CustomProps) => Promise<CustomResult>
}

After setting up the Fetcher, you can use Data Composables (like useGetOne, useGetList) to work with your data. The framework handles the details, like telling the provider which data to get (using resource or id).

Ginjou supports localizing data operations by using multiple fetchers simultaneously (e.g., getting products from a REST API and users from a GraphQL API).
mermaid
flowchart LR
    A[App] -- Calls composables --> B[Data Composables]
    B -- Calls fetcher methods<br/>(via TanStack Query) --> C[Fetcher]
    C -- HTTP Request --> D[API]

Fetching Data

Let's say we want to get a record with ID 123 from the products list. We can use the useGetOne composable. It simply calls the fetcher.getOne method for you.

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

const { record, isFetching } = useGetOne({
    resource: 'products',
    id: '123',
})
</script>

<template>
    <div v-if="isFetching">
        Loading...
    </div>
    <div v-else>
        <h1>{{ record?.name }}</h1>
        <p>{{ record?.price }}</p>
    </div>
</template>

Updating Data

To update a record with ID 123 in products, use the useUpdateOne composable. This calls the fetcher.updateOne method.

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

const { mutate: update } = useUpdateOne({
    resource: 'products',
    id: '123',
})

function save() {
    update({
        params: { price: 2000 },
    })
}
</script>

Listing Data

If you need a list of records from products, use useGetList or useGetInfiniteList. These call fetcher.getList and give you the data and total count (total).

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

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

<template>
    <ul>
        <li v-for="item in records" :key="item.id">
            {{ item.name }}
        </li>
    </ul>
</template>

Filters, Sorters and Pagination

Usually, you don't want to load all the data. You probably want just some of it.

Using these parameters at the fetcher level ensures that data processing (like filtering and sorting) is handled by the backend, reducing client-side overhead.

Let's look at a more realistic example. Suppose we want to:

  • Get 5 products.
  • Where the material is wooden.
  • Sorted by ID (newest to oldest).

We can do this by passing filters, sorters, and pagination to useGetList.

useGetList then calls fetcher.getList with these settings, and your API request is updated accordingly.

You can also build more complex queries. For example, finding products that:

  • Are made of wood.
  • Belong to category ID 45.
  • OR have a price between 1000 and 2000.
<script setup lang="ts">
import { useGetList } from '@ginjou/vue'

const { records } = useGetList({
    resource: 'products',
    pagination: {
        current: 1,
        pageSize: 5,
    },
    filters: [
        {
            field: 'material',
            operator: 'eq',
            value: 'wooden',
        },
    ],
    sorters: [
        {
            field: 'id',
            order: 'desc',
        },
    ],
})
</script>

Multiple Fetchers

In Ginjou, you can use multiple fetchers in one app. This is great if you have different APIs.

Each fetcher has its own settings. This makes it easy to handle complex data.

For example, you might want to fetch:

  • products from https://api.products.example.com
  • users from https://api.users.example.com.

In the example below:

  • We define multiple fetchers.
  • We use fetcherName to tell the composable which one to use.
<script setup lang="ts">
import { useGetList } from '@ginjou/vue'

const { records: products } = useGetList({
    resource: 'products',
    fetcherName: 'products-api',
})

const { records: users } = useGetList({
    resource: 'users',
    fetcherName: 'users-api',
})
</script>

Relationships

Ginjou helps you manage connections between data using composables like useGetOne and useGetMany. This gives you a flexible way to handle relationships.

One-to-One

This is when one item matches exactly one other item. Like a partnership.

For example, a Product has only one Product Detail.

mermaid
erDiagram
    direction LR
    Products ||--o| ProductDetail : "has"
    Products {
        string id
        string name
        number price
        string description
    }
    ProductDetail {
        string id
        number weight
        string dimensions
        string productId
    }

Use useGetOne to get the details for a product.

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

// Get the main product
const { record: product } = useGetOne({
    resource: 'products',
    id: '123',
})

// Get the related detail
const { record: detail } = useGetOne({
    resource: 'product_details',
    id: '123', // Assuming shared ID strategy
})
</script>

One-to-Many

This is when one item is connected to many others. Like a parent with many children.

For example, a Product can have many Reviews.

mermaid
erDiagram
    direction LR
    Products ||--o{ Reviews : "has"
    Products {
        string id
        string name
        number price
        string description
        string detail
    }
    Reviews {
        string id
        number rating
        string comment
        string user
        string product
    }

Use useGetList and filter by the product ID to get its reviews.

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

const { records: reviews } = useGetList({
    resource: 'reviews',
    filters: [
        {
            field: 'productId',
            operator: 'eq',
            value: '123',
        },
    ],
})
</script>

Many-to-Many

This is when items from one group can match many items in another group, and vice versa.

For example, Products can have many Categories, and Categories can have many Products.

mermaid
erDiagram
    Products ||--o{ ProductCategories : "has"
    Categories ||--o{ ProductCategories : "has"
    Products {
        string id
        string name
        number price
        string description
        string detail
    }
    ProductCategories {
        string id
        string productId
        string categoryId
    }
    Categories {
        string id
        string name
        string description
    }

Here, use useGetMany twice: once to get the categories for a product, or once to get the products for a category.

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

// Assuming you have a list of category IDs from a product
const categoryIds = ['cat1', 'cat2', 'cat3']

const { records: categories } = useGetMany({
    resource: 'categories',
    ids: categoryIds,
})
</script>

Invalidating Data

When you create, update, or delete data, the cached query results become outdated. Invalidation marks these cached queries as stale so they are refetched from the server. This keeps your UI in sync with the backend.

Invalidation Targets

Specify which cached queries to refresh using these targets:

TargetDescription
allInvalidate all queries for a fetcher.
resourceInvalidate all queries for a specific resource.
listInvalidate only list queries.
manyInvalidate only "many" queries for specific IDs.
oneInvalidate only single-item queries for specific IDs.

Default Invalidation Behavior

Most mutation composables automatically invalidate relevant caches:

ComposableDefault Targets
useCreateOne, useCreateMany['list', 'many']
useUpdateOne, useUpdateMany['list', 'many', 'one']
useDeleteOne, useDeleteMany['list', 'many']

Customizing Invalidation

Override the default behavior by passing the invalidates prop:

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

// Only invalidate the specific item, not the list
const { mutate: update } = useUpdateOne({
    resource: 'products',
    id: '123',
    invalidates: ['one'],
})

function save() {
    update({ params: { price: 2000 } })
}
</script>

Disable Invalidation

To disable automatic invalidation and handle it manually:

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

const queryClient = useQueryClientContext()

const { mutate: update } = useUpdateOne({
    resource: 'products',
    id: '123',
    invalidates: false, // Disabled trigger invalidates
})

async function save() {
    await update({ params: { price: 2000 } })
}
</script>

Data Composables

ComposableMethodDescription
useGetOnegetOneGet a single record.
useUpdateOneupdateOneUpdate an existing record.
useCreateOnecreateOneCreate a new record.
useDeleteOnedeleteOneDelete a single record.
useGetListgetListGet a list of records.
useGetInfiniteListgetListGet a list of records with infinite scroll.
useGetManygetManyGet multiple records.
useCreateManycreateManyCreate multiple records.
useDeleteManydeleteManyDelete multiple records.
useUpdateManyupdateManyUpdate multiple records.
useCustomcustomMake custom API requests.
useCustomMutationcustomMake custom API mutation requests.