Skip to content

Bug: Default Navigation Mode (Page) Clicks Have No Effect — Stale Closure Root Cause #908

@hotlong

Description

@hotlong

Summary

Clicking a row in the grid/list when the default navigation mode is Page does nothing. This has been reported and patched multiple times but the underlying root cause was never addressed. The bug is a stale closure chain caused by unmemoized objects/callbacks passed into useNavigationOverlay.

Screenshot

Navigation mode dropdown showing Page is selected but clicks do nothing

Root Cause Analysis (3 Compounding Issues)

Issue 1: { mode: 'page' } fallback creates a new object every render

In apps/console/src/components/ObjectView.tsx line 423:

const detailNavigation: ViewNavigationConfig = activeView?.navigation ?? objectDef.navigation ?? { mode: 'page' };

{ mode: 'page' } is a new object literal on every render. This causes the navigation option passed to useNavigationOverlay to change reference every render, which in turn causes its internal handleClick useCallback to be recreated every render.

Issue 2: onNavigate callback is not memoized

In apps/console/src/components/ObjectView.tsx lines 428-445, the onNavigate is an inline arrow function:

const navOverlay = useNavigationOverlay({
    navigation: detailNavigation,
    objectName: objectDef.name,
    onNavigate: (recordId: string | number, action?: string) => {
        // inline — new function identity every render
        if (action === 'view' || !action || action === 'page') {
            navigate(`record/${String(recordId)}`);
        }
    },
});

Because both navigation AND onNavigate have unstable identities, React may batch-skip the render where handleClick picks up the fresh onNavigate, resulting in a stale (or initial undefined) closure being called.

Issue 3: Cascade instability through renderListView

navOverlay (returned from useMemo) gets a new reference every render because its deps (handleClick, etc.) changed. This propagates through renderListView's useCallback deps, causing the <ListView> to re-render with potentially stale intermediate closures during React's batched updates.

Call Chain Trace

User clicks grid row
  → ObjectGrid.onRowClick
    → PluginObjectView.handleRowClick (line 397)
      → Console's onRowClick lambda (line 840)
        → navOverlay.handleClick
          → useNavigationOverlay.handleClick (line 120)
            → mode === 'page' → calls onNavigate(recordId, 'view')
              → ⚠️ onNavigate may be stale/undefined due to unmemoized deps

Proposed Fix

Memoize the navigation config and onNavigate callback in apps/console/src/components/ObjectView.tsx:

// 1. Memoize the fallback navigation config
const detailNavigation: ViewNavigationConfig = useMemo(
    () => activeView?.navigation ?? objectDef.navigation ?? { mode: 'page' },
    [activeView?.navigation, objectDef.navigation]
);

// 2. Memoize the onNavigate callback
const handleNavOverlayNavigate = useCallback(
    (recordId: string | number, action?: string) => {
        if (action === 'new_window') {
            const basePath = window.location.pathname.replace(/\/view\/.*$/, '');
            window.open(`${basePath}/record/${String(recordId)}`, '_blank');
            return;
        }
        if (action === 'view' || !action || action === 'page') {
            if (viewId) {
                navigate(`../../record/${String(recordId)}`, { relative: 'path' });
            } else {
                navigate(`record/${String(recordId)}`);
            }
        }
    },
    [navigate, viewId]
);

// 3. Use memoized values
const navOverlay = useNavigationOverlay({
    navigation: detailNavigation,
    objectName: objectDef.name,
    onNavigate: handleNavOverlayNavigate,
});

Files Affected

File Change
apps/console/src/components/ObjectView.tsx Memoize detailNavigation with useMemo, extract onNavigate into useCallback
packages/react/src/__tests__/useNavigationOverlay.test.ts Add test for stale closure scenario
ROADMAP.md Document fix

Acceptance Criteria

  • Clicking a grid row with navigation.mode: 'page' (default) navigates to record detail page
  • Clicking a grid row with navigation.mode: 'drawer' opens drawer overlay
  • Clicking a grid row with navigation.mode: 'modal' opens modal overlay
  • Clicking a grid row with navigation.mode: 'none' does nothing
  • All existing useNavigationOverlay tests pass (29 tests)
  • All existing ObjectView tests pass (32 tests)
  • New test: verify handleClick uses latest onNavigate after re-render
  • Update ROADMAP.md with root cause and fix

Labels

bug, P0, navigation

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions