mirror of
https://github.com/layer-systems/website.git
synced 2026-09-13 14:14:07 +02:00
fix: stop the render loop that crashes the mobile app switcher (#17)
`mapWindow` flagged the state as changed whenever the target window id matched, even when the updater handed the very same window object back. `SET_TITLE` is guarded against a no-op title, but that guard was defeated: the reducer still returned a fresh state object, so the context value changed, every consumer re-rendered, and each app's `setTitle` effect — keyed on a `setTitle` callback that the mobile shell re-created on every render — dispatched `SET_TITLE` again. That loop ran continuously while an app was open on mobile; opening the app-switcher Sheet on top of it made Radix re-attach its composed refs on every one of those renders until React bailed out with "Maximum update depth exceeded" and the ErrorBoundary took over. Only count a window as changed when the updater actually returned a different object, and memoise the mobile shell's `setTitle`/`setParams` per window, matching what `WindowFrame` already does. Fixes #14 Claude-Session: https://claude.ai/code/session_01Trku191Ww2a2YmDWQF3fTS Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { Suspense, useMemo, useState } from 'react';
|
||||
import { Suspense, useCallback, useMemo, useState } from 'react';
|
||||
import { ChevronLeft, LayoutGrid, Zap } from 'lucide-react';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { ErrorBoundary } from '@/components/ErrorBoundary';
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
import { useWindowManager } from '@/os/useWindowManager';
|
||||
import { desktopApps, getApp } from '@/os/registry';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { AppParams } from '@/os/types';
|
||||
|
||||
/**
|
||||
* On a phone the window metaphor only gets in the way, so the same apps and
|
||||
@@ -30,6 +31,23 @@ export function MobileAppShell() {
|
||||
[windows, focusedId],
|
||||
);
|
||||
const activeApp = active ? getApp(active.appId) : undefined;
|
||||
const activeId = active?.id;
|
||||
|
||||
// Stable per-window callbacks: apps set their title from an effect keyed on
|
||||
// `setTitle`, so a fresh function on every shell render would re-run it.
|
||||
const setTitle = useCallback(
|
||||
(title: string) => {
|
||||
if (activeId) setWindowTitle(activeId, title);
|
||||
},
|
||||
[activeId, setWindowTitle],
|
||||
);
|
||||
|
||||
const setParams = useCallback(
|
||||
(params: AppParams) => {
|
||||
if (activeId) setWindowParams(activeId, params);
|
||||
},
|
||||
[activeId, setWindowParams],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col bg-background">
|
||||
@@ -108,8 +126,8 @@ export function MobileAppShell() {
|
||||
<activeApp.component
|
||||
windowId={active.id}
|
||||
params={active.params}
|
||||
setTitle={(title) => setWindowTitle(active.id, title)}
|
||||
setParams={(params) => setWindowParams(active.id, params)}
|
||||
setTitle={setTitle}
|
||||
setParams={setParams}
|
||||
/>
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
|
||||
Reference in New Issue
Block a user