Skip to content

Refactored several React class components as functional components #8750

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Jul 15, 2025

Conversation

hotzenklotz
Copy link
Member

@hotzenklotz hotzenklotz commented Jul 3, 2025

In this PR I converted several class components to functional components using modern React hooks. Most of the components are located in the general frontend/javascripts/components directory.

Steps to test:

Check that the view are still showing

Issues:

Contributes to #8747


(Please delete unneeded items, merge only when none are left open)

  • Added changelog entry (create a $PR_NUMBER.md file in unreleased_changes or use ./tools/create-changelog-entry.py)
  • Added migration guide entry if applicable (edit the same file as for the changelog)
  • Updated documentation if applicable
  • Adapted wk-libs python client if relevant API parts change
  • Removed dev-only changes like prints and application.conf edits
  • Considered common edge cases
  • Needs datastore update after deployment

@hotzenklotz hotzenklotz self-assigned this Jul 3, 2025
Copy link
Contributor

coderabbitai bot commented Jul 3, 2025

📝 Walkthrough

Walkthrough

Several React components were refactored from class-based implementations to functional components using React hooks. Lifecycle methods and state management were replaced by hooks such as useState and useEffect. Helper methods were moved outside components where appropriate, and minor style and class name adjustments were applied without changing core behavior.

Changes

File(s) Change Summary
frontend/javascripts/components/disable_generic_dnd.ts Converted class component to functional component; event handler extracted outside component.
frontend/javascripts/components/fixed_expandable_table.tsx Converted class PureComponent to generic functional component; state and lifecycle replaced by hooks; helper method moved outside.
frontend/javascripts/components/highlightable_row.tsx Converted class PureComponent to functional component; state managed with hooks.
frontend/javascripts/components/loop.ts Converted class component to functional component; interval timer managed with useEffect hook.
frontend/javascripts/components/select_experience_domain.tsx Converted class PureComponent to functional component; data fetching replaced by React Query hook; props made optional with defaults.
frontend/javascripts/components/text_with_description.tsx Converted class PureComponent to functional component; improved type safety; minor style and class name changes.
frontend/javascripts/libs/shortcut_component.ts Converted class component to functional component; keyboard shortcut setup managed with useEffect.

Suggested reviewers

  • MichaelBuessemeyer

Poem

In the meadow of code, where React hooks bloom,
Old class-based carrots swept out of the room.
With useEffect and useState, we hop and we play,
Functional components now brighten the day!
🥕✨
Hopping forward, code clean and bright—
A rabbit’s delight in the soft morning light!


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9826776 and db72024.

📒 Files selected for processing (2)
  • frontend/javascripts/components/fixed_expandable_table.tsx (2 hunks)
  • frontend/javascripts/components/loop.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • frontend/javascripts/components/loop.ts
  • frontend/javascripts/components/fixed_expandable_table.tsx
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: build-smoketest-push
  • GitHub Check: frontend-tests
  • GitHub Check: backend-tests
✨ Finishing Touches
  • 📝 Generate Docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (6)
frontend/javascripts/components/loop.ts (1)

9-19: LGTM: Correct useEffect implementation, but consider callback stability.

The useEffect hook correctly manages the interval lifecycle. However, including onTick in the dependency array could cause unnecessary effect re-runs if the parent component recreates this function on every render.

Consider using useCallback in the parent component for the onTick prop to prevent unnecessary interval resets.

frontend/javascripts/libs/shortcut_component.ts (1)

11-28: LGTM: Correct useEffect implementation with proper cleanup.

The useEffect hook correctly manages the keyboard instance lifecycle with proper cleanup. The dependency array includes all relevant props to ensure the effect re-runs when needed.

Similar to the Loop component, consider using useCallback for the onTrigger prop in parent components to prevent unnecessary keyboard instance recreations.

frontend/javascripts/components/select_experience_domain.tsx (1)

18-83: LGTM: Correct hooks implementation with proper state and effect management.

The refactoring correctly uses useState for state management and useEffect for data fetching. The logic maintains the same behavior as the original class component.

Consider optimizing the getUnusedDomains function with useCallback to prevent unnecessary recreations on every render:

+import { useCallback } from "react";

-  const getUnusedDomains = (): ExperienceDomainList => {
+  const getUnusedDomains = useCallback((): ExperienceDomainList => {
     return domains.filter((domain) => !alreadyUsedDomains.includes(domain));
-  };
+  }, [domains, alreadyUsedDomains]);
frontend/javascripts/components/fixed_expandable_table.tsx (1)

18-24: Improve type safety in getAllRowIds function

The function uses any types which reduces type safety. Consider using generics to maintain type information.

-const getAllRowIds = (
-  dataSource: readonly any[] | undefined,
-  rowKey: string | number | symbol | GetRowKey<any> | undefined,
-) => {
-  const canUseRowKey = typeof rowKey === "string";
-  return dataSource != null && canUseRowKey ? dataSource.map((row) => row[rowKey]) : [];
-};
+const getAllRowIds = <RecordType = any>(
+  dataSource: readonly RecordType[] | undefined,
+  rowKey: string | number | symbol | GetRowKey<RecordType> | undefined,
+): React.Key[] => {
+  const canUseRowKey = typeof rowKey === "string";
+  return dataSource != null && canUseRowKey ? dataSource.map((row) => row[rowKey]) : [];
+};
frontend/javascripts/components/text_with_description.tsx (2)

59-59: Remove unnecessary type assertion

The type assertion can be avoided by using proper TypeScript discriminated union handling.

-          <EditableTextLabel {...(editableProps as EditableTextLabelProp)} />
+          <EditableTextLabel {...(isEditable ? props : {} as EditableTextLabelProp)} />

Or better yet, restructure the component to avoid the need for type assertions entirely.


67-73: Eliminate repetitive type assertions

The repeated (props as NonEditableProps) assertions can be simplified by extracting the non-editable props once.

+        {(() => {
+          if (isEditable) {
+            return <EditableTextLabel {...props} />;
+          }
+          const nonEditableProps = props as NonEditableProps;
+          return (
             <span
               style={{
                 margin: "0 10px",
                 display: "inline-block",
               }}
             >
-              {(props as NonEditableProps).markdown ? (
+              {nonEditableProps.markdown ? (
                 <span>
-                  <Markdown>{(props as NonEditableProps).value}</Markdown>
+                  <Markdown>{nonEditableProps.value}</Markdown>
                 </span>
               ) : (
-                (props as NonEditableProps).value
+                nonEditableProps.value
               )}
             </span>
+          );
+        })()}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b01f062 and 02ceb7f.

📒 Files selected for processing (7)
  • frontend/javascripts/components/disable_generic_dnd.ts (1 hunks)
  • frontend/javascripts/components/fixed_expandable_table.tsx (2 hunks)
  • frontend/javascripts/components/highlightable_row.tsx (1 hunks)
  • frontend/javascripts/components/loop.ts (1 hunks)
  • frontend/javascripts/components/select_experience_domain.tsx (2 hunks)
  • frontend/javascripts/components/text_with_description.tsx (2 hunks)
  • frontend/javascripts/libs/shortcut_component.ts (2 hunks)
🧰 Additional context used
🧠 Learnings (5)
📓 Common learnings
Learnt from: MichaelBuessemeyer
PR: scalableminds/webknossos#8221
File: CHANGELOG.unreleased.md:14-14
Timestamp: 2024-11-25T14:52:23.201Z
Learning: For the `webknossos` project, changelog entries in `CHANGELOG.unreleased.md` should be concise and avoid unnecessary detailed explanations.
Learnt from: philippotto
PR: scalableminds/webknossos#8542
File: frontend/javascripts/viewer/model/sagas/volumetracing_saga.tsx:0-0
Timestamp: 2025-05-30T12:36:09.930Z
Learning: The WebKnossos frontend follows Redux immutability principles where reducers act immutably and create new state objects when changes occur. This makes reference equality checks (like `prevVolumeTracing.segments !== volumeTracing.segments`) reliable and efficient for detecting actual state changes, allowing memoization to work correctly.
Learnt from: knollengewaechs
PR: scalableminds/webknossos#8321
File: frontend/javascripts/components/slider.tsx:0-0
Timestamp: 2025-02-05T20:16:45.712Z
Learning: When tracking focus state that needs to be accessed by native event listeners (e.g., wheel events), use `useRef` instead of `useState` to ensure the event handlers always have access to the current value. This is because event listeners added via `addEventListener` would capture the initial state value if `useState` were used.
frontend/javascripts/libs/shortcut_component.ts (1)
Learnt from: knollengewaechs
PR: scalableminds/webknossos#8321
File: frontend/javascripts/components/slider.tsx:0-0
Timestamp: 2025-02-05T20:16:45.712Z
Learning: When tracking focus state that needs to be accessed by native event listeners (e.g., wheel events), use `useRef` instead of `useState` to ensure the event handlers always have access to the current value. This is because event listeners added via `addEventListener` would capture the initial state value if `useState` were used.
frontend/javascripts/components/disable_generic_dnd.ts (1)
Learnt from: knollengewaechs
PR: scalableminds/webknossos#8321
File: frontend/javascripts/components/slider.tsx:0-0
Timestamp: 2025-02-05T20:16:45.712Z
Learning: When tracking focus state that needs to be accessed by native event listeners (e.g., wheel events), use `useRef` instead of `useState` to ensure the event handlers always have access to the current value. This is because event listeners added via `addEventListener` would capture the initial state value if `useState` were used.
frontend/javascripts/components/highlightable_row.tsx (1)
Learnt from: knollengewaechs
PR: scalableminds/webknossos#8321
File: frontend/javascripts/components/slider.tsx:0-0
Timestamp: 2025-02-05T20:16:45.712Z
Learning: When tracking focus state that needs to be accessed by native event listeners (e.g., wheel events), use `useRef` instead of `useState` to ensure the event handlers always have access to the current value. This is because event listeners added via `addEventListener` would capture the initial state value if `useState` were used.
frontend/javascripts/components/select_experience_domain.tsx (1)
Learnt from: knollengewaechs
PR: scalableminds/webknossos#8266
File: frontend/javascripts/components/mag_selection.tsx:51-60
Timestamp: 2024-12-16T15:48:37.065Z
Learning: In the `MagSelection` component (`frontend/javascripts/components/mag_selection.tsx`), the selected magnification `value` will always be present in `allMags` because only existing values can be selected by the user.
⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: build-smoketest-push
  • GitHub Check: backend-tests
🔇 Additional comments (11)
frontend/javascripts/components/disable_generic_dnd.ts (2)

4-6: LGTM: Good practice moving the event handler outside the component.

Moving the preventDefault function outside the component is optimal since it doesn't depend on props or state, preventing unnecessary recreations.


8-20: LGTM: Correct useEffect implementation for event listener management.

The useEffect hook properly replaces the lifecycle methods, correctly adding event listeners on mount and removing them on unmount.

frontend/javascripts/components/highlightable_row.tsx (1)

14-28: LGTM: Correct useState implementation maintaining original behavior.

The useState hook correctly replaces the class state, properly initializing persistedShouldHighlight with the initial shouldHighlight prop value. This maintains the same behavior as the original class component where the highlight state was persisted from the initial render.

frontend/javascripts/components/select_experience_domain.tsx (1)

85-88: LGTM: Correct default props assignment for functional component.

The default props are correctly assigned after the functional component definition, maintaining the same default behavior as the original class component.

frontend/javascripts/components/fixed_expandable_table.tsx (6)

3-3: LGTM: Proper hook import added

The addition of useEffect to the React import is correct for the functional component refactoring.


16-16: Good performance optimization with EMPTY_ARRAY constant

Using a constant empty array prevents unnecessary re-renders by maintaining referential equality.


26-33: LGTM: Clean functional component signature

The destructuring of props with proper TypeScript generics is well implemented.


34-34: LGTM: Proper useState initialization

Using the EMPTY_ARRAY constant is a good optimization for state initialization.


36-39: LGTM: Correct useEffect replacement for componentDidUpdate

The useEffect properly replaces the lifecycle method to reset expanded rows when dataSource changes. The biome-ignore comment is appropriate since setExpandedRows is stable and doesn't need to be in the dependency array.


69-81: LGTM: Proper Table component rendering

The Table component is correctly rendered with all necessary props passed through, maintaining the same behavior as the class component.

frontend/javascripts/components/text_with_description.tsx (1)

31-78: LGTM: Functional component logic preserves original behavior

The refactored component maintains the same rendering logic and behavior as the original class component, with proper conditional rendering and styling.

Comment on lines +19 to +21
const TextWithDescription: React.FC<Props> = (props) => {
const { isEditable, description, ...editableProps } = props;
const hasDescription = description !== "";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve discriminated union type handling

The current destructuring doesn't work well with discriminated unions. Consider using a type guard or conditional logic to handle the different prop types more safely.

-const TextWithDescription: React.FC<Props> = (props) => {
-  const { isEditable, description, ...editableProps } = props;
-  const hasDescription = description !== "";
+const TextWithDescription: React.FC<Props> = (props) => {
+  const { isEditable, description } = props;
+  const hasDescription = description !== "";

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In frontend/javascripts/components/text_with_description.tsx around lines 19 to
21, the destructuring of props does not safely handle discriminated union types.
Refactor the code to use a type guard or conditional checks to differentiate
between the union types before accessing their specific properties. This will
ensure type safety and prevent runtime errors by handling each variant of the
props appropriately.

@@ -1,6 +1,6 @@
import { Popover, Tooltip } from "antd";
import Markdown from "libs/markdown_adapter";
import * as React from "react";
import type React from "react";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix React import for runtime usage

The type-only import will cause a runtime error since React.FC<Props> requires React at runtime.

-import type React from "react";
+import React from "react";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import type React from "react";
-import type React from "react";
+import React from "react";
🤖 Prompt for AI Agents
In frontend/javascripts/components/text_with_description.tsx at line 3, the
import of React is currently type-only, which causes runtime errors because
React.FC<Props> needs React at runtime. Change the import from a type-only
import to a regular import so that React is available during runtime.

Copy link
Contributor

@MichaelBuessemeyer MichaelBuessemeyer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for another iteration of functional component refactoring.

Ok here is my code related feedback. I did not test, will do this after the feedback is applied :)

allowCreation,
alreadyUsedDomains = [],
}) => {
const [domains, setDomains] = React.useState<ExperienceDomainList>([]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this file I would also remove the React. in front of the hooks

Comment on lines 33 to 38
const fetchData = async () => {
const existingDomains = await getExistingExperienceDomains();
setDomains(existingDomains);
};
void fetchData();
}, []);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe use useGuardedFetch instead?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should converged on a standard method for fetching. A while back Philipp introduced TanStack Query and that seems an excellent, and well documented solution by the broader React community. I think I will use that everywhere going forward.

Comment on lines 85 to 88
SelectExperienceDomain.defaultProps = {
alreadyUsedDomains: [],
allowCreation: false,
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this cna be removed. According to the props allowCreation is required and the optional alreadyUsedDomains already has a default value

hotzenklotz and others added 6 commits July 9, 2025 14:57
@hotzenklotz
Copy link
Member Author

@MichaelBuessemeyer thank your for the review I addressed all your feedback and made the appropriate changes. Ready to launch.

@hotzenklotz
Copy link
Member Author

@MichaelBuessemeyer Ping

@MichaelBuessemeyer MichaelBuessemeyer self-requested a review July 14, 2025 08:53
Copy link
Contributor

@MichaelBuessemeyer MichaelBuessemeyer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Neat looks good as well 👍

@hotzenklotz hotzenklotz enabled auto-merge (squash) July 15, 2025 07:25
@hotzenklotz hotzenklotz merged commit 76e3882 into master Jul 15, 2025
5 checks passed
@hotzenklotz hotzenklotz deleted the reafactor_components branch July 15, 2025 07:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants