diff --git a/biome.json b/biome.json index 249aa9d3..57754593 100644 --- a/biome.json +++ b/biome.json @@ -146,6 +146,10 @@ }, "suspicious": { "noExplicitAny": "off" + }, + "a11y": { + "useKeyWithClickEvents": "off", + "useKeyWithMouseEvents": "off" } } }, diff --git a/docs/scripts/generate-hooks-data.ts b/docs/scripts/generate-hooks-data.ts index 6ecb7f2f..e1f5f082 100644 --- a/docs/scripts/generate-hooks-data.ts +++ b/docs/scripts/generate-hooks-data.ts @@ -9,7 +9,7 @@ const __filename = fileURLToPath(import.meta.url) const __dirname = dirname(__filename) const hooksSrc = resolve(__dirname, '../../packages/react-use/src') -const ignoredDirs = ['utils', 'use-track-ref-state', 'use-versioned-action', 'use-web-observer'] +const ignoredDirs = ['utils', 'use-track-ref-state', 'use-web-observer'] const dirents = await fs.readdir(hooksSrc, { withFileTypes: true }) const hooksDirents = dirents.filter((d) => d.isDirectory() && ignoredDirs.every((e) => e !== d.name)) diff --git a/packages/react-use/src/index.ts b/packages/react-use/src/index.ts index e5817483..46dd7835 100644 --- a/packages/react-use/src/index.ts +++ b/packages/react-use/src/index.ts @@ -51,6 +51,7 @@ export * from './use-fullscreen' export * from './use-geolocation' export * from './use-getter-ref' export * from './use-hover' +export * from './use-infinite-list' export * from './use-infinite-scroll' export * from './use-input-composition' export * from './use-intersection-observer' diff --git a/packages/react-use/src/use-async-effect/index.zh-cn.mdx b/packages/react-use/src/use-async-effect/index.zh-cn.mdx index a3041337..25be2211 100644 --- a/packages/react-use/src/use-async-effect/index.zh-cn.mdx +++ b/packages/react-use/src/use-async-effect/index.zh-cn.mdx @@ -18,7 +18,7 @@ import { HooksType } from '@/components' ::: -## 场景 Scenes \{#scenes} +## 场景 \{#scenes} - **异步数据请求场景:** 实现页面加载时或依赖项更改时的异步数据请求 - **状态更新监控场景:** 在依赖项状态更新后执行异步状态同步更新操作 diff --git a/packages/react-use/src/use-boolean/index.zh-cn.mdx b/packages/react-use/src/use-boolean/index.zh-cn.mdx index 1749358c..4ee6d056 100644 --- a/packages/react-use/src/use-boolean/index.zh-cn.mdx +++ b/packages/react-use/src/use-boolean/index.zh-cn.mdx @@ -18,7 +18,7 @@ import { HooksType, Since } from '@/components' ::: -## 场景 Scenes \{#scenes} +## 场景 \{#scenes} - **控制元素显隐场景:** 用于控制页面元素如对话框、下拉菜单的显示和隐藏 - **表单开关输入场景:** 管理表单中开关类型输入(如复选框)的状态 diff --git a/packages/react-use/src/use-clamp/index.zh-cn.mdx b/packages/react-use/src/use-clamp/index.zh-cn.mdx index 7eab5c59..9d5480b5 100644 --- a/packages/react-use/src/use-clamp/index.zh-cn.mdx +++ b/packages/react-use/src/use-clamp/index.zh-cn.mdx @@ -12,7 +12,7 @@ import { HooksType } from '@/components' 本质上,它只是设置了 `min` 和 `max` 选项的 [useCounter](/reference/use-counter) 的更加语意化的版本。 -## 场景 Scenes \{#scenes} +## 场景 \{#scenes} - **处理数量计算场景:** 提供增加、减少、设置、获取、重置计数器的功能 - **界面交互场景:** 实现用户界面上数量的动态更新和显示,如购物车商品数量、轮播图切换 diff --git a/packages/react-use/src/use-cloned-state/index.zh-cn.mdx b/packages/react-use/src/use-cloned-state/index.zh-cn.mdx index 0889e41c..ebfac928 100644 --- a/packages/react-use/src/use-cloned-state/index.zh-cn.mdx +++ b/packages/react-use/src/use-cloned-state/index.zh-cn.mdx @@ -10,7 +10,7 @@ import { HooksType } from '@/components' 一个用来创建支持修改、同步操作、相互隔离的克隆状态的 React Hook,支持自定义 `clone` 函数,默认使用 `JSON.parse(JSON.stringify(source))`。 -## 场景 Scenes \{#scenes} +## 场景 \{#scenes} - **数据状态克隆与隔离场景:** 实现数据的深拷贝,创建独立状态,用于编辑不影响原始数据 - **编辑历史记录场景:** 维护数据的编辑历史,支持撤销、重做功能 diff --git a/packages/react-use/src/use-counter/index.zh-cn.mdx b/packages/react-use/src/use-counter/index.zh-cn.mdx index 3767d43a..f0132e9d 100644 --- a/packages/react-use/src/use-counter/index.zh-cn.mdx +++ b/packages/react-use/src/use-counter/index.zh-cn.mdx @@ -10,7 +10,7 @@ import { HooksType } from '@/components' 一个提供包含增加、减少和重置功能的计数器的 React Hook。 -## 场景 Scenes \{#scenes} +## 场景 \{#scenes} - **处理数量计算场景:** 提供增加、减少、设置、获取、重置计数器的功能 - **界面交互场景:** 实现用户界面上数量的动态更新和显示,如购物车商品数量、轮播图切换 diff --git a/packages/react-use/src/use-infinite-list/demo.tsx b/packages/react-use/src/use-infinite-list/demo.tsx new file mode 100644 index 00000000..d9b9ebf7 --- /dev/null +++ b/packages/react-use/src/use-infinite-list/demo.tsx @@ -0,0 +1,221 @@ +import { Button, Card, Input, KeyValue, Zone, cn, wait } from '@/components' +import { generateLoremIpsum, useInfiniteList, useUpdateEffect } from '@shined/react-use' +import { useRef } from 'react' + +interface Data { + data: { id: number; name: string }[] + total: number +} + +const genders = ['Boy', 'Girl'] as const +const colors = ['Red', 'Orange', 'Yellow', 'Green', 'Cyan', 'Blue', 'Violet'] as const + +export function App() { + const ref = useRef(null) + + const { form, list, fullList, paginationState, selection, loading, isLoadDone } = useInfiniteList< + Data, + Data['data'][number], + { name: string; gender: string; color: string[] } + >({ + target: ref, + fetcher: fetchPagination, + mapFullList: (d) => d.data, + canLoadMore: (previousData, dataList, fullList) => { + if (!previousData) return true // initial load + return fullList.length < previousData.total + }, + form: { + initialValue: { + name: '', + gender: 'Boy', + color: ['Red'], + }, + }, + pagination: { pageSize: 10 }, + immediateQueryKeys: ['color', 'gender'], + }) + + // when you use third-party components, you can use `selection.isPartiallySelected` directly + useUpdateEffect(() => { + const selectAllInput = document.querySelector('input[name="select-all"]') as HTMLInputElement + selectAllInput.indeterminate = selection.isPartiallySelected + }, [selection.isPartiallySelected]) + + return ( + +

1. Scroll to Load More

+
+ + + Name: + + + + + + Gender: + {genders.map((gender) => ( + + ))} + + + Color: + {colors.map((color) => ( + + ))} + + +
+ + + + + e.data).length} /> + + + + + + + + + +
+ {fullList.map((item) => { + return ( +
selection.toggle(item)} + > + { + selection.toggle(item) + }} + checked={selection.isItemSelected(item)} + /> + {item.id} - {item.name} +
+ ) + })} + + {loading &&
Loading...
} + {isLoadDone &&
No more data
} +
+ + + {selection.selected.map((item) => ( +
{item.name}
+ ))} + {selection.selected.length === 0 &&
No selected
} +
+ +
+ +

2. Click to Load More

