A Comprehensive Guide to Building a Reusable useList Hook for CRUD Pages in Vue 3
This article explains how to create a generic useList hook for Vue 3 that handles pagination, data fetching, filtering, resetting, exporting, and customizable success/error hooks, complete with TypeScript generics and Element‑Plus UI integration.
When developing admin back‑ends, many pages share the same CRUD logic such as fetching list data, pagination, and filtering. Copy‑pasting code leads to high coupling, so extracting reusable functions or components is essential.
We will encapsulate a generic useList hook that can be used across most CRUD list pages, making development faster and allowing developers to leave on time.
Prerequisites
Vue 3
Vue Composition API
Hook Definition
The hook extracts common parameters and functions into a reusable composition function.
Pagination State
export default function useList() {
// loading state
const loading = ref(false);
// current page
const curPage = ref(1);
// total items
const total = ref(0);
// page size
const pageSize = ref(10);
}Fetching List Data
The useList function receives a listRequestFn parameter that performs the actual API request.
export default function useList<ItemType extends Object>(listRequestFn: Function) {
const list = ref<ItemType[]>([]);
// other code omitted
}Inside the hook we create a loadData function that calls the request function, updates list and total , and handles loading state.
const loadData = async (page = curPage.value) => {
loading.value = true;
try {
const { data, meta: { total: count } } = await listRequestFn(pageSize.value, page);
list.value = data;
total.value = count;
} catch (error) {
console.log("request error", error);
} finally {
loading.value = false;
}
};Pagination Watcher
Use watch to react to changes in curPage or pageSize and reload data.
watch([curPage, pageSize], () => {
loadData(curPage.value);
});Filter Support
Define filter fields in a ref and pass a filterOption object to the request function.
export default function useList<ItemType extends Object, FilterOption extends Object>(listRequestFn: Function, filterOption: Ref<Object>) {
const loadData = async (page = curPage.value) => {
loading.value = true;
try {
const { data, meta: { total: count } } = await listRequestFn(pageSize.value, page, filterOption.value);
list.value = data;
total.value = count;
} catch (error) {
console.log("request error", error);
} finally {
loading.value = false;
}
};
}Reset Filters
Use Reflect to set all filter fields to undefined and reload the list.
const reset = () => {
if (!filterOption.value) return;
const keys = Reflect.ownKeys(filterOption.value);
filterOption.value = {} as FilterOption;
keys.forEach(key => {
Reflect.set(filterOption.value!, key, undefined);
});
loadData();
};Export Functionality
Optionally provide an exportRequestFn that returns a download link; the hook exposes an exportFile function to trigger the download.
const exportFile = async () => {
if (!exportRequestFn) throw new Error("exportRequestFn not provided");
if (typeof exportRequestFn !== "function") throw new Error("exportRequestFn must be a function");
try {
const { data: { link } } = await exportRequestFn(filterOption.value);
window.open(link);
} catch (error) {
console.log("export failed", error);
}
};Customizable Hooks and Messages
Introduce an Options object that can contain success/failure callbacks and message strings. Default messages are provided.
export interface MessageType {
GET_DATA_IF_FAILED?: string;
GET_DATA_IF_SUCCEED?: string;
EXPORT_DATA_IF_FAILED?: string;
EXPORT_DATA_IF_SUCCEED?: string;
}
export interface OptionsType {
requestError?: () => void;
requestSuccess?: () => void;
exportError?: () => void;
exportSuccess?: () => void;
message: MessageType;
}Inside loadData and exportFile the hook now shows messages via Element‑Plus ElMessage and calls the provided callbacks.
Usage Example
In a Vue component, import the hook and bind it to a filter object, then use the returned state and methods in the template.
<template>
<el-collapse class="mb-6">
<el-collapse-item title="Filter" name="1">
<el-form :model="filterOption">
<el-form-item label="Username">
<el-input v-model="filterOption.name" placeholder="Filter by name" />
</el-form-item>
<el-form-item label="Register Time">
<el-date-picker v-model="filterOption.timeRange" type="daterange" format="YYYY-MM-DD HH:mm" />
</el-form-item>
<el-button type="primary" @click="filter">Filter</el-button>
<el-button type="primary" @click="reset">Reset</el-button>
</el-form>
</el-collapse-item>
</el-collapse>
<el-table :data="list" v-loading="loading"> ... </el-table>
<el-pagination v-model:current-page="curPage" v-model:page-size="pageSize" :total="total" />
</template>
<script setup lang="ts">
import useList from '@/lib/hooks/useList';
import { UserInfoApi } from '@/network/api/User';
const filterOption = ref<UserInfoApi.FilterOptionType>({});
const { list, loading, curPage, pageSize, total, loadData, reset, filter } =
useList<UserInfoApi.UserInfo[], UserInfoApi.FilterOptionType>(UserInfoApi.list, filterOption);
</script>The full source code of the useList hook is available on GitHub, and readers are encouraged to submit pull requests or comments for improvements.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.