Skip to content

Replace React Context with Zustand for form state management#29

Open
kertal wants to merge 1 commit into
mainfrom
claude/add-zustand-state-fQ06X
Open

Replace React Context with Zustand for form state management#29
kertal wants to merge 1 commit into
mainfrom
claude/add-zustand-state-fQ06X

Conversation

@kertal

@kertal kertal commented Feb 19, 2026

Copy link
Copy Markdown
Owner

Summary

Migrated form state management from React Context (FormContext) to Zustand store (useFormStore), eliminating the need for a context provider while maintaining the same functionality and improving component subscription patterns.

Key Changes

  • Removed React Context: Deleted FormContext and useFormContext hook from App.tsx, along with the context provider wrapper around child components
  • Created Zustand store: Added new src/store/useFormStore.ts with a centralized store containing all form state and action callbacks
  • Synced state in App.tsx: Added a useEffect hook that syncs state from existing hooks into the Zustand store, keeping hooks as the source of truth while enabling fine-grained subscriptions in child components
  • Updated all consumers: Replaced useFormContext() imports with useFormStore() in:
    • SearchForm.tsx
    • SettingsDialog.tsx
    • EventView.tsx
    • IssuesAndPRsList.tsx
    • Summary.tsx
  • Updated test infrastructure:
    • Removed context mocking from test/setup.ts and replaced with direct Zustand store seeding
    • Simplified test-utils.tsx to remove context providers and added seedFormStore() helper
    • Updated component tests to mock useFormStore instead of useFormContext
  • Cleaned up types: Removed FormContextType and ResultsContextType from types.ts (context types now live in store file)
  • Added dependency: Added zustand@^5.0.11 to package.json

Implementation Details

The migration maintains backward compatibility by:

  • Keeping all state and action signatures identical
  • Syncing from App.tsx hooks to the store via useEffect, so the hooks remain the source of truth
  • Allowing child components to subscribe to individual store slices without a Provider wrapper
  • Simplifying test setup by directly seeding the store instead of mocking context providers

https://claude.ai/code/session_01BM8QfufDNGMCvJPVBEAw5w

Summary by CodeRabbit

Release Notes

  • Refactor
    • Modernized the internal state management architecture for improved code maintainability and scalability.

Introduces zustand as the state management library, replacing the
FormContext/Provider pattern. The existing hooks remain the source of
truth while App.tsx syncs their values into the zustand store. Consumer
components now use useFormStore() instead of useFormContext(), gaining
fine-grained subscriptions without needing a Provider wrapper.

- Add zustand dependency
- Create src/store/useFormStore.ts as the central form store
- Remove FormContext, createContext/useContext, and Provider from App.tsx
- Update all consumers (SearchForm, SettingsDialog, EventView,
  IssuesAndPRsList, Summary) to import from useFormStore
- Update test mocks and test-utils to use zustand store
- Remove unused FormContextType and ResultsContextType from types.ts

https://claude.ai/code/session_01BM8QfufDNGMCvJPVBEAw5w

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR migrates form state management from React Context to Zustand, eliminating the need for a Context Provider wrapper while maintaining functionality. The implementation uses a hybrid approach where existing hooks in App.tsx remain the source of truth, and a useEffect hook syncs their values to a Zustand store that child components subscribe to.

Changes:

  • Created a new Zustand store (useFormStore) to replace FormContext and useFormContext
  • Added synchronization logic in App.tsx to push hook state into the Zustand store via useEffect
  • Updated all consumer components and tests to use useFormStore instead of useFormContext

Reviewed changes

