#12547·signoz

useLogsData: stale closure in log accumulation discards pages under concurrent scroll

Author: harsh4vardhanCreated Aug 13, 2026Updated Sep 16, 2026

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:

typescript
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 excluded

Failure scenario

  1. User scrolls to bottom → page 1 request fires.
  2. User scrolls to bottom again before response → page 2 request fires.
  3. Both responses arrive. React 18 may batch both data updates into one re-render cycle.
  4. Both useEffect callbacks run with the same stale logs = [].
  5. First callback: setLogs([...[], ...page1]) = page1.
  6. Second callback (runs immediately after): setLogs([...[], ...page2]) = page2 - page 1 entries are gone.
  7. 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:

typescript
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.