Skip to content

[feature] Refactor labextension React components from class to functional with hooks #648

@ederign

Description

@ederign

Feature Area

What is the problem?

The labextension uses React class components throughout. The largest — KubeflowKaleLeftPanel (~700 lines) — is a concrete example of why this is a problem: it manages all sidebar state in one monolithic IState blob, causing real bugs and making the code hard to reason about. But the same patterns (manual signal wiring, interleaved state, no effect cleanup) exist across InlineCellsMetadata, CellMetadataEditor, DeployProgress, and others.

Concrete issues today

  1. Mutable external state read during render (#647). getActiveNotebook() read this.props.tracker.currentWidget — a mutable JupyterLab object — in render(). React can't track this, so the UI stayed stuck on the disabled toggle after opening a notebook. On clusters, the sequential RPC awaits in setNotebookPanel (kernel startup + 6 backend calls) stacked up to 30+ seconds of latency before any setState fired. Locally the calls fail fast, which is why nobody caught it. We patched it by adding activeNotebook to state, but a useActiveNotebook(tracker) hook would have prevented this class of bug entirely.

  2. Giant interleaved state. 11 state fields with different lifecycles share one setState path. resetState has to manually preserve isEnabled and activeNotebook — each new field that needs preservation is a landmine.

  3. Async state management without cleanup. setNotebookPanel makes multiple setState calls across 7+ await points with no cancellation. Switching notebooks mid-flight leaves the old chain running and writing stale data. No try/catch either — a KernelError becomes an unhandled rejection and shows an error toast.

  4. No separation of concerns. Deploy logic, metadata persistence, experiment fetching, notebook lifecycle, and UI rendering all live in one component. Testing any concern requires instantiating the entire panel.

What would a refactor look like?

Migrate to functional components with hooks:

  • useActiveNotebook(tracker) — hook that subscribes to tracker.currentChanged and returns the current NotebookPanel | null. Replaces the manual signal wiring + state tracking.
  • useNotebookMetadata(notebook) — hook that reads/writes Kale metadata from the active notebook. Handles the async session-ready wait internally with proper cleanup on notebook change.
  • useKaleBackend(notebook, kernel) — hook that fetches namespace, base image, experiments, KFP host. Returns loading/error/data states. Cancels in-flight requests on notebook switch.
  • useDeploys() — hook that manages deploy progress state independently.
  • Split the render into focused components: KaleHeader, EnableToggle, PipelineMetadataForm, DeploySection, DeployProgress.

Each hook owns its own lifecycle and cleanup. No more 11-field IState. No more resetState manually preserving fields. No more mutable external reads in render.

Why this matters

  • #647 — stuck toggle went unnoticed for months because the state management is opaque and only manifests on clusters with real network latency
  • The async race condition in setNotebookPanel is a latent bug waiting to surface
  • Adding any feature to the sidebar requires understanding all 700 lines
  • Class components don't support effect cleanup or cancellation

Migration path

This doesn't need to happen all at once. React supports mixing class and functional components. A reasonable order:

  1. Extract useActiveNotebook hook and EnableToggle component (smallest, most self-contained)
  2. Extract useNotebookMetadata and the metadata form
  3. Extract deploy logic
  4. Convert the shell to a functional component
  5. Delete the class component

Each step is independently shippable and testable.

What is the use case or pain point?

Developer productivity and bug prevention. The current architecture produced a real bug (stuck toggle), has a latent race condition (notebook switching during async setup), and makes every sidebar change riskier than it needs to be.

Is there a workaround currently?

We patch individual symptoms as they appear (e.g., adding activeNotebook to state). This works but accumulates complexity rather than reducing it.


Love this idea? Give it a 👍.

Metadata

Metadata

Assignees

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