fix: make useLiveData a context that is pausable
This commit is contained in:
parent
98d02bb595
commit
c066b3064d
@ -5,7 +5,6 @@ import type { LoadContext } from '~/server';
|
|||||||
import { ResponseError } from '~/server/headscale/api-client';
|
import { ResponseError } from '~/server/headscale/api-client';
|
||||||
import cn from '~/utils/cn';
|
import cn from '~/utils/cn';
|
||||||
import log from '~/utils/log';
|
import log from '~/utils/log';
|
||||||
import { useLiveData } from '~/utils/useLiveData';
|
|
||||||
|
|
||||||
export async function loader({
|
export async function loader({
|
||||||
request,
|
request,
|
||||||
@ -36,7 +35,6 @@ export async function loader({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Layout() {
|
export default function Layout() {
|
||||||
useLiveData({ interval: 3000 });
|
|
||||||
const { healthy } = useLoaderData<typeof loader>();
|
const { healthy } = useLoaderData<typeof loader>();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
34
app/root.tsx
34
app/root.tsx
@ -12,6 +12,7 @@ import { ErrorPopup } from '~/components/Error';
|
|||||||
import ProgressBar from '~/components/ProgressBar';
|
import ProgressBar from '~/components/ProgressBar';
|
||||||
import ToastProvider from '~/components/ToastProvider';
|
import ToastProvider from '~/components/ToastProvider';
|
||||||
import stylesheet from '~/tailwind.css?url';
|
import stylesheet from '~/tailwind.css?url';
|
||||||
|
import { LiveDataProvider } from '~/utils/live-data';
|
||||||
import { useToastQueue } from '~/utils/toast';
|
import { useToastQueue } from '~/utils/toast';
|
||||||
|
|
||||||
export const meta: MetaFunction = () => [
|
export const meta: MetaFunction = () => [
|
||||||
@ -29,21 +30,26 @@ export const links: LinksFunction = () => [
|
|||||||
export function Layout({ children }: { readonly children: React.ReactNode }) {
|
export function Layout({ children }: { readonly children: React.ReactNode }) {
|
||||||
const toastQueue = useToastQueue();
|
const toastQueue = useToastQueue();
|
||||||
|
|
||||||
|
// LiveDataProvider is wrapped at the top level since dialogs and things
|
||||||
|
// that control its state are usually open in portal containers which
|
||||||
|
// are not a part of the normal React tree.
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<LiveDataProvider>
|
||||||
<head>
|
<html lang="en">
|
||||||
<meta charSet="utf-8" />
|
<head>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta charSet="utf-8" />
|
||||||
<Meta />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<Links />
|
<Meta />
|
||||||
</head>
|
<Links />
|
||||||
<body className="overscroll-none dark:bg-headplane-900 dark:text-headplane-50">
|
</head>
|
||||||
{children}
|
<body className="overscroll-none dark:bg-headplane-900 dark:text-headplane-50">
|
||||||
<ToastProvider queue={toastQueue} />
|
{children}
|
||||||
<ScrollRestoration />
|
<ToastProvider queue={toastQueue} />
|
||||||
<Scripts />
|
<ScrollRestoration />
|
||||||
</body>
|
<Scripts />
|
||||||
</html>
|
</body>
|
||||||
|
</html>
|
||||||
|
</LiveDataProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
67
app/utils/live-data.tsx
Normal file
67
app/utils/live-data.tsx
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import { createContext, useContext, useEffect, useState } from 'react';
|
||||||
|
import { useRevalidator } from 'react-router';
|
||||||
|
import { useInterval } from 'usehooks-ts';
|
||||||
|
|
||||||
|
const LiveDataPausedContext = createContext({
|
||||||
|
paused: false,
|
||||||
|
setPaused: (_: boolean) => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
interface LiveDataProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LiveDataProvider({ children }: LiveDataProps) {
|
||||||
|
const [paused, setPaused] = useState(false);
|
||||||
|
const revalidator = useRevalidator();
|
||||||
|
|
||||||
|
// Document is marked as optional here because it's not available in SSR
|
||||||
|
// The optional chain means if document is not defined, visible is false
|
||||||
|
const [visible, setVisible] = useState(
|
||||||
|
() =>
|
||||||
|
typeof document !== 'undefined' && document.visibilityState === 'visible',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Function to revalidate safely
|
||||||
|
const revalidateIfIdle = () => {
|
||||||
|
if (revalidator.state === 'idle') {
|
||||||
|
revalidator.revalidate();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleVisibilityChange = () => {
|
||||||
|
setVisible(document.visibilityState === 'visible');
|
||||||
|
if (!paused && document.visibilityState === 'visible') {
|
||||||
|
revalidateIfIdle();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('online', revalidateIfIdle);
|
||||||
|
document.addEventListener('focus', revalidateIfIdle);
|
||||||
|
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('online', revalidateIfIdle);
|
||||||
|
document.removeEventListener('focus', revalidateIfIdle);
|
||||||
|
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||||
|
};
|
||||||
|
}, [paused, revalidator]);
|
||||||
|
|
||||||
|
// Poll only when visible and not paused
|
||||||
|
useInterval(revalidateIfIdle, visible && !paused ? 3000 : null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LiveDataPausedContext.Provider value={{ paused, setPaused }}>
|
||||||
|
{children}
|
||||||
|
</LiveDataPausedContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useLiveData() {
|
||||||
|
const context = useContext(LiveDataPausedContext);
|
||||||
|
return {
|
||||||
|
pause: () => context.setPaused(true),
|
||||||
|
resume: () => context.setPaused(false),
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -2,7 +2,7 @@ import { useEffect, useMemo, useRef } from 'react';
|
|||||||
import { useFetcher } from 'react-router';
|
import { useFetcher } from 'react-router';
|
||||||
import { HostInfo } from '~/types';
|
import { HostInfo } from '~/types';
|
||||||
|
|
||||||
export default function useAgent(nodeIds: string[], interval = 3000) {
|
export default function useAgent(nodeIds: string[]) {
|
||||||
const fetcher = useFetcher<Record<string, HostInfo>>();
|
const fetcher = useFetcher<Record<string, HostInfo>>();
|
||||||
const qp = useMemo(
|
const qp = useMemo(
|
||||||
() => new URLSearchParams({ node_ids: nodeIds.join(',') }),
|
() => new URLSearchParams({ node_ids: nodeIds.join(',') }),
|
||||||
@ -15,15 +15,7 @@ export default function useAgent(nodeIds: string[], interval = 3000) {
|
|||||||
fetcher.load(`/api/agent?${qp.toString()}`);
|
fetcher.load(`/api/agent?${qp.toString()}`);
|
||||||
idRef.current = nodeIds;
|
idRef.current = nodeIds;
|
||||||
}
|
}
|
||||||
|
}, [qp.toString()]);
|
||||||
const intervalID = setInterval(() => {
|
|
||||||
fetcher.load(`/api/agent?${qp.toString()}`);
|
|
||||||
}, interval);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
clearInterval(intervalID);
|
|
||||||
};
|
|
||||||
}, [interval, qp]);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: fetcher.data,
|
data: fetcher.data,
|
||||||
|
|||||||
@ -1,35 +0,0 @@
|
|||||||
import { useRevalidator } from 'react-router';
|
|
||||||
import { useEffect } from 'react';
|
|
||||||
import { useInterval } from 'usehooks-ts';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
interval: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useLiveData({ interval }: Props) {
|
|
||||||
const revalidator = useRevalidator();
|
|
||||||
|
|
||||||
// Handle normal stale-while-revalidate behavior
|
|
||||||
useInterval(() => {
|
|
||||||
if (revalidator.state === 'idle') {
|
|
||||||
revalidator.revalidate();
|
|
||||||
}
|
|
||||||
}, interval);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handler = () => {
|
|
||||||
if (revalidator.state === 'idle') {
|
|
||||||
revalidator.revalidate();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener('online', handler);
|
|
||||||
document.addEventListener('focus', handler);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('online', handler);
|
|
||||||
document.removeEventListener('focus', handler);
|
|
||||||
};
|
|
||||||
}, [revalidator]);
|
|
||||||
return revalidator;
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue
Block a user