mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-10-01 07:37:55 -05:00
Optimize first load rerenders
This commit is contained in:
@@ -35,6 +35,15 @@ export function useEventsReadyState(): EventsReadyState {
|
||||
);
|
||||
}
|
||||
|
||||
/** Whether the shared SSE connection is up; unlike the ready state, the CLOSED to CONNECTING flip doesn't re-render. */
|
||||
export function useEventsConnected(): boolean {
|
||||
return React.useSyncExternalStore(
|
||||
eventsClient.subscribeToReadyState,
|
||||
getIsConnected,
|
||||
getServerIsConnected,
|
||||
);
|
||||
}
|
||||
|
||||
/** Calls `listener` for every server event received over the shared SSE connection. */
|
||||
export function useServerEventListener(listener: (event: ServerEvent) => void) {
|
||||
const handleEvent = React.useEffectEvent(listener);
|
||||
@@ -64,7 +73,7 @@ export function useEventStreamCatchUp({
|
||||
enabled: boolean;
|
||||
onCatchUp: () => void;
|
||||
}) {
|
||||
const readyState = useEventsReadyState();
|
||||
const connected = useEventsConnected();
|
||||
const latestOnCatchUp = React.useRef(onCatchUp);
|
||||
latestOnCatchUp.current = onCatchUp;
|
||||
const scheduledRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
@@ -88,7 +97,7 @@ export function useEventStreamCatchUp({
|
||||
[],
|
||||
);
|
||||
|
||||
useCatchUpOnConnect(enabled, readyState, catchUp);
|
||||
useCatchUpOnConnect(enabled, connected, catchUp);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!enabled) return;
|
||||
@@ -97,11 +106,11 @@ export function useEventStreamCatchUp({
|
||||
}, [enabled, catchUp]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!enabled || readyState === "CONNECTED") return;
|
||||
if (!enabled || connected) return;
|
||||
|
||||
const interval = setInterval(catchUp, EVENTS_DOWN_CATCH_UP_MS);
|
||||
return () => clearInterval(interval);
|
||||
}, [enabled, readyState, catchUp]);
|
||||
}, [enabled, connected, catchUp]);
|
||||
|
||||
return catchUp;
|
||||
}
|
||||
@@ -112,7 +121,7 @@ export function useEventStreamCatchUp({
|
||||
*/
|
||||
function useCatchUpOnConnect(
|
||||
enabled: boolean,
|
||||
readyState: EventsReadyState,
|
||||
connected: boolean,
|
||||
onConnect: () => void,
|
||||
) {
|
||||
const hasConnectedRef = React.useRef(false);
|
||||
@@ -126,7 +135,7 @@ function useCatchUpOnConnect(
|
||||
}
|
||||
listeningSinceRef.current ??= Date.now();
|
||||
|
||||
if (readyState !== "CONNECTED") return;
|
||||
if (!connected) return;
|
||||
|
||||
const isFirstConnect = !hasConnectedRef.current;
|
||||
hasConnectedRef.current = true;
|
||||
@@ -138,7 +147,7 @@ function useCatchUpOnConnect(
|
||||
}
|
||||
|
||||
onConnect();
|
||||
}, [enabled, readyState, onConnect]);
|
||||
}, [enabled, connected, onConnect]);
|
||||
}
|
||||
|
||||
const returnListeners = new Set<() => void>();
|
||||
@@ -187,3 +196,5 @@ function noticeReturn() {
|
||||
}
|
||||
|
||||
const getServerReadyState = (): EventsReadyState => "CLOSED";
|
||||
const getIsConnected = () => eventsClient.getReadyState() === "CONNECTED";
|
||||
const getServerIsConnected = () => false;
|
||||
|
||||
@@ -19,12 +19,10 @@ interface LayoutData {
|
||||
interface LayoutDataContextValue extends LayoutData {
|
||||
/** Refetches the app shell data, without touching the page's own loaders. */
|
||||
refresh: () => void;
|
||||
isRefreshing: boolean;
|
||||
}
|
||||
|
||||
const LayoutDataContext = React.createContext<LayoutDataContextValue>({
|
||||
refresh: () => {},
|
||||
isRefreshing: false,
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -44,14 +42,11 @@ export function LayoutDataProvider({
|
||||
refresh,
|
||||
} = useBackgroundResource<SerializeFrom<typeof loader>>(LAYOUT_DATA_ROUTE);
|
||||
|
||||
// a ref so a poll elsewhere does not re-run the effect and restart the interval before it fires
|
||||
const isLoadingRef = React.useRef(isLoading);
|
||||
isLoadingRef.current = isLoading;
|
||||
const lastRefreshedAtRef = React.useRef(0);
|
||||
|
||||
React.useEffect(() => {
|
||||
const loadIfIdle = () => {
|
||||
if (isLoadingRef.current) return;
|
||||
if (isLoading()) return;
|
||||
// alt-tabbing back is not worth a full app shell rebuild if one just happened
|
||||
if (Date.now() - lastRefreshedAtRef.current < ONE_MINUTE) return;
|
||||
|
||||
@@ -72,7 +67,7 @@ export function LayoutDataProvider({
|
||||
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
||||
clearInterval(interval);
|
||||
};
|
||||
}, [refresh]);
|
||||
}, [refresh, isLoading]);
|
||||
|
||||
const newest = useNewestOf(data, polledData);
|
||||
|
||||
@@ -85,7 +80,6 @@ export function LayoutDataProvider({
|
||||
const value: LayoutDataContextValue = {
|
||||
...newest,
|
||||
refresh,
|
||||
isRefreshing: isLoading,
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -2,7 +2,7 @@ import * as React from "react";
|
||||
import { useFetchers, useLocation, useNavigation } from "react-router";
|
||||
import {
|
||||
useEventStreamCatchUp,
|
||||
useEventsReadyState,
|
||||
useEventsConnected,
|
||||
useServerEventListener,
|
||||
} from "~/features/events/events-hooks";
|
||||
import { useBackgroundResource } from "~/hooks/useBackgroundResource";
|
||||
@@ -39,10 +39,10 @@ export function NotificationsProvider({
|
||||
const { data, refresh } = useBackgroundResource<SerializeFrom<typeof loader>>(
|
||||
NOTIFICATIONS_DATA_ROUTE,
|
||||
);
|
||||
const readyState = useEventsReadyState();
|
||||
const connected = useEventsConnected();
|
||||
|
||||
const loggedIn = Boolean(user);
|
||||
const eventsDown = loggedIn && readyState !== "CONNECTED";
|
||||
const eventsDown = loggedIn && !connected;
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!loggedIn) return;
|
||||
|
||||
@@ -3,16 +3,17 @@ import * as React from "react";
|
||||
/**
|
||||
* Keeps an app shell resource route's JSON fresh outside the router: an in-flight `useFetcher` load is folded
|
||||
* into a starting navigation and must settle first, so it would hold up every page change it overlaps.
|
||||
* Only a landed response renders; whether a fetch is in flight is read on demand via `isLoading()`.
|
||||
*/
|
||||
export function useBackgroundResource<T>(url: string) {
|
||||
const [data, setData] = React.useState<T>();
|
||||
const [isLoading, setIsLoading] = React.useState(false);
|
||||
const inFlightRef = React.useRef(false);
|
||||
const latestRequestRef = React.useRef(0);
|
||||
|
||||
// stable so effects refreshing after a mutation don't re-run every render
|
||||
const refresh = React.useCallback(async () => {
|
||||
const requestId = ++latestRequestRef.current;
|
||||
setIsLoading(true);
|
||||
inFlightRef.current = true;
|
||||
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
@@ -26,9 +27,12 @@ export function useBackgroundResource<T>(url: string) {
|
||||
} catch {
|
||||
// a background refresh failing just leaves the last data in place
|
||||
} finally {
|
||||
if (requestId === latestRequestRef.current) setIsLoading(false);
|
||||
if (requestId === latestRequestRef.current) inFlightRef.current = false;
|
||||
}
|
||||
}, [url]);
|
||||
|
||||
// stable for the same reason
|
||||
const isLoading = React.useCallback(() => inFlightRef.current, []);
|
||||
|
||||
return { data, isLoading, refresh };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user