Revert "refactor(plexus): migrate Digraph to functional component"#3600
Conversation
…egertracing#3534)" This reverts commit 5114ee3. Signed-off-by: Yuri Shkuro <[email protected]>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #3600 +/- ##
==========================================
- Coverage 90.78% 89.13% -1.65%
==========================================
Files 304 304
Lines 9731 9712 -19
Branches 2564 2505 -59
==========================================
- Hits 8834 8657 -177
- Misses 894 1052 +158
Partials 3 3 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
This PR reverts the previously-merged refactor that migrated plexus’s Digraph from a class component to a functional component, returning the implementation to a React.PureComponent-based class and removing the associated zoom cleanup API and tests.
Changes:
- Reintroduces class-based
Digraphimplementation (includingmemoize-oneclassName factory). - Removes
ZoomManager.dispose()and the Digraph test file added with the functional refactor. - Adds
memoize-oneas a direct dependency of@jaegertracing/plexus.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| packages/plexus/src/zoom/ZoomManager.ts | Removes the dispose() API that detached d3-zoom listeners. |
| packages/plexus/src/Digraph/index.tsx | Reverts Digraph back to a React.PureComponent class implementation. |
| packages/plexus/src/Digraph/index.test.tsx | Deletes the Digraph test suite introduced in the reverted refactor. |
| packages/plexus/package.json | Adds memoize-one dependency needed by the restored class implementation. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
| private setSizeVertices = (senderKey: string, sizeVertices: TSizeVertex<T>[]) => { | ||
| const { edges, layoutManager, measurableNodesKey: expectedKey } = this.props; | ||
| if (senderKey !== expectedKey) { | ||
| const values = `expected ${JSON.stringify(expectedKey)}, recieved ${JSON.stringify(senderKey)}`; |
| this.setState({ sizeVertices }); | ||
| const { layout } = layoutManager.getLayout(edges, sizeVertices); | ||
| layout.then(this.onLayoutDone); | ||
| this.setState({ sizeVertices, layoutPhase: ELayoutPhase.CalcPositions }); |
| export default class Digraph<T = unknown, U = unknown> extends React.PureComponent< | ||
| TDigraphProps<T, U>, | ||
| TDigraphState<T, U> | ||
| > { |
| private setSizeVertices = (senderKey: string, sizeVertices: TSizeVertex<T>[]) => { | ||
| const { edges, layoutManager, measurableNodesKey: expectedKey } = this.props; | ||
| if (senderKey !== expectedKey) { | ||
| const values = `expected ${JSON.stringify(expectedKey)}, recieved ${JSON.stringify(senderKey)}`; |
There was a problem hiding this comment.
Typo: 'recieved' should be 'received'
The error message contains a spelling error. The functional component version correctly had 'received' (line 168-169 of deleted code).
Fix:
const values = `expected ${JSON.stringify(expectedKey)}, received ${JSON.stringify(senderKey)}`;| const values = `expected ${JSON.stringify(expectedKey)}, recieved ${JSON.stringify(senderKey)}`; | |
| const values = `expected ${JSON.stringify(expectedKey)}, received ${JSON.stringify(senderKey)}`; |
Spotted by Graphite
Is this helpful? React 👍 or 👎 to let us know.
This reverts commit 5114ee3.
Code Review: PR #3534 —
refactor(plexus): migrate Digraph to functional componentStatus: MERGED | 1,053 additions, 531 deletions across 5 files
Overview
This PR migrates
packages/plexus/src/Digraph/index.tsxfrom a class component to a functionalcomponent, as part of the broader React modernization effort (#2610). It also adds a
dispose()method to
ZoomManagerand introduces a new test file. The general direction is correct and themigration is largely sound.
Positive Aspects
dispose()onZoomManageris a valuable addition — it properly removes d3-zoom eventlisteners to prevent leaks when zoom is toggled or the component unmounts.
builtinStylebased onzoomEnabledprop instead of checkingzoomManagerRef.currenteliminates the initial paint/layout shift bug present in the original design.
setStateinsetSizeVerticesis an improvement over the original classcomponent which had two
setStatecalls (first settingsizeVerticesalone, then redundantlyre-setting it alongside
layoutPhase).baseIdcorrectly gates theidCounter++side-effect behind anif (!baseIdRef.current)guard.zoomTransformRefpattern forgetZoomTransformis the right approach — stable callback via[]deps that always returns the latest transform.Issues
1.
setSizeVerticesstability regression (potential performance issue)In the original class component,
setSizeVerticeswas a stable method reference — it never changedidentity. Now it is recreated whenever
edgesorlayoutManagerchanges. This meansMeasurableNodesLayer(which receivessetSizeVerticesas a prop) will re-render on every parentre-render that produces a new
edgesarray reference, even if the edge data hasn't changed.Consider using a ref-based approach like
onLayoutDonedoes — captureedgesandlayoutManagerin a ref updated each render, and give
setSizeVertices[]deps.2.
onLayoutDoneempty dep array is correct but undocumentedThis is correct because
setStateis stable andzoomManagerRefis a ref. However, there is nocomment explaining why empty deps are appropriate here, which can look like a mistake to future
readers.
3. Generic type erasure with
React.memoThe
as unknown as TDigraphComponentcast drops the generic<T, U>parameters. Downstreamcallers using
<Digraph<MyNode, MyEdge> .../>lose type safety. The comment in the PR acknowledgesthis is a known React limitation — that is fine for now, but it should be tracked as a follow-up
since it is a type regression from the original.
4. Redundant
zoomManagerRefzoomManagerRefis only used inonLayoutDone(to avoid stale closure with[]deps). Thepattern works, but mutating a ref during render (
zoomManagerRef.current = zoomManager) istechnically a side effect in the render phase. Since
zoomManagercomes fromuseMemo, this isbenign in practice, but a comment would help clarify the intent.
5. Unrelated dependency updates in
package-lock.jsonThe PR mixes the functional component refactor with
package-lock.jsonupdates(
@typescript-eslint8.50→8.56, various@rc-component/*bumps, rollup 4.53→4.59, etc.). Theselockfile-only changes should be in a separate PR for easier review and bisectability.
6. New test file: copyright header inconsistency
// Copyright (c) 2026 Uber Technologies, Inc.Other plexus source files use
Uber Technologies, Inc.(consistent with the original 2019 header),but
CLAUDE.mdspecifiesThe Jaeger Authors.as the expected copyright holder for new files.This should be reconciled.
7. Test:
getLayoutassertion has edge-case fragilitygetLayoutis called insidesetSizeVertices, which is triggered byMeasurableNodesLayermeasuring nodes. The test relies on
measureNode: () => ({ height: 10, width: 10 })being calledsynchronously during render. This works today but is implementation-dependent. A comment explaining
why this fires synchronously (RTL flushes sync measurement in the DOM render) would help future
maintainers.
Summary
The migration is functionally correct and the tests pass. The main concerns are:
setSizeVerticesrecreated onedgeschange — potential render churnReact.memocast (needs follow-up tracking)package-lock.jsonchanges mixed into refactor PR[]dep arrays