@ryuzaki13/react-foundation-lib 2.15.0 → 2.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/virtualizer/index.d.ts +2 -1
- package/dist/virtualizer/index.d.ts.map +1 -1
- package/dist/virtualizer/index.js +18 -6
- package/dist/virtualizer/index.js.map +1 -1
- package/dist/virtualizer/useFetchNextPageEffect.d.ts +6 -7
- package/dist/virtualizer/useFetchNextPageEffect.d.ts.map +1 -1
- package/dist/virtualizer/useFetchNextPageEffectTypes.d.ts +11 -0
- package/dist/virtualizer/useFetchNextPageEffectTypes.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/virtualizer/README.mdx +39 -16
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/virtualizer/index.ts"],"names":[],"mappings":"AAAA,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/virtualizer/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAClE,OAAO,EAAE,KAAK,6BAA6B,EAAE,MAAM,+BAA+B,CAAC"}
|
|
@@ -1,16 +1,28 @@
|
|
|
1
1
|
import { useEffect as e, useRef as t } from "react";
|
|
2
2
|
//#region src/virtualizer/useFetchNextPageEffect.ts
|
|
3
|
-
function n({ virtualItems: n, currentItemsCount: r, fetchNextPage: i, hasNextPage: a }) {
|
|
4
|
-
let
|
|
5
|
-
e(() => {
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
function n({ virtualItems: n, currentItemsCount: r, fetchNextPage: i, hasNextPage: a, onError: o }) {
|
|
4
|
+
let s = t(!1), c = t(null), l = t(!1);
|
|
5
|
+
e(() => (l.current = !0, () => {
|
|
6
|
+
l.current = !1;
|
|
7
|
+
}), []), e(() => {
|
|
8
|
+
(!a || c.current !== r) && (c.current = null), !(!n.length || !r || !a || s.current || c.current === r) && n[n.length - 1].index >= r - 1 && (s.current = !0, Promise.resolve().then(i).then(() => {
|
|
9
|
+
s.current = !1;
|
|
10
|
+
}, async (e) => {
|
|
11
|
+
c.current = r;
|
|
12
|
+
try {
|
|
13
|
+
l.current && o ? await o(e) : console.error("Не удалось загрузить следующую страницу виртуализированного списка.", e);
|
|
14
|
+
} catch (t) {
|
|
15
|
+
console.error("Не удалось обработать ошибку дозагрузки виртуализированного списка.", e, t);
|
|
16
|
+
} finally {
|
|
17
|
+
s.current = !1;
|
|
18
|
+
}
|
|
8
19
|
}));
|
|
9
20
|
}, [
|
|
10
21
|
n,
|
|
11
22
|
r,
|
|
12
23
|
a,
|
|
13
|
-
i
|
|
24
|
+
i,
|
|
25
|
+
o
|
|
14
26
|
]);
|
|
15
27
|
}
|
|
16
28
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../src/virtualizer/useFetchNextPageEffect.ts"],"sourcesContent":["import { useEffect, useRef } from \"react\";\n\nimport {
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/virtualizer/useFetchNextPageEffect.ts"],"sourcesContent":["import { useEffect, useRef } from \"react\";\n\nimport { type UseFetchNextPageEffectOptions } from \"./useFetchNextPageEffectTypes\";\n\n/**\n * Достигнутый край запускает одну дозагрузку. Отказ не превращается в detached\n * rejection и не повторяется из-за новых virtual items при каждом scroll/render.\n */\nexport function useFetchNextPageEffect({\n\tvirtualItems,\n\tcurrentItemsCount,\n\tfetchNextPage,\n\thasNextPage,\n\tonError\n}: UseFetchNextPageEffectOptions): void {\n\tconst fetchingRef = useRef(false);\n\tconst failedItemsCountRef = useRef<number | null>(null);\n\tconst mountedRef = useRef(false);\n\n\tuseEffect(() => {\n\t\tmountedRef.current = true;\n\t\treturn () => {\n\t\t\t// Transport принадлежит caller и не отменяется; поздний отказ остаётся\n\t\t\t// диагностикой, но уже не вызывает callback размонтированного UI.\n\t\t\tmountedRef.current = false;\n\t\t};\n\t}, []);\n\n\tuseEffect(() => {\n\t\t// Новые данные либо явное выключение pagination разрешают следующую\n\t\t// попытку. Identity callback и видимого диапазона не являются retry.\n\t\tif (!hasNextPage || failedItemsCountRef.current !== currentItemsCount) failedItemsCountRef.current = null;\n\t\tif (\n\t\t\t!virtualItems.length ||\n\t\t\t!currentItemsCount ||\n\t\t\t!hasNextPage ||\n\t\t\tfetchingRef.current ||\n\t\t\tfailedItemsCountRef.current === currentItemsCount\n\t\t)\n\t\t\treturn;\n\t\tconst lastVirtual = virtualItems[virtualItems.length - 1];\n\n\t\tif (lastVirtual.index >= currentItemsCount - 1) {\n\t\t\tfetchingRef.current = true;\n\t\t\t// Начало через Promise охватывает и синхронный throw transport callback.\n\t\t\t// Обе ветки завершения обработаны: finally не создаёт второй rejected Promise.\n\t\t\tvoid Promise.resolve()\n\t\t\t\t.then(fetchNextPage)\n\t\t\t\t.then(\n\t\t\t\t\t() => {\n\t\t\t\t\t\tfetchingRef.current = false;\n\t\t\t\t\t},\n\t\t\t\t\tasync (error: unknown) => {\n\t\t\t\t\t\tfailedItemsCountRef.current = currentItemsCount;\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tif (mountedRef.current && onError) await onError(error);\n\t\t\t\t\t\t\telse console.error(\"Не удалось загрузить следующую страницу виртуализированного списка.\", error);\n\t\t\t\t\t\t} catch (reporterError: unknown) {\n\t\t\t\t\t\t\tconsole.error(\"Не удалось обработать ошибку дозагрузки виртуализированного списка.\", error, reporterError);\n\t\t\t\t\t\t} finally {\n\t\t\t\t\t\t\tfetchingRef.current = false;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t);\n\t\t}\n\t}, [virtualItems, currentItemsCount, hasNextPage, fetchNextPage, onError]);\n}\n"],"mappings":";;AAQA,SAAgB,EAAuB,EACtC,iBACA,sBACA,kBACA,gBACA,cACuC;CACvC,IAAM,IAAc,EAAO,EAAK,GAC1B,IAAsB,EAAsB,IAAI,GAChD,IAAa,EAAO,EAAK;CAW/B,AATA,SACC,EAAW,UAAU,UACR;EAGZ,EAAW,UAAU;CACtB,IACE,CAAC,CAAC,GAEL,QAAgB;EAGf,CAAI,CAAC,KAAe,EAAoB,YAAY,OAAmB,EAAoB,UAAU,OAEpG,GAAC,EAAa,UACd,CAAC,KACD,CAAC,KACD,EAAY,WACZ,EAAoB,YAAY,MAGb,EAAa,EAAa,SAAS,EAEnD,CAAY,SAAS,IAAoB,MAC5C,EAAY,UAAU,IAGtB,QAAa,QAAQ,CAAC,CACpB,KAAK,CAAa,CAAC,CACnB,WACM;GACL,EAAY,UAAU;EACvB,GACA,OAAO,MAAmB;GACzB,EAAoB,UAAU;GAC9B,IAAI;IACH,AAAI,EAAW,WAAW,IAAS,MAAM,EAAQ,CAAK,IACjD,QAAQ,MAAM,uEAAuE,CAAK;GAChG,SAAS,GAAwB;IAChC,QAAQ,MAAM,uEAAuE,GAAO,CAAa;GAC1G,UAAU;IACT,EAAY,UAAU;GACvB;EACD,CACD;CAEH,GAAG;EAAC;EAAc;EAAmB;EAAa;EAAe;CAAO,CAAC;AAC1E"}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
}): void;
|
|
1
|
+
import { type UseFetchNextPageEffectOptions } from "./useFetchNextPageEffectTypes";
|
|
2
|
+
/**
|
|
3
|
+
* Достигнутый край запускает одну дозагрузку. Отказ не превращается в detached
|
|
4
|
+
* rejection и не повторяется из-за новых virtual items при каждом scroll/render.
|
|
5
|
+
*/
|
|
6
|
+
export declare function useFetchNextPageEffect({ virtualItems, currentItemsCount, fetchNextPage, hasNextPage, onError }: UseFetchNextPageEffectOptions): void;
|
|
8
7
|
//# sourceMappingURL=useFetchNextPageEffect.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useFetchNextPageEffect.d.ts","sourceRoot":"","sources":["../../src/virtualizer/useFetchNextPageEffect.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"useFetchNextPageEffect.d.ts","sourceRoot":"","sources":["../../src/virtualizer/useFetchNextPageEffect.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,6BAA6B,EAAE,MAAM,+BAA+B,CAAC;AAEnF;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,EACtC,YAAY,EACZ,iBAAiB,EACjB,aAAa,EACb,WAAW,EACX,OAAO,EACP,EAAE,6BAA6B,GAAG,IAAI,CAoDtC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type VirtualItem } from "@tanstack/react-virtual";
|
|
2
|
+
/** Дозагрузка не владеет данными и retry: caller передаёт состояние своей пагинации. */
|
|
3
|
+
export type UseFetchNextPageEffectOptions = Readonly<{
|
|
4
|
+
virtualItems: readonly VirtualItem[];
|
|
5
|
+
currentItemsCount: number;
|
|
6
|
+
hasNextPage: boolean;
|
|
7
|
+
fetchNextPage: () => Promise<unknown>;
|
|
8
|
+
/** Ошибка передаётся один раз; без callback остаётся явная console-диагностика. */
|
|
9
|
+
onError?: (error: unknown) => void | Promise<void>;
|
|
10
|
+
}>;
|
|
11
|
+
//# sourceMappingURL=useFetchNextPageEffectTypes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useFetchNextPageEffectTypes.d.ts","sourceRoot":"","sources":["../../src/virtualizer/useFetchNextPageEffectTypes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAE3D,wFAAwF;AACxF,MAAM,MAAM,6BAA6B,GAAG,QAAQ,CAAC;IACpD,YAAY,EAAE,SAAS,WAAW,EAAE,CAAC;IACrC,iBAAiB,EAAE,MAAM,CAAC;IAC1B,WAAW,EAAE,OAAO,CAAC;IACrB,aAAa,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;IACtC,mFAAmF;IACnF,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACnD,CAAC,CAAC"}
|
package/package.json
CHANGED
|
@@ -36,16 +36,18 @@ useFetchNextPageEffect({
|
|
|
36
36
|
virtualItems,
|
|
37
37
|
currentItemsCount,
|
|
38
38
|
fetchNextPage,
|
|
39
|
-
hasNextPage
|
|
39
|
+
hasNextPage,
|
|
40
|
+
onError
|
|
40
41
|
});
|
|
41
42
|
```
|
|
42
43
|
|
|
43
|
-
| Поле | Значение
|
|
44
|
-
| ------------------- |
|
|
45
|
-
| `virtualItems` | Текущий массив `VirtualItem[]`
|
|
46
|
-
| `currentItemsCount` | Число реально загруженных data items
|
|
47
|
-
| `hasNextPage` | Есть ли следующая server page
|
|
48
|
-
| `fetchNextPage` | Stable функция, возвращающая Promise
|
|
44
|
+
| Поле | Значение |
|
|
45
|
+
| ------------------- | ------------------------------------------------------------------- |
|
|
46
|
+
| `virtualItems` | Текущий массив `readonly VirtualItem[]` |
|
|
47
|
+
| `currentItemsCount` | Число реально загруженных data items |
|
|
48
|
+
| `hasNextPage` | Есть ли следующая server page |
|
|
49
|
+
| `fetchNextPage` | Stable функция, возвращающая Promise |
|
|
50
|
+
| `onError` | Необязательный callback `(error: unknown) => void \| Promise<void>` |
|
|
49
51
|
|
|
50
52
|
Hook ничего не возвращает.
|
|
51
53
|
|
|
@@ -81,25 +83,41 @@ Trigger происходит строго на последнем загруже
|
|
|
81
83
|
|
|
82
84
|
## Защита от параллельного fetch
|
|
83
85
|
|
|
84
|
-
Внутренний
|
|
86
|
+
Внутренний single-flight ref устанавливается до вызова и сбрасывается после
|
|
87
|
+
успеха либо завершения обработки ошибки:
|
|
85
88
|
|
|
86
89
|
```text
|
|
87
90
|
idle ── trigger ──► fetchingRef=true
|
|
88
91
|
│
|
|
89
92
|
Promise pending
|
|
90
93
|
│
|
|
91
|
-
|
|
94
|
+
resolve или завершение onError
|
|
92
95
|
▼
|
|
93
96
|
fetchingRef=false
|
|
94
97
|
```
|
|
95
98
|
|
|
96
99
|
Защита действует только внутри одного mounted hook instance. TanStack Query также должен владеть дедупликацией/network state.
|
|
97
100
|
|
|
98
|
-
|
|
101
|
+
## Ошибки и повторная попытка
|
|
102
|
+
|
|
103
|
+
Promise rejection и синхронный throw `fetchNextPage` передаются в `onError`
|
|
104
|
+
один раз. Hook ожидает async callback; его собственный отказ также обрабатывается.
|
|
105
|
+
Без `onError`, при отказе самого callback либо после unmount исходная ошибка
|
|
106
|
+
остаётся в явной `console.error`-диагностике с контекстом дозагрузки. Hook не
|
|
107
|
+
создаёт дополнительный unhandled Promise rejection и не решает, как показывать
|
|
108
|
+
ошибку пользователю: это остаётся у владельца Query и UI.
|
|
109
|
+
|
|
110
|
+
После отказа hook не пытается загрузить ту же страницу при каждом scroll/render.
|
|
111
|
+
Неудачная попытка привязана к `currentItemsCount`: новое количество загруженных
|
|
112
|
+
строк либо явное выключение `hasNextPage` и последующее включение разрешают новую
|
|
113
|
+
попытку. Смена identity `fetchNextPage`, `onError` или `virtualItems` не является
|
|
114
|
+
retry. Изменение данных без изменения количества само по себе latch не снимает;
|
|
115
|
+
явная ручная retry-команда остаётся у владельца Query. Интервалов и собственной
|
|
116
|
+
retry-policy в hook нет.
|
|
99
117
|
|
|
100
118
|
## Referential stability
|
|
101
119
|
|
|
102
|
-
Effect зависит от `virtualItems`, `currentItemsCount`, `hasNextPage`, `fetchNextPage`.
|
|
120
|
+
Effect зависит от `virtualItems`, `currentItemsCount`, `hasNextPage`, `fetchNextPage`, `onError`.
|
|
103
121
|
|
|
104
122
|
- Используйте значение virtualizer как предусмотрено его API.
|
|
105
123
|
- Не создавайте лишний async callback с новой identity без причины.
|
|
@@ -110,7 +128,9 @@ Effect зависит от `virtualItems`, `currentItemsCount`, `hasNextPage`, `
|
|
|
110
128
|
|
|
111
129
|
Сам hook не читает `window`, но является React effect и не выполняет fetch во время server render. В browser effect запускается после commit.
|
|
112
130
|
|
|
113
|
-
При unmount Promise не отменяется. Abort/cancel должен быть реализован query/transport owner.
|
|
131
|
+
При unmount Promise не отменяется. Abort/cancel должен быть реализован query/transport owner.
|
|
132
|
+
Поздний отказ не вызывает `onError` размонтированного UI, но остаётся в явной
|
|
133
|
+
console-диагностике. Уже начатый async `onError` не отменяется, его отказ не теряется.
|
|
114
134
|
|
|
115
135
|
## Типичные ошибки
|
|
116
136
|
|
|
@@ -118,17 +138,20 @@ Effect зависит от `virtualItems`, `currentItemsCount`, `hasNextPage`, `
|
|
|
118
138
|
- Включить client-side sorting/filtering одной страницы при server-side модели.
|
|
119
139
|
- Добавить фиктивный loader row в `currentItemsCount`.
|
|
120
140
|
- Ожидать preload заранее — trigger находится на самом конце.
|
|
121
|
-
-
|
|
141
|
+
- Ожидать, что hook сам покажет Query-ошибку или автоматически повторит failed page.
|
|
122
142
|
- Выполнять fetch прямо в row render вместо effect.
|
|
123
143
|
- Использовать изменяемый index как domain identity строки.
|
|
124
144
|
|
|
125
145
|
## API
|
|
126
146
|
|
|
127
147
|
```ts
|
|
128
|
-
|
|
129
|
-
virtualItems: VirtualItem[];
|
|
148
|
+
type UseFetchNextPageEffectOptions = Readonly<{
|
|
149
|
+
virtualItems: readonly VirtualItem[];
|
|
130
150
|
currentItemsCount: number;
|
|
131
151
|
hasNextPage: boolean;
|
|
132
152
|
fetchNextPage: () => Promise<unknown>;
|
|
133
|
-
|
|
153
|
+
onError?: (error: unknown) => void | Promise<void>;
|
|
154
|
+
}>;
|
|
155
|
+
|
|
156
|
+
function useFetchNextPageEffect(options: UseFetchNextPageEffectOptions): void;
|
|
134
157
|
```
|