normalizeShellState rebuilds the object and its profiles array on every call. useShellConnection was calling it during render, so consumers that put state.profiles in a useEffect dep array (AppInner does, for the shell-connection status effect) re-ran the effect on every render. The async getShellConnectionNativeResult resolution then triggered another render, feeding the loop. On desktop it stabilized; on iOS Safari it could pin the main thread. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
32 lines
1.3 KiB
TypeScript
32 lines
1.3 KiB
TypeScript
import { useCallback, useMemo } from "react";
|
|
import { useShellContext } from "../context/ShellContext";
|
|
import {
|
|
createOrUpdateProfile,
|
|
deleteProfile,
|
|
normalizeShellState,
|
|
selectActiveProfile,
|
|
} from "../utils/shell-connection-settings";
|
|
import type { ShellConnectionProfileInput } from "../types/native-shell";
|
|
|
|
export function useShellConnection() {
|
|
const context = useShellContext();
|
|
|
|
// Memoize the normalized state. normalizeShellState rebuilds the object and
|
|
// its profiles array on every call, so without this any consumer that puts
|
|
// `state.profiles` in a useEffect dep array (see AppInner) re-runs the
|
|
// effect every render — which can pin the main thread on iOS Safari.
|
|
const state = useMemo(() => normalizeShellState(context.state), [context.state]);
|
|
|
|
const saveProfile = useCallback((profile: ShellConnectionProfileInput) => createOrUpdateProfile(context.shellApi, profile), [context.shellApi]);
|
|
const removeProfile = useCallback((profileId: string) => deleteProfile(context.shellApi, profileId), [context.shellApi]);
|
|
const setActiveProfile = useCallback((profileId: string | null) => selectActiveProfile(context.shellApi, profileId), [context.shellApi]);
|
|
|
|
return {
|
|
...context,
|
|
state,
|
|
saveProfile,
|
|
removeProfile,
|
|
setActiveProfile,
|
|
};
|
|
}
|