UNPKG

6.96 kBJavaScriptView Raw
1import React, { useCallback, useMemo, useState, useRef } from 'react';
2import { cacheGet, cacheSet } 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*/
82export function useSWRPages(pageKey, pageFn, SWRToOffset, deps = []) {
83 const pageCountKey = `_swr_page_count_` + pageKey;
84 const pageOffsetKey = `_swr_page_offset_` + pageKey;
85 const [pageCount, setPageCount] = useState(cacheGet(pageCountKey) || 1);
86 const [pageOffsets, setPageOffsets] = useState(cacheGet(pageOffsetKey) || [null]);
87 const [pageSWRs, setPageSWRs] = useState([]);
88 const pageCacheRef = useRef([]);
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 cacheSet(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 setPageSWRs(swrs => {
127 const _swrs = [...swrs];
128 _swrs[id] = {
129 data: swr.data,
130 error: swr.error,
131 revalidate: swr.revalidate,
132 isValidating: swr.isValidating
133 };
134 return _swrs;
135 });
136 if (typeof swr.data !== 'undefined') {
137 // set next page's offset
138 const newPageOffset = SWRToOffset(swr, id);
139 if (pageOffsets[id + 1] !== newPageOffset) {
140 setPageOffsets(arr => {
141 const _arr = [...arr];
142 _arr[id + 1] = newPageOffset;
143 cacheSet(pageOffsetKey, _arr);
144 return _arr;
145 });
146 }
147 }
148 }
149 return swr;
150 };
151 // render each page
152 const p = [];
153 const pageCache = pageCacheRef.current;
154 for (let i = 0; i < pageCount; ++i) {
155 if (!pageCache[i] ||
156 pageCache[i].offset !== pageOffsets[i] ||
157 pageCache[i].pageFn !== _pageFn) {
158 // when props change or at init
159 // render the page and cache it
160 pageCache[i] = {
161 component: (React.createElement(Page, { key: `page-${pageOffsets[i]}-${i}`, offset: pageOffsets[i], withSWR: getWithSWR(i) })),
162 pageFn: _pageFn,
163 offset: pageOffsets[i]
164 };
165 }
166 p.push(pageCache[i].component);
167 }
168 return p;
169 }, [_pageFn, pageCount, pageSWRs, pageOffsets, pageKey]);
170 return {
171 pages,
172 pageCount,
173 pageSWRs,
174 isLoadingMore,
175 isReachingEnd,
176 isEmpty,
177 loadMore
178 };
179}