web: clear set-state-in-effect lint warning on the annotations page

The annotations overview pushed the ESLint warning count to 100, one over
the frozen --max-warnings 99 cap, failing CI since 66d25f57's run. The
react-hooks/set-state-in-effect rule flags any direct effect call into a
state-setting useCallback, even with every setState behind an await.

Restructure instead of suppressing: loading/error begin-state moves from
fetchPage into the interaction sites (filter selects, load-more; mount is
covered by the initial state), and the effect invokes fetchPage through an
inner async wrapper so no setState is synchronously reachable. Lint is
back to exactly 99.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Oliver Kieselbach
2026-08-12 17:19:52 +02:00
parent dcec6a1863
commit fda52666d6
@@ -55,10 +55,11 @@ export default function AnnotationsPage() {
const lanes = visibleLanes(user);
// Callers must set loading=true / loadError=null before invoking (the initial
// state covers the mount fetch) — no synchronous setState here, this runs
// inside the effect below.
const fetchPage = useCallback(
async (url: string, append: boolean) => {
setLoading(true);
setLoadError(null);
try {
const res = await authenticatedFetch(url, getAccessToken);
if (res.status === 404) {
@@ -83,13 +84,18 @@ export default function AnnotationsPage() {
);
useEffect(() => {
fetchPage(
api.annotations.list({
verdict: verdictFilter || undefined,
lane: laneFilter || undefined,
}),
false
);
// Inner async wrapper: set-state-in-effect flags a direct call to a
// state-setting callback even when every setState sits behind an await.
const loadFirstPage = async () => {
await fetchPage(
api.annotations.list({
verdict: verdictFilter || undefined,
lane: laneFilter || undefined,
}),
false
);
};
void loadFirstPage();
}, [fetchPage, verdictFilter, laneFilter]);
return (
@@ -109,7 +115,11 @@ export default function AnnotationsPage() {
<div className="flex flex-wrap items-center gap-3 mb-4">
<select
value={verdictFilter}
onChange={(e) => setVerdictFilter(e.target.value)}
onChange={(e) => {
setVerdictFilter(e.target.value);
setLoading(true);
setLoadError(null);
}}
className="border border-gray-300 rounded-md px-3 py-1.5 text-sm text-gray-700 focus:outline-none focus:ring-2 focus:ring-green-500"
aria-label="Filter by verdict"
>
@@ -120,7 +130,11 @@ export default function AnnotationsPage() {
</select>
<select
value={laneFilter}
onChange={(e) => setLaneFilter(e.target.value)}
onChange={(e) => {
setLaneFilter(e.target.value);
setLoading(true);
setLoadError(null);
}}
className="border border-gray-300 rounded-md px-3 py-1.5 text-sm text-gray-700 focus:outline-none focus:ring-2 focus:ring-green-500"
aria-label="Filter by role"
>
@@ -198,7 +212,11 @@ export default function AnnotationsPage() {
{loading && <span className="text-sm text-gray-400">Loading…</span>}
{!loading && nextLink && (
<button
onClick={() => fetchPage(`${API_BASE_URL}${nextLink}`, true)}
onClick={() => {
setLoading(true);
setLoadError(null);
fetchPage(`${API_BASE_URL}${nextLink}`, true);
}}
className="px-4 py-1.5 text-sm font-medium bg-white border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50"
>
Load more