When an app is gated by async readiness (hub connected/ready, auth, data bootstrap), prefer keeping the main app mounted and switching visibility to preserve user state—BUT gate side-effects so hide/show doesn’t re-run one-time initialization. Also, when failure occurs, replace (don’t cover) the app UI if any mounted dialogs/popovers could keep focus traps or disable body pointer events.
Apply this pattern:
<App /> mounted; hide it via a visibility controller (e.g., an Activity-like component) instead of unmounting.ref so it runs only after the first successful initialization; still refresh derived state on every reconnect.visible → hidden → visible and for “failed first load → successful later load”.Example sketch:
function useRemoteEnvironmentBootstrap() {
const hasBootstrappedRef = useRef(false);
useEffect(() => {
if (!initializationSucceeded) return;
// Always refresh active environment on load/reconnect.
dispatch({ type: "refresh-active-environment" });
// Only run initial navigation once.
if (!hasBootstrappedRef.current) {
hasBootstrappedRef.current = true;
dispatch({ type: "select-environment-draft", payload: { draftId } });
}
}, [initializationSucceeded, dispatch, draftId]);
}
export function Home({ hubStatus }) {
const [loaded, setLoaded] = useState(false);
const hubReady = hubStatus === "ready";
const hubFailed = hubStatus === "failed";
useEffect(() => {
if (hubReady) setLoaded(true);
}, [hubReady]);
const showApp = loaded && !hubFailed;
return (
<>
{showApp ? (
<Activity mode={hubReady ? "visible" : "hidden"}>
<App />
</Activity>
) : (
<HubStartupScreen />
)}
</>
);
}
This standard reduces focus/pointer bugs, preserves user context across transient connectivity, and prevents incorrect “re-navigation” on every hide/show cycle.