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:
2026-04-23 09:18:31 +02:00
parent 1bcc336c9b
commit 10c534d090
3 changed files with 96 additions and 41 deletions

View 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);
});
});

View File

@@ -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,
};
}