Skip to content

fix(frontend): resolve set-state-in-effect in app search page (#1063)#1078

Merged
ericsocrat merged 1 commit intomainfrom
chore/react-compiler-search-page
May 1, 2026
Merged

fix(frontend): resolve set-state-in-effect in app search page (#1063)#1078
ericsocrat merged 1 commit intomainfrom
chore/react-compiler-search-page

Conversation

@ericsocrat
Copy link
Copy Markdown
Owner

Resolves 3 react-hooks/set-state-in-effect violations in src/app/app/search/page.tsx (Phase 2 of #1063).

Changes

  • Mount-load effect → lazy useState initializers for recentSearches, showAvoided, viewMode. The helpers are already SSR-safe via globalThis.localStorage guards (return [] / false / 'grid' when unavailable).
  • debouncedQuery sync effect → Pattern A render-phase tracker with composite submitKey of debouncedQuery + filters.
  • page reset effect → Pattern A render-phase tracker with composite pageResetKey of submittedQuery + filters.

Validation

  • npx vitest run src/app/app/search/page.test.tsx → 54/54 pass
  • npx eslint src/app/app/search/page.tsx → clean (no set-state-in-effect)
  • npx tsc --noEmit → clean

Phase 2 Progress

Final non-deferred multi-violation file. Phase 2 cumulative after this PR merges: 15/19.

Remaining 4 violations are deferred / require separate authorization (complex async/SSR cases — see CURRENT_STATE.md follow-ups).

Cross-references

Resolves 3 react-hooks/set-state-in-effect violations in src/app/app/search/page.tsx (Phase 2 of #1063):

- Mount-load effect: replaced with lazy useState initializers for recentSearches, showAvoided, viewMode (helpers already SSR-safe via globalThis.localStorage guards)

- debouncedQuery sync effect: replaced with Pattern A render-phase tracker (composite submitKey of debouncedQuery + filters)

- page reset effect: replaced with Pattern A render-phase tracker (composite pageResetKey of submittedQuery + filters)

Validated: 54/54 vitest pass, eslint clean, tsc clean. Phase 2 cumulative: 15/19 (final non-deferred multi-violation file).

Cross-refs: #1067 (Phase 1), #1069-#1077 (Phase 2 micro-PRs).
Copilot AI review requested due to automatic review settings May 1, 2026 12:01
@vercel
Copy link
Copy Markdown

vercel Bot commented May 1, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
tryvit Ready Ready Preview, Comment May 1, 2026 0:03am

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 1, 2026

📸 PR Screenshots

Captured 2 screenshots (1 mobile, 1 desktop) for 1 changed page(s):

  • search

📥 Download screenshots from workflow artifacts.

Captured by PR Screenshots workflow • 2026-05-01

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 1, 2026

Bundle Size Report

Metric Value
Main baseline 0 KB
This PR 0 KB
Delta +0 KB (+0%)
JS chunks 0
Hard limit 4000 KB

✅ Bundle size is within acceptable limits.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7c59b6c221

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

const [filters, setFilters] = useState<SearchFilters>({});
const [page, setPage] = useState(1);
const [showAvoided, setShowAvoided] = useState(false);
const [showAvoided, setShowAvoided] = useState(getShowAvoided);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Avoid localStorage-backed initial state during first render

Initializing state from getShowAvoided/getViewMode/getRecentSearches in render causes server/client divergence on this Next.js page: server prerender sees no localStorage and renders defaults, while the browser's first render may immediately use persisted values, producing different HTML and hydration warnings or forced client re-render. This regression is introduced by replacing the mount useEffect with lazy initializers, and it affects any user with saved search preferences/history.

Useful? React with 👍 / 👎.

@ericsocrat ericsocrat merged commit 5a13041 into main May 1, 2026
20 checks passed
@ericsocrat ericsocrat deleted the chore/react-compiler-search-page branch May 1, 2026 12:05
Copy link
Copy Markdown

Copilot AI left a comment

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 updates the frontend search page to eliminate react-hooks/set-state-in-effect lint warnings by replacing mount/effect-driven state synchronization with lazy useState initialization and render-phase “adjust state during render” trackers (per the React guidance used elsewhere in the codebase).

Changes:

  • Load recentSearches, showAvoided, and viewMode via lazy useState initializers instead of a mount useEffect.
  • Replace the debouncedQuerysubmittedQuery sync effect with a render-phase tracker keyed off debouncedQuery + filters.
  • Replace the “reset page on query/filter change” effect with a render-phase tracker keyed off submittedQuery + filters.

Comment on lines +107 to +116
const [showAvoided, setShowAvoided] = useState(getShowAvoided);
const [showAutocomplete, setShowAutocomplete] = useState(false);
const [showFilters, setShowFilters] = useState(false);
const [showSaveDialog, setShowSaveDialog] = useState(false);
const [autocompleteActiveId, setAutocompleteActiveId] = useState<
string | undefined
>(undefined);
const [recentSearches, setRecentSearches] = useState<string[]>([]);
const [viewMode, setViewMode] = useState<ViewMode>("grid");
const [recentSearches, setRecentSearches] =
useState<string[]>(getRecentSearches);
const [viewMode, setViewMode] = useState<ViewMode>(getViewMode);
Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

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

getShowAvoided/getViewMode are now invoked during render via lazy useState initializers. Unlike getRecentSearches (which wraps localStorage access in try/catch), these helpers call globalThis.localStorage.getItem directly; in some browser/privacy contexts localStorage access can throw and would now crash initial render. Consider wrapping the get/set helpers in try/catch (and returning defaults on failure) so render stays resilient.

Copilot uses AI. Check for mistakes.
Comment on lines +127 to 131
const submitKey = `${debouncedQuery}|${JSON.stringify(filters)}`;
const [prevSubmitKey, setPrevSubmitKey] = useState(submitKey);
if (submitKey !== prevSubmitKey) {
setPrevSubmitKey(submitKey);
const trimmed = debouncedQuery.trim();
Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

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

submitKey is built via string concatenation with | plus JSON.stringify(filters). Since debouncedQuery is user-entered, it can contain |, which can create key collisions (different query+filters pairs producing the same key) and incorrectly skip the tracker update. Prefer a structured/unambiguous key (e.g., stringify a tuple/object) and also avoid duplicating JSON.stringify(filters) work for both trackers (compute once/memoize).

Copilot uses AI. Check for mistakes.
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