Note

← Back to notes

Evolving a Redux-era Dashboard Without a Rewrite

Senior frontend architecture is not about replacing the stack with the newest tools. It is about understanding where the current architecture still serves the product, where it starts creating scaling pressure, and how to evolve the highest-pressure areas incrementally.

Architecture React Redux Dashboards

Intro

A data-heavy infrastructure dashboard can grow naturally from a pragmatic React/Redux architecture. That is not a failure. It often means the product found real workflows and the UI kept absorbing them.

Redux itself was not the problem. The problem was that one workflow page gradually became responsible for too many things: fetching server data, polling availability, managing filters and sorting, rendering marketplace results, handling row actions, and supporting inline edits.

The workflow

A GPU resource workflow can combine discovery, management, deployment status, and operational feedback in the same surface. The user is not thinking in frontend boundaries. They are trying to find capacity, understand what they already have, and take the next action with confidence.

  • discovering available GPU capacity
  • managing already-rented GPU nodes
  • machine catalog
  • community inventory
  • user deployments
  • deployment events
  • regional availability
  • filters, sorting, row actions, and polling

That density is where the architecture starts to matter. Without boundaries, the page becomes the place where every request lifecycle, table behavior, and action rule accumulates.

State boundaries

The goal is not to store less state everywhere. The goal is to store each kind of state at the right boundary.

Global app state

  • auth
  • current user
  • organization
  • project
  • feature flags
  • app shell state
  • shared catalogs

Server state

  • nodes
  • inventory
  • events
  • availability

Local UI state

  • selected tab
  • filters
  • sorting
  • open menus
  • inline editing

Derived state

  • filtered machine lists
  • category counts
  • location options
  • price ranges
  • availability-merged rows

A simplified shape might look like this:

type GpuWorkflowState = {
  globalContext: {
    userId: string;
    orgId: string;
    projectId: string;
  };
  localUi: {
    selectedTab: "find-gpu" | "my-nodes";
    filters: GpuFilters;
    sort: SortState;
    activeMenuId: string | null;
  };
  derived: {
    filteredMachines: Machine[];
    categoryCounts: Record<string, number>;
    locationOptions: string[];
  };
};

Extract stable workflow boundaries first

The safest refactor is not a rewrite. It is extracting stable boundaries from the highest-pressure page, then letting the page coordinate those pieces.

Before:
GpuWorkflowPage
├── fetch nodes
├── fetch events
├── poll availability
├── manage filters
├── sort table
├── render marketplace
└── render row actions

After:
GpuWorkflowPage
├── useGpuNodes(projectId)
├── useGpuMarketplace(projectId)
├── useGpuFilters(...)
├── GpuMarketplaceView
└── GpuNodesTable

useGpuNodes owns rented nodes, deployment status, mutations, and readiness polling. useGpuMarketplace owns catalog, inventory, and availability polling. useGpuFilters owns derived filtering, options, counts, and price ranges.

Presentational components can then focus on rendering and interaction callbacks. The page still composes the workflow, but it no longer has to be the storage location for every detail.

Standardize polling and stale updates

Polling is one of the first places a dashboard starts to feel fragile. It needs a clear owner instead of scattered intervals across one large component.

  • an enabled condition
  • interval ownership
  • cleanup
  • stale response protection
  • clear loading/error ownership
function usePolling({
  enabled,
  intervalMs,
  callback,
}: {
  enabled: boolean;
  intervalMs: number;
  callback: () => Promise<void>;
}) {
  useEffect(() => {
    if (!enabled) return;

    let cancelled = false;

    async function run() {
      if (cancelled) return;
      await callback();
    }

    run();
    const id = window.setInterval(run, intervalMs);

    return () => {
      cancelled = true;
      window.clearInterval(id);
    };
  }, [enabled, intervalMs, callback]);
}

That hook can later be replaced or supported by React Query or RTK Query if the app adopts a server-state library. The important move is making interval behavior, cleanup, and ownership explicit before the next feature adds another polling path.

Introduce modern server-state tools where they pay off

I would not replace Redux everywhere. I would keep it where it models application context well, and introduce a server-state tool only where the request lifecycle is complex enough to justify it.

Redux can continue to hold auth, org, project, app shell, and feature-flag state. A tool like React Query or RTK Query can be introduced around server state that benefits from caching, deduping, retries, invalidation, mutations, and polling.

The migration path should follow pressure. Start with the workflow that has the most request lifecycle complexity. Do not migrate everything at once just to make the architecture look newer.

Closing takeaway

Senior frontend work is often not about replacing the stack. It is about knowing where the current architecture is still helping, where it is starting to hurt, and how to evolve it without a risky rewrite.

Related case

Related case: AI Infrastructure Product Workflows

The same boundary-setting work matters across model APIs, deployments, GPU resources, storage, billing, analytics, and admin operations.