mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-13 23:11:40 +00:00
fix(ui): prevent infinite re-render loop in VirtualKeysTable
`keys?.keys || []` produces a new array reference on every render when `keys` is undefined (during initial load). The useEffect([keys, filters]) in useFilterLogic treated each new reference as a change, called setFilteredKeys, triggered a re-render, and looped indefinitely. Stabilise the reference with useMemo before passing it to the hook: const keysList = useMemo(() => keys?.keys ?? [], [keys?.keys]); Also add two regression tests in filter_logic.test.tsx that verify the hook does not hang when re-rendered with a new empty-array reference. Co-Authored-By: Claude Sonnet 4 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b8f7d61400
commit
2d02176c9f
2 changed files with 51 additions and 2 deletions
|
|
@ -94,11 +94,14 @@ export function VirtualKeysTable({ teams, organizations, onSortChange, currentSo
|
|||
});
|
||||
const [expandedAccordions, setExpandedAccordions] = useState<Record<string, boolean>>({});
|
||||
|
||||
// Use the filter logic hook
|
||||
// Stable reference: `|| []` creates a new array literal on every render, which
|
||||
// causes the useEffect in useFilterLogic to fire on every render → infinite loop.
|
||||
const keysList = useMemo(() => keys?.keys ?? [], [keys?.keys]);
|
||||
|
||||
// Use the filter logic hook
|
||||
const { filters, filteredKeys, filteredTotalCount, allTeams, allOrganizations, handleFilterChange, handleFilterReset } =
|
||||
useFilterLogic({
|
||||
keys: keys?.keys || [],
|
||||
keys: keysList,
|
||||
teams,
|
||||
organizations,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -32,6 +32,52 @@ const makeApiResponse = (overrides: { keys?: any[]; total_count?: number; total_
|
|||
total_pages: overrides.total_pages ?? 1,
|
||||
});
|
||||
|
||||
describe("useFilterLogic – stability", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(keyListCall).mockResolvedValue(makeApiResponse());
|
||||
});
|
||||
|
||||
it("should not enter an infinite render loop when keys prop is re-rendered with a new empty-array reference", async () => {
|
||||
// Regression: callers that write `keys?.keys || []` produce a fresh `[]`
|
||||
// on every render (when keys is undefined/null). The useEffect([keys, filters])
|
||||
// must not treat every new-reference empty array as a change that requires
|
||||
// another setFilteredKeys call, which would re-render the consumer, which
|
||||
// would produce yet another new `[]`, ad infinitum.
|
||||
const { result, rerender } = renderHook(
|
||||
({ keys }) => useFilterLogic({ keys, teams: [], organizations: [] }),
|
||||
{ initialProps: { keys: [] as any[] } },
|
||||
);
|
||||
|
||||
// Simulate the || [] pattern: each rerender gets a brand-new [] literal
|
||||
act(() => {
|
||||
rerender({ keys: [] });
|
||||
rerender({ keys: [] });
|
||||
rerender({ keys: [] });
|
||||
});
|
||||
|
||||
// If we reach here the hook did not loop.
|
||||
// filteredKeys should reflect the empty input.
|
||||
expect(result.current.filteredKeys).toEqual([]);
|
||||
});
|
||||
|
||||
it("should update filteredKeys when keys prop changes from empty to populated", async () => {
|
||||
const { result, rerender } = renderHook(
|
||||
({ keys }) => useFilterLogic({ keys, teams: [], organizations: [] }),
|
||||
{ initialProps: { keys: [] as any[] } },
|
||||
);
|
||||
|
||||
expect(result.current.filteredKeys).toEqual([]);
|
||||
|
||||
act(() => {
|
||||
rerender({ keys: [mockKey as any] });
|
||||
});
|
||||
|
||||
expect(result.current.filteredKeys).toHaveLength(1);
|
||||
expect(result.current.filteredKeys[0]).toBe(mockKey);
|
||||
});
|
||||
});
|
||||
|
||||
describe("useFilterLogic – filteredTotalCount", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue