UNPKG

7.4 kBJavaScriptView Raw
1import React, { useCallback, useMemo, useState, useRef } from 'react';
2import { cache } from './config';
3/*
4The idea
5
6A "Page" component renders the content of 1 API request, it accepts an offset (in this example it's from),
7uses a SWR hook (useSWR(API + '?limit=' + limit + '&from=' + from)) and returns items (Projects).
8
9The UI:
10 +------------------------------------------+
11 | Projects |
12+------------------------------------------------------+
13| | +----------------+ | |
14| | | |
15| | +------------+ | |
16| | | +--> 1 Page
17| | +-----------------+ | |
18| | | | /projects/list?limit=4
19| | +---------+ | |
20+------------------------------------------------------+
21 | |
22 | +------------+ | + /projects/list?limit=4&from=123
23 | | |
24 | +----------------+ | |
25 | | |
26 | +---------+ | |
27 | | |
28 | +--------------+ | +
29 | |
30 | +-------------------+ | + /projects/list?limit=4&from=456
31 | | |
32 | +------------+ | |
33 | | |
34 | +----------------+ | |
35 | | |
36 | | +
37
38The API
39// (inside `render`)
40
41function App () {
42 const {
43 pages, // an array of each page component
44 pageSWRs, // an array of SWRs of each page
45 isLoadingMore,
46 isReachingEnd,
47 isEmpty,
48 loadMore
49 } = useSWRPages(
50 'project-page', // key of this page
51
52 // ======== the actual Page component!
53 ({ offset, withSWR }) => {
54 // required: use `withSWR` to wrap your main SWR (source of your pagination API)
55 const { data } = withSWR(
56 useSWR(API + '?limit=10&from=' + offset) // request projects with offset
57 )
58 if (!data) return <Placeholder>
59 return data.projects.map(project => <Card project={project} team={team}>)
60 },
61 // ========
62
63 // a function accepts a SWR's `data`, and returns the offset of the next page (or null)
64 data => data && data.length >= 10 ? data[data.length - 1].createdAt : null,
65
66 // (optional) outside deps of your Page component. in this case it's empty
67 []
68 )
69
70 // ...
71
72 if (isEmpty) return <EmptyProjectsPage/>
73
74 return <div>
75 {pages}
76 {isReachingEnd
77 ? null
78 : <button loading={isLoadingMore} onClick={loadMore}>Load More</button>}
79 </div>
80}
81*/
82const pageCacheMap = new Map();
83export function useSWRPages(pageKey, pageFn, SWRToOffset, deps = []) {
84 const pageCountKey = `_swr_page_count_` + pageKey;
85 const pageOffsetKey = `_swr_page_offset_` + pageKey;
86 const [pageCount, setPageCount] = useState(cache.get(pageCountKey) || 1);
87 const [pageOffsets, setPageOffsets] = useState(cache.get(pageOffsetKey) || [null]);
88 const [pageSWRs, setPageSWRs] = useState([]);
89 const pageFnRef = useRef(pageFn);
90 const emptyPageRef = useRef(false);
91 // Page component (wraps `pageFn`)
92 // for performance reason we need to memorize it
93 const Page = useCallback(props => {
94 // render the page component
95 const dataList = pageFnRef.current(props);
96 // if dataList is [], we can assume this page is empty
97 // TODO: this API is not stable
98 if (dataList && !dataList.length) {
99 emptyPageRef.current = true;
100 }
101 else {
102 emptyPageRef.current = false;
103 }
104 return dataList;
105 }, []);
106 // Doesn't have a next page
107 const isReachingEnd = pageOffsets[pageCount] === null;
108 const isLoadingMore = pageCount === pageOffsets.length;
109 const isEmpty = isReachingEnd && pageCount === 1 && emptyPageRef.current;
110 const loadMore = useCallback(() => {
111 if (isLoadingMore || isReachingEnd)
112 return;
113 setPageCount(c => {
114 cache.set(pageCountKey, c + 1);
115 return c + 1;
116 });
117 }, [isLoadingMore || isReachingEnd]);
118 const _pageFn = useCallback(pageFn, deps);
119 pageFnRef.current = _pageFn;
120 const pages = useMemo(() => {
121 const getWithSWR = id => swr => {
122 if (!pageSWRs[id] ||
123 pageSWRs[id].data !== swr.data ||
124 pageSWRs[id].error !== swr.error ||
125 pageSWRs[id].revalidate !== swr.revalidate) {
126 // hoist side effects: setPageSWRs and setPageOffsets -- https://reactjs.org/blog/2020/02/26/react-v16.13.0.html#warnings-for-some-updates-during-render
127 setTimeout(() => {
128 setPageSWRs(swrs => {
129 const _swrs = [...swrs];
130 _swrs[id] = {
131 data: swr.data,
132 error: swr.error,
133 revalidate: swr.revalidate,
134 isValidating: swr.isValidating,
135 mutate: swr.mutate
136 };
137 return _swrs;
138 });
139 if (typeof swr.data !== 'undefined') {
140 // set next page's offset
141 const newPageOffset = SWRToOffset(swr, id);
142 if (pageOffsets[id + 1] !== newPageOffset) {
143 setPageOffsets(arr => {
144 const _arr = [...arr];
145 _arr[id + 1] = newPageOffset;
146 cache.set(pageOffsetKey, _arr);
147 return _arr;
148 });
149 }
150 }
151 });
152 }
153 return swr;
154 };
155 // render each page
156 const p = [];
157 if (!pageCacheMap.has(pageKey)) {
158 pageCacheMap.set(pageKey, []);
159 }
160 const pageCache = pageCacheMap.get(pageKey);
161 for (let i = 0; i < pageCount; ++i) {
162 if (!pageCache[i] ||
163 pageCache[i].offset !== pageOffsets[i] ||
164 pageCache[i].pageFn !== _pageFn) {
165 // when props change or at init
166 // render the page and cache it
167 pageCache[i] = {
168 component: (React.createElement(Page, { key: `page-${pageOffsets[i]}-${i}`, offset: pageOffsets[i], withSWR: getWithSWR(i) })),
169 pageFn: _pageFn,
170 offset: pageOffsets[i]
171 };
172 }
173 p.push(pageCache[i].component);
174 }
175 return p;
176 }, [_pageFn, pageCount, pageSWRs, pageOffsets, pageKey]);
177 return {
178 pages,
179 pageCount,
180 pageSWRs,
181 isLoadingMore,
182 isReachingEnd,
183 isEmpty,
184 loadMore
185 };
186}