refact(frontend): simplify hooks, remove wrappers and over-defensiveness
- Remove tunnelCommand.ts pass-throughs, import exposeName directly - Replace dedupeStrings/dedupeNumbers with [...new Set()] - Remove over-defensive array equality checks in useList effects - Inline cheap useMemo, change constants from useState to useMemo - Simplify ensureJsonEnvelope validation - Parameterize UDP/TCP settings handlers - Replace verbose promise duck-typing with instanceof Promise
cognitive committed
Apr 4, 2026 at 06:43 UTC
8468063ee290e3a50e991a5418a1be2c70962c7d
6 files changed
+38
-144
frontend/src/components/ServerCard.tsx
+1
-1
@@ -102,7 +102,7 @@ export function ServerCard({
102
103
try {
104
const result = action();
105
- if (result && typeof (result as Promise<void>).then === "function") {
105
+ if (result instanceof Promise) {
106
void result.catch((error) => {
107
console.error("Failed admin action", error);
108
});
frontend/src/hooks/useAdmin.ts
+16
-40
@@ -142,21 +142,6 @@ function normalizeApprovalMode(value: string | undefined): ApprovalMode {
142
return value === "manual" ? "manual" : "auto";
143
}
144
145
-function dedupeStrings(values: string[]): string[] {
146
- const seen = new Set<string>();
147
- const output: string[] = [];
148
-
149
- values.forEach((value) => {
150
- if (seen.has(value)) {
151
- return;
152
- }
153
- seen.add(value);
154
- output.push(value);
155
- });
156
-
157
- return output;
158
-}
159
-
145
interface AdminSnapshot {
146
serverData: AdminLeaseData[];
147
approvalMode: ApprovalMode;
@@ -349,31 +334,22 @@ export function useAdmin() {
334
});
335
};
336
352
- const handleUDPSettingsChange = async (settings: UDPSettings) => {
353
- await runAdminAction(async () => {
354
- const response = await apiClient.post<{ enabled: boolean; max_leases: number }>(
355
- API_PATHS.admin.udpSettings,
356
- { enabled: settings.enabled, max_leases: settings.maxLeases }
357
- );
358
- setUDPSettings({
359
- enabled: response?.enabled ?? settings.enabled,
360
- maxLeases: response?.max_leases ?? settings.maxLeases,
337
+ const handleSettingsChange = (path: string, setter: (s: { enabled: boolean; maxLeases: number }) => void) =>
338
+ async (settings: { enabled: boolean; maxLeases: number }) => {
339
+ await runAdminAction(async () => {
340
+ const response = await apiClient.post<{ enabled: boolean; max_leases: number }>(path, {
341
+ enabled: settings.enabled,
342
+ max_leases: settings.maxLeases,
343
+ });
344
+ setter({
345
+ enabled: response?.enabled ?? settings.enabled,
346
+ maxLeases: response?.max_leases ?? settings.maxLeases,
347
+ });
348
});
362
- });
363
- };
349
+ };
350
365
- const handleTCPPortSettingsChange = async (settings: TCPPortSettings) => {
366
- await runAdminAction(async () => {
367
- const response = await apiClient.post<{ enabled: boolean; max_leases: number }>(
368
- API_PATHS.admin.tcpPortSettings,
369
- { enabled: settings.enabled, max_leases: settings.maxLeases }
370
- );
371
- setTCPPortSettings({
372
- enabled: response?.enabled ?? settings.enabled,
373
- maxLeases: response?.max_leases ?? settings.maxLeases,
374
- });
375
- });
376
- };
351
+ const handleUDPSettingsChange = handleSettingsChange(API_PATHS.admin.udpSettings, setUDPSettings);
352
+ const handleTCPPortSettingsChange = handleSettingsChange(API_PATHS.admin.tcpPortSettings, setTCPPortSettings);
353
354
const handleLandingPageEnabledChange = async (enabled: boolean) => {
355
await runAdminAction(async () => {
@@ -405,9 +381,9 @@ export function useAdmin() {
381
});
382
383
const runBulkLeaseAction = async (identityKeys: string[], action: LeaseAction) => {
408
- const normalizedIdentityKeys = dedupeStrings(
384
+ const normalizedIdentityKeys = [...new Set(
385
identityKeys.filter((identityKey) => identityKey.length > 0)
410
- );
386
+ )];
387
if (normalizedIdentityKeys.length === 0) {
388
throw new Error("No valid leases selected");
389
}
frontend/src/hooks/useList.ts
+4
-35
@@ -36,19 +36,6 @@ export interface UseListReturn<T extends BaseServer> {
36
handleToggleFavorite: (serverId: number) => void;
37
}
38
39
-function dedupeNumbers(values: number[]): number[] {
40
- const seen = new Set<number>();
41
- const next: number[] = [];
42
- values.forEach((value) => {
43
- if (seen.has(value)) {
44
- return;
45
- }
46
- seen.add(value);
47
- next.push(value);
48
- });
49
- return next;
50
-}
51
-
39
function readStoredFavorites(storageKey: string): number[] {
40
let raw: string | null = null;
41
try {
@@ -66,11 +53,11 @@ function readStoredFavorites(storageKey: string): number[] {
53
if (!Array.isArray(parsed)) {
54
return [];
55
}
69
- return dedupeNumbers(
56
+ return [...new Set(
57
parsed.filter(
58
(value): value is number => Number.isInteger(value) && value > 0
59
)
73
- );
60
+ )];
61
} catch {
62
return [];
63
}
@@ -140,30 +127,12 @@ export function useList<T extends BaseServer>({
127
128
useEffect(() => {
129
const validIDs = new Set(servers.map((server) => server.id));
143
- setFavorites((prev) => {
144
- const next = dedupeNumbers(prev.filter((id) => validIDs.has(id)));
145
- if (
146
- next.length === prev.length &&
147
- next.every((value, index) => value === prev[index])
148
- ) {
149
- return prev;
150
- }
151
- return next;
152
- });
130
+ setFavorites((prev) => prev.filter((id) => validIDs.has(id)));
131
}, [servers]);
132
133
useEffect(() => {
134
const availableTagSet = new Set(availableTags);
157
- setSelectedTags((prev) => {
158
- const next = prev.filter((tag) => availableTagSet.has(tag));
159
- if (
160
- next.length === prev.length &&
161
- next.every((value, index) => value === prev[index])
162
- ) {
163
- return prev;
164
- }
165
- return next;
166
- });
135
+ setSelectedTags((prev) => prev.filter((tag) => availableTagSet.has(tag)));
136
}, [availableTags]);
137
138
const filteredServers = useMemo(() => {
frontend/src/hooks/useTunnelCommand.ts
+9
-16
@@ -1,9 +1,11 @@
1
import { useEffect, useMemo, useState, type ChangeEvent } from "react";
2
import {
3
- buildDefaultTunnelName,
3
+ buildDefaultExposeName,
4
+ normalizeExposeName,
5
+} from "@/lib/exposeName";
6
+import {
7
buildTunnelCommand,
8
buildTunnelDisplayCommand,
6
- normalizeTunnelCommandName,
9
type TunnelCommandOS,
10
} from "@/lib/tunnelCommand";
11
@@ -73,8 +75,8 @@ interface TunnelCommandExtras {
75
}
76
77
export function useTunnelCommand(extras: TunnelCommandExtras = {}) {
76
- const [currentOrigin] = useState(readCurrentOrigin);
77
- const [nameSeed] = useState(readTunnelNameSeed);
78
+ const currentOrigin = useMemo(() => readCurrentOrigin(), []);
79
+ const nameSeed = useMemo(() => readTunnelNameSeed(), []);
80
81
const [target, setTarget] = useState(DEFAULT_HOST);
82
const [name, setName] = useState("");
@@ -82,18 +84,9 @@ export function useTunnelCommand(extras: TunnelCommandExtras = {}) {
84
const [copied, setCopied] = useState(false);
85
const [os, setOs] = useState<TunnelCommandOS>("unix");
86
85
- const resolvedNameSeed = useMemo(
86
- () => `${nameSeed}:${nameShuffleKey}`,
87
- [nameSeed, nameShuffleKey]
88
- );
89
- const generatedName = useMemo(
90
- () => buildDefaultTunnelName(target, resolvedNameSeed),
91
- [resolvedNameSeed, target]
92
- );
93
- const normalizedName = useMemo(
94
- () => normalizeTunnelCommandName(name),
95
- [name]
96
- );
87
+ const resolvedNameSeed = `${nameSeed}:${nameShuffleKey}`;
88
+ const generatedName = buildDefaultExposeName(target, resolvedNameSeed);
89
+ const normalizedName = normalizeExposeName(name);
90
const effectiveName = normalizedName === "" ? generatedName : normalizedName;
91
const commandOptions = useMemo(
92
() => ({
frontend/src/lib/apiClient.ts
+6
-31
@@ -41,45 +41,20 @@ function headersToObject(headers?: HeadersInit): Record<string, string> {
41
}
42
43
function ensureJsonEnvelope<T>(raw: unknown, path: string, status: number): APIEnvelope<T> {
44
- if (!isRecord(raw)) {
45
- throw new APIClientError(
46
- `Unexpected API response for ${path}: envelope is not an object`,
47
- status,
48
- "invalid_envelope",
49
- raw
50
- );
44
+ if (!isRecord(raw) || typeof raw.ok !== "boolean") {
45
+ throw new APIClientError(`Invalid API response for ${path}`, status, "invalid_envelope", raw);
46
}
52
-
53
- const okValue = raw.ok;
54
- if (typeof okValue !== "boolean") {
55
- throw new APIClientError(
56
- `Unexpected API response for ${path}: missing ok flag`,
57
- status,
58
- "invalid_envelope",
59
- raw
60
- );
47
+ if (raw.error !== undefined && !isRecord(raw.error)) {
48
+ throw new APIClientError(`Invalid error payload for ${path}`, status, "invalid_envelope", raw.error);
49
}
62
-
50
const errorValue = raw.error;
64
- if (errorValue !== undefined && !isRecord(errorValue)) {
65
- throw new APIClientError(
66
- `Unexpected API response for ${path}: invalid error payload`,
67
- status,
68
- "invalid_envelope",
69
- errorValue
70
- );
71
- }
72
-
51
return {
74
- ok: okValue,
52
+ ok: raw.ok,
53
data: (raw as { data?: T }).data,
54
error: errorValue
55
? {
56
code: typeof errorValue.code === "string" ? errorValue.code : "request_failed",
79
- message:
80
- typeof errorValue.message === "string"
81
- ? errorValue.message
82
- : "Request failed",
57
+ message: typeof errorValue.message === "string" ? errorValue.message : "Request failed",
58
}
59
: undefined,
60
};
frontend/src/lib/tunnelCommand.ts
+2
-21
@@ -1,9 +1,5 @@
1
import { API_PATHS } from "@/lib/apiPaths";
2
-import {
3
- buildDefaultExposeName,
4
- normalizeExposeName,
5
- resolveExposeName,
6
-} from "@/lib/exposeName";
2
+import { resolveExposeName } from "@/lib/exposeName";
3
4
export type TunnelCommandOS = "unix" | "windows";
5
@@ -20,16 +16,6 @@ export interface TunnelCommandOptions {
16
os: TunnelCommandOS;
17
}
18
23
-export function buildDefaultTunnelName(
24
- target: string,
25
- nameSeed: string
26
-): string {
27
- return buildDefaultExposeName(target, nameSeed);
28
-}
29
-
30
-export function normalizeTunnelCommandName(value: string): string {
31
- return normalizeExposeName(value);
32
-}
19
20
export function buildTunnelCommand({
21
currentOrigin,
@@ -163,7 +149,7 @@ export function buildTunnelPreviewURL(
149
target: string,
150
nameSeed: string
151
): string {
166
- const baseHost = getTunnelBaseHost(origin);
152
+ const baseHost = getRelayOriginHost(origin);
153
const subdomain = resolveExposeName(name, target, nameSeed);
154
return `https://${subdomain}.${baseHost}`;
155
}
@@ -199,11 +185,6 @@ export function normalizeAbsoluteHTTPURL(raw: string): string {
185
}
186
}
187
202
-function getTunnelBaseHost(origin: string): string {
203
- const relayHost = getRelayOriginHost(origin);
204
- return relayHost;
205
-}
206
-
188
function getRelayOriginHost(origin: string): string {
189
try {
190
const parsed = new URL(origin);