Skip to content

Commit 6fbb303

Browse files
authored
Merge branch 'master' into clear-cache
2 parents be9c82b + da7f6f6 commit 6fbb303

File tree

9 files changed

+106
-81
lines changed

9 files changed

+106
-81
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
{
22
"name": "swr",
3-
"version": "1.0.0-beta.8",
3+
"version": "1.0.0-beta.9",
44
"description": "React Hooks library for remote data fetching",
55
"main": "./dist/index.js",
66
"module": "./dist/index.esm.js",
77
"exports": {
8+
"./package.json": "./package.json",
89
".": {
910
"import": "./dist/index.esm.js",
1011
"require": "./dist/index.js",

src/use-swr.ts

Lines changed: 68 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,12 @@ const broadcastState: Broadcaster = (
4949
return Promise.all(promises).then(() => cache.get(key))
5050
}
5151

52-
async function internalMutate<Data = any>(
52+
const internalMutate = async <Data>(
5353
cache: Cache,
5454
_key: Key,
5555
_data?: Data | Promise<Data | undefined> | MutatorCallback<Data>,
5656
shouldRevalidate = true
57-
): Promise<Data | undefined> {
57+
) => {
5858
const [key, , keyErr] = serialize(_key)
5959
if (!key) return UNDEFINED
6060

@@ -91,26 +91,27 @@ async function internalMutate<Data = any>(
9191
}
9292
}
9393

94+
// `_data` is a promise/thenable, resolve the final data first.
9495
if (_data && typeof (_data as Promise<Data>).then === 'function') {
95-
// `_data` is a promise/thenable, resolve the final data.
96+
// This means that the mutation is async, we need to check timestamps to
97+
// avoid race conditions.
9698
try {
9799
data = await _data
98100
} catch (err) {
99101
error = err
100102
}
103+
104+
// Check if other mutations have occurred since we've started this mutation.
105+
// If there's a race we don't update cache or broadcast the change,
106+
// just return the data.
107+
if (beforeMutationTs !== MUTATION_TS[key]) {
108+
if (error) throw error
109+
return data
110+
}
101111
} else {
102112
data = _data
103113
}
104114

105-
// Check if other mutations have occurred since we've started this mutation.
106-
const shouldAbort = beforeMutationTs !== MUTATION_TS[key]
107-
108-
// If there's a race we don't update cache or broadcast change, just return the data.
109-
if (shouldAbort) {
110-
if (error) throw error
111-
return data
112-
}
113-
114115
if (!isUndefined(data)) {
115116
// update cached data
116117
cache.set(key, data)
@@ -161,11 +162,11 @@ const addRevalidator = (
161162
}
162163
}
163164

164-
export function useSWRHandler<Data = any, Error = any>(
165+
export const useSWRHandler = <Data = any, Error = any>(
165166
_key: Key,
166167
fn: Fetcher<Data> | null,
167168
config: typeof defaultConfig & SWRConfiguration<Data, Error>
168-
): SWRResponse<Data, Error> {
169+
) => {
169170
const {
170171
cache,
171172
compare,
@@ -457,45 +458,20 @@ export function useSWRHandler<Data = any, Error = any>(
457458
const keyChanged = initialMountedRef.current
458459
const softRevalidate = () => revalidate({ dedupe: true })
459460

460-
// Mark the component as mounted and update corresponding refs.
461-
unmountedRef.current = false
462-
keyRef.current = key
463-
464-
// When `key` updates, reset the state to the initial value
465-
// and trigger a rerender if necessary.
466-
if (keyChanged) {
467-
setState({
468-
data,
469-
error,
470-
isValidating
471-
})
472-
}
473-
474-
// Trigger a revalidation.
475-
if (keyChanged || shouldRevalidateOnMount()) {
476-
if (isUndefined(data) || IS_SERVER) {
477-
// Revalidate immediately.
478-
softRevalidate()
479-
} else {
480-
// Delay the revalidate if we have data to return so we won't block
481-
// rendering.
482-
rAF(softRevalidate)
483-
}
484-
}
485-
486461
const isActive = () =>
487462
configRef.current.isVisible() && configRef.current.isOnline()
488463

489464
// Add event listeners.
490-
let pending = false
465+
let nextFocusRevalidatedAt = 0
491466
const onFocus = () => {
492-
if (configRef.current.revalidateOnFocus && !pending && isActive()) {
493-
pending = true
467+
const now = Date.now()
468+
if (
469+
configRef.current.revalidateOnFocus &&
470+
now > nextFocusRevalidatedAt &&
471+
isActive()
472+
) {
473+
nextFocusRevalidatedAt = now + configRef.current.focusThrottleInterval
494474
softRevalidate()
495-
setTimeout(
496-
() => (pending = false),
497-
configRef.current.focusThrottleInterval
498-
)
499475
}
500476
}
501477

@@ -534,6 +510,32 @@ export function useSWRHandler<Data = any, Error = any>(
534510
const unsubReconn = addRevalidator(RECONNECT_REVALIDATORS, key, onReconnect)
535511
const unsubUpdate = addRevalidator(CACHE_REVALIDATORS, key, onUpdate)
536512

513+
// Mark the component as mounted and update corresponding refs.
514+
unmountedRef.current = false
515+
keyRef.current = key
516+
517+
// When `key` updates, reset the state to the initial value
518+
// and trigger a rerender if necessary.
519+
if (keyChanged) {
520+
setState({
521+
data,
522+
error,
523+
isValidating
524+
})
525+
}
526+
527+
// Trigger a revalidation.
528+
if (keyChanged || shouldRevalidateOnMount()) {
529+
if (isUndefined(data) || IS_SERVER) {
530+
// Revalidate immediately.
531+
softRevalidate()
532+
} else {
533+
// Delay the revalidate if we have data to return so we won't block
534+
// rendering.
535+
rAF(softRevalidate)
536+
}
537+
}
538+
537539
// Finally, the component is mounted.
538540
initialMountedRef.current = true
539541

@@ -549,36 +551,38 @@ export function useSWRHandler<Data = any, Error = any>(
549551

550552
// Polling
551553
useIsomorphicLayoutEffect(() => {
552-
let timer: any = 0
553-
554-
function nextTick() {
555-
if (refreshInterval) {
556-
timer = setTimeout(tick, refreshInterval)
554+
let timer: any
555+
556+
function next() {
557+
// We only start next interval if `refreshInterval` is not 0, and:
558+
// - `force` is true, which is the start of polling
559+
// - or `timer` is not 0, which means the effect wasn't canceled
560+
if (refreshInterval && timer !== -1) {
561+
timer = setTimeout(execute, refreshInterval)
557562
}
558563
}
559564

560-
async function tick() {
565+
function execute() {
566+
// Check if it's OK to execute:
567+
// Only revalidate when the page is visible, online and not errored.
561568
if (
562569
!stateRef.current.error &&
563570
(refreshWhenHidden || config.isVisible()) &&
564571
(refreshWhenOffline || config.isOnline())
565572
) {
566-
// only revalidate when the page is visible
567-
// if API request errored, we stop polling in this round
568-
// and let the error retry function handle it
569-
await revalidate({ dedupe: true })
573+
revalidate({ dedupe: true }).then(() => next())
574+
} else {
575+
// Schedule next interval to check again.
576+
next()
570577
}
571-
572-
// Read the latest refreshInterval
573-
if (timer) nextTick()
574578
}
575579

576-
nextTick()
580+
next()
577581

578582
return () => {
579583
if (timer) {
580584
clearTimeout(timer)
581-
timer = 0
585+
timer = -1
582586
}
583587
}
584588
}, [refreshInterval, refreshWhenHidden, refreshWhenOffline, revalidate])
@@ -640,13 +644,10 @@ export const mutate = internalMutate.bind(
640644
defaultConfig.cache
641645
) as ScopedMutator
642646

643-
export function createCache<Data>(
647+
export const createCache = <Data = any>(
644648
provider: Cache,
645649
options?: Partial<ProviderOptions>
646-
): {
647-
cache: Cache
648-
mutate: ScopedMutator<Data>
649-
} {
650+
) => {
650651
const cache = wrapCache<Data>(provider, options)
651652
return {
652653
cache,

src/utils/cache.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ function setupGlobalEvents(cache: Cache, options: ProviderOptions) {
3636
export function wrapCache<Data = any>(
3737
provider: Cache<Data>,
3838
options?: Partial<ProviderOptions>
39-
): Cache {
39+
): Cache<Data> {
4040
// Initialize global state for the specific data storage that will be used to
4141
// deduplicate requests and store listeners.
4242
SWRGlobalState.set(provider, [{}, {}, {}, {}, {}, {}, {}])

src/utils/config.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,7 @@ import { wrapCache } from './cache'
44
import { preset } from './web-preset'
55
import { slowConnection } from './env'
66
import { Configuration, RevalidatorOptions, Revalidator } from '../types'
7-
import { UNDEFINED } from './helper'
8-
9-
const noop = () => {}
7+
import { UNDEFINED, noop } from './helper'
108

119
// error retry
1210
function onErrorRetry(

src/utils/helper.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
// `undefined` can possibly be replaced by something else.
22
export const UNDEFINED: undefined = ({} as any)[0]
33
export const isUndefined = (v: any) => v === UNDEFINED
4+
export const noop = () => {}

src/utils/normalize-args.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,5 @@ export function normalize<KeyType = Key, Data = any>(
99
): [KeyType, Fetcher<Data> | null, Partial<SWRConfiguration<Data>>] {
1010
return typeof args[1] === 'function'
1111
? [args[0], args[1], args[2] || {}]
12-
: [args[0], null, (typeof args[1] === 'object' ? args[1] : args[2]) || {}]
12+
: [args[0], null, (args[1] === null ? args[2] : args[1]) || {}]
1313
}

src/utils/state.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,7 @@ export default function useStateWithDeps<Data, Error, S = State<Data, Error>>(
4848
let shouldRerender = false
4949

5050
const currentState = stateRef.current
51-
for (const _ of Object.keys(payload)) {
52-
// Type casting to work around the `for...in` loop
53-
// https://github.com/Microsoft/TypeScript/issues/3500
51+
for (const _ in payload) {
5452
const k = _ as keyof S & StateKeys
5553

5654
// If the property has changed, update the state and mark rerender as

src/utils/web-preset.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ProviderOptions } from '../types'
2-
import { isUndefined } from './helper'
2+
import { isUndefined, noop } from './helper'
33

44
/**
55
* Due to bug https://bugs.chromium.org/p/chromium/issues/detail?id=678075,
@@ -12,12 +12,12 @@ let online = true
1212
const isOnline = () => online
1313
const hasWindow = typeof window !== 'undefined'
1414
const hasDocument = typeof document !== 'undefined'
15-
const add = 'addEventListener'
16-
function noop() {}
15+
const ADD_EVENT_LISTENER = 'addEventListener'
1716

1817
// For node and React Native, `add/removeEventListener` doesn't exist on window.
19-
const onWindowEvent = hasWindow && window[add] ? window[add] : noop
20-
const onDocumentEvent = hasDocument ? document[add] : noop
18+
const onWindowEvent =
19+
hasWindow && window[ADD_EVENT_LISTENER] ? window[ADD_EVENT_LISTENER] : noop
20+
const onDocumentEvent = hasDocument ? document[ADD_EVENT_LISTENER] : noop
2121

2222
const isVisible = () => {
2323
const visibilityState = hasDocument && document.visibilityState

test/unit.test.tsx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { normalize } from '../src/utils/normalize-args'
2+
3+
describe('Unit tests', () => {
4+
it('should normalize arguments correctly', async () => {
5+
const fetcher = () => {}
6+
const opts = { revalidateOnFocus: false }
7+
8+
// Only the `key` argument is passed
9+
expect(normalize(['key'])).toEqual(['key', null, {}])
10+
11+
// `key` and `null` as fetcher (no fetcher)
12+
expect(normalize(['key', null])).toEqual(['key', null, {}])
13+
14+
// `key` and `fetcher`
15+
expect(normalize(['key', fetcher])).toEqual(['key', fetcher, {}])
16+
17+
// `key` and `options`
18+
expect(normalize(['key', opts])).toEqual(['key', null, opts])
19+
20+
// `key`, `null` as fetcher, and `options`
21+
expect(normalize(['key', null, opts])).toEqual(['key', null, opts])
22+
23+
// `key`, `fetcher`, and `options`
24+
expect(normalize(['key', fetcher, opts])).toEqual(['key', fetcher, opts])
25+
})
26+
})

0 commit comments

Comments
 (0)