+ +
+ ) +} + +function LoadMoreList() { + const { loadMore, fullList, loading, isLoadDone } = useInfiniteList({ + fetcher: fetchPagination, + mapFullList: (d) => d.data, + canLoadMore: (previousData, dataList, fullList) => { + if (!previousData) return true // initial load + return fullList.length < previousData.total + }, + }) + + return ( +
+ {fullList.map((item) => { + return ( +
+ {item.id} - {item.name} +
+ ) + })} + + {isLoadDone ? ( +
No more data
+ ) : ( + + )} +
+ ) +} + +async function fetchPagination(params: { page: number; pageSize: number }): Promise { + await wait(600) + + const total = 57 + const isLastPage = params.page * params.pageSize >= total && (params.page - 1) * params.pageSize < total + + const startIdx = (params.page - 1) * params.pageSize + const returnLength = isLastPage ? total - startIdx : params.pageSize + + return { + data: Array.from({ length: returnLength }).map((_, i) => ({ + id: startIdx + i + 1, + name: generateLoremIpsum(), + })), + total, + } +} diff --git a/packages/react-use/src/use-infinite-list/index.mdx b/packages/react-use/src/use-infinite-list/index.mdx new file mode 100644 index 00000000..d48261cc --- /dev/null +++ b/packages/react-use/src/use-infinite-list/index.mdx @@ -0,0 +1,176 @@ +--- +category: ProUtilities +--- + +# useInfiniteList + +import { HooksType, Since } from '@/components' + + + + + +A React Hook designed to handle infinite lists, supporting scenarios such as infinite scrolling, click-to-load-more, and equipped with features for data fetching, bottom detection, form management, pagination, multi-selection, and loading status. + +`useInfiniteList` is a higher-order encapsulation of the following Hooks: + +- [useInfiniteScroll](/reference/use-infinite-scroll) offers bottom detection and data loading completion checks. +- [useForm](/reference/use-form) provides form management functionalities for search, filtering, etc. +- [useAsyncFn](/reference/use-async-fn) offers data fetching and lifecycle management functionalities. +- [usePagination](/reference/use-pagination) manages pagination states and parameters. +- [useMultiSelect](/reference/use-multi-select) provides multi-selection states, commonly used for batch operations. + +## Scenes \{#scenes} + +`useInfiniteList` = Infinite Scrolling / Click-to-Load + Automatic Pagination Management + Form Filtering (Optional) + Multi-Select Operations (Optional) + +## Demo \{#demo} + +import { App } from './demo' + + + +## Usage \{#usage} + +```tsx +interface Item { id: number; name: string; } +interface Data { data: Item[]; total: number; } +interface FormState { name: string; gender: string; color: string[]; } + +const ref = useRef(null) // The target element for bottom detection, needed only for scroll loading + +const { + list, // The list of return values composed of the data returned by fetcher, Data[] + fullList, // The item list formed by unfolding the list in the data returned by fetcher, Item[] + form, // Form state and operations + selection, // Multi-selection state and operations + paginationState, // Pagination state, information of the requested page + loading, // Loading state + isLoadDone // Whether the loading is completed +} = useInfiniteList({ + target: ref, // The target element for bottom detection, needed only for scroll loading + fetcher: fetchPagination, // Function to fetch data + mapFullList: (d) => d.data, // Map function for obtaining data list from every return + canLoadMore: (previousData, dataList, fullList) => { + if (!previousData) return true // If there is no result from the last request, it means it's the first load + // Continue loading if the current list length is less than the total, otherwise stop loading + return fullList.length < previousData.total + }, + form: { // Configure useForm + initialValue: { + name: '', + gender: 'Boy', + color: ['Red'], + }, + }, + pagination: { pageSize: 10 }, // Configure usePagination + immediateQueryKeys: ['color', 'gender'], // Form fields for immediate querying +}) + +return ( +
+ +
+ +
+
+) +``` + +## Source \{#source} + +import { Source } from '@/components' + + + +## API \{#api} + +```tsx +const { + list, fullList, form, selection, + paginationState, loading, isLoadDone +} = useInfiniteList(options) +``` + +### Options \{#options} + +```tsx +export interface UseInfiniteListOptions< + Data, + Item, + FormState extends object, + Fetcher extends AnyFunc, + Container extends HTMLElement, +> { + /** + * The container element for bottom detection + */ + target?: ElementTarget + /** + * Data fetching function, able to return data similar to { data: Item[], total: number } + */ + fetcher?: Fetcher + /** + * Data mapping function, for obtaining the data list from each return + */ + mapFullList?: (data: Data) => Item[] + /** + * Form options, for configuring `useForm`, refer to `useForm` for more details + */ + form?: UseFormOptions + /** + * Asynchronous function options, for configuring `useAsyncFn`, refer to `useAsyncFn` for more details + */ + asyncFn?: Omit, 'initialParams'> + /** + * Pagination data options, for configuring `usePagination`, refer to `usePagination` for more details + */ + pagination?: UsePaginationOptions + /** + * Function to decide whether more items can be loaded + */ + canLoadMore?: (previousData: Data | undefined, dataList: Data[], fullList: Item[]) => boolean + /** + * Scroll loading options, for configuring `useInfiniteScroll`, refer to `useInfiniteScroll` for more details + */ + infiniteScroll?: Omit, 'canLoadMore'> + /** + * Immediate query form fields, when form fields change, reset data and start a new round of queries + */ + immediateQueryKeys?: (keyof FormState)[] +} +``` + +### Returns \{#returns} + +The return value includes [useInfiniteScroll](/reference/use-infinite-scroll)'s return value, refer to [useInfiniteScroll](/reference/use-infinite-scroll) for more details. + +```tsx +export interface UseInfiniteListReturns + extends Omit { + /** + * Resets all states and restarts querying + */ + reset: () => void + /** + * Form state and operations + */ + form: UseFormReturns + /** + * Loading state + */ + list: Data[] + /** + * Whether the loading is completed + */ + fullList: Item[] + /** + * Multi-selection state and operations + */ + selection: UseMultiSelectReturnsState & UseMultiSelectReturnsActions + /** + * Pagination state + */ + paginationState: UsePaginationReturnsState +} +``` diff --git a/packages/react-use/src/use-infinite-list/index.ts b/packages/react-use/src/use-infinite-list/index.ts new file mode 100644 index 00000000..e964d166 --- /dev/null +++ b/packages/react-use/src/use-infinite-list/index.ts @@ -0,0 +1,257 @@ +import { useMemo, useRef } from 'react' +import { useAsyncFn } from '../use-async-fn' +import { useForm } from '../use-form' +import { useInfiniteScroll } from '../use-infinite-scroll' +import { useLatest } from '../use-latest' +import { useMultiSelect } from '../use-multi-select' +import { usePagination } from '../use-pagination' +import { useStableFn } from '../use-stable-fn' +import { useTrackedRefState } from '../use-tracked-ref-state' +import { shallowEqual } from '../utils/equal' + +import type { UseAsyncFnOptions } from '../use-async-fn' +import type { UseFormOptions, UseFormReturns } from '../use-form' +import type { UseInfiniteScrollOptions, UseInfiniteScrollReturns } from '../use-infinite-scroll' +import type { UseMultiSelectReturnsActions, UseMultiSelectReturnsState } from '../use-multi-select' +import type { UsePaginationOptions, UsePaginationReturnsState } from '../use-pagination' +import type { ElementTarget } from '../use-target-element' +import type { AnyFunc } from '../utils/basic' + +export interface UseInfiniteListOptions< + Data, + Item, + FormState extends object, + Fetcher extends AnyFunc, + Container extends HTMLElement, +> { + /** + * The container element + */ + target?: ElementTarget + /** + * The fetcher function, should return a object with data item list. + */ + fetcher?: Fetcher + /** + * The map function to map each data to item list + */ + mapFullList?: (data: Data) => Item[] + /** + * The form options + * + * see `useForm` for more details + */ + form?: UseFormOptions + /** + * The async function options + * + * see `useAsyncFn` for more details + */ + asyncFn?: Omit, 'initialParams'> + /** + * The pagination options + * + * see `usePagination` for more details + */ + pagination?: UsePaginationOptions + /** + * Whether can load more + */ + canLoadMore?: (previousData: Data | undefined, dataList: Data[], fullList: Item[]) => boolean + /** + * The infinite scroll options + * + * see `useInfiniteScroll` for more details + */ + infiniteScroll?: Omit, 'canLoadMore'> + /** + * The keys of form state that will trigger a new query when changed + */ + immediateQueryKeys?: (keyof FormState)[] +} + +export interface UseInfiniteListFetcherParams { + /** + * previous data + */ + previousData: Data | undefined + /** + * current page + */ + page: number + /** + * page size + */ + pageSize: number + /** + * form state + */ + form: FormState +} + +export type UseInfiniteListFetcher = ( + params: UseInfiniteListFetcherParams, +) => Promise + +export interface UseInfiniteListReturns + extends Omit { + /** + * reset the list, form, pagination, selection, and refetch the data + */ + reset: () => void + /** + * The form state + */ + form: UseFormReturns + /** + * The list data + */ + list: Data[] + /** + * The full list data + */ + fullList: Item[] + /** + * The selection state and action + */ + selection: UseMultiSelectReturnsState & UseMultiSelectReturnsActions + /** + * The pagination state + */ + paginationState: UsePaginationReturnsState +} + +/** + * + * @since 1.7.0 + */ +export function useInfiniteList< + Data, + Item = any, + FormState extends object = object, + Fetcher extends UseInfiniteListFetcher = UseInfiniteListFetcher, + Container extends HTMLElement = HTMLElement, +>( + options: UseInfiniteListOptions = {}, +): UseInfiniteListReturns { + const { target, fetcher = () => {} } = options + + const [{ dataList }, { updateRefState }, stateRef] = useTrackedRefState<{ dataList: Data[] }>({ dataList: [] }) + const previousDataRef = useRef(undefined) + const previousFormRef = useRef((options.form?.initialValue || {}) as FormState) + + const form = useForm({ + ...options.form, + onChange: (form, ...args) => { + const nextForm = form + const keys = options.immediateQueryKeys || [] + + for (const key of keys) { + const isChanged = !shallowEqual(previousFormRef.current[key], nextForm[key]) + + if (isChanged) { + reset() + break + } + } + + previousFormRef.current = nextForm + + return latest.current.options.form?.onChange?.(form, ...args) + }, + onSubmit: (form) => { + reset() + return latest.current.options.form?.onSubmit?.(form) + }, + onReset: () => { + reset() + return latest.current.options.form?.onReset?.() + }, + }) + + const loadFn = useAsyncFn(fetcher as Fetcher, { + ...options.asyncFn, + immediate: !options.target, + initialParams: [ + { + page: options.pagination?.page ?? 1, + pageSize: options.pagination?.pageSize ?? 10, + form: form.value, + previousData: undefined, + }, + ] as Parameters, + onSuccess(data, ...reset) { + updateRefState('dataList', [...stateRef.dataList.value, data]) + paginationActions.next() + previousDataRef.current = data + return latest.current.options.asyncFn?.onSuccess?.(data, ...reset) + }, + }) + + const infiniteScroll = useInfiniteScroll( + target, + () => { + return loadFn.run({ + form: form.value, + page: latest.current.paginationState.page ?? 10, + pageSize: latest.current.paginationState.pageSize ?? 10, + previousData: previousDataRef.current, + }) + }, + { + ...options.infiniteScroll, + canLoadMore: (pre) => { + const dataList = stateRef.dataList.value + const fullList = dataList.flatMap((data) => latestMapFullList.current?.(data) ?? []) as Item[] + return latest.current.options.canLoadMore?.(pre, dataList, fullList) ?? true + }, + }, + ) + + const reset = useStableFn(() => { + loadFn.cancel() + previousDataRef.current = undefined + updateRefState('dataList', []) + selectActions.setSelected([]) + paginationActions.go(1) + infiniteScroll.reset() + !latest.current.options.target && infiniteScroll.loadMore() + }) + + const latestMapFullList = useLatest(options.mapFullList) + + const fullList = useMemo(() => { + return dataList.flatMap((data) => latestMapFullList.current?.(data) ?? []) as Item[] + }, [dataList]) + + const [selectState, selectActions] = useMultiSelect(fullList) + + const [paginationState, paginationActions] = usePagination({ + ...options.pagination, + onPageSizeChange: (paging) => { + reset() + return latest.current.options.pagination?.onPageSizeChange?.(paging) + }, + }) + + const latest = useLatest({ + dataList, + fullList, + options, + selectState, + paginationState, + }) + + return { + ...infiniteScroll, + reset, + form, + list: dataList, + fullList, + paginationState, + selection: { + ...selectState, + ...selectActions, + }, + } +} diff --git a/packages/react-use/src/use-infinite-list/index.zh-cn.mdx b/packages/react-use/src/use-infinite-list/index.zh-cn.mdx new file mode 100644 index 00000000..49196543 --- /dev/null +++ b/packages/react-use/src/use-infinite-list/index.zh-cn.mdx @@ -0,0 +1,176 @@ +--- +category: ProUtilities +--- + +# useInfiniteList + +import { HooksType, Since } from '@/components' + + + + + +一个用来处理无限列表的 React Hook,支持无限滚动、点击加载更多等场景,内置了数据请求、探底检测、表单管理、分页、多选、加载状态等功能。 + +`useInfiniteList` 是以下几个 Hooks 的上层封装: + +- [useInfiniteScroll](/reference/use-infinite-scroll) 提供探底检测、数据是否加载完毕等功能 +- [useForm](/reference/use-form) 提供表单管理功能,用于搜索、筛选等 +- [useAsyncFn](/reference/use-async-fn) 提供数据请求、生命周期等功能 +- [usePagination](/reference/use-pagination) 提供分页状态,管理分页查询参数 +- [useMultiSelect](/reference/use-multi-select) 提供多选状态,常用于多选进行批量操作 + +## 场景 \{#scenes} + +`useInfiniteList` = 无限滚动/点击加载 + 自动管理分页 + 表单查询(可选) + 多选操作(可选) + +## 演示 \{#demo} + +import { App } from './demo' + + + +## 用法 \{#usage} + +```tsx +interface Item { id: number; name: string; } +interface Data { data: Item[]; total: number; } +interface FormState { name: string; gender: string; color: string[]; } + +const ref = useRef(null) // 用于探底检测的目标元素,仅滚动加载需要 + +const { + list, // fetcher 返回的数据组成返回值列表,Data[] + fullList, // fetcher 返回的数据里的列表展开后组成的项目列表,Item[] + form, // 表单状态和操作 + selection, // 多选状态和操作 + paginationState, // 分页状态,请求的页码信息 + loading, // 加载状态 + isLoadDone // 是否加载完毕 +} = useInfiniteList({ + target: ref, // 用于探底检测的目标元素,仅滚动加载需要 + fetcher: fetchPagination, // 请求数据的函数 + mapFullList: (d) => d.data, // 映射数据列表的 map 函数 + canLoadMore: (previousData, dataList, fullList) => { + if (!previousData) return true // 无上次请求结果,则为首次加载 + // 当前列表长度小于总数时,可以继续加载,否则不再加载 + return fullList.length < previousData.total + }, + form: { // 配置 useForm + initialValue: { + name: '', + gender: 'Boy', + color: ['Red'], + }, + }, + pagination: { pageSize: 10 }, // 配置 usePagination + immediateQueryKeys: ['color', 'gender'], // 立即查询的表单字段 +}) + +return ( +
+ +
+ +
+
+) +``` + +## 源码 \{#source} + +import { Source } from '@/components' + + + +## API \{#api} + +```tsx +const { + list, fullList, form, selection, + paginationState, loading, isLoadDone +} = useInfiniteList(options) +``` + +### 选项 Options \{#options} + +```tsx +export interface UseInfiniteListOptions< + Data, + Item, + FormState extends object, + Fetcher extends AnyFunc, + Container extends HTMLElement, +> { + /** + * 容器元素,用于探底检测 + */ + target?: ElementTarget + /** + * 数据获取函数,可返回类似 { data: Item[], total: number } 的数据 + */ + fetcher?: Fetcher + /** + * 数据映射函数,用于获取每一次返回里的数据列表 + */ + mapFullList?: (data: Data) => Item[] + /** + * 表单选项,用于配置 `useForm`,参考 `useForm` 了解更多 + */ + form?: UseFormOptions + /** + * 异步函数选项,用于配置 `useAsyncFn`,参考 `useAsyncFn` 了解更多 + */ + asyncFn?: Omit, 'initialParams'> + /** + * 分页数据选项,用于配置 `usePagination`,参考 `usePagination` 了解更多 + */ + pagination?: UsePaginationOptions + /** + * 是否可以继续加载更多的判断函数 + */ + canLoadMore?: (previousData: Data | undefined, dataList: Data[], fullList: Item[]) => boolean + /** + * 滚动加载选项,用于配置 `useInfiniteScroll`,参考 `useInfiniteScroll` 了解更多 + */ + infiniteScroll?: Omit, 'canLoadMore'> + /** + * 立即查询的表单字段,当表单字段变化时立即重置数据并开启新的一轮查询 + */ + immediateQueryKeys?: (keyof FormState)[] +} +``` + +### 返回值 \{#returns} + +返回值包含 [useInfiniteScroll](/reference/use-infinite-scroll) 的返回值,参考 [useInfiniteScroll](/reference/use-infinite-scroll) 了解更多。 + +```tsx +export interface UseInfiniteListReturns + extends Omit { + /** + * 重置所有状态,并重新查询 + */ + reset: () => void + /** + * 表单状态和操作 + */ + form: UseFormReturns + /** + * 加载状态 + */ + list: Data[] + /** + * 是否加载完毕 + */ + fullList: Item[] + /** + * 多选状态和操作 + */ + selection: UseMultiSelectReturnsState & UseMultiSelectReturnsActions + /** + * 分页状态 + */ + paginationState: UsePaginationReturnsState +} +``` diff --git a/packages/react-use/src/use-infinite-scroll/demo.tsx b/packages/react-use/src/use-infinite-scroll/demo.tsx index e2372d2e..aa6cf3d3 100644 --- a/packages/react-use/src/use-infinite-scroll/demo.tsx +++ b/packages/react-use/src/use-infinite-scroll/demo.tsx @@ -1,45 +1,66 @@ -import { Card, KeyValue, Zone, wait as mockFetch } from '@/components' -import { generateLoremIpsum, useInfiniteScroll, useSafeState } from '@shined/react-use' +import { Button, Card, KeyValue, Zone, cn, wait as mockFetch } from '@/components' +import { generateLoremIpsum, useInfiniteScroll, useSafeState, useVersionedAction } from '@shined/react-use' import { useRef } from 'react' export function App() { const ref = useRef(null) const [list, setList] = useSafeState<{ idx: number; text: string }[]>([]) + const [incVersion, runVersionedAction] = useVersionedAction() const fetchData = async () => { - await mockFetch(Math.random() * 600 + 200) + const version = incVersion() - const newData = Array.from({ length: 20 }, (_, i) => ({ - idx: list.length + (i + 1), - text: generateLoremIpsum(), - })) + await mockFetch(1000) - setList([...list, ...newData]) + return await runVersionedAction(version, async () => { + const newData = Array.from({ length: 20 }, (_, i) => ({ + idx: list.length + (i + 1), + text: generateLoremIpsum(), + })) - return newData + setList([...list, ...newData]) + + return newData + }) } - const scroll = useInfiniteScroll(ref, fetchData, { + const infiniteScroll = useInfiniteScroll(ref, fetchData, { canLoadMore: (pre) => { return pre ? pre[pre.length - 1].idx <= 100 : true }, }) + const reset = () => { + incVersion() + setList([]) + infiniteScroll.reset() + } + return ( - - + + + -
+
{list.map((item) => (
{item.text}
))} - {scroll.isLoading &&
Loading...
} - {scroll.isLoadDone &&
No more data
} + {infiniteScroll.loading && ( +
Loading...
+ )} + {infiniteScroll.isLoadDone &&
No more data
}
+ ) } diff --git a/packages/react-use/src/use-infinite-scroll/index.mdx b/packages/react-use/src/use-infinite-scroll/index.mdx index 0de538cb..dae90aa9 100644 --- a/packages/react-use/src/use-infinite-scroll/index.mdx +++ b/packages/react-use/src/use-infinite-scroll/index.mdx @@ -8,7 +8,9 @@ import { HooksType } from '@/components' -A React Hook that allows you to use infinite scroll in your components. +A React Hook that performs a bottom-detection (accurately detecting when a container has scrolled to the bottom) and invokes a provided callback function, commonly used for implementing simple custom scroll loading logic. + +If you need more comprehensive, business-oriented, and highly integrated features, consider checking out [useInfiniteList](/reference/use-infinite-list). ## Demo diff --git a/packages/react-use/src/use-infinite-scroll/index.ts b/packages/react-use/src/use-infinite-scroll/index.ts index 06283c91..23c93b76 100644 --- a/packages/react-use/src/use-infinite-scroll/index.ts +++ b/packages/react-use/src/use-infinite-scroll/index.ts @@ -2,9 +2,11 @@ import { useRef } from 'react' import { useEventListener } from '../use-event-listener' import { useLatest } from '../use-latest' import { useMount } from '../use-mount' -import { useRafState } from '../use-raf-state' import { useStableFn } from '../use-stable-fn' import { useTargetElement } from '../use-target-element' +import { useTrackedRefState } from '../use-tracked-ref-state' +import { useUpdateEffect } from '../use-update-effect' +import { useVersionedAction } from '../use-versioned-action' import type { ElementTarget } from '../use-target-element' @@ -45,17 +47,37 @@ export interface UseInfiniteScrollOptions { export interface UseInfiniteScrollReturns { /** - * loading state + * Loading state + * + * @deprecated use `loading` instead */ isLoading: boolean /** - * load done state + * Loading state + * + * @since 1.7.0 + */ + loading: boolean + /** + * Load done state */ isLoadDone: boolean /** - * calculate the current scroll position to determine whether to load more + * Load more function + * + * @since 1.7.0 + */ + loadMore: () => Promise + /** + * Calculate the current scroll position to determine whether to load more */ calculate(): void + /** + * Reset the state to the initial state. + * + * @since 1.7.0 + */ + reset(): void } /** @@ -77,51 +99,80 @@ export function useInfiniteScroll( const el = useTargetElement(target) const previousReturn = useRef(undefined) - const [state, setState] = useRafState({ isLoading: false, isLoadDone: false }, { deep: true }) - const latest = useLatest({ state, canLoadMore, direction, onScroll, onLoadMore, interval }) - - const calculate = useStableFn(async () => { - if (!latest.current.canLoadMore(previousReturn.current)) return - - if (!el.current || latest.current.state.isLoading) return - - const { scrollHeight, scrollTop, clientHeight, scrollWidth, clientWidth } = el.current + const [incVersion, runVersionedAction] = useVersionedAction() + const [state, { updateRefState }, stateRef] = useTrackedRefState({ + version: 0, + loading: false, + isLoadDone: false, + }) - const isYScroll = latest.current.direction === 'bottom' || latest.current.direction === 'top' - const isScrollNarrower = isYScroll ? scrollHeight <= clientHeight : scrollWidth <= clientWidth - const isAlmostBottom = scrollHeight - scrollTop <= clientHeight + distance + const latest = useLatest({ state, canLoadMore, direction, onScroll, onLoadMore, interval }) - if (!isScrollNarrower && !isAlmostBottom) return + const loadMore = useStableFn(async () => { + if (stateRef.isLoadDone.value || stateRef.loading.value) return - setState({ isLoadDone: false, isLoading: true }) + const version = incVersion() + updateRefState('loading', true) const [result, _] = await Promise.all([ latest.current.onLoadMore(previousReturn.current), new Promise((resolve) => setTimeout(resolve, latest.current.interval)), ]) - previousReturn.current = result - - setState({ - isLoading: false, - isLoadDone: !latest.current.canLoadMore(previousReturn.current), + runVersionedAction(version, () => { + previousReturn.current = result + updateRefState('loading', false) + updateRefState('isLoadDone', !latest.current.canLoadMore(previousReturn.current)) }) }) + const calculate = useStableFn(async () => { + if (!el.current || stateRef.isLoadDone.value || stateRef.loading.value) return + + const { scrollHeight, scrollTop, clientHeight, scrollWidth, clientWidth } = el.current + + const isYScroll = latest.current.direction === 'bottom' || latest.current.direction === 'top' + const isScrollNarrower = isYScroll ? scrollHeight <= clientHeight : scrollWidth <= clientWidth + + const isAlmostBottom = isYScroll + ? scrollHeight - scrollTop <= clientHeight + distance + : scrollWidth - scrollTop <= clientWidth + distance + + if (isScrollNarrower || isAlmostBottom) { + await loadMore() + } + }) + useMount(immediate && calculate) + // biome-ignore lint/correctness/useExhaustiveDependencies: need to detect `isLoadDone` change + useUpdateEffect(() => { + calculate() + }, [state.isLoadDone, state.loading, state.version]) + useEventListener( el, 'scroll', (event) => { - latest.current.onScroll?.(event) calculate() + latest.current.onScroll?.(event) }, { passive: true }, ) + const reset = useStableFn(() => { + previousReturn.current = undefined + incVersion() + updateRefState('loading', false) + updateRefState('isLoadDone', false) + updateRefState('version', stateRef.version.value + 1) + }) + return { ...state, + isLoading: state.loading, + loadMore, + reset, calculate, } } diff --git a/packages/react-use/src/use-infinite-scroll/index.zh-cn.mdx b/packages/react-use/src/use-infinite-scroll/index.zh-cn.mdx index edc44a54..d34bb36f 100644 --- a/packages/react-use/src/use-infinite-scroll/index.zh-cn.mdx +++ b/packages/react-use/src/use-infinite-scroll/index.zh-cn.mdx @@ -8,7 +8,9 @@ import { HooksType } from '@/components' -一个 React 钩子(Hook),允许您在组件中使用无限滚动功能。 +一个进行探底检测(精确检测容器滚动到底部的状态),并调用提供的回调函数的 React Hook,常用来实现简单的自定义滚动加载逻辑。 + +如果你需要更多更全、更业务化、高度集成的丰富功能,可以查看 [useInfiniteList](/reference/use-infinite-list)。 ## 演示 \{#demo} diff --git a/packages/react-use/src/use-multi-select/index.ts b/packages/react-use/src/use-multi-select/index.ts index 3f4a4e96..6e92c692 100644 --- a/packages/react-use/src/use-multi-select/index.ts +++ b/packages/react-use/src/use-multi-select/index.ts @@ -25,7 +25,7 @@ export interface UseMultiSelectReturnsState { isPartiallySelected: boolean } -export interface UseMultiSelectActions { +export interface UseMultiSelectReturnsActions { /** * Whether the item is selected */ @@ -76,7 +76,7 @@ export interface UseMultiSelectActions { toggleAll(): void } -export type UseMultiSelectReturns = readonly [UseMultiSelectReturnsState, UseMultiSelectActions] +export type UseMultiSelectReturns = readonly [UseMultiSelectReturnsState, UseMultiSelectReturnsActions] /** * A React Hook that manages multi-select state. diff --git a/packages/react-use/src/use-paging-list/demo.tsx b/packages/react-use/src/use-paging-list/demo.tsx index 3d2ca3e4..ffb81a76 100644 --- a/packages/react-use/src/use-paging-list/demo.tsx +++ b/packages/react-use/src/use-paging-list/demo.tsx @@ -16,31 +16,29 @@ interface Item { } export function App() { - const { list, loading, form, refresh, query, pagination, selection } = usePagingList( - async (params) => { + const { list, loading, form, refresh, pagination, selection } = usePagingList({ + fetcher: async (params) => { const { page, pageSize, form, setTotal } = params const { data, total } = await fetchPagination({ page, pageSize }) setTotal(total) return data }, - { - form: { - initialValue: { - name: '', - gender: 'Boy', - color: ['Red'], - }, + form: { + initialValue: { + name: '', + gender: 'Boy', + color: ['Red'], }, - query: { - refreshInterval: 6_000, - }, - pagination: { - page: 1, - pageSize: 5, - }, - immediateQueryKeys: ['color', 'gender'], }, - ) + query: { + refreshInterval: 6_000, + }, + pagination: { + page: 1, + pageSize: 5, + }, + immediateQueryKeys: ['color', 'gender'], + }) // when you use third-party components, you can use `selection.isPartiallySelected` directly useUpdateEffect(() => { @@ -113,20 +111,18 @@ export function App() { {list.map((item, idx) => (
selection.toggle(item)} - onKeyDown={() => selection.toggle(item)} > { - if (e.target.checked) { - selection.select(item) - } else { - selection.unSelect(item) - } + selection.toggle(item) }} /> Index: {idx}, Data: {JSON.stringify(item)} @@ -146,7 +142,7 @@ export function App() { {Array.from({ length: pagination.pageCount }).map((_, i) => ( - // biome-ignore lint/suspicious/noArrayIndexKey: + // biome-ignore lint/suspicious/noArrayIndexKey: for demo diff --git a/packages/react-use/src/use-paging-list/index.mdx b/packages/react-use/src/use-paging-list/index.mdx index 8c52b2d5..1aa6a423 100644 --- a/packages/react-use/src/use-paging-list/index.mdx +++ b/packages/react-use/src/use-paging-list/index.mdx @@ -1,6 +1,6 @@ --- category: ProUtilities -features: ['Pausable', 'DepCollect'] +features: ['DepCollect'] --- # usePagingList @@ -11,20 +11,18 @@ import { HooksType, Since } from '@/components' -A hook for handling paging lists that integrates the functionalities of [useQuery](/reference/use-query), [useForm](/reference/use-form), [useMultiSelect](/reference/use-multi-select), and [usePagination](/reference/use-pagination). +A Hook for handling pagination lists that integrates functionalities from [useQuery](/reference/use-query), [useForm](/reference/use-form), [useMultiSelect](/reference/use-multi-select), and [usePagination](/reference/use-pagination). -It has the following features: +It possesses the following features: -- Based on `useQuery`, it includes [numerous data request functionalities](/reference/use-query#usage) that can be enabled as needed. -- Uses `useForm` to automatically manage form states, providing various states and lifecycles, with automatic requests, etc. -- Uses `usePagination` to automatically manage pagination states, providing pagination events and operations, built-in state monitoring, and automatic re-requests. -- Uses `useMultiSelect` for multi-select on list data, supporting select all, deselect, clear, etc. +- Based on `useQuery`, it is built-in with [multiple data fetching functionalities](/reference/use-query#usage), which can be enabled as needed +- Uses `useForm` to automatically manage form state, providing various statuses and life cycles, and auto-fetching +- Uses `usePagination` to automatically manage pagination state, providing pagination events and operations, built-in state monitoring, and auto-re-fetching +- Uses `useMultiSelect` for multiple selection operations on list data, supporting select all, inverse selection, clear, etc. ## Scenes \{#scenes} -- **Form-associated Query:** Utilize form state to trigger re-queries of list data. -- **Pagination Management Application:** Implement pagination functionality within the data list, providing page switching, setting per page data amount. -- **Selection State Management:** Manage the selection state of list items, supporting multi-select functionality. +`usePagingList` = UseQuery features + Pagination state and management + Form querying (optional) + Multiple selection operation (optional) ## Demo \{#demo} @@ -36,60 +34,30 @@ import { App } from './demo' ```tsx const { - form, - query, - pagination, - selection, - list, - refresh, - setTotal, - loading -} = usePagingList(fetcher, options) - -// Pass a function that accepts page, pageSize pagination parameters, which needs to return a data list -// Use setTotal inside this function to set the total data amount, so the paginator can display the total correctly -const { form, query, pagination, selection, list, refresh, setTotal, loading } = usePagingList( - async (params) => { + form, pagination, selection, + list, refresh, loading +} = usePagingList(options) + +// Pass in a function that accepts page, pageSize pagination parameters, it should return a data list +// Use setTotal within this function to set the total number of data, so that the paginator can display the total correctly +const { form, pagination, selection, list, refresh, loading } = usePagingList( + { + fetcher: async (params) => { const { page, pageSize, form } = params const { list, total } = await fetchPaginationList({ page, pageSize }) setTotal(total) return list }, - { - // Pass in the configuration for useQuery, like refreshInterval for periodic refreshes - query: { refreshInterval: 3_000 }, - // Pass in the configuration for useForm, like initialValues for form initialization - form: { initialValues: { name: '', select: '' } }, - // Pass in the configuration for usePagination, like setting default page number and items per page - pagination: { page: 1, pageSize: 10 }, - // Declare form fields for immediate query, when form field changes, it automatically triggers a query - immediateQueryKeys: ['select'], - }, + // Pass in useQuery configuration, such as refreshInterval for timed refresh + query: { refreshInterval: 3_000 }, + // Pass in useForm configuration, such as initialValues for form initialization + form: { initialValues: { name: '', select: '' } }, + // Pass in usePagination configuration, such as setting default page number and quantity per page + pagination: { page: 1, pageSize: 10 }, + // Declare form fields for immediate querying, changes in these fields will automatically trigger querying + immediateQueryKeys: ['select'], + }, ) - -// useForm returns the form object, refer to useForm documentation -form - -// useQuery returns the query object, refer to useQuery documentation -query - -// usePagination returns the pagination object, merging state and actions, refer to usePagination documentation -pagination - -// useMultiSelect returns the selection object, merging state and actions, refer to useMultiSelect documentation -selection - -// Data list -list - -// Refresh query -refresh - -// Set total data amount -setTotal - -// Whether it is loading -loading ``` ## Source \{#source} @@ -102,20 +70,12 @@ import { Source } from '@/components' ```tsx const { - form, - query, - pagination, - selection, - list, - refresh, - setTotal, - loading -} = usePagingList(fetcher, options) + form, pagination, selection, + list, refresh, loading +} = usePagingList(options) ``` -### Data Fetch Function Fetcher \{#fetcher} - -An asynchronous function that takes `params` parameter and returns a data list, which requires calling `setTotal` internally to set the total data amount. +### Options \{#options} ```tsx export interface UsePagingListFetcherParams { @@ -128,7 +88,7 @@ export interface UsePagingListFetcherParams { */ page: number /** - * Number of items displayed per page + * Quantity per page */ pageSize: number /** @@ -136,7 +96,7 @@ export interface UsePagingListFetcherParams { */ form: FormState /** - * Set total data amount + * Set the total number of data */ setTotal: ReactSetState } @@ -144,32 +104,32 @@ export interface UsePagingListFetcherParams { export type UsePagingListFetcher = ( params: UsePagingListFetcherParams, ) => Promise -``` - -### Options \{#options} -```tsx export interface UsePagingListOptions { /** - * `useForm` configuration, refer to `useForm` documentation + * Data fetcher function + */ + fetcher?: Fetcher + /** + * `useForm` configuration, see useForm documentation * * @defaultValue undefined */ form?: UseFormOptions /** - * `useQuery` configuration, refer to `useQuery` documentation + * `useQuery` configuration, see `useQuery` documentation * * @defaultValue undefined */ - query?: Omit, 'initialParams' | 'initialData'> + query?: Omit, 'initialParams' | 'initialData'> /** - * `usePagination` configuration, refer to `usePagination` document + * `usePagination` configuration, see `usePagination` documentation * * @defaultValue undefined */ pagination?: UsePaginationOptions> /** - * Form fields for immediate query, automatically triggers a query when form field changes + * Form fields for immediate querying, changes in these fields will automatically trigger querying * * @defaultValue [] */ @@ -195,27 +155,19 @@ export interface UsePagingListReturns< */ list: Data /** - * form instance, refer to useForm documentation + * Form instance, see useForm documentation */ form: UseFormReturns /** - * query instance, refer to useQuery documentation - */ - query: UseQueryOptions - /** - * refresh query + * Refresh data using the last request parameters (form state, etc.) */ refresh: () => void /** - * Set total data amount - */ - setTotal: ReactSetState - /** - * Selection state and operations, refer to useMultiSelect documentation + * Selection status and operations, see useMultiSelect documentation */ selection: UseMultiSelectReturnsState & UseMultiSelectReturns /** - * Pagination state and operations, refer to usePagination documentation + * Pagination status and operations, see usePagination documentation */ pagination: UsePaginationReturnsState & UsePaginationReturnsActions } diff --git a/packages/react-use/src/use-paging-list/index.ts b/packages/react-use/src/use-paging-list/index.ts index 7fcf541f..957d2eff 100644 --- a/packages/react-use/src/use-paging-list/index.ts +++ b/packages/react-use/src/use-paging-list/index.ts @@ -9,13 +9,17 @@ import { useStableFn } from '../use-stable-fn' import { shallowEqual } from '../utils/equal' import type { UseFormOptions, UseFormReturns } from '../use-form' -import type { UseMultiSelectReturns, UseMultiSelectReturnsState } from '../use-multi-select' +import type { UseMultiSelectReturnsActions, UseMultiSelectReturnsState } from '../use-multi-select' import type { UsePaginationOptions, UsePaginationReturnsActions, UsePaginationReturnsState } from '../use-pagination' import type { UseQueryOptions } from '../use-query' import type { ReactSetState } from '../use-safe-state' import type { AnyFunc } from '../utils/basic' export interface UsePagingListOptions { + /** + * fetcher function that will be called when the query is triggered + */ + fetcher?: Fetcher /** * options for `useForm`, see `useForm` for more details * @@ -69,12 +73,7 @@ export type UsePagingListFetcher = ( params: UsePagingListFetcherParams, ) => Promise -export interface UsePagingListReturns< - Item, - FormState extends object, - Data extends Item[], - Fetcher extends UsePagingListFetcher, -> { +export interface UsePagingListReturns { /** * loading status */ @@ -87,22 +86,14 @@ export interface UsePagingListReturns< * form state */ form: UseFormReturns - /** - * query instance - */ - query: UseQueryOptions /** * refresh query */ refresh: () => void - /** - * set total count - */ - setTotal: ReactSetState /** * selection state */ - selection: UseMultiSelectReturnsState & UseMultiSelectReturns + selection: UseMultiSelectReturnsState & UseMultiSelectReturnsActions /** * pagination state */ @@ -116,10 +107,10 @@ export interface UsePagingListReturns< */ export function usePagingList< Item, - FormState extends object, + FormState extends object = object, Data extends Item[] = Item[], Fetcher extends UsePagingListFetcher = UsePagingListFetcher, ->(fetcher: Fetcher, options: UsePagingListOptions = {}) { +>(options: UsePagingListOptions = {}): UsePagingListReturns { const previousDataRef = useRef(undefined) const previousFormRef = useRef((options.form?.initialValue || {}) as FormState) const previousSelectedRef = useRef([]) @@ -203,7 +194,7 @@ export function usePagingList< }, }) - const query = useQuery(fetcher, { + const query = useQuery((options.fetcher ?? (() => {})) as Fetcher, { ...options.query, initialParams: [ { @@ -215,8 +206,8 @@ export function usePagingList< }, ] as Parameters, onBefore(...args) { - if (select.selected.length) { - previousSelectedRef.current = select.selected + if (latest.current.selectState.selected.length) { + previousSelectedRef.current = latest.current.selectState.selected } latest.current.options.query?.onBefore?.(...args) }, @@ -239,9 +230,9 @@ export function usePagingList< }, }) - const [select, selectActions] = useMultiSelect(query.data ?? [], []) + const [selectState, selectActions] = useMultiSelect(query.data ?? [], []) - const latest = useLatest({ options, paginationState }) + const latest = useLatest({ options, selectState, paginationState }) const refresh = useStableFn(() => query.refresh()) @@ -249,13 +240,11 @@ export function usePagingList< get loading() { return query.loading }, - list: query.data ?? [], + list: (query.data ?? []) as Data, form, - query, - setTotal, refresh, selection: { - ...select, + ...selectState, ...selectActions, }, pagination: { diff --git a/packages/react-use/src/use-paging-list/index.zh-cn.mdx b/packages/react-use/src/use-paging-list/index.zh-cn.mdx index 91de0f12..89128836 100644 --- a/packages/react-use/src/use-paging-list/index.zh-cn.mdx +++ b/packages/react-use/src/use-paging-list/index.zh-cn.mdx @@ -1,6 +1,6 @@ --- category: ProUtilities -features: ['Pausable', 'DepCollect'] +features: ['DepCollect'] --- # usePagingList @@ -22,9 +22,7 @@ import { HooksType, Since } from '@/components' ## 场景 \{#scenes} -- **表单关联查询:** 利用表单状态触发列表数据的重新查询 -- **分页管理应用:** 在数据列表中实现分页功能,提供页码切换、设定每页显示数据量 -- **选中状态管理:** 管理数据列表中条目的选中状态,支持多选功能 +`usePagingList` = useQuery 特性 + 分页状态与管理 + 表单查询(可选) + 多选操作(可选) ## 演示 \{#demo} @@ -36,60 +34,30 @@ import { App } from './demo' ```tsx const { - form, - query, - pagination, - selection, - list, - refresh, - setTotal, - loading -} = usePagingList(fetcher, options) + form, pagination, selection, + list, refresh, loading +} = usePagingList(options) // 传入一个接受 page, pageSize 分页参数的函数,它需要返回一个数据列表 // 在这个函数里面使用 setTotal 来设置数据总数,以便分页器能够正确显示总数 -const { form, query, pagination, selection, list, refresh, setTotal, loading } = usePagingList( - async (params) => { +const { form, pagination, selection, list, refresh, loading } = usePagingList( + { + fetcher: async (params) => { const { page, pageSize, form } = params const { list, total } = await fetchPaginationList({ page, pageSize }) setTotal(total) return list - }, - { - // 传入 useQuery 的配置项,如 refreshInterval 进行定时刷新 - query: { refreshInterval: 3_000 }, - // 传入 useForm 的配置项,如 initialValues 进行表单初始化 - form: { initialValues: { name: '', select: '' } }, - // 传入 usePagination 的配置项,如设置默认页码和每页显示数量 - pagination: { page: 1, pageSize: 10 }, - // 声明立即查询的表单字段,当表单字段变化时,会自动触发查询 - immediateQueryKeys: ['select'], - }, + }. + // 传入 useQuery 的配置项,如 refreshInterval 进行定时刷新 + query: { refreshInterval: 3_000 }, + // 传入 useForm 的配置项,如 initialValues 进行表单初始化 + form: { initialValues: { name: '', select: '' } }, + // 传入 usePagination 的配置项,如设置默认页码和每页显示数量 + pagination: { page: 1, pageSize: 10 }, + // 声明立即查询的表单字段,当表单字段变化时,会自动触发查询 + immediateQueryKeys: ['select'], + }, ) - -// useForm 返回的 form 对象,参考 useForm 文档 -form - -// useQuery 返回的 query 对象,参考 useQuery 文档 -query - -// usePagination 返回的 pagination 对象,合并了 state 和 actions,参考 usePagination 文档 -pagination - -// useMultiSelect 返回的 selection 对象,合并了 state 和 actions,参考 useMultiSelect 文档 -selection - -// 数据列表 -list - -// 刷新数据 -refresh - -// 设置数据总数 -setTotal - -// 是否正在加载 -loading ``` ## 源码 \{#source} @@ -102,20 +70,12 @@ import { Source } from '@/components' ```tsx const { - form, - query, - pagination, - selection, - list, - refresh, - setTotal, - loading -} = usePagingList(fetcher, options) + form, pagination, selection, + list, refresh, loading +} = usePagingList(options) ``` -### 数据获取函数 Fetcher \{#fetcher} - -一个接受 `params` 参数的异步函数,返回一个数据列表,需要在内部调用 `setTotal` 来设置数据总数。 +### 配置项 Options \{#options} ```tsx export interface UsePagingListFetcherParams { @@ -144,12 +104,12 @@ export interface UsePagingListFetcherParams { export type UsePagingListFetcher = ( params: UsePagingListFetcherParams, ) => Promise -``` -### 配置项 Options \{#options} - -```tsx export interface UsePagingListOptions { + /** + * 数据获取函数 + */ + fetcher?: Fetcher /** * `useForm` 的配置项,参考 useForm 文档 * @@ -198,18 +158,10 @@ export interface UsePagingListReturns< * form 实例,参考 useForm 文档 */ form: UseFormReturns - /** - * query 实例,参考 useQuery 文档 - */ - query: UseQueryOptions /** * 使用上次请求参数(表单状态等),刷新数据 */ refresh: () => void - /** - * 设置数据总数 - */ - setTotal: ReactSetState /** * 选中状态和操作,参考 useMultiSelect 文档 */ diff --git a/packages/react-use/src/use-previous/index.zh-cn.mdx b/packages/react-use/src/use-previous/index.zh-cn.mdx index 0307b128..d7037a4a 100644 --- a/packages/react-use/src/use-previous/index.zh-cn.mdx +++ b/packages/react-use/src/use-previous/index.zh-cn.mdx @@ -10,7 +10,7 @@ import { HooksType } from '@/components' 一个用于保留状态上次一渲染的「**不同的**」值的 React Hook。 -## 场景 Scenes \{#scenes} +## 场景 \{#scenes} - **记录上一状态:** 用于记录状态的变化,以便在下一次渲染时进行相关操作。 - ... diff --git a/packages/react-use/src/use-query/index.tsx b/packages/react-use/src/use-query/index.tsx index 57d59d6d..6038f9b6 100644 --- a/packages/react-use/src/use-query/index.tsx +++ b/packages/react-use/src/use-query/index.tsx @@ -189,10 +189,13 @@ export function useQuery>, E = any> ): UseQueryReturns { const [cache, cacheActions] = useQueryCache(options) - const latest = useLatest({ fetcher, cache, ...options }) const debounceOptions = isNumber(options.debounce) ? { wait: options.debounce } : options.debounce const throttleOptions = isNumber(options.throttle) ? { wait: options.throttle } : options.throttle + const enableRateControl = Boolean(debounceOptions || throttleOptions) + + const latest = useLatest({ fetcher, cache, enableRateControl, ...options }) + const service = useLoadingSlowFn( useRetryFn( ((...args) => { @@ -297,17 +300,17 @@ export function useQuery>, E = any> return service.mutate(nextData, nextParams) }) - const refreshWithCacheAndRateControl = useStableFn(async (params?: Parameters | []) => { + const refreshWithCache = useStableFn(async (params?: Parameters | []) => { const outerParams = cacheActions.isCacheEnabled ? latest.current.cache.params : service.params const actualParams = params ?? (outerParams || []) - return refreshWithRateControl(actualParams) + return latest.current.enableRateControl ? refreshWithRateControl(actualParams) : service.refresh(actualParams) }) return { ...pausable, mutate: mutateWithCache, - refresh: refreshWithCacheAndRateControl, - run: serviceWithRateControl, + refresh: refreshWithCache, + run: enableRateControl ? serviceWithRateControl : service.run, cancel: service.cancel, get params() { return cacheActions.isCacheEnabled ? cache.params : service.params diff --git a/packages/react-use/src/use-raf-state/index.zh-cn.mdx b/packages/react-use/src/use-raf-state/index.zh-cn.mdx index f9b0c697..d552567f 100644 --- a/packages/react-use/src/use-raf-state/index.zh-cn.mdx +++ b/packages/react-use/src/use-raf-state/index.zh-cn.mdx @@ -10,7 +10,7 @@ import { HooksType } from '@/components' 一个使用 [requestAnimationFrame](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame) 在下一帧更新状态以获得更好性能的 React Hook,功能类似于 [React.useState](https://react.dev/reference/react/useState),只是更新时机不同。 -## 场景 Scenes \{#scenes} +## 场景 \{#scenes} - **动画场景优化**: 用于在动画或帧绘制中提高性能,只在下一帧更新状态 - **高频事件处理**: 例如滚动、拖拽时减少状态更新频率,防止性能损耗 diff --git a/packages/react-use/src/use-reset-state/index.zh-cn.mdx b/packages/react-use/src/use-reset-state/index.zh-cn.mdx index 6b268ea7..9edbf596 100644 --- a/packages/react-use/src/use-reset-state/index.zh-cn.mdx +++ b/packages/react-use/src/use-reset-state/index.zh-cn.mdx @@ -10,7 +10,7 @@ import { HooksType } from '@/components' 一个类似于 [React.useState](https://react.dev/reference/react/useState) 的 React Hook,但额外提供了一个 `reset` 函数,用来将状态重置为初始值。 -## 场景 Scenes \{#scenes} +## 场景 \{#scenes} - **状态初始化与重置场景:** 允许在组件生命周期内重置状态至初始值 - **交互元素状态管理:** 用于得分、步数等需要可重置的状态管理 diff --git a/packages/react-use/src/use-safe-state/index.zh-cn.mdx b/packages/react-use/src/use-safe-state/index.zh-cn.mdx index debd896e..01de49d3 100644 --- a/packages/react-use/src/use-safe-state/index.zh-cn.mdx +++ b/packages/react-use/src/use-safe-state/index.zh-cn.mdx @@ -13,7 +13,7 @@ import { HooksType } from '@/components' 有关 `useSafeState` 优势的详细解释,请参阅 [安全状态](/docs/optimization/safe-state)。 -## 场景 Scenes \{#scenes} +## 场景 \{#scenes} - **状态管理**: 用于替代 `React.useState`,以避免 `React <= 17` 时的误报警告。 - **性能优化**: 通过深度比较新旧状态(`deep` 选项,默认关闭),避免不必要的重渲染。 diff --git a/packages/react-use/src/use-set-state/index.zh-cn.mdx b/packages/react-use/src/use-set-state/index.zh-cn.mdx index 0e91f77b..95286bbf 100644 --- a/packages/react-use/src/use-set-state/index.zh-cn.mdx +++ b/packages/react-use/src/use-set-state/index.zh-cn.mdx @@ -10,7 +10,7 @@ import { HooksType } from '@/components' 一个使用 React 类组件(Class Component)的 `this.setState` 的方式来使用 State 的 React Hook。 -## 场景 Scenes \{#scenes} +## 场景 \{#scenes} - **贴近开发者习惯:** 与 Class Component 中的 `this.setState` 类似,更贴近开发者的旧习惯 - **状态管理场景:** 实现组件状态的增量更新,无需替换整个状态对象 diff --git a/packages/react-use/src/use-signal-state/index.zh-cn.mdx b/packages/react-use/src/use-signal-state/index.zh-cn.mdx index c07ade1d..644ebfd3 100644 --- a/packages/react-use/src/use-signal-state/index.zh-cn.mdx +++ b/packages/react-use/src/use-signal-state/index.zh-cn.mdx @@ -10,7 +10,7 @@ import { HooksType } from '@/components' 一个类似于 [Solid](https://www.solidjs.com) 中 `createSignal` 的 React Hook,以 `state()` 方式读取状态值,**避免了闭包问题**。 -## 场景 Scenes \{#scenes} +## 场景 \{#scenes} - **防止闭包问题:** 有效解决传统 `useState` 在闭包场景下可能遇到的过期闭包问题 - ... diff --git a/packages/react-use/src/use-toggle/index.zh-cn.mdx b/packages/react-use/src/use-toggle/index.zh-cn.mdx index 80f68028..37cee7ca 100644 --- a/packages/react-use/src/use-toggle/index.zh-cn.mdx +++ b/packages/react-use/src/use-toggle/index.zh-cn.mdx @@ -16,7 +16,7 @@ import { HooksType } from '@/components' ::: -## 场景 Scenes \{#scenes} +## 场景 \{#scenes} - **状态切换场景:** 快速在两个状态间切换,如主题切换(暗模式/亮模式) - **表单项选择:** 在选项中管理状态,如性别选择(男/女) diff --git a/packages/react-use/src/use-versioned-action/demo.tsx b/packages/react-use/src/use-versioned-action/demo.tsx new file mode 100644 index 00000000..07b68fe4 --- /dev/null +++ b/packages/react-use/src/use-versioned-action/demo.tsx @@ -0,0 +1,30 @@ +import { Button, Card, KeyValue, OTP, Zone, wait as mockFetch } from '@/components' +import { useSafeState, useVersionedAction } from '@shined/react-use' + +export function App() { + const [value, setValue] = useSafeState('Click to Fetch') + const [incVersion, runVersionedAction] = useVersionedAction() + + const fetch = async () => { + await mockFetch(300 + Math.random() * 1_000) + setValue(OTP()) + } + + const versionedFetch = async () => { + const version = incVersion() + await mockFetch(300 + Math.random() * 1_000) + runVersionedAction(version, () => { + setValue(OTP()) + }) + } + + return ( + + + + + + + + ) +} diff --git a/packages/react-use/src/use-versioned-action/index.mdx b/packages/react-use/src/use-versioned-action/index.mdx new file mode 100644 index 00000000..31a71cd0 --- /dev/null +++ b/packages/react-use/src/use-versioned-action/index.mdx @@ -0,0 +1,72 @@ +--- +category: Utilities +features: ['LowLevel'] +--- + +# useVersionedAction + +import { HooksType, Since } from '@/components' + + + + + +A low-level React Hook designed to facilitate the use of "versioned" actions, commonly employed in asynchronous scenarios for filtering operations to ensure only the latest action is executed. + +## Scenes \{#scenes} + +- **Asynchronous operation filtering**: Handling multiple asynchronous operations, executing only the latest operation, ignoring "expired" ones. + +## Demo \{#demo} + +import { App } from './demo' + + + +## Usage \{#usage} + +When `doSomething()` is called consecutively, only the most recent operation is executed, avoiding "expired" operations that could lead to screen flicker or unexpected results. + +```tsx +const [incVersion, runVersionedAction] = useVersionedAction() + +const doSomething = async () => { + // Increment the version number with incVersion() + const version = incVersion() + + // Asynchronous operation, like fetching data + const result = await fetchSomething() + + // Ensure only the latest action is executed with runVersionedAction() + runVersionedAction(version, async () => { + setResult(result) + }) +} +``` + +## Source \{#source} + +import { Source } from '@/components' + + + +## API + +```tsx +const [incVersion, runVersionedAction] = useVersionedAction() +``` + +### Returns \{#returns} + +```tsx +export type UseVersionedActionReturns = readonly [ + /** + * Increment the version number and return the current version number + */ + incVersion: () => number, + /** + * Executes the versioned operation, only if the version number matches, ensuring that only the latest action is executed + */ + runVersionedAction: (version: number, handler: T) => ReturnType | undefined, +] +``` diff --git a/packages/react-use/src/use-versioned-action/index.ts b/packages/react-use/src/use-versioned-action/index.ts index 2d801012..d0444e6c 100644 --- a/packages/react-use/src/use-versioned-action/index.ts +++ b/packages/react-use/src/use-versioned-action/index.ts @@ -3,14 +3,25 @@ import { useStableFn } from '../use-stable-fn' import type { AnyFunc } from '../utils/basic' -export function useVersionedAction() { +export type UseVersionedActionReturns = readonly [ + /** + * Increase the version + */ + incVersion: () => number, + /** + * Run the action with the specified version + */ + runVersionedAction: (version: number, handler: T) => ReturnType | undefined, +] + +export function useVersionedAction(): UseVersionedActionReturns { const versionRef = useRef(0) const incVersion = useStableFn(() => ++versionRef.current) - const runVersionedAction = useStableFn((version: number, handler: AnyFunc) => { + const runVersionedAction = useStableFn((version: number, handler: T) => { if (version !== versionRef.current) return - return handler() + return handler() as ReturnType }) return [incVersion, runVersionedAction] as const diff --git a/packages/react-use/src/use-versioned-action/index.zh-cn.mdx b/packages/react-use/src/use-versioned-action/index.zh-cn.mdx new file mode 100644 index 00000000..21f52232 --- /dev/null +++ b/packages/react-use/src/use-versioned-action/index.zh-cn.mdx @@ -0,0 +1,74 @@ +--- +category: Utilities +features: ['LowLevel'] +--- + +# useVersionedAction + +import { HooksType, Since } from '@/components' + + + + + +一个用于帮助使用“带版本”动作的 React Hook,常用于异步场景下进行操作过滤,以保证只执行最新的操作。 + +## 场景 \{#scenes} + +- **异步操作过滤**:处理多个异步操作,只执行最新的操作,忽略“已过期”的操作。 + +## 演示 \{#demo} + +快速点击下面的两个按钮,只有“带版本”的操作会忽略“已过期”的操作,确保只执行最新的操作。 + +import { App } from './demo' + + + +## 用法 \{#usage} + +当 `doSomething()` 被连续调用时,只有最新的操作会被执行,而不会出现“过期”的操作导致的「屏闪」或者「非预期结果」。 + +```tsx +const [incVersion, runVersionedAction] = useVersionedAction() + +const doSomething = async () => { + // 通过 incVersion() 来增加版本号 + const version = incVersion() + + // 异步操作,如请求数据 + const result = await fetchSomething() + + // 通过 runVersionedAction() 来确保只执行最新的操作 + runVersionedAction(version, async () => { + setResult(result) + }) +} +``` + +## 源码 \{#source} + +import { Source } from '@/components' + + + +## API + +```tsx +const [incVersion, runVersionedAction] = useVersionedAction() +``` + +### 返回值 \{#returns} + +```tsx +export type UseVersionedActionReturns = readonly [ + /** + * 增加版本号,并返回当前版本号 + */ + incVersion: () => number, + /** + * 执行带版本号的操作,只有当版本号匹配时才会执行,以确保只执行最新的操作 + */ + runVersionedAction: (version: number, handler: T) => ReturnType | undefined, +] +``` diff --git a/packages/react-use/src/use-virtual-list/index.zh-cn.mdx b/packages/react-use/src/use-virtual-list/index.zh-cn.mdx index 4fe66869..81c1458e 100644 --- a/packages/react-use/src/use-virtual-list/index.zh-cn.mdx +++ b/packages/react-use/src/use-virtual-list/index.zh-cn.mdx @@ -12,7 +12,7 @@ import { HooksType, Since } from '@/components' 一个通过只渲染用户可见项目来帮助更高效地渲染大型列表的 React Hook,即**虚拟列表**,支持动态高度,横向、纵向滚动等场景。 -## 场景 Scenes \{#scenes} +## 场景 \{#scenes} - 长列表性能优化: 通过只渲染可视区域内的元素,优化长列表的滚动性能 - 动态内容尺寸处理: 支持基于内容尺寸动态计算并渲染可视区内的项目 diff --git a/packages/react-use/src/use-web-socket/demo.tsx b/packages/react-use/src/use-web-socket/demo.tsx index 8517a154..0051f924 100644 --- a/packages/react-use/src/use-web-socket/demo.tsx +++ b/packages/react-use/src/use-web-socket/demo.tsx @@ -11,17 +11,17 @@ export function App() { } export function PassedUrl() { - const wsUrl = useControlledComponent('wss://echo.websocket.org') + const wsUrl = 'wss://echo.websocket.org' const [messageList, setMessageList] = useSafeState([]) - const ws = useWebSocket(wsUrl.value, { + const ws = useWebSocket(wsUrl, { heartbeat: { - interval: 3000, + interval: 1000, message: 'ping', - responseTimeout: 5000, + responseTimeout: 3000, }, filter: (event) => event.data === 'ping', - onOpen() { + onClose() { setMessageList([]) }, onMessage(message) { @@ -44,7 +44,7 @@ export function PassedUrl() {

Edit the WS URL in the input field below and it will automatically reconnect when the URL changes.

- +