mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 12:42:38 +00:00
fix(web): hooks-refs batch 2.1 — useLatest helper; useAuthenticatedFetch + presence off render-phase ref writes
Render-phase ref mirror writes tear under concurrent rendering (react-hooks/refs). New hooks/useLatest.ts syncs the mirror in a passive effect instead; refs from custom hooks are not linter-known-stable, so consumers list them in dep arrays (identity is stable — no re-creation). Lint ratchet 141 -> 139. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -82,7 +82,7 @@ jobs:
|
||||
# in eslint.config.mjs + exhaustive-deps) — when you fix warnings, lower
|
||||
# the cap to match; never raise it.
|
||||
- name: Lint
|
||||
run: npm run lint -- --max-warnings 141
|
||||
run: npm run lint -- --max-warnings 139
|
||||
|
||||
- name: Tests
|
||||
run: npm test
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { api } from "@/lib/api";
|
||||
import { useLatest } from "@/hooks/useLatest";
|
||||
import { useAuth } from "../../../contexts/AuthContext";
|
||||
import { authenticatedFetch, TokenExpiredError } from "@/lib/authenticatedFetch";
|
||||
|
||||
@@ -58,8 +59,7 @@ export default function PresencePage() {
|
||||
const [lastUpdated, setLastUpdated] = useState<Date | null>(null);
|
||||
|
||||
// Keep the latest window value available to the interval callback without re-arming the timer.
|
||||
const windowRef = useRef(windowMinutes);
|
||||
windowRef.current = windowMinutes;
|
||||
const windowRef = useLatest(windowMinutes);
|
||||
|
||||
const fetchPresence = useCallback(
|
||||
async (showRefreshing = false) => {
|
||||
@@ -87,7 +87,7 @@ export default function PresencePage() {
|
||||
setRefreshing(false);
|
||||
}
|
||||
},
|
||||
[getAccessToken],
|
||||
[getAccessToken, windowRef],
|
||||
);
|
||||
|
||||
// Initial load + whenever the window changes.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback, useRef } from "react";
|
||||
import { useLatest } from "@/hooks/useLatest";
|
||||
import { useAuth } from "@/contexts/AuthContext";
|
||||
import { useNotifications } from "@/contexts/NotificationContext";
|
||||
import {
|
||||
@@ -47,8 +48,7 @@ export function useAuthenticatedFetch<T = unknown>(
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Stabilize callback refs so `execute` identity doesn't change on every render
|
||||
const optionsRef = useRef(options);
|
||||
optionsRef.current = options;
|
||||
const optionsRef = useLatest(options);
|
||||
|
||||
// Latest-wins guard: when execute() is called again while a previous request is still in
|
||||
// flight (e.g. the tenant-scope selector seeds a persisted foreign tenant right after mount,
|
||||
@@ -140,7 +140,7 @@ export function useAuthenticatedFetch<T = unknown>(
|
||||
}
|
||||
}
|
||||
},
|
||||
[getAccessToken, addNotification],
|
||||
[getAccessToken, addNotification, optionsRef],
|
||||
);
|
||||
|
||||
return { data, loading, error, execute, clearError, setData };
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, type RefObject } from "react";
|
||||
|
||||
/**
|
||||
* Returns a ref that always holds the latest `value`, synced in a passive effect
|
||||
* (never during render — render-phase ref writes tear under concurrent rendering,
|
||||
* flagged by react-hooks/refs).
|
||||
*
|
||||
* Use for long-lived closures (SignalR handlers, fetch callbacks, interval timers)
|
||||
* that must see current props/state without being re-created per render.
|
||||
*
|
||||
* Timing contract: `ref.current` updates AFTER commit. That is safe for async
|
||||
* callbacks and event handlers, and for sibling effects declared after the hook
|
||||
* call (effects flush in registration order). Do NOT read it during render, and
|
||||
* do not rely on it from layout effects or child-component effects — those run
|
||||
* before this hook's sync effect and would observe the previous render's value.
|
||||
*/
|
||||
export function useLatest<T>(value: T): RefObject<T> {
|
||||
const ref = useRef(value);
|
||||
useEffect(() => {
|
||||
ref.current = value;
|
||||
});
|
||||
return ref;
|
||||
}
|
||||
Reference in New Issue
Block a user