Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/_internal/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,11 @@ export interface PublicConfiguration<
* @defaultValue false
*/
refreshWhenOffline?: boolean
/**
* polling when the window is not focused (if `refreshInterval` is enabled)
* @defaultValue false
*/
refreshWhenUnfocused?: boolean
/**
* automatically revalidate when window gets focused
*
Expand Down Expand Up @@ -317,6 +322,11 @@ export interface PublicConfiguration<
* @see {@link https://swr.vercel.app/docs/advanced/react-native#customize-focus-and-reconnect-events}
*/
isVisible: () => boolean
/**
* hasFocus is a function that returns a boolean, to determine if the window has focus. Used for controlling polling behavior via refreshWhenUnfocused.
* @see {@link https://swr.vercel.app/docs/advanced/react-native#customize-focus-and-reconnect-events}
*/
hasFocus: () => boolean
}

export type FullConfiguration<
Expand Down
13 changes: 12 additions & 1 deletion src/_internal/utils/web-preset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@ const isVisible = () => {
return isUndefined(visibilityState) || visibilityState !== 'hidden'
}

const hasFocus = () => {
if (!isDocumentDefined) return true

try {
return typeof document.hasFocus === 'function' ? document.hasFocus() : true
} catch (err) {
return true
}
}

const initFocus = (callback: () => void) => {
// focus revalidate
if (isDocumentDefined) {
Expand Down Expand Up @@ -60,7 +70,8 @@ const initReconnect = (callback: () => void) => {

export const preset = {
isOnline,
isVisible
isVisible,
hasFocus
} as const

export const defaultConfigOptions: ProviderConfiguration = {
Expand Down
52 changes: 30 additions & 22 deletions src/index/use-swr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,39 +39,37 @@ import type {
GlobalState
} from '../_internal'

const use =
const use: <T>(usable: PromiseLike<T> | Promise<T>) => T =
React.use ||
// This extra generic is to avoid TypeScript mixing up the generic and JSX sytax
// and emitting an error.
// Fallback for React versions without `use()`.
// We assume that this is only for the `use(thenable)` case, not `use(context)`.
// https://github.com/facebook/react/blob/aed00dacfb79d17c53218404c52b1c7aa59c4a89/packages/react-server/src/ReactFizzThenable.js#L45
(<T, _>(
thenable: Promise<T> & {
(thenable => {
const t = thenable as Promise<any> & {
status?: 'pending' | 'fulfilled' | 'rejected'
value?: T
value?: any
reason?: unknown
}
): T => {
switch (thenable.status) {
switch (t.status) {
case 'pending':
throw thenable
throw t
case 'fulfilled':
return thenable.value as T
return t.value
case 'rejected':
throw thenable.reason
throw t.reason
default:
thenable.status = 'pending'
thenable.then(
t.status = 'pending'
t.then(
v => {
thenable.status = 'fulfilled'
thenable.value = v
t.status = 'fulfilled'
t.value = v
},
e => {
thenable.status = 'rejected'
thenable.reason = e
t.status = 'rejected'
t.reason = e
}
)
throw thenable
throw t
}
})

Expand Down Expand Up @@ -134,6 +132,7 @@ export const useSWRHandler = <Data = any, Error = any>(
refreshInterval,
refreshWhenHidden,
refreshWhenOffline,
refreshWhenUnfocused,
keepPreviousData,
strictServerPrefetchWarning
} = config
Expand All @@ -160,7 +159,9 @@ export const useSWRHandler = <Data = any, Error = any>(
const fetcherRef = useRef(fetcher)
const configRef = useRef(config)
const getConfig = () => configRef.current
const isActive = () => getConfig().isVisible() && getConfig().isOnline()
const isActive = () =>
(getConfig().isVisible() || getConfig().hasFocus()) &&
getConfig().isOnline()

const [getCache, setCache, subscribeCache, getInitialCache] =
createCacheHelper<
Expand Down Expand Up @@ -743,11 +744,12 @@ export const useSWRHandler = <Data = any, Error = any>(

function execute() {
// Check if it's OK to execute:
// Only revalidate when the page is visible, online, and not errored.
// Only revalidate when the page is visible, online, focused, and not errored.
if (
!getCache().error &&
(refreshWhenHidden || getConfig().isVisible()) &&
(refreshWhenOffline || getConfig().isOnline())
(refreshWhenOffline || getConfig().isOnline()) &&
(refreshWhenUnfocused || getConfig().hasFocus())
) {
revalidate(WITH_DEDUPE).then(next)
} else {
Expand All @@ -764,7 +766,13 @@ export const useSWRHandler = <Data = any, Error = any>(
timer = -1
}
}
}, [refreshInterval, refreshWhenHidden, refreshWhenOffline, key])
}, [
refreshInterval,
refreshWhenHidden,
refreshWhenOffline,
refreshWhenUnfocused,
key
])

// Display debug info in React DevTools.
useDebugValue(returnedData)
Expand Down
6 changes: 6 additions & 0 deletions test/jest-setup.ts
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
import '@testing-library/jest-dom'

// jsdom's document.hasFocus() returns false by default (no real window).
// Mock it to return true to match the real browser default state.
if (typeof document !== 'undefined') {
jest.spyOn(document, 'hasFocus').mockReturnValue(true)
}
30 changes: 30 additions & 0 deletions test/unit/web-preset-visible.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { preset } from '../../src/_internal/utils/web-preset'

describe('web-preset isVisible', () => {
it('returns true when visibilityState is visible', () => {
expect(preset.isVisible()).toBe(true)
})

it('returns false when visibilityState is hidden', () => {
const spy = jest.spyOn(document, 'visibilityState', 'get')
spy.mockReturnValue('hidden')
expect(preset.isVisible()).toBe(false)
spy.mockRestore()
})
})

describe('web-preset hasFocus', () => {
it('returns true when document.hasFocus() is true', () => {
const spy = jest.spyOn(document, 'hasFocus')
spy.mockReturnValue(true)
expect(preset.hasFocus()).toBe(true)
spy.mockRestore()
})

it('returns false when document.hasFocus() is false', () => {
const spy = jest.spyOn(document, 'hasFocus')
spy.mockReturnValue(false)
expect(preset.hasFocus()).toBe(false)
spy.mockRestore()
})
})
7 changes: 6 additions & 1 deletion test/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,12 @@ export const hydrateWithConfig = (
export const mockVisibilityHidden = () => {
const mockVisibilityState = jest.spyOn(document, 'visibilityState', 'get')
mockVisibilityState.mockImplementation(() => 'hidden')
return () => mockVisibilityState.mockRestore()
const mockHasFocus = jest.spyOn(document, 'hasFocus')
mockHasFocus.mockReturnValue(false)
return () => {
mockVisibilityState.mockRestore()
mockHasFocus.mockRestore()
}
}

// Using `act()` will cause React 18 to batch updates.
Expand Down
Loading