diff --git a/src/lib/useOnVisibilityChange.ts b/src/lib/useOnVisibilityChange.ts new file mode 100644 index 0000000..a09febc --- /dev/null +++ b/src/lib/useOnVisibilityChange.ts @@ -0,0 +1,20 @@ +import { DependencyList, useEffect } from "react"; +import { isBrowser } from "browser-or-node"; + +type Callback = (isVisible: boolean) => void; + +export const useOnVisibilityChange = ( + cb: Callback, + deps: DependencyList, +): void => { + useEffect(() => { + if (isBrowser) { + document.addEventListener("visibilitychange", () => cb(!document.hidden)); + return () => { + document.removeEventListener("visibilitychange", () => + cb(!document.hidden), + ); + }; + } + }, deps); +}; diff --git a/src/resource/types.ts b/src/resource/types.ts index 8bd6015..96042ce 100644 --- a/src/resource/types.ts +++ b/src/resource/types.ts @@ -24,6 +24,7 @@ export type UseWatchResourceOptions = { useSuspense?: boolean; autoRefresh?: DurationLikeObject; refreshOnWindowFocus?: boolean; + refreshOnDocumentVisibilityChange?: boolean; } & GetAsyncResourceOptions; export type NoSuspenseReturnType = Readonly< diff --git a/src/resource/useWatchResourceValue.test.tsx b/src/resource/useWatchResourceValue.test.tsx index 9d942cd..e9f34cc 100644 --- a/src/resource/useWatchResourceValue.test.tsx +++ b/src/resource/useWatchResourceValue.test.tsx @@ -119,6 +119,35 @@ test("focus event does not trigger resource refresh, if 'refreshOnWindowFocus' i expectValue("Foo"); }); +test("visibilitychange event triggers resource refresh, if 'refreshOnDocumentVisibilityChange' is enabled", async () => { + options.refreshOnDocumentVisibilityChange = true; + render(); + await waitToBeLoaded(); + expectValue("Foo"); + + getName.mockReturnValue("Bar"); + act(() => { + document.dispatchEvent(new Event("visibilitychange")); + }); + + await waitToBeLoaded(); + expectValue("Bar"); +}); + +test("visibilitychange event does not trigger resource refresh, if 'refreshOnVisibilityChange' is not enabled", async () => { + render(); + await waitToBeLoaded(); + expectValue("Foo"); + + getName.mockReturnValue("Bar"); + act(() => { + document.dispatchEvent(new Event("visibilitychange")); + }); + + await waitToBeLoaded(); + expectValue("Foo"); +}); + describe("with disabled suspense", () => { beforeEach(() => { options.useSuspense = false; diff --git a/src/resource/useWatchResourceValue.ts b/src/resource/useWatchResourceValue.ts index a4487f1..57bb5cc 100644 --- a/src/resource/useWatchResourceValue.ts +++ b/src/resource/useWatchResourceValue.ts @@ -4,6 +4,7 @@ import { useWatchObservableValue } from "../observable-value/useWatchObservableV import { UseWatchResourceOptions, UseWatchResourceResult } from "./types.js"; import { hash } from "object-code"; import { useOnWindowFocused } from "../lib/useOnWindowFocused.js"; +import { useOnVisibilityChange } from "../lib/useOnVisibilityChange.js"; export const useWatchResourceValue = < T, @@ -18,6 +19,7 @@ export const useWatchResourceValue = < keepValueWhileLoading = true, useSuspense = true, refreshOnWindowFocus = false, + refreshOnDocumentVisibilityChange = refreshOnWindowFocus, autoRefresh, } = options; @@ -37,6 +39,15 @@ export const useWatchResourceValue = < } }, [resource, refreshOnWindowFocus]); + useOnVisibilityChange( + (isVisible) => { + if (refreshOnDocumentVisibilityChange && isVisible) { + resource.refresh(); + } + }, + [resource, refreshOnDocumentVisibilityChange], + ); + setTimeout(() => { void resource.load(); }, 0);