mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-06 13:14:11 +00:00
perf(clients): de-duplicate options + paged list fetches
Two issues caused each clients-page load to fire its requests twice: 1. setQuery in the hook took whatever object the consumer passed and stored it as-is. The consumer (ClientsPage) constructs a new object literal in an effect, so even when nothing actually changed the ref was new — the hook's useEffect saw a new query and re-fetched. Wrapped setQuery with a shallow value compare so identical params are a no-op. 2. The picker /inbounds/options fetch was bundled into refresh() with a length==0 guard, but the two back-to-back refreshes both saw an empty inbounds array (the first hadn't resolved yet) so both fired the request. Moved the options fetch into its own one-shot effect.
This commit is contained in:
parent
b74465e869
commit
4242f8c881
1 changed files with 37 additions and 16 deletions
|
|
@ -95,7 +95,24 @@ export function useClients() {
|
||||||
const [onlines, setOnlines] = useState<string[]>([]);
|
const [onlines, setOnlines] = useState<string[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [fetched, setFetched] = useState(false);
|
const [fetched, setFetched] = useState(false);
|
||||||
const [query, setQuery] = useState<ClientQueryParams>(DEFAULT_QUERY);
|
const [query, setQueryState] = useState<ClientQueryParams>(DEFAULT_QUERY);
|
||||||
|
// Shallow-compare against the previous query so callers can pass a fresh
|
||||||
|
// object on every render (the common React pattern) without triggering a
|
||||||
|
// re-fetch when nothing actually changed.
|
||||||
|
const setQuery = useCallback((next: ClientQueryParams) => {
|
||||||
|
setQueryState((prev) => {
|
||||||
|
if (
|
||||||
|
prev.page === next.page
|
||||||
|
&& prev.pageSize === next.pageSize
|
||||||
|
&& (prev.search ?? '') === (next.search ?? '')
|
||||||
|
&& (prev.filter ?? '') === (next.filter ?? '')
|
||||||
|
&& (prev.protocol ?? '') === (next.protocol ?? '')
|
||||||
|
&& (prev.sort ?? '') === (next.sort ?? '')
|
||||||
|
&& (prev.order ?? '') === (next.order ?? '')
|
||||||
|
) return prev;
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
const [subSettings, setSubSettings] = useState<SubSettings>({
|
const [subSettings, setSubSettings] = useState<SubSettings>({
|
||||||
enable: false, subURI: '', subJsonURI: '', subJsonEnable: false,
|
enable: false, subURI: '', subJsonURI: '', subJsonEnable: false,
|
||||||
});
|
});
|
||||||
|
|
@ -129,26 +146,30 @@ export function useClients() {
|
||||||
try {
|
try {
|
||||||
const params = override ?? queryRef.current;
|
const params = override ?? queryRef.current;
|
||||||
const qs = buildQS(params);
|
const qs = buildQS(params);
|
||||||
const [clientsMsg, inboundsMsg] = await Promise.all([
|
const msg = await HttpUtil.get(`/panel/api/clients/list/paged?${qs}`) as ApiMsg<ClientPageResponse>;
|
||||||
HttpUtil.get(`/panel/api/clients/list/paged?${qs}`) as Promise<ApiMsg<ClientPageResponse>>,
|
if (msg?.success && msg.obj) {
|
||||||
inbounds.length === 0
|
setClients(Array.isArray(msg.obj.items) ? msg.obj.items : []);
|
||||||
? HttpUtil.get('/panel/api/inbounds/options') as Promise<ApiMsg<InboundOption[]>>
|
setTotal(msg.obj.total ?? 0);
|
||||||
: Promise.resolve(null as ApiMsg<InboundOption[]> | null),
|
setFiltered(msg.obj.filtered ?? 0);
|
||||||
]);
|
if (msg.obj.summary) setSummary(msg.obj.summary);
|
||||||
if (clientsMsg?.success && clientsMsg.obj) {
|
|
||||||
setClients(Array.isArray(clientsMsg.obj.items) ? clientsMsg.obj.items : []);
|
|
||||||
setTotal(clientsMsg.obj.total ?? 0);
|
|
||||||
setFiltered(clientsMsg.obj.filtered ?? 0);
|
|
||||||
if (clientsMsg.obj.summary) setSummary(clientsMsg.obj.summary);
|
|
||||||
}
|
|
||||||
if (inboundsMsg?.success) {
|
|
||||||
setInbounds(Array.isArray(inboundsMsg.obj) ? inboundsMsg.obj : []);
|
|
||||||
}
|
}
|
||||||
setFetched(true);
|
setFetched(true);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [inbounds.length]);
|
}, []);
|
||||||
|
|
||||||
|
// Inbound options are picker-shaped and don't depend on the clients query —
|
||||||
|
// fetch them once on mount instead of every refresh.
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
(async () => {
|
||||||
|
const msg = await HttpUtil.get('/panel/api/inbounds/options') as ApiMsg<InboundOption[]>;
|
||||||
|
if (cancelled) return;
|
||||||
|
if (msg?.success) setInbounds(Array.isArray(msg.obj) ? msg.obj : []);
|
||||||
|
})();
|
||||||
|
return () => { cancelled = true; };
|
||||||
|
}, []);
|
||||||
|
|
||||||
const fetchSubSettings = useCallback(async () => {
|
const fetchSubSettings = useCallback(async () => {
|
||||||
const msg = await HttpUtil.post('/panel/setting/defaultSettings') as ApiMsg<Record<string, unknown>>;
|
const msg = await HttpUtil.post('/panel/setting/defaultSettings') as ApiMsg<Record<string, unknown>>;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue