Data is the most important part of any app. It lets users see and update information from a database or API.
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).
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>
<!-- WIP -->
<script>
// ...
</script>
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>
<!-- WIP -->
<script>
// ...
</script>
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>
<!-- WIP -->
<script>
// ...
</script>
Usually, you don't want to load all the data. You probably want just some of it.
Let's look at a more realistic example. Suppose we want to:
material is wooden.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:
<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>
<!-- WIP -->
<script>
// ...
</script>
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.comusers from https://api.users.example.com.In the example below:
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>
<!-- WIP -->
<script>
// ...
</script>
Ginjou helps you manage connections between data using composables like useGetOne and useGetMany. This gives you a flexible way to handle relationships.
This is when one item matches exactly one other item. Like a partnership.
For example, a Product has only one Product Detail.
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>
<!-- WIP -->
<script>
// ...
</script>
This is when one item is connected to many others. Like a parent with many children.
For example, a Product can have many Reviews.
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>
<!-- WIP -->
<script>
// ...
</script>
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.
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>
<!-- WIP -->
<script>
// ...
</script>
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.
Specify which cached queries to refresh using these targets:
| Target | Description |
|---|---|
all | Invalidate all queries for a fetcher. |
resource | Invalidate all queries for a specific resource. |
list | Invalidate only list queries. |
many | Invalidate only "many" queries for specific IDs. |
one | Invalidate only single-item queries for specific IDs. |
Most mutation composables automatically invalidate relevant caches:
| Composable | Default Targets |
|---|---|
useCreateOne, useCreateMany | ['list', 'many'] |
useUpdateOne, useUpdateMany | ['list', 'many', 'one'] |
useDeleteOne, useDeleteMany | ['list', 'many'] |
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>
<!-- WIP -->
<script>
// ...
</script>
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>
<!-- WIP -->
<script>
// ...
</script>
| Composable | Method | Description |
|---|---|---|
useGetOne | getOne | Get a single record. |
useUpdateOne | updateOne | Update an existing record. |
useCreateOne | createOne | Create a new record. |
useDeleteOne | deleteOne | Delete a single record. |
useGetList | getList | Get a list of records. |
useGetInfiniteList | getList | Get a list of records with infinite scroll. |
useGetMany | getMany | Get multiple records. |
useCreateMany | createMany | Create multiple records. |
useDeleteMany | deleteMany | Delete multiple records. |
useUpdateMany | updateMany | Update multiple records. |
useCustom | custom | Make custom API requests. |
useCustomMutation | custom | Make custom API mutation requests. |