Frontend Development 13 min read

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.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
A Comprehensive Guide to Building a Reusable useList Hook for CRUD Pages in Vue 3

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.

typescriptVuepaginationComposition APIHookfilteringExport
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.