Convert SvgEdges to functional component#3402
Convert SvgEdges to functional component#3402thc1006 wants to merge 7 commits intojaegertracing:mainfrom
Conversation
Add test coverage for SvgEdges component before refactoring to functional component. Tests cover: - Basic rendering with multiple edges - Empty layoutEdges array handling - Props propagation to child SvgEdge components - Optional marker props handling Part of jaegertracing#2610 Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR converts the SvgEdges component from a class-based to a functional component using React.memo with a custom comparison function to maintain the original shouldComponentUpdate optimization behavior.
Changes:
- Converted
SvgEdgesfrom class component to functional component withReact.memo - Implemented
arePropsEqualcomparator function that mirrors the originalshouldComponentUpdatelogic - Added comprehensive unit tests achieving 100% test coverage
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| packages/plexus/src/Digraph/SvgEdges.tsx | Refactored component from class to functional with React.memo, preserving optimization logic |
| packages/plexus/src/Digraph/SvgEdges.test.js | Added new test file with comprehensive test cases for the component |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
96c0e7c to
076e27d
Compare
Convert SvgEdges from class-based to functional component using React.memo with custom comparator to preserve the original shouldComponentUpdate behavior. Resolves jaegertracing#3397 Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
076e27d to
49b9cdf
Compare
|
unit tests are failing |
Move mockSvgEdgeProps array clearing to beforeEach hook to ensure consistent test isolation regardless of test execution order. This prevents potential flaky test behavior in CI environments. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| describe('SvgEdges', () => { | ||
| beforeEach(() => { | ||
| mockSvgEdgeProps.length = 0; | ||
| }); | ||
|
|
||
| const mockGetClassName = name => `plexus--${name}`; | ||
|
|
||
| const mockRenderUtils = { | ||
| getGlobalId: id => `global-${id}`, | ||
| getZoomTransform: () => ({ k: 1, x: 0, y: 0 }), | ||
| }; | ||
|
|
||
| // Create mock edges for testing | ||
| const createMockEdge = (from, to) => ({ | ||
| edge: { from, to }, | ||
| pathPoints: [ | ||
| [0, 0], | ||
| [100, 100], | ||
| ], | ||
| }); | ||
|
|
||
| const mockLayoutEdges = [createMockEdge('node-a', 'node-b'), createMockEdge('node-b', 'node-c')]; | ||
|
|
||
| // Wrap in svg container since SvgEdge renders SVG elements | ||
| const renderInSvg = edges => { | ||
| return render( | ||
| <svg> | ||
| <SvgEdges getClassName={mockGetClassName} layoutEdges={edges} renderUtils={mockRenderUtils} /> | ||
| </svg> | ||
| ); | ||
| }; | ||
|
|
||
| it('renders SvgEdge for each layout edge', () => { | ||
| const { getAllByTestId } = renderInSvg(mockLayoutEdges); | ||
| const edges = getAllByTestId('svg-edge'); | ||
| expect(edges).toHaveLength(2); | ||
| }); | ||
|
|
||
| it('renders nothing when layoutEdges is empty', () => { | ||
| const { queryAllByTestId } = renderInSvg([]); | ||
| const edges = queryAllByTestId('svg-edge'); | ||
| expect(edges).toHaveLength(0); | ||
| }); | ||
|
|
||
| it('passes from and to correctly to SvgEdge', () => { | ||
| const { getAllByTestId } = renderInSvg(mockLayoutEdges); | ||
| const edges = getAllByTestId('svg-edge'); | ||
|
|
||
| // Check first edge | ||
| expect(edges[0]).toHaveAttribute('data-from', 'node-a'); | ||
| expect(edges[0]).toHaveAttribute('data-to', 'node-b'); | ||
|
|
||
| // Check second edge | ||
| expect(edges[1]).toHaveAttribute('data-from', 'node-b'); | ||
| expect(edges[1]).toHaveAttribute('data-to', 'node-c'); | ||
| }); | ||
|
|
||
| it('handles optional marker props', () => { | ||
| const { getAllByTestId } = render( | ||
| <svg> | ||
| <SvgEdges | ||
| getClassName={mockGetClassName} | ||
| layoutEdges={mockLayoutEdges} | ||
| renderUtils={mockRenderUtils} | ||
| markerEndId="arrow-end" | ||
| markerStartId="arrow-start" | ||
| /> | ||
| </svg> | ||
| ); | ||
| expect(getAllByTestId('svg-edge')).toHaveLength(2); | ||
| }); | ||
|
|
||
| it('passes setOnEdge to child components', () => { | ||
| const mockSetOnEdge = jest.fn(); | ||
| const { getAllByTestId } = render( | ||
| <svg> | ||
| <SvgEdges | ||
| getClassName={mockGetClassName} | ||
| layoutEdges={mockLayoutEdges} | ||
| renderUtils={mockRenderUtils} | ||
| setOnEdge={mockSetOnEdge} | ||
| /> | ||
| </svg> | ||
| ); | ||
| expect(getAllByTestId('svg-edge')).toHaveLength(2); | ||
|
|
||
| // Verify setOnEdge is passed to each child component | ||
| mockSvgEdgeProps.forEach(props => { | ||
| expect(props.setOnEdge).toBe(mockSetOnEdge); | ||
| }); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
The tests do not verify the memoization behavior of the React.memo wrapper. Since the conversion from a class component with shouldComponentUpdate to a functional component with React.memo is a key aspect of this PR, consider adding tests that verify the component does not re-render when props haven't changed according to the arePropsEqual logic. This could involve using the rerender function from testing-library and tracking render counts.
Add tests to verify React.memo memoization works correctly: - Component does not re-render when same props are passed - Component re-renders when layoutEdges changes - Component re-renders when markerEndId changes This addresses AI review feedback to ensure the class-to-functional component conversion preserves the original shouldComponentUpdate optimization behavior. Also improves SvgEdges.tsx coverage to 100%. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
Done! |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Add test cases for all props compared in arePropsEqual: - markerStartId changes - getClassName changes - renderUtils changes - setOnEdge changes This addresses Copilot feedback requesting complete coverage of the memoization logic for all props, not just layoutEdges and markerEndId. Signed-off-by: thc1006 <109750520+thc1006@users.noreply.github.com> Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
Restore the original Uber Technologies copyright in SvgEdges.tsx and use consistent Uber copyright in test file to match other plexus package files. Per maintainer guidance: "never remove copyright, but OK to add Jaeger copyright on top" (PR jaegertracing#3407) Signed-off-by: thc1006 <109750520+thc1006@users.noreply.github.com> Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
Dear @yurishkuro, Following up on the previous fixes, I have addressed the additional feedback:
All 21 tests pass with 100% coverage for SvgEdges.tsx. Regarding the latest Copilot suggestion about testing setOnEdge as an array: the current tests verify memoization behavior for function references, which covers the primary use case. The array comparison logic is an implementation detail of isSamePropSetter utility. I can add those tests if you think they are necessary for this PR. Ready for review when you have a chance. Thank you. |
|
This pull request has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. You may re-open it if you need more time. |
| /> | ||
| )); | ||
| } | ||
| // Comparison function that mirrors the original shouldComponentUpdate logic |
There was a problem hiding this comment.
Why can't we simply rely on React doing the comparison? That is literally the reason why React was created.
There was a problem hiding this comment.
Sorry for the delayed reply, I was at KubeCon EU these past two weeks.
Thank you for the review. You're right, the custom comparator shouldn't be here. I raised the same point on #3403 and offered to simplify, but should have just gone ahead and done it. The isSamePropSetter call on setOnEdge is handling something the caller should stabilize with useMemo, not something the child should work around.
I'll switch to plain React.memo() here and on #3403. Both need a rebase against the toolchain changes anyway, so I'll handle them together.
|
Apologies for the back and forth here. After taking a closer look at main, I realized SvgEdges.tsx was already converted to a functional component in #3518 by @hharshhsaini. I should have checked before replying to the arePropsEqual comment earlier. Thank you for the feedback regardless, it clarified the right approach for Nodes (#3403) as well. One thing that might still have value: this branch includes a 326-line test file for SvgEdges covering rendering, memoization behavior, and edge cases. There don't appear to be tests for this component on main yet. Would a standalone test PR (migrated to Vitest) be welcome? Happy to put one together if so. |
Which problem is this PR solving?
Resolves #3397
Description of the changes
Converted the
SvgEdgesclass component to a functional component using React.memo with custom comparator.How was this change tested?
npm run testin packages/plexus directoryChecklist
jaeger-ui:npm run lintandnpm run test