Copilot reviewed 15 out of 16 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/store/useFormStore.ts New Zustand store with form state and actions; replaces FormContext
src/App.tsx Removed Context provider wrapper and added useEffect to sync state to Zustand store
src/components/SearchForm.tsx Updated to import and use useFormStore instead of useFormContext
src/components/SettingsDialog.tsx Updated to import and use useFormStore instead of useFormContext
src/views/EventView.tsx Updated to import and use useFormStore instead of useFormContext
src/views/IssuesAndPRsList.tsx Updated to import and use useFormStore instead of useFormContext
src/views/Summary.tsx Updated to import and use useFormStore instead of useFormContext
src/types.ts Removed FormContextType and ResultsContextType (now in store file)
src/test/setup.ts Replaced context mocking with direct Zustand store seeding
src/test/test-utils.tsx Removed context providers and added seedFormStore helper
src/components/tests/Summary.test.tsx Updated mock to use useFormStore instead of useFormContext
src/components/tests/SearchForm-RaceCondition.test.tsx Updated mock to use useFormStore instead of useFormContext
src/views/tests/IssuesAndPRsList.test.tsx Updated mock to use useFormStore instead of useFormContext
src/components/SearchForm.test.tsx Updated mock to use useFormStore instead of useFormContext
package.json Added zustand@^5.0.11 dependency
package-lock.json Added zustand dependency with metadata

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/App.tsx
Comment on lines +114 to +126
useFormStore.setState({
username, startDate, endDate, githubToken, apiMode, searchText,
loading, loadingProgress, error,
searchItemsCount, eventsCount, rawEventsCount,
setUsername, setStartDate, setEndDate, setGithubToken, setApiMode, setSearchText,
handleSearch, validateUsernameFormat, addAvatarsToCache,
});
}, [
username, startDate, endDate, githubToken, apiMode, searchText,
loading, loadingProgress, error,
searchItemsCount, eventsCount, rawEventsCount,
setUsername, setStartDate, setEndDate, setGithubToken, setApiMode, setSearchText,
handleSearch, validateUsernameFormat, addAvatarsToCache,

Copilot AI Feb 19, 2026

Copy link

Choose a reason for hiding this comment

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

The useEffect hook that syncs state to the Zustand store runs on every change to any of the dependencies (lines 121-127). This approach creates a potential performance issue because every state change triggers the entire store to be updated via setState, which will notify all subscribers even if they're only interested in a single field. While Zustand does optimize re-renders through selectors, this synchronization pattern negates some of those benefits. Consider whether this hybrid approach (hooks as source of truth + Zustand for distribution) is necessary, or if migrating fully to Zustand would be more performant and maintainable.

Suggested change
useFormStore.setState({
username, startDate, endDate, githubToken, apiMode, searchText,
loading, loadingProgress, error,
searchItemsCount, eventsCount, rawEventsCount,
setUsername, setStartDate, setEndDate, setGithubToken, setApiMode, setSearchText,
handleSearch, validateUsernameFormat, addAvatarsToCache,
});
}, [
username, startDate, endDate, githubToken, apiMode, searchText,
loading, loadingProgress, error,
searchItemsCount, eventsCount, rawEventsCount,
setUsername, setStartDate, setEndDate, setGithubToken, setApiMode, setSearchText,
handleSearch, validateUsernameFormat, addAvatarsToCache,
const currentState = useFormStore.getState() as any;
const nextState = {
username,
startDate,
endDate,
githubToken,
apiMode,
searchText,
loading,
loadingProgress,
error,
searchItemsCount,
eventsCount,
rawEventsCount,
setUsername,
setStartDate,
setEndDate,
setGithubToken,
setApiMode,
setSearchText,
handleSearch,
validateUsernameFormat,
addAvatarsToCache,
};
const changedState: Partial<typeof nextState> = {};
let hasChanges = false;
(Object.keys(nextState) as (keyof typeof nextState)[]).forEach((key) => {
if (currentState[key] !== nextState[key]) {
changedState[key] = nextState[key];
hasChanges = true;
}
});
if (hasChanges) {
useFormStore.setState(changedState);
}
}, [
username,
startDate,
endDate,
githubToken,
apiMode,
searchText,
loading,
loadingProgress,
error,
searchItemsCount,
eventsCount,
rawEventsCount,
setUsername,
setStartDate,
setEndDate,
setGithubToken,
setApiMode,
setSearchText,
handleSearch,
validateUsernameFormat,
addAvatarsToCache,

Copilot uses AI. Check for mistakes.
Comment thread src/store/useFormStore.ts
setGithubToken: (token: string) => void;
setApiMode: (mode: ApiMode) => void;
setSearchText: (text: string) => void;
handleSearch: () => void;

Copilot AI Feb 19, 2026

Copy link

Choose a reason for hiding this comment

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

The type signature for handleSearch is incorrect. The actual implementation in useGitHubDataFetching returns Promise<void> (line 25 in useGitHubDataFetching.ts), but this type signature specifies () => void. This type mismatch can cause TypeScript type checking issues and potentially hide bugs where the promise is not handled correctly. The signature should be handleSearch: () => Promise<void>; to match the actual implementation.

Copilot uses AI. Check for mistakes.
Comment thread src/store/useFormStore.ts
Comment on lines +42 to +44
* State values and action callbacks are synced from App.tsx via
* `useFormStoreSync` so that the existing hooks remain the source of truth
* while consumers get fine-grained subscriptions without a Provider.

Copilot AI Feb 19, 2026

Copy link

Choose a reason for hiding this comment

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

The documentation references useFormStoreSync which doesn't exist. The actual synchronization happens via a useEffect hook in App.tsx (lines 113-127). This should be updated to accurately reflect the implementation, perhaps: "State values and action callbacks are synced from App.tsx via a useEffect hook..."

Suggested change
* State values and action callbacks are synced from App.tsx via
* `useFormStoreSync` so that the existing hooks remain the source of truth
* while consumers get fine-grained subscriptions without a Provider.
* State values and action callbacks are synced from App.tsx via a `useEffect`
* hook so that the existing hooks remain the source of truth while consumers
* get fine-grained subscriptions without a Provider.

Copilot uses AI. Check for mistakes.
Comment on lines 8 to 27
@@ -26,9 +26,8 @@ const mockFormContext = {
rawEventsCount: 0,
};

Copilot AI Feb 19, 2026

Copy link

Choose a reason for hiding this comment

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

The mock is missing required fields that are used by the Summary component. The searchText, setSearchText, and addAvatarsToCache fields are used in the component (line 75 in Summary.tsx) but are not included in this mock. This could cause the test to fail or behave unexpectedly. These fields should be added to match the complete FormStore interface.

Copilot uses AI. Check for mistakes.
@kertal
kertal requested a review from Copilot February 19, 2026 16:10
@kertal

kertal commented Feb 19, 2026

Copy link
Copy Markdown
Owner Author

@copilot open a new pull request to apply changes based on the comments in this thread

Copilot AI commented Feb 19, 2026

Copy link
Copy Markdown
Contributor

@kertal I've opened a new pull request, #30, to work on those changes. Once the pull request is ready, I'll request review from you.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 15 out of 16 changed files in this pull request and generated 5 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +115 to 116
const { searchText, setSearchText } = useFormStore();

Copilot AI Feb 19, 2026

Copy link

Choose a reason for hiding this comment

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

Using useFormStore() without a selector subscribes this component to the entire store, so any store update will re-render this (potentially heavy) list. To get the fine-grained subscription benefits, select only the needed slices (e.g., searchText and setSearchText), optionally using Zustand's shallow helper when selecting multiple values.

Copilot uses AI. Check for mistakes.
Comment thread src/views/EventView.tsx
rawEvents = [],
}: EventViewProps) {
const { searchText, setSearchText } = useFormContext();
const { searchText, setSearchText } = useFormStore();

Copilot AI Feb 19, 2026

Copy link

Choose a reason for hiding this comment

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

Using useFormStore() without a selector subscribes this view to the entire store, so unrelated updates (counts, loading, etc.) will re-render the whole list. Switch to selecting only searchText / setSearchText (and use shallow if selecting multiple fields) to avoid unnecessary renders.

Suggested change
const { searchText, setSearchText } = useFormStore();
const searchText = useFormStore((state) => state.searchText);
const setSearchText = useFormStore((state) => state.setSearchText);

Copilot uses AI. Check for mistakes.
Comment thread src/views/Summary.tsx
Comment on lines +75 to 76
const { startDate, endDate, searchText, setSearchText } = useFormStore();

Copilot AI Feb 19, 2026

Copy link

Choose a reason for hiding this comment

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

Using useFormStore() without a selector subscribes SummaryView to the entire store, which can cause expensive re-renders when unrelated state changes (loading, counts, etc.). Prefer selecting only the needed slices (here: startDate, endDate, searchText, setSearchText), using shallow for the multi-field selection.

Copilot uses AI. Check for mistakes.
Comment thread src/store/useFormStore.ts
Comment on lines +30 to +34
setApiMode: (mode: ApiMode) => void;
setSearchText: (text: string) => void;
handleSearch: () => void;
validateUsernameFormat: (username: string) => void;
addAvatarsToCache: (avatarUrls: { [username: string]: string }) => void;

Copilot AI Feb 19, 2026

Copy link

Choose a reason for hiding this comment

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

handleSearch is typed as returning void, but the real handleSearch coming from useGitHubDataFetching returns a Promise<void> (and App.tsx uses .then() on it). To keep signatures consistent and allow consumers to await it, update the store action type to return Promise<void>.

Copilot uses AI. Check for mistakes.
Comment thread src/App.tsx
Comment on lines +111 to +120
// Sync state and actions from hooks into the zustand store so that
// child components can subscribe to individual slices without a Provider.
useEffect(() => {
useFormStore.setState({
username, startDate, endDate, githubToken, apiMode, searchText,
loading, loadingProgress, error,
searchItemsCount, eventsCount, rawEventsCount,
setUsername, setStartDate, setEndDate, setGithubToken, setApiMode, setSearchText,
handleSearch, validateUsernameFormat, addAvatarsToCache,
});

Copilot AI Feb 19, 2026

Copy link

Choose a reason for hiding this comment

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

Syncing the store from App via useEffect means children render once with the store's default state and no-op action stubs, and only get real values after the effect runs. This can cause a brief UI flicker and (in edge cases) user interactions on the first paint to be dropped. Consider using useLayoutEffect for the sync (or otherwise ensuring the store is populated before interactive children mount).

Copilot uses AI. Check for mistakes.
@coderabbitai

coderabbitai Bot commented Feb 20, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This pull request migrates form state management from React Context to Zustand store. A new store file is created, context-based infrastructure (FormContext, useFormContext) is removed from App.tsx, and all consumer components and tests are updated to use the store instead, while adding zustand as a dependency.

Changes

Cohort / File(s) Summary
Store Implementation
package.json, src/store/useFormStore.ts
Added zustand ^5.0.11 dependency. Created new Zustand store with FormStoreState, FormStoreActions, and ApiMode type definitions; store manages form UI state (dates, search text, API mode) and action handlers.
App Architecture
src/App.tsx
Removed FormContext, FormContextType, and useFormContext. Replaced context provider pattern with useEffect that syncs form state/handlers into Zustand store. Simplified rendering by removing Provider wrapper.
Components
src/components/SearchForm.tsx, src/components/SettingsDialog.tsx, src/views/EventView.tsx, src/views/IssuesAndPRsList.tsx, src/views/Summary.tsx
Updated all five files to import and use useFormStore instead of useFormContext, maintaining same destructured field usage (searchText, setSearchText, token, dates, etc.).
Type Definitions
src/types.ts
Removed FormContextType and ResultsContextType exported interfaces (57 lines deleted); store types now defined in useFormStore.ts.
Test Infrastructure
src/test/setup.ts, src/test/test-utils.tsx
Replaced explicit context mocks (useFormContext, useResultsContext) with Zustand store seeding via seedFormStore() utility and mockFormStoreValues constant. Removed AllTheProviders Provider wrappers.
Component Tests
src/components/SearchForm.test.tsx, src/components/__tests__/SearchForm-RaceCondition.test.tsx, src/components/__tests__/Summary.test.tsx, src/views/__tests__/IssuesAndPRsList.test.tsx
Updated four test files to mock useFormStore (from store/useFormStore) instead of useFormContext (from App); expanded mock store shapes to include all store state and action fields.

Sequence Diagram

sequenceDiagram
    participant App as App.tsx
    participant Store as Zustand Store
    participant Component as Components<br/>(SearchForm, etc.)
    
    App->>Store: useEffect: sync form state<br/>& handlers into store via setState()
    Component->>Store: Import useFormStore
    Component->>Store: Call useFormStore() to<br/>subscribe to store slices
    Store-->>Component: Return searchText, setSearchText,<br/>token, dates, handlers, etc.
    Component->>Component: Render with store state
    Component->>Store: Call setters/handlers<br/>(e.g., setSearchText)
    Store->>Store: Update state
    Store-->>Component: Notify subscribers<br/>(re-render)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

Bye-bye context, hello Zustand store! 🐰
State management simplified to the core,
Subscriptions so clean, we couldn't ask for more,
This rabbit's rejoicing—what's there not to adore? ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: migrating form state management from React Context to Zustand, which is the primary objective and central theme across all modified files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch claude/add-zustand-state-fQ06X

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

@kertal

kertal commented Feb 20, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Feb 20, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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 (1)
src/components/__tests__/Summary.test.tsx (1)

7-27: ⚠️ Potential issue | 🟡 Minor

Mock store is missing searchText / setSearchText.
SummaryView destructures these from useFormStore, so the mock should include them to avoid undefined behavior in tests.

✅ Suggested fix
 const mockFormStore = {
   githubToken: 'test-token',
   startDate: '2024-01-01',
   endDate: '2024-01-31',
   username: 'testuser',
+  searchText: '',
   apiMode: 'summary' as const,
+  setSearchText: vi.fn(),
   setUsername: vi.fn(),
   setStartDate: vi.fn(),
   setEndDate: vi.fn(),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/__tests__/Summary.test.tsx` around lines 7 - 27, The
mockFormStore used in the Summary.test.tsx is missing the searchText and
setSearchText properties which SummaryView destructures from useFormStore;
update the mockFormStore object to include searchText (e.g., default string) and
setSearchText (a vi.fn()), so SummaryView and any tests that call useFormStore
see those keys and avoid undefined behavior when rendering or interacting with
components.
🧹 Nitpick comments (3)
src/store/useFormStore.ts (1)

61-70: Avoid silent no-op actions when the store isn’t synced.
Consider a dev-time guard (warn/throw) so mis-wiring doesn’t fail silently if useFormStoreSync isn’t invoked.

💡 Example guard to surface missing sync
+const createStub = (name: keyof FormStoreActions) => () => {
+  if (import.meta.env?.DEV) {
+    console.warn(`useFormStore action "${name}" called before sync`);
+  }
+};
...
-  setUsername: () => {},
-  setStartDate: () => {},
-  setEndDate: () => {},
-  setGithubToken: () => {},
-  setApiMode: () => {},
-  setSearchText: () => {},
-  handleSearch: () => {},
-  validateUsernameFormat: () => {},
-  addAvatarsToCache: () => {},
+  setUsername: createStub('setUsername'),
+  setStartDate: createStub('setStartDate'),
+  setEndDate: createStub('setEndDate'),
+  setGithubToken: createStub('setGithubToken'),
+  setApiMode: createStub('setApiMode'),
+  setSearchText: createStub('setSearchText'),
+  handleSearch: createStub('handleSearch'),
+  validateUsernameFormat: createStub('validateUsernameFormat'),
+  addAvatarsToCache: createStub('addAvatarsToCache'),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/store/useFormStore.ts` around lines 61 - 70, Replace the silent no-op
action stubs in useFormStore (setUsername, setStartDate, setEndDate,
setGithubToken, setApiMode, setSearchText, handleSearch, validateUsernameFormat,
addAvatarsToCache) with dev-time guards that surface when the store hasn’t been
synced (useFormStoreSync); implement each stub to, when NODE_ENV !==
'production', either console.error/console.warn (including the action name and a
hint to call useFormStoreSync) or throw an Error to fail fast, and in production
fall back to a no-op to avoid breaking users — update the stub implementations
to reference their own names so logs clearly point to the missing sync.
src/components/__tests__/SearchForm-RaceCondition.test.tsx (1)

28-48: Minor inconsistency: error value differs from other mocks.

This mock sets error: '' (empty string) at line 43, while other test mocks use error: null (e.g., setup.ts line 36, test-utils.tsx line 26). If FormStore.error is typed as string | null, consider using null consistently across all mocks to avoid potential type mismatches or subtle test behavior differences.

Suggested fix for consistency
     validateUsernameFormat: mockValidateUsernameFormat,
     addAvatarsToCache: mockAddAvatarsToCache,
     loading: false,
-    error: '',
+    error: null,
     searchItemsCount: 0,
     rawEventsCount: 0,
     githubToken: 'test-token',
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/__tests__/SearchForm-RaceCondition.test.tsx` around lines 28 -
48, The mock of the zustand store (useFormStore) sets error: '' but other test
mocks use error: null, causing a potential type/behavior mismatch; update the
mock in SearchForm-RaceCondition.test.tsx (the vi.mock returning useFormStore)
to set error: null to match the FormStore error type and other test setups
(e.g., setup.ts/test-utils.tsx) so tests are consistent.
src/components/SearchForm.test.tsx (1)

30-48: Mock is missing fields present in other test mocks and the FormStore type.

The mock is missing githubToken, eventsCount, and loadingProgress which are present in the store type and in other test file mocks (e.g., SearchForm-RaceCondition.test.tsx line 46, setup.ts lines 22, 35, 38). If SearchForm accesses these fields, tests may behave unexpectedly.

Suggested additions for consistency
     addAvatarsToCache: mockAddAvatarsToCache,
     loading: false,
     error: null,
     searchItemsCount: 0,
     rawEventsCount: 0,
+    eventsCount: 0,
+    loadingProgress: '',
+    githubToken: 'test-token',
   }),
 }));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/SearchForm.test.tsx` around lines 30 - 48, The test mock for
useFormStore is missing fields from the FormStore type which can cause
unexpected behavior; update the mock returned by useFormStore in
SearchForm.test.tsx to include githubToken, eventsCount, and loadingProgress
(and ensure their corresponding setter/mutator mocks exist or are no-ops) so it
matches other test mocks (e.g., SearchForm-RaceCondition.test.tsx and setup.ts)
and the FormStore interface; locate the useFormStore mock definition and add
these properties with sensible defaults consistent with other tests (e.g.,
githubToken: '', eventsCount: 0, loadingProgress: 0) and corresponding mock
setter functions if used.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/App.tsx`:
- Around line 111-127: The effect in App.tsx keeps rerunning because
setSearchText (from useLocalStorage) is an unstable setter; memoize that
function to stabilize the dependency: update useLocalStorage to wrap the setter
in useCallback (or expose a memoized setter variant) so the reference returned
as setSearchText is stable, then keep the useEffect that calls
useFormStore.setState unchanged; locate useLocalStorage and the exported
setSearchText name and ensure it returns a useCallback-wrapped setter used by
useGitHubFormState/useEffect.

In `@src/test/setup.ts`:
- Around line 15-40: The current test setup seeds the zustand store once via
useFormStore.setState but never resets it between tests, so state mutations
leak; update the teardown to reset the store after each test by adding a call to
restore the default form state (either call useFormStore.setState with the same
default object used in this file or invoke the shared seedFormStore() helper
from test-utils.tsx) inside the existing afterEach (in addition to
vi.clearAllMocks()), referencing useFormStore and/or seedFormStore so the store
values (username, startDate, endDate, githubToken, apiMode, etc.) are restored
for each test run.

---

Outside diff comments:
In `@src/components/__tests__/Summary.test.tsx`:
- Around line 7-27: The mockFormStore used in the Summary.test.tsx is missing
the searchText and setSearchText properties which SummaryView destructures from
useFormStore; update the mockFormStore object to include searchText (e.g.,
default string) and setSearchText (a vi.fn()), so SummaryView and any tests that
call useFormStore see those keys and avoid undefined behavior when rendering or
interacting with components.

---

Nitpick comments:
In `@src/components/__tests__/SearchForm-RaceCondition.test.tsx`:
- Around line 28-48: The mock of the zustand store (useFormStore) sets error: ''
but other test mocks use error: null, causing a potential type/behavior
mismatch; update the mock in SearchForm-RaceCondition.test.tsx (the vi.mock
returning useFormStore) to set error: null to match the FormStore error type and
other test setups (e.g., setup.ts/test-utils.tsx) so tests are consistent.

In `@src/components/SearchForm.test.tsx`:
- Around line 30-48: The test mock for useFormStore is missing fields from the
FormStore type which can cause unexpected behavior; update the mock returned by
useFormStore in SearchForm.test.tsx to include githubToken, eventsCount, and
loadingProgress (and ensure their corresponding setter/mutator mocks exist or
are no-ops) so it matches other test mocks (e.g.,
SearchForm-RaceCondition.test.tsx and setup.ts) and the FormStore interface;
locate the useFormStore mock definition and add these properties with sensible
defaults consistent with other tests (e.g., githubToken: '', eventsCount: 0,
loadingProgress: 0) and corresponding mock setter functions if used.

In `@src/store/useFormStore.ts`:
- Around line 61-70: Replace the silent no-op action stubs in useFormStore
(setUsername, setStartDate, setEndDate, setGithubToken, setApiMode,
setSearchText, handleSearch, validateUsernameFormat, addAvatarsToCache) with
dev-time guards that surface when the store hasn’t been synced
(useFormStoreSync); implement each stub to, when NODE_ENV !== 'production',
either console.error/console.warn (including the action name and a hint to call
useFormStoreSync) or throw an Error to fail fast, and in production fall back to
a no-op to avoid breaking users — update the stub implementations to reference
their own names so logs clearly point to the missing sync.

Comment thread src/App.tsx
Comment on lines +111 to +127
// Sync state and actions from hooks into the zustand store so that
// child components can subscribe to individual slices without a Provider.
useEffect(() => {
useFormStore.setState({
username, startDate, endDate, githubToken, apiMode, searchText,
loading, loadingProgress, error,
searchItemsCount, eventsCount, rawEventsCount,
setUsername, setStartDate, setEndDate, setGithubToken, setApiMode, setSearchText,
handleSearch, validateUsernameFormat, addAvatarsToCache,
});
}, [
username, startDate, endDate, githubToken, apiMode, searchText,
loading, loadingProgress, error,
searchItemsCount, eventsCount, rawEventsCount,
setUsername, setStartDate, setEndDate, setGithubToken, setApiMode, setSearchText,
handleSearch, validateUsernameFormat, addAvatarsToCache,
]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

fd -t f -e ts -e tsx -e js -e jsx | xargs rg "useGitHubFormState|const useGitHubFormState" -l

Repository: kertal/git-vegas

Length of output: 158


🏁 Script executed:

cat -n src/hooks/useGitHubFormState.ts

Repository: kertal/git-vegas

Length of output: 9580


🏁 Script executed:

cat -n src/App.tsx | head -150 | tail -80

Repository: kertal/git-vegas

Length of output: 3041


🏁 Script executed:

cat -n src/App.tsx | head -70

Repository: kertal/git-vegas

Length of output: 3090


🏁 Script executed:

cat -n src/hooks/useLocalStorage.ts

Repository: kertal/git-vegas

Length of output: 10236


setSearchText from useLocalStorage is not memoized, causing unnecessary effect reruns.

Most setter functions from useGitHubFormState are properly wrapped in useCallback. However, setSearchText comes from useLocalStorage (line 42) and is the raw React setState function—not memoized. Since this unstable reference is included in the effect dependency array (line 125), the effect runs on every render, unnecessarily syncing to Zustand even when no actual state changed.

Wrap setSearchText with useCallback in useLocalStorage or consider extracting the setter memoization logic into a custom hook to ensure stable references.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/App.tsx` around lines 111 - 127, The effect in App.tsx keeps rerunning
because setSearchText (from useLocalStorage) is an unstable setter; memoize that
function to stabilize the dependency: update useLocalStorage to wrap the setter
in useCallback (or expose a memoized setter variant) so the reference returned
as setSearchText is stable, then keep the useEffect that calls
useFormStore.setState unchanged; locate useLocalStorage and the exported
setSearchText name and ensure it returns a useCallback-wrapped setter used by
useGitHubFormState/useEffect.

Comment thread src/test/setup.ts
Comment on lines +15 to 40
// Seed the zustand form store with default test values
import { useFormStore } from '../store/useFormStore';

useFormStore.setState({
username: 'testuser',
startDate: '2023-01-01',
endDate: '2023-12-31',
githubToken: 'test-token',
apiMode: 'search',
searchText: '',
setUsername: vi.fn(),
setStartDate: vi.fn(),
setEndDate: vi.fn(),
setGithubToken: vi.fn(),
setApiMode: vi.fn(),
setSearchText: vi.fn(),
handleSearch: vi.fn(),
validateUsernameFormat: vi.fn(),
addAvatarsToCache: vi.fn(),
loading: false,
loadingProgress: '',
error: null,
searchItemsCount: 0,
eventsCount: 0,
rawEventsCount: 0,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Store state is not reset between tests, which may cause test isolation issues.

The store is seeded once at module load (lines 18-40), but the afterEach hook (line 412) only calls vi.clearAllMocks() which resets the mock functions, not the store's state values (username, startDate, etc.). If any test modifies the store state directly, those changes will leak into subsequent tests.

Consider resetting the store in afterEach to ensure test isolation:

Suggested fix
 afterEach(() => {
   // Clear all mocks
   vi.clearAllMocks();
   // Cleanup React Testing Library
   cleanup();
   // Clear storages
   localStorageMock.clear();
   sessionStorageMock.clear();
   // Reset location
   locationMock.href = BASE_URL;
+  // Reset zustand store to default test values
+  useFormStore.setState({
+    username: 'testuser',
+    startDate: '2023-01-01',
+    endDate: '2023-12-31',
+    githubToken: 'test-token',
+    apiMode: 'search',
+    searchText: '',
+    setUsername: vi.fn(),
+    setStartDate: vi.fn(),
+    setEndDate: vi.fn(),
+    setGithubToken: vi.fn(),
+    setApiMode: vi.fn(),
+    setSearchText: vi.fn(),
+    handleSearch: vi.fn(),
+    validateUsernameFormat: vi.fn(),
+    addAvatarsToCache: vi.fn(),
+    loading: false,
+    loadingProgress: '',
+    error: null,
+    searchItemsCount: 0,
+    eventsCount: 0,
+    rawEventsCount: 0,
+  });
 });

Alternatively, import and use seedFormStore() from test-utils.tsx to avoid duplication.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/setup.ts` around lines 15 - 40, The current test setup seeds the
zustand store once via useFormStore.setState but never resets it between tests,
so state mutations leak; update the teardown to reset the store after each test
by adding a call to restore the default form state (either call
useFormStore.setState with the same default object used in this file or invoke
the shared seedFormStore() helper from test-utils.tsx) inside the existing
afterEach (in addition to vi.clearAllMocks()), referencing useFormStore and/or
seedFormStore so the store values (username, startDate, endDate, githubToken,
apiMode, etc.) are restored for each test run.

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.

4 participants