UNPKG

6.75 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] = swr;
129 return _swrs;
130 });
131 if (typeof swr.data !== 'undefined') {
132 // set next page's offset
133 const newPageOffset = SWRToOffset(swr, id);
134 if (pageOffsets[id + 1] !== newPageOffset) {
135 setPageOffsets(arr => {
136 const _arr = [...arr];
137 _arr[id + 1] = newPageOffset;
138 cacheSet(pageOffsetKey, _arr);
139 return _arr;
140 });
141 }
142 }
143 }
144 return swr;
145 };
146 // render each page
147 const p = [];
148 const pageCache = pageCacheRef.current;
149 for (let i = 0; i < pageCount; ++i) {
150 if (!pageCache[i] ||
151 pageCache[i].offset !== pageOffsets[i] ||
152 pageCache[i].pageFn !== _pageFn) {
153 // when props change or at init
154 // render the page and cache it
155 pageCache[i] = {
156 component: (React.createElement(Page, { key: `page-${pageOffsets[i]}-${i}`, offset: pageOffsets[i], withSWR: getWithSWR(i) })),
157 pageFn: _pageFn,
158 offset: pageOffsets[i]
159 };
160 }
161 p.push(pageCache[i].component);
162 }
163 return p;
164 }, [_pageFn, pageCount, pageSWRs, pageOffsets, pageKey]);
165 return {
166 pages,
167 pageCount,
168 pageSWRs,
169 isLoadingMore,
170 isReachingEnd,
171 isEmpty,
172 loadMore
173 };
174}