Skip to content

Conversation

@tomusdrw
Copy link
Contributor

Supersedes #132 #131 #130 #129 #118

dependabot bot and others added 14 commits November 4, 2025 15:40
Bumps [eslint-plugin-react-hooks](https://github.com/facebook/react/tree/HEAD/packages/eslint-plugin-react-hooks) from 5.2.0 to 7.0.1.
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/packages/eslint-plugin-react-hooks/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/HEAD/packages/eslint-plugin-react-hooks)

---
updated-dependencies:
- dependency-name: eslint-plugin-react-hooks
  dependency-version: 7.0.1
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
Bumps [@vitest/coverage-v8](https://github.com/vitest-dev/vitest/tree/HEAD/packages/coverage-v8) from 3.2.4 to 4.0.8.
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.0.8/packages/coverage-v8)

---
updated-dependencies:
- dependency-name: "@vitest/coverage-v8"
  dependency-version: 4.0.8
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
Bumps [globals](https://github.com/sindresorhus/globals) from 16.4.0 to 16.5.0.
- [Release notes](https://github.com/sindresorhus/globals/releases)
- [Commits](sindresorhus/globals@v16.4.0...v16.5.0)

---
updated-dependencies:
- dependency-name: globals
  dependency-version: 16.5.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
Bumps [zod](https://github.com/colinhacks/zod) from 4.1.11 to 4.1.12.
- [Release notes](https://github.com/colinhacks/zod/releases)
- [Commits](colinhacks/zod@v4.1.11...v4.1.12)

---
updated-dependencies:
- dependency-name: zod
  dependency-version: 4.1.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
Bumps [jsdom](https://github.com/jsdom/jsdom) from 26.1.0 to 27.1.0.
- [Release notes](https://github.com/jsdom/jsdom/releases)
- [Changelog](https://github.com/jsdom/jsdom/blob/main/Changelog.md)
- [Commits](jsdom/jsdom@26.1.0...27.1.0)

---
updated-dependencies:
- dependency-name: jsdom
  dependency-version: 27.1.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
@netlify
Copy link

netlify bot commented Nov 11, 2025

Deploy Preview for jam-state-viewer failed. Why did it fail? →

Name Link
🔨 Latest commit 8882a8d
🔍 Latest deploy log https://app.netlify.com/projects/jam-state-viewer/deploys/69133ce513619400089b1929

@coderabbitai
Copy link

coderabbitai bot commented Nov 11, 2025

📝 Walkthrough

Walkthrough

The PR updates dependencies across the project, refactors test mocks from arrow functions to traditional function expressions, adds ESLint suppressions for state updates within effects and memoization contexts, relocates a state exposure effect, mocks UI components in tests, and introduces async content loading in UploadScreen with loading state integration.

Changes

Cohort / File(s) Change Summary
Dependency Updates
package.json
Bumped dependencies: @fluffylabs/shared-ui (^0.4.3→^0.4.5), @typeberry/lib (^0.2.0-ca77d76→^0.3.1), zod (^4.1.11→^4.1.12). Bumped devDependencies: @vitest/coverage-v8 (^3.0.5→^4.0.8), eslint-plugin-react-hooks (^5.2.0→^7.0.1), globals (^16.4.0→^16.5.0), jsdom (^26.0.0→^27.1.0), vitest (^3.0.5→^4.0.8)
Mock Implementation Refactoring
src/App.test.tsx, src/components/JsonEditorDialog.test.tsx, src/components/UploadScreen.test.tsx, src/utils/jsonValidation.test.ts
Replaced arrow function mocks with traditional function expressions for window.matchMedia, MutationObserver, and FileReader. Binding context changed but observable behavior preserved. Added type assertions where needed.
ESLint Suppressions for State Updates
src/components/ServiceViewer.tsx, src/components/SettingsDialog.tsx, src/trie/TrieView.tsx, src/trie/components/trie/index.tsx
Added eslint-disable-next-line react-hooks/set-state-in-effect comments around state-setter invocations within useEffect and useMemo hooks. No behavioral changes.
Effect Timing Adjustments
src/components/InspectStateViewer.tsx, src/components/JsonEditorDialog.tsx
Relocated effect logic: moved window property exposure in InspectStateViewer to useEffect with proper dependency array; relocated useEffect in JsonEditorDialog to different location in file with identical functionality.
Async Content Loading Feature
src/components/UploadScreen.tsx
Changed ExampleFile.content from string to async factory function (content: () => Promise). Updated EXAMPLE_FILES entries to provide content via dynamic imports. Modified handleExampleLoad to accept async function. Integrated isLoading state with UI feedback (disabled buttons, loading indicator, conditional rendering).
Test Infrastructure Enhancements
src/components/service/ServiceCard.test.tsx
Added comprehensive mocks for @fluffylabs/shared-ui components (Button, ButtonGroup) and UI tabs components (Tabs, TabsList, TabsTrigger, TabsContent). Refactored StorageQuery, PreimageQuery, LookupHistoryQuery mocks to return objects via functions. Adjusted tab label strings by removing separator characters.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant UI as UploadScreen UI
    participant Modal as ExamplesModal
    participant Handler as handleExampleLoad
    participant Loader as Content Loader
    participant State as State & Validation

    User->>UI: Click example option
    UI->>Modal: Open modal
    User->>Modal: Select example file
    Modal->>Handler: onSelect(content: async factory)
    Note over Handler: isLoading = true, disable buttons
    Handler->>Loader: Invoke content() to load async
    Loader->>Handler: Return Promise<string>
    Note over Handler: await content()
    Handler->>State: validateJson(content string)
    Note over State: Validate JSON
    Handler->>State: setEditorContent(content)
    Handler->>UI: isLoading = false, re-enable buttons
    Note over UI: Show "Loading example…" during<br/>async operation, hide browse button
    UI->>User: Display loaded content
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • UploadScreen.tsx: The async content loading feature requires careful review of Promise handling, loading state lifecycle, and UI state management during async operations. Verify that all concurrent load attempts are properly prevented and that error handling is complete.
  • ServiceCard.test.tsx: Review the new mock implementations for UI components to ensure they correctly simulate component behavior and that tab label changes do not mask missing functionality.
  • Mock refactoring across test files: While repetitive and lower-risk individually, verify consistency in the function-style mock implementations and type assertions across all affected test files.
  • ESLint suppressions: Confirm that each suppression is justified and does not mask genuine state update anti-patterns that should be refactored instead.

Pre-merge checks and finishing touches

❌ Failed checks (2 inconclusive)
Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'Update dependencies' is too vague and does not capture the substantial code changes beyond dependency bumps, including mock refactoring, async content loading, and lint suppressions. Consider a more descriptive title that reflects the primary changes, such as 'Refactor mocks and async content loading, update dependencies' or narrow the scope if dependencies are the only intended changes.
Description check ❓ Inconclusive The description only references superseded PRs without explaining what changes are included in this PR, making it impossible to understand the intent from the description alone. Add a brief explanation of the main changes being introduced in this PR beyond just superseding previous pull requests.
✅ Passed checks (1 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch td-deps

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/trie/TrieView.tsx (1)

48-64: Critical: Move state updates from useMemo to useEffect.

Setting state inside useMemo is a React anti-pattern that violates the rules of hooks. useMemo executes during the render phase and should only compute derived values, not trigger side effects like state updates. This can cause unexpected re-renders, infinite loops, or inconsistent state.

Apply this diff to properly separate computation from side effects:

  const trie = useMemo(() => {
    if (rows.length === 0) {
      return;
    }

-   // eslint-disable-next-line react-hooks/set-state-in-render
-   setError(null);
    try {
      return getTrie(rows);
    } catch (error: unknown) {
-     if (error instanceof Error) {
-       // eslint-disable-next-line react-hooks/set-state-in-render
-       setError(error.message);
-     }
-     return null;
+     return { error: error instanceof Error ? error.message : 'Unknown error' };
    }
  }, [rows]);

+ useEffect(() => {
+   if (!trie) {
+     setError(null);
+   } else if (typeof trie === 'object' && 'error' in trie) {
+     setError(trie.error);
+   } else {
+     setError(null);
+   }
+ }, [trie]);

Alternatively, refactor to avoid error state entirely:

- const [error, setError] = useState<string | null>(null);

  const trie = useMemo(() => {
    if (rows.length === 0) {
-     return;
+     return null;
    }

    try {
      return getTrie(rows);
    } catch (error: unknown) {
-     if (error instanceof Error) {
-       setError(error.message);
-     }
-     return null;
+     return { error: error instanceof Error ? error.message : 'Unknown error' };
    }
  }, [rows]);

+ const error = trie && typeof trie === 'object' && 'error' in trie ? trie.error : null;
src/components/UploadScreen.tsx (1)

135-159: Add error handling for async content loading failures.

If exampleContent() throws (e.g., import failure), the content variable remains an empty string and validation proceeds with '', potentially showing confusing error messages to the user.

Apply this diff to handle errors properly:

  const handleExampleLoad = useCallback(async (exampleContent: () => Promise<string>) => {
    clearUpload();
    setIsLoading(true);
-   let content = '';
    try {
-     content = await exampleContent();
+     const content = await exampleContent();
+     const validation = validateJsonContent(content);
+
+     const newUploadState = {
+       file: null,
+       content: validation.content,
+       error: validation.error,
+       isValidJson: validation.isValid,
+       format: validation.format,
+       formatDescription: validation.formatDescription,
+       availableStates: validation.availableStates,
+       selectedState: validation.availableStates?.includes('diff') ? 'diff' : validation.availableStates?.[0],
+     };
+
+     handleUploadStateWithStorage(newUploadState, validation);
+   } catch (error) {
+     // Handle import or loading failures
+     const errorMessage = error instanceof Error ? error.message : 'Failed to load example content';
+     handleUploadStateWithStorage({
+       file: null,
+       content: '',
+       error: errorMessage,
+       isValidJson: false,
+       format: 'unknown',
+       formatDescription: '',
+     });
    } finally {
      setIsLoading(false);
    }
-
-   const validation = validateJsonContent(content);
-
-   const newUploadState = {
-     file: null,
-     content: validation.content,
-     error: validation.error,
-     isValidJson: validation.isValid,
-     format: validation.format,
-     formatDescription: validation.formatDescription,
-     availableStates: validation.availableStates,
-     selectedState: validation.availableStates?.includes('diff') ? 'diff' : validation.availableStates?.[0],
-   };
-
-   handleUploadStateWithStorage(newUploadState, validation);
  }, [clearUpload, handleUploadStateWithStorage]);
🧹 Nitpick comments (4)
src/components/SettingsDialog.tsx (1)

25-31: Consider using a key to reset dialog state instead of effect-based state updates.

While resetting form state on dialog open is a legitimate pattern, the lint suppression suggests an opportunity for a cleaner approach using component remounting.

Apply this diff to use a key-based reset:

  useEffect(() => {
    if (!isOpen) return;
-   // eslint-disable-next-line react-hooks/set-state-in-effect
    setSelectedGpVersion(utils.CURRENT_VERSION as unknown as string);
-   // eslint-disable-next-line react-hooks/set-state-in-effect
    setSelectedSuite(utils.CURRENT_SUITE as unknown as string);
  }, [isOpen]);

  if (!isOpen) return null;

Alternative approach (wrap dialog content in a component with key):

{isOpen && <SettingsDialogContent key={String(isOpen)} onClose={onClose} onApply={handleApply} />}

This forces React to create a fresh component instance with initial state each time the dialog opens.

src/trie/components/trie/index.tsx (1)

117-124: Remove elements from effect dependencies to prevent potential loops.

While the deepEqual check guards against infinite loops, including elements in the dependency array of an effect that updates elements is fragile. If deepEqual ever fails to detect equality correctly, this will cause an infinite loop.

Apply this diff to remove the circular dependency:

  useEffect(() => {
    const graphElements = buildCytoscapeGraphData(treeData, containerSize.width, containerSize.height);
    // Perform deep equal to make sure the values are different and prevent trie re-rendering. It's more expensive operation
    if (!deepEqual(graphElements, elements)) {
-     // eslint-disable-next-line react-hooks/set-state-in-effect
      setElements(graphElements);
    }
- }, [containerSize.height, containerSize.width, elements, treeData]);
+ }, [containerSize.height, containerSize.width, treeData]);

The deepEqual check already prevents updates when elements haven't changed, so the dependency is redundant and potentially problematic.

src/components/JsonEditorDialog.tsx (1)

69-76: Consider alternatives to setting state within effects.

Setting state inside useEffect can lead to unnecessary re-renders and is generally discouraged (hence the ESLint rule). Consider these alternatives:

  1. Derive state from props: If editorContent always mirrors initialContent when the dialog opens, you might not need separate state.
  2. Use a key prop: Reset component state by adding key={isOpen ? initialContent : 'closed'} to force remount.
  3. Set state in event handlers: Move setEditorContent(initialContent) to onClose or when the dialog opens via user interaction.

Example using derived state:

-  const [editorContent, setEditorContent] = useState(initialContent);
+  const [editorContent, setEditorContent] = useState('');
+  
+  // Derive editor content from initialContent when dialog is open
+  const effectiveContent = isOpen && !editorContent ? initialContent : editorContent;

-  // Update editor content when initialContent changes
-  useEffect(() => {
-    if (isOpen) {
-      // eslint-disable-next-line react-hooks/set-state-in-effect
-      setEditorContent(initialContent);
-      validateJson(initialContent);
-    }
-  }, [isOpen, initialContent]);
+  // Validate when dialog opens
+  useEffect(() => {
+    if (isOpen) {
+      validateJson(initialContent);
+    }
+  }, [isOpen, initialContent]);

   // Handle content changes in editor
   const handleContentChange = (value: string) => {
     setEditorContent(value);
     validateJson(value);
   };

Then update the editor to use effectiveContent:

   <JsonEditor
-    editorContent={editorContent}
+    editorContent={effectiveContent}
     onContentChange={handleContentChange}
     isDark={isDark}
   />
src/components/UploadScreen.tsx (1)

60-60: Loading state implementation looks good.

The loading indicator and conditional button rendering provide clear feedback during async content loading. However, the button label logic could be slightly simplified.

Consider extracting the label condition for better readability:

+  const hasContent = uploadState.file || uploadState.content;
+
   {!isLoading && (
     <div className="flex flex-wrap gap-3 justify-center">
       <Button
         onClick={open}
         variant="primary"
         size="lg"
       >
         <FolderOpen className="h-4 w-4" />
-        <span>{(!uploadState.file && !uploadState.content) ? 'Upload' : 'Change'}</span>
+        <span>{hasContent ? 'Change' : 'Upload'}</span>
       </Button>

       <Button
         onClick={handleManualEdit}
         variant="secondary"
         size="lg"
       >
         <Edit className="h-4 w-4" />
-        <span>{(!uploadState.file && !uploadState.content) ? 'JSON' : 'Edit'}</span>
+        <span>{hasContent ? 'Edit' : 'JSON'}</span>
       </Button>
     </div>
   )}

Also applies to: 266-269, 283-304

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between eac2741 and 8882a8d.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (13)
  • package.json (3 hunks)
  • src/App.test.tsx (1 hunks)
  • src/components/InspectStateViewer.tsx (2 hunks)
  • src/components/JsonEditorDialog.test.tsx (1 hunks)
  • src/components/JsonEditorDialog.tsx (1 hunks)
  • src/components/ServiceViewer.tsx (1 hunks)
  • src/components/SettingsDialog.tsx (1 hunks)
  • src/components/UploadScreen.test.tsx (1 hunks)
  • src/components/UploadScreen.tsx (10 hunks)
  • src/components/service/ServiceCard.test.tsx (6 hunks)
  • src/trie/TrieView.tsx (1 hunks)
  • src/trie/components/trie/index.tsx (1 hunks)
  • src/utils/jsonValidation.test.ts (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/components/UploadScreen.tsx (1)
src/utils/jsonValidation.ts (1)
  • validateJsonContent (209-228)
🪛 GitHub Actions: CI/CD Pipeline
src/components/service/ServiceCard.test.tsx

[error] 26-26: TS2347: Untyped function calls may not accept type arguments.

🪛 GitHub Check: Build
src/components/service/ServiceCard.test.tsx

[failure] 26-26:
Untyped function calls may not accept type arguments.

🔇 Additional comments (13)
src/components/service/ServiceCard.test.tsx (3)

7-20: LGTM!

The mock implementation correctly forwards role attributes and props, which is essential for the accessibility-focused tests in this file.


80-106: LGTM! Intentional refactor to traditional function expressions.

The change from arrow functions to traditional function expressions aligns with the PR's broader refactoring strategy for test mocks, enabling proper mock behavior with function boundaries.


157-159: LGTM! Tab name updates align with component changes.

The updated tab label expectations correctly reflect the component's new naming convention, removing separators while maintaining the subscript identifiers (aₛ, aₚ, aₗ).

src/components/JsonEditorDialog.test.tsx (1)

36-60: LGTM! Test mock updates align with dependency upgrades.

The conversion from arrow functions to function expressions for window.matchMedia and MutationObserver mocks changes the binding context, which is likely required for compatibility with the updated test environment (jsdom ^27.1.0, vitest ^4.0.8). The as any type assertion on the MutationObserver mock is a pragmatic approach for test doubles.

src/components/InspectStateViewer.tsx (1)

59-64: Excellent! Side effects properly moved to useEffect.

Moving the window property assignments from the render phase into useEffect follows React best practices. Side effects like mutating global objects should occur in effects, not during render, to avoid issues with concurrent rendering and Strict Mode double-invocations.

The dependencies [preStateAccess, stateAccess] correctly ensure the exposed values stay synchronized with the state access objects.

src/utils/jsonValidation.test.ts (3)

56-64: LGTM! FileReader mock updated for test environment compatibility.

The conversion from arrow functions to function expressions for FileReader mocks aligns with the pattern used in other test files and is required for compatibility with the updated test environment (jsdom ^27.1.0, vitest ^4.0.8).


288-296: LGTM! Consistent mock pattern applied.

The FileReader mock follows the same function-expression pattern as line 58, maintaining consistency across the test file.


598-606: LGTM! Mock helper updated consistently.

The mockFileReaderWithContent helper uses the same function-expression pattern, ensuring consistency across all FileReader mocks in the test file.

package.json (1)

57-67: Versions confirmed as latest stable and secure; complete remaining manual verification.

The automated checks confirm all three packages are at their latest stable releases with no active security vulnerabilities:

To complete verification, manually confirm:

  1. All tests pass with [email protected]
  2. Breaking changes from major bumps (especially eslint-plugin-react-hooks) are addressed—the original comment mentions "new lint warnings," suggesting these have already surfaced and need resolution
src/App.test.tsx (1)

79-90: LGTM!

The refactor from arrow function to traditional function expression maintains the same mock behavior and aligns with the consistent test mocking style applied across the PR.

src/components/UploadScreen.test.tsx (1)

31-42: LGTM!

The mock refactor maintains identical behavior and is consistent with the test mocking style updates across the PR.

src/components/UploadScreen.tsx (2)

15-15: LGTM! Good use of dynamic imports for code splitting.

Converting example content to async factories enables lazy loading of fixture data, reducing the initial bundle size. The dynamic import() statements will only load these fixtures when users click the example buttons.

Also applies to: 22-22, 27-27, 32-32, 37-37


171-171: LGTM! Proper loading state management.

Disabling example buttons during loading prevents race conditions from multiple simultaneous content loads.

Also applies to: 180-180, 189-189, 198-198, 214-214

Comment on lines +22 to +78
// Mock the UI components
vi.mock('../ui', () => {
const React = require('react');

const TabsContext = React.createContext<{ value?: string; onValueChange?: (value: string) => void }>({});

const Tabs = ({ children, value, onValueChange, ...props }: any) => {
const [internalValue, setInternalValue] = React.useState('info');
const currentValue = value ?? internalValue;
const handleValueChange = React.useCallback((newValue: string) => {
if (value === undefined) {
setInternalValue(newValue);
}
onValueChange?.(newValue);
}, [value, onValueChange]);

return (
<TabsContext.Provider value={{ value: currentValue, onValueChange: handleValueChange }}>
<div {...props}>{children}</div>
</TabsContext.Provider>
);
};

const TabsList = ({ children, ...props }: any) => (
<div role="tablist" {...props}>{children}</div>
);

const TabsTrigger = ({ children, value, className, ...props }: any) => {
const context = React.useContext(TabsContext);
const isActive = context.value === value;

return (
<button
role="tab"
aria-selected={isActive}
className={className}
onClick={() => context.onValueChange?.(value)}
{...props}
>
{children}
</button>
);
};

const TabsContent = ({ children, value, ...props }: any) => {
const context = React.useContext(TabsContext);
if (context.value !== value) return null;
return <div role="tabpanel" {...props}>{children}</div>;
};

return {
Tabs,
TabsList,
TabsTrigger,
TabsContent,
};
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Fix TypeScript error: use import instead of require for typed React.

The build is failing because require('react') returns an untyped module, so calling createContext with a type argument is not allowed.

Apply this diff to fix the TypeScript error:

 vi.mock('../ui', () => {
-  const React = require('react');
+  import React from 'react';
 
   const TabsContext = React.createContext<{ value?: string; onValueChange?: (value: string) => void }>({});

Alternative fix (if import syntax causes issues in the mock context):

-  const TabsContext = React.createContext<{ value?: string; onValueChange?: (value: string) => void }>({});
+  const TabsContext = React.createContext({});

The second approach removes the type argument, which is acceptable for test mocks.

📝 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
// Mock the UI components
vi.mock('../ui', () => {
const React = require('react');
const TabsContext = React.createContext<{ value?: string; onValueChange?: (value: string) => void }>({});
const Tabs = ({ children, value, onValueChange, ...props }: any) => {
const [internalValue, setInternalValue] = React.useState('info');
const currentValue = value ?? internalValue;
const handleValueChange = React.useCallback((newValue: string) => {
if (value === undefined) {
setInternalValue(newValue);
}
onValueChange?.(newValue);
}, [value, onValueChange]);
return (
<TabsContext.Provider value={{ value: currentValue, onValueChange: handleValueChange }}>
<div {...props}>{children}</div>
</TabsContext.Provider>
);
};
const TabsList = ({ children, ...props }: any) => (
<div role="tablist" {...props}>{children}</div>
);
const TabsTrigger = ({ children, value, className, ...props }: any) => {
const context = React.useContext(TabsContext);
const isActive = context.value === value;
return (
<button
role="tab"
aria-selected={isActive}
className={className}
onClick={() => context.onValueChange?.(value)}
{...props}
>
{children}
</button>
);
};
const TabsContent = ({ children, value, ...props }: any) => {
const context = React.useContext(TabsContext);
if (context.value !== value) return null;
return <div role="tabpanel" {...props}>{children}</div>;
};
return {
Tabs,
TabsList,
TabsTrigger,
TabsContent,
};
});
// Mock the UI components
vi.mock('../ui', () => {
const React = require('react');
const TabsContext = React.createContext({});
const Tabs = ({ children, value, onValueChange, ...props }: any) => {
const [internalValue, setInternalValue] = React.useState('info');
const currentValue = value ?? internalValue;
const handleValueChange = React.useCallback((newValue: string) => {
if (value === undefined) {
setInternalValue(newValue);
}
onValueChange?.(newValue);
}, [value, onValueChange]);
return (
<TabsContext.Provider value={{ value: currentValue, onValueChange: handleValueChange }}>
<div {...props}>{children}</div>
</TabsContext.Provider>
);
};
const TabsList = ({ children, ...props }: any) => (
<div role="tablist" {...props}>{children}</div>
);
const TabsTrigger = ({ children, value, className, ...props }: any) => {
const context = React.useContext(TabsContext);
const isActive = context.value === value;
return (
<button
role="tab"
aria-selected={isActive}
className={className}
onClick={() => context.onValueChange?.(value)}
{...props}
>
{children}
</button>
);
};
const TabsContent = ({ children, value, ...props }: any) => {
const context = React.useContext(TabsContext);
if (context.value !== value) return null;
return <div role="tabpanel" {...props}>{children}</div>;
};
return {
Tabs,
TabsList,
TabsTrigger,
TabsContent,
};
});
🧰 Tools
🪛 GitHub Actions: CI/CD Pipeline

[error] 26-26: TS2347: Untyped function calls may not accept type arguments.

🪛 GitHub Check: Build

[failure] 26-26:
Untyped function calls may not accept type arguments.

🤖 Prompt for AI Agents
In src/components/service/ServiceCard.test.tsx around lines 22 to 78, the mock
uses require('react') which yields an untyped module causing a TypeScript error
when calling createContext with a type parameter; replace the require call with
an ES import for React (e.g., import * as React from 'react') at the top of the
mock so createContext can be typed, or as an alternative remove the generic type
argument from createContext and any typed callbacks (use untyped {} or unknown)
to keep the mock TypeScript-clean if using import is not possible in this mock
scope.

Comment on lines 25 to 30
useEffect(() => {
if (discoveredServiceIds.length > 0) {
// eslint-disable-next-line react-hooks/set-state-in-effect
setServiceIdsInput(discoveredServiceIds.map(formatServiceIdUnsigned).join(', '));
}
}, [discoveredServiceIds]);
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Refactor to avoid state synchronization in effect.

Setting state inside useEffect to mirror derived values is an anti-pattern. The lint suppression masks a design issue rather than a legitimate exception.

Consider these alternatives:

  1. Derive directly from state (preferred): Remove serviceIdsInput state and compute the display value from discoveredServiceIds in the render phase.
  2. Use a reset key: If user edits are needed, add a key to the input component that changes when discoveredServiceIds changes, triggering a re-mount with fresh initial state.

Apply this diff to derive the value directly:

  const discoveredServiceIds = useMemo(() => {
    return extractServiceIdsFromState(state);
  }, [state]);

- const [serviceIdsInput, setServiceIdsInput] = useState(() => {
-   return discoveredServiceIds.length > 0 ? discoveredServiceIds.map(formatServiceIdUnsigned).join(', ') : '0';
- });
-
- useEffect(() => {
-   if (discoveredServiceIds.length > 0) {
-     // eslint-disable-next-line react-hooks/set-state-in-effect
-     setServiceIdsInput(discoveredServiceIds.map(formatServiceIdUnsigned).join(', '));
-   }
- }, [discoveredServiceIds]);
+ const defaultServiceIdsInput = useMemo(() => {
+   return discoveredServiceIds.length > 0 ? discoveredServiceIds.map(formatServiceIdUnsigned).join(', ') : '0';
+ }, [discoveredServiceIds]);
+
+ const [serviceIdsInput, setServiceIdsInput] = useState(defaultServiceIdsInput);

  const serviceIds = useMemo(() => {
    return parseServiceIds(serviceIdsInput);
  }, [serviceIdsInput]);

If the input needs to reset when state changes, add a key to force remount:

- <ServiceIdsInput
+ <ServiceIdsInput
+   key={JSON.stringify(discoveredServiceIds)}
    value={serviceIdsInput}
    onChange={setServiceIdsInput}
  />

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

🤖 Prompt for AI Agents
In src/components/ServiceViewer.tsx around lines 25 to 30 the component sets
serviceIdsInput inside a useEffect to mirror discoveredServiceIds (with an
eslint suppression) which is an anti-pattern; remove the effect and either
(preferred) delete the serviceIdsInput state and compute the input display value
directly from discoveredServiceIds in render (e.g.
value={discoveredServiceIds.map(formatServiceIdUnsigned).join(', ')}) or (if
users must edit and you need to reset on discovery changes) keep local state but
add a changing key on the input tied to discoveredServiceIds (e.g.
key={discoveredServiceIds.join(',')} ) so the input remounts with fresh initial
state when discoveredServiceIds change.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants