Stabilize function references in useJails with useCallback
Previously, the withRefresh helper and all operations (startJail, stopJail, setIdle, reloadJail, reloadAll) were recreated on every render because they were defined in the hook body without useCallback. This caused unnecessary re-renders of child components using React.memo when parent state changed. Now each operation is wrapped in useCallback with [load] as its dependency. This ensures function references remain stable between renders, allowing React.memo optimizations to work correctly in JailOverviewSection. Tests confirm that function references are now stable between consecutive renders. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
54
frontend/src/hooks/__tests__/useJailList.test.ts
Normal file
54
frontend/src/hooks/__tests__/useJailList.test.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { renderHook } from "@testing-library/react";
|
||||
import { useJails } from "../useJailList";
|
||||
|
||||
// Mock the API calls
|
||||
vi.mock("../../api/jails", () => ({
|
||||
fetchJails: vi.fn().mockResolvedValue({
|
||||
jails: [],
|
||||
total: 0,
|
||||
}),
|
||||
startJail: vi.fn(),
|
||||
stopJail: vi.fn(),
|
||||
setJailIdle: vi.fn(),
|
||||
reloadJail: vi.fn(),
|
||||
reloadAllJails: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../../utils/fetchError", () => ({
|
||||
handleFetchError: vi.fn(),
|
||||
}));
|
||||
|
||||
describe("useJails", () => {
|
||||
it("returns stable function references between renders", () => {
|
||||
const { result, rerender } = renderHook(() => useJails());
|
||||
|
||||
const firstRender = {
|
||||
startJail: result.current.startJail,
|
||||
stopJail: result.current.stopJail,
|
||||
setIdle: result.current.setIdle,
|
||||
reloadJail: result.current.reloadJail,
|
||||
reloadAll: result.current.reloadAll,
|
||||
refresh: result.current.refresh,
|
||||
};
|
||||
|
||||
rerender();
|
||||
|
||||
const secondRender = {
|
||||
startJail: result.current.startJail,
|
||||
stopJail: result.current.stopJail,
|
||||
setIdle: result.current.setIdle,
|
||||
reloadJail: result.current.reloadJail,
|
||||
reloadAll: result.current.reloadAll,
|
||||
refresh: result.current.refresh,
|
||||
};
|
||||
|
||||
// Function references should be the same between renders
|
||||
expect(firstRender.startJail).toBe(secondRender.startJail);
|
||||
expect(firstRender.stopJail).toBe(secondRender.stopJail);
|
||||
expect(firstRender.setIdle).toBe(secondRender.setIdle);
|
||||
expect(firstRender.reloadJail).toBe(secondRender.reloadJail);
|
||||
expect(firstRender.reloadAll).toBe(secondRender.reloadAll);
|
||||
expect(firstRender.refresh).toBe(secondRender.refresh);
|
||||
});
|
||||
});
|
||||
@@ -70,12 +70,45 @@ export function useJails(): UseJailsResult {
|
||||
};
|
||||
}, [load]);
|
||||
|
||||
const withRefresh =
|
||||
(fn: (name: string) => Promise<unknown>) =>
|
||||
const startJailMemo = useCallback(
|
||||
async (name: string): Promise<void> => {
|
||||
await fn(name);
|
||||
await startJail(name);
|
||||
load();
|
||||
};
|
||||
},
|
||||
[load],
|
||||
);
|
||||
|
||||
const stopJailMemo = useCallback(
|
||||
async (name: string): Promise<void> => {
|
||||
await stopJail(name);
|
||||
load();
|
||||
},
|
||||
[load],
|
||||
);
|
||||
|
||||
const reloadJailMemo = useCallback(
|
||||
async (name: string): Promise<void> => {
|
||||
await reloadJail(name);
|
||||
load();
|
||||
},
|
||||
[load],
|
||||
);
|
||||
|
||||
const setIdleMemo = useCallback(
|
||||
(name: string, on: boolean): Promise<void> =>
|
||||
setJailIdle(name, on).then(() => {
|
||||
load();
|
||||
}),
|
||||
[load],
|
||||
);
|
||||
|
||||
const reloadAllMemo = useCallback(
|
||||
(): Promise<void> =>
|
||||
reloadAllJails().then(() => {
|
||||
load();
|
||||
}),
|
||||
[load],
|
||||
);
|
||||
|
||||
return {
|
||||
jails,
|
||||
@@ -83,14 +116,10 @@ export function useJails(): UseJailsResult {
|
||||
loading,
|
||||
error,
|
||||
refresh: load,
|
||||
startJail: withRefresh(startJail),
|
||||
stopJail: withRefresh(stopJail),
|
||||
setIdle: (name, on) => setJailIdle(name, on).then(() => {
|
||||
load();
|
||||
}),
|
||||
reloadJail: withRefresh(reloadJail),
|
||||
reloadAll: () => reloadAllJails().then(() => {
|
||||
load();
|
||||
}),
|
||||
startJail: startJailMemo,
|
||||
stopJail: stopJailMemo,
|
||||
setIdle: setIdleMemo,
|
||||
reloadJail: reloadJailMemo,
|
||||
reloadAll: reloadAllMemo,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user