UNPKG

18.4 kBJavaScriptView Raw
1import { useCallback, useContext, useEffect, useLayoutEffect, useState, useRef, useMemo } from 'react';
2import defaultConfig, { CACHE_REVALIDATORS, CONCURRENT_PROMISES, CONCURRENT_PROMISES_TS, FOCUS_REVALIDATORS, MUTATION_TS, cache } from './config';
3import isDocumentVisible from './libs/is-document-visible';
4import isOnline from './libs/is-online';
5import throttle from './libs/throttle';
6import SWRConfigContext from './swr-config-context';
7const IS_SERVER = typeof window === 'undefined';
8// React currently throws a warning when using useLayoutEffect on the server.
9// To get around it, we can conditionally useEffect on the server (no-op) and
10// useLayoutEffect in the browser.
11const useIsomorphicLayoutEffect = IS_SERVER ? useEffect : useLayoutEffect;
12const trigger = (_key, shouldRevalidate = true) => {
13 // we are ignoring the second argument which correspond to the arguments
14 // the fetcher will receive when key is an array
15 const [key, , keyErr] = cache.serializeKey(_key);
16 if (!key)
17 return;
18 const updaters = CACHE_REVALIDATORS[key];
19 if (key && updaters) {
20 const currentData = cache.get(key);
21 const currentError = cache.get(keyErr);
22 for (let i = 0; i < updaters.length; ++i) {
23 updaters[i](shouldRevalidate, currentData, currentError, i > 0);
24 }
25 }
26};
27const broadcastState = (key, data, error) => {
28 const updaters = CACHE_REVALIDATORS[key];
29 if (key && updaters) {
30 for (let i = 0; i < updaters.length; ++i) {
31 updaters[i](false, data, error);
32 }
33 }
34};
35const mutate = async (_key, _data, shouldRevalidate = true) => {
36 const [key] = cache.serializeKey(_key);
37 if (!key)
38 return;
39 // if there is no new data, call revalidate against the key
40 if (typeof _data === 'undefined')
41 return trigger(_key, shouldRevalidate);
42 // update timestamp
43 MUTATION_TS[key] = Date.now() - 1;
44 let data, error;
45 if (_data && typeof _data === 'function') {
46 // `_data` is a function, call it passing current cache value
47 try {
48 data = await _data(cache.get(key));
49 }
50 catch (err) {
51 error = err;
52 }
53 }
54 else if (_data && typeof _data.then === 'function') {
55 // `_data` is a promise
56 try {
57 data = await _data;
58 }
59 catch (err) {
60 error = err;
61 }
62 }
63 else {
64 data = _data;
65 }
66 if (typeof data !== 'undefined') {
67 // update cached data, avoid notifying from the cache
68 cache.set(key, data, false);
69 }
70 // update existing SWR Hooks' state
71 const updaters = CACHE_REVALIDATORS[key];
72 if (updaters) {
73 for (let i = 0; i < updaters.length; ++i) {
74 updaters[i](!!shouldRevalidate, data, error, i > 0);
75 }
76 }
77 // throw error or return data to be used by caller of mutate
78 if (error)
79 throw error;
80 return data;
81};
82function useSWR(...args) {
83 let _key, fn, config = {};
84 if (args.length >= 1) {
85 _key = args[0];
86 }
87 if (args.length > 2) {
88 fn = args[1];
89 config = args[2];
90 }
91 else {
92 if (typeof args[1] === 'function') {
93 fn = args[1];
94 }
95 else if (typeof args[1] === 'object') {
96 config = args[1];
97 }
98 }
99 // we assume `key` as the identifier of the request
100 // `key` can change but `fn` shouldn't
101 // (because `revalidate` only depends on `key`)
102 // `keyErr` is the cache key for error objects
103 const [key, fnArgs, keyErr] = cache.serializeKey(_key);
104 config = Object.assign({}, defaultConfig, useContext(SWRConfigContext), config);
105 if (typeof fn === 'undefined') {
106 // use a global fetcher
107 fn = config.fetcher;
108 }
109 const initialData = cache.get(key) || config.initialData;
110 const initialError = cache.get(keyErr);
111 // if a state is accessed (data, error or isValidating),
112 // we add the state to dependencies so if the state is
113 // updated in the future, we can trigger a rerender
114 const stateDependencies = useRef({
115 data: false,
116 error: false,
117 isValidating: false
118 });
119 const stateRef = useRef({
120 data: initialData,
121 error: initialError,
122 isValidating: false
123 });
124 const rerender = useState(null)[1];
125 let dispatch = useCallback(payload => {
126 let shouldUpdateState = false;
127 for (let k in payload) {
128 stateRef.current[k] = payload[k];
129 if (stateDependencies.current[k]) {
130 shouldUpdateState = true;
131 }
132 }
133 if (shouldUpdateState || config.suspense) {
134 rerender({});
135 }
136 }, []);
137 // error ref inside revalidate (is last request errored?)
138 const unmountedRef = useRef(false);
139 const keyRef = useRef(key);
140 const boundMutate = useCallback((data, shouldRevalidate) => {
141 return mutate(key, data, shouldRevalidate);
142 }, [key]);
143 // start a revalidation
144 const revalidate = useCallback(async (revalidateOpts = {}) => {
145 if (!key || !fn)
146 return false;
147 if (unmountedRef.current)
148 return false;
149 revalidateOpts = Object.assign({ dedupe: false }, revalidateOpts);
150 let loading = true;
151 let shouldDeduping = typeof CONCURRENT_PROMISES[key] !== 'undefined' && revalidateOpts.dedupe;
152 // start fetching
153 try {
154 dispatch({
155 isValidating: true
156 });
157 let newData;
158 let startAt;
159 if (shouldDeduping) {
160 // there's already an ongoing request,
161 // this one needs to be deduplicated.
162 startAt = CONCURRENT_PROMISES_TS[key];
163 newData = await CONCURRENT_PROMISES[key];
164 }
165 else {
166 // if not deduping the request (hard revalidate) but
167 // there're other ongoing request(s) at the same time,
168 // we need to ignore the other result(s) to avoid
169 // possible race conditions:
170 // req1------------------>res1
171 // req2-------->res2
172 // in that case, the second response should not be overridden
173 // by the first one.
174 if (CONCURRENT_PROMISES[key]) {
175 // we can mark it as a mutation to ignore
176 // all requests which are fired before this one
177 MUTATION_TS[key] = Date.now() - 1;
178 }
179 // if no cache being rendered currently (it shows a blank page),
180 // we trigger the loading slow event.
181 if (config.loadingTimeout && !cache.get(key)) {
182 setTimeout(() => {
183 if (loading)
184 config.onLoadingSlow(key, config);
185 }, config.loadingTimeout);
186 }
187 if (fnArgs !== null) {
188 CONCURRENT_PROMISES[key] = fn(...fnArgs);
189 }
190 else {
191 CONCURRENT_PROMISES[key] = fn(key);
192 }
193 CONCURRENT_PROMISES_TS[key] = startAt = Date.now();
194 newData = await CONCURRENT_PROMISES[key];
195 setTimeout(() => {
196 delete CONCURRENT_PROMISES[key];
197 delete CONCURRENT_PROMISES_TS[key];
198 }, config.dedupingInterval);
199 // trigger the success event,
200 // only do this for the original request.
201 config.onSuccess(newData, key, config);
202 }
203 // if the revalidation happened earlier than the local mutation,
204 // we have to ignore the result because it could override.
205 // meanwhile, a new revalidation should be triggered by the mutation.
206 if (MUTATION_TS[key] && startAt <= MUTATION_TS[key]) {
207 dispatch({ isValidating: false });
208 return false;
209 }
210 cache.set(key, newData, false);
211 cache.set(keyErr, undefined, false);
212 // new state for the reducer
213 const newState = {
214 isValidating: false
215 };
216 if (typeof stateRef.current.error !== 'undefined') {
217 // we don't have an error
218 newState.error = undefined;
219 }
220 if (config.compare(stateRef.current.data, newData)) {
221 // deep compare to avoid extra re-render
222 // do nothing
223 }
224 else {
225 // data changed
226 newState.data = newData;
227 }
228 // merge the new state
229 dispatch(newState);
230 if (!shouldDeduping) {
231 // also update other hooks
232 broadcastState(key, newData, undefined);
233 }
234 }
235 catch (err) {
236 delete CONCURRENT_PROMISES[key];
237 delete CONCURRENT_PROMISES_TS[key];
238 cache.set(keyErr, err, false);
239 // get a new error
240 // don't use deep equal for errors
241 if (stateRef.current.error !== err) {
242 // we keep the stale data
243 dispatch({
244 isValidating: false,
245 error: err
246 });
247 if (!shouldDeduping) {
248 // also broadcast to update other hooks
249 broadcastState(key, undefined, err);
250 }
251 }
252 // events and retry
253 config.onError(err, key, config);
254 if (config.shouldRetryOnError) {
255 // when retrying, we always enable deduping
256 const retryCount = (revalidateOpts.retryCount || 0) + 1;
257 config.onErrorRetry(err, key, config, revalidate, Object.assign({ dedupe: true }, revalidateOpts, { retryCount }));
258 }
259 }
260 loading = false;
261 return true;
262 }, [key]);
263 // mounted (client side rendering)
264 useIsomorphicLayoutEffect(() => {
265 if (!key)
266 return undefined;
267 // after `key` updates, we need to mark it as mounted
268 unmountedRef.current = false;
269 // after the component is mounted (hydrated),
270 // we need to update the data from the cache
271 // and trigger a revalidation
272 const currentHookData = stateRef.current.data;
273 const latestKeyedData = cache.get(key) || config.initialData;
274 // update the state if the key changed (not the inital render) or cache updated
275 if (keyRef.current !== key ||
276 !config.compare(currentHookData, latestKeyedData)) {
277 dispatch({ data: latestKeyedData });
278 keyRef.current = key;
279 }
280 // revalidate with deduping
281 const softRevalidate = () => revalidate({ dedupe: true });
282 // trigger a revalidation
283 if (!config.initialData) {
284 if (typeof latestKeyedData !== 'undefined' &&
285 !IS_SERVER &&
286 window['requestIdleCallback']) {
287 // delay revalidate if there's cache
288 // to not block the rendering
289 window['requestIdleCallback'](softRevalidate);
290 }
291 else {
292 softRevalidate();
293 }
294 }
295 // whenever the window gets focused, revalidate
296 let onFocus;
297 if (config.revalidateOnFocus) {
298 // throttle: avoid being called twice from both listeners
299 // and tabs being switched quickly
300 onFocus = throttle(softRevalidate, config.focusThrottleInterval);
301 if (!FOCUS_REVALIDATORS[key]) {
302 FOCUS_REVALIDATORS[key] = [onFocus];
303 }
304 else {
305 FOCUS_REVALIDATORS[key].push(onFocus);
306 }
307 }
308 // register global cache update listener
309 const onUpdate = (shouldRevalidate = true, updatedData, updatedError, dedupe = true) => {
310 // update hook state
311 const newState = {};
312 let needUpdate = false;
313 if (typeof updatedData !== 'undefined' &&
314 !config.compare(stateRef.current.data, updatedData)) {
315 newState.data = updatedData;
316 needUpdate = true;
317 }
318 // always update error
319 // because it can be `undefined`
320 if (stateRef.current.error !== updatedError) {
321 newState.error = updatedError;
322 needUpdate = true;
323 }
324 if (needUpdate) {
325 dispatch(newState);
326 }
327 if (shouldRevalidate) {
328 if (dedupe) {
329 return softRevalidate();
330 }
331 else {
332 return revalidate();
333 }
334 }
335 return false;
336 };
337 // add updater to listeners
338 if (!CACHE_REVALIDATORS[key]) {
339 CACHE_REVALIDATORS[key] = [onUpdate];
340 }
341 else {
342 CACHE_REVALIDATORS[key].push(onUpdate);
343 }
344 // set up reconnecting when the browser regains network connection
345 let reconnect = null;
346 if (!IS_SERVER && window.addEventListener && config.revalidateOnReconnect) {
347 window.addEventListener('online', (reconnect = softRevalidate));
348 }
349 return () => {
350 // cleanup
351 dispatch = () => null;
352 // mark it as unmounted
353 unmountedRef.current = true;
354 if (onFocus && FOCUS_REVALIDATORS[key]) {
355 const revalidators = FOCUS_REVALIDATORS[key];
356 const index = revalidators.indexOf(onFocus);
357 if (index >= 0) {
358 // 10x faster than splice
359 // https://jsperf.com/array-remove-by-index
360 revalidators[index] = revalidators[revalidators.length - 1];
361 revalidators.pop();
362 }
363 }
364 if (CACHE_REVALIDATORS[key]) {
365 const revalidators = CACHE_REVALIDATORS[key];
366 const index = revalidators.indexOf(onUpdate);
367 if (index >= 0) {
368 revalidators[index] = revalidators[revalidators.length - 1];
369 revalidators.pop();
370 }
371 }
372 if (!IS_SERVER && window.removeEventListener && reconnect !== null) {
373 window.removeEventListener('online', reconnect);
374 }
375 };
376 }, [key, revalidate]);
377 // set up polling
378 useIsomorphicLayoutEffect(() => {
379 let timer = null;
380 const tick = async () => {
381 if (!stateRef.current.error &&
382 (config.refreshWhenHidden || isDocumentVisible()) &&
383 (config.refreshWhenOffline || isOnline())) {
384 // only revalidate when the page is visible
385 // if API request errored, we stop polling in this round
386 // and let the error retry function handle it
387 await revalidate({ dedupe: true });
388 }
389 if (config.refreshInterval) {
390 timer = setTimeout(tick, config.refreshInterval);
391 }
392 };
393 if (config.refreshInterval) {
394 timer = setTimeout(tick, config.refreshInterval);
395 }
396 return () => {
397 if (timer)
398 clearTimeout(timer);
399 };
400 }, [
401 config.refreshInterval,
402 config.refreshWhenHidden,
403 config.refreshWhenOffline,
404 revalidate
405 ]);
406 // suspense
407 if (config.suspense) {
408 if (IS_SERVER)
409 throw new Error('Suspense on server side is not yet supported!');
410 // in suspense mode, we can't return empty state
411 // (it should be suspended)
412 // try to get data and error from cache
413 let latestData = cache.get(key);
414 let latestError = cache.get(keyErr);
415 if (typeof latestData === 'undefined' &&
416 typeof latestError === 'undefined') {
417 // need to start the request if it hasn't
418 if (!CONCURRENT_PROMISES[key]) {
419 // trigger revalidate immediately
420 // to get the promise
421 revalidate();
422 }
423 if (CONCURRENT_PROMISES[key] &&
424 typeof CONCURRENT_PROMISES[key].then === 'function') {
425 // if it is a promise
426 throw CONCURRENT_PROMISES[key];
427 }
428 // it's a value, return it directly (override)
429 latestData = CONCURRENT_PROMISES[key];
430 }
431 if (typeof latestData === 'undefined' && latestError) {
432 // in suspense mode, throw error if there's no content
433 throw latestError;
434 }
435 // return the latest data / error from cache
436 // in case `key` has changed
437 return {
438 error: latestError,
439 data: latestData,
440 revalidate,
441 mutate: boundMutate,
442 isValidating: stateRef.current.isValidating
443 };
444 }
445 // define returned state
446 // can be memorized since the state is a ref
447 return useMemo(() => {
448 const state = { revalidate, mutate: boundMutate };
449 Object.defineProperties(state, {
450 error: {
451 // `key` might be changed in the upcoming hook re-render,
452 // but the previous state will stay
453 // so we need to match the latest key and data (fallback to `initialData`)
454 get: function () {
455 stateDependencies.current.error = true;
456 return stateRef.current.error;
457 }
458 },
459 data: {
460 get: function () {
461 stateDependencies.current.data = true;
462 return stateRef.current.data;
463 }
464 },
465 isValidating: {
466 get: function () {
467 stateDependencies.current.isValidating = true;
468 return stateRef.current.isValidating;
469 }
470 }
471 });
472 return state;
473 }, [revalidate]);
474}
475const SWRConfig = SWRConfigContext.Provider;
476export { trigger, mutate, SWRConfig };
477export default useSWR;