useLogsData: stale closure in log accumulation discards pages under concurrent scroll
Bug Description
useLogsData.ts accumulates log entries using [...logs, ...currentLogs] inside a useEffect, where logs is captured from the enclosing closure. Because logs is excluded from the dependency array (with an eslint-disable comment), the effect always closes over the value of logs from the render in which it was last created - not the latest state. Under fast scrolling (two scroll-to-bottom events before the first response arrives), the second response's effect writes [stale_empty_logs, ...page2], discarding page 1's entries permanently.
Affected file
frontend/src/hooks/useLogsData.ts, lines 158–171:
useEffect(() => {
const currentData = data?.payload?.data?.newResult?.data?.result || [];
if (currentData.length > 0 && currentData[0].list) {
const currentLogs: ILog[] = currentData[0].list.map((item) => ({
...item.data,
timestamp: item.timestamp,
}));
const newLogs = [...logs, ...currentLogs]; // ← `logs` is stale
setLogs(newLogs);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data]); // ← `logs` deliberately excludedFailure scenario
- User scrolls to bottom → page 1 request fires.
- User scrolls to bottom again before response → page 2 request fires.
- Both responses arrive. React 18 may batch both
dataupdates into one re-render cycle. - Both
useEffectcallbacks run with the same stalelogs = []. - First callback:
setLogs([...[], ...page1])=page1. - Second callback (runs immediately after):
setLogs([...[], ...page2])=page2- page 1 entries are gone. - User sees a gap in the log list. The missing entries are not recoverable without a page reload.
Fix
Use the functional form of setLogs which receives the guaranteed-latest previous state, eliminating the stale closure:
useEffect(() => {
const currentData = data?.payload?.data?.newResult?.data?.result || [];
if (currentData.length > 0 && currentData[0].list) {
const currentLogs: ILog[] = currentData[0].list.map((item) => ({
...item.data,
timestamp: item.timestamp,
}));
setLogs((prevLogs) => [...prevLogs, ...currentLogs]); // ← functional update
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data]);This also makes the eslint-disable comment unnecessary.
Environment
SigNoz main branch (2026-08-13), React 18.
Source: SigNoz/signoz