chore(eslint): fix @eslint-react/purity warning — move new Date() to useMemo (#1471)#1640
chore(eslint): fix @eslint-react/purity warning — move new Date() to useMemo (#1471)#1640steilerDev wants to merge 1 commit into
Conversation
…o useEffect (#1471) Fixes #1471 Part of #1455 Move `new Date()` call in CriticalPathCard from the render body into a `useMemo` hook. The @eslint-react/purity rule flags date construction during render as an impure operation since `new Date()` is non-deterministic (it reads the system clock). Structural change: hooks must be called unconditionally before early returns, so the component's derived state computation (incompleteCritical, nextItem, deadline) is now hoisted above the early returns, and the two guard returns check `criticalItems.length === 0` and `!nextItem` respectively. Co-Authored-By: Claude frontend-developer (Haiku 4.5) <noreply@anthropic.com>
|
[frontend-developer PR Review] Verdict: ✅ APPROVEDReact Hook Rules ✅
Dependency Array ✅
Functional Equivalence ✅
Early Returns & Hook Discipline ✅All derived state hoisted unconditionally above guard checks:
Guard condition correctly updated: Performance & Re-renders ✅
Code Quality
Ready to merge. No concerns. |
steilerDev
left a comment
There was a problem hiding this comment.
[ux-designer]
Design Review — Visual Parity Verification ✅
This is a logic-only refactor. I performed a line-by-line comparison of the rendered JSX tree (before and after) and verified that the visual output is completely unchanged.
Structural / JSX analysis
The entire return tree (both early-return paths and the full card) is byte-for-byte identical between main and the PR branch. Only the position and wrapping of the computation logic changed.
| Concern | Finding |
|---|---|
daysRemaining value |
Identical computation — same arithmetic, same Math.ceil, same midnight normalisation. Wrapped in useMemo([deadline]) instead of an inline if block; result is the same. |
incompleteCritical / nextItem logic |
Moved above the first early-return; semantically equivalent. Old code only reached this block when criticalItems.length > 0, and the new code computes it unconditionally but the result is unused if the early return fires — no observable difference. |
nextItem null safety |
Old: incompleteCritical[0]! (non-null assertion after length guard). New: incompleteCritical[0] ?? null. The !nextItem guard that follows is equivalent to the old incompleteCritical.length === 0 guard. |
deadline type change |
Old: string | undefined (from nextItem.endDate). New: string | null (via ?? null). formatDate(deadline) handles both null and undefined identically (returns '—') — verified in formatters.test.ts. |
daysRemaining when no deadline |
Old: initialised to 0, stays 0 if no deadline. New: useMemo returns 0 when !deadline. Same value. |
| Health badge colours / labels | Driven entirely by the same daysRemaining thresholds (< 0, < 7, <= 14). Unchanged. |
| Dark mode / responsive behaviour | No CSS, no style props, no class names were modified. Dark mode and responsive behaviour are entirely unaffected. |
| Hooks-before-returns rule | The refactor correctly moves useMemo above both early returns, fixing the ESLint @eslint-react/purity violation without altering rendered output. |
Verdict
All rendered paths produce identical output. The refactor is a correct fix for the hooks-before-early-returns rule. APPROVED (self-authored PR — comment used instead of formal approve).
steilerDev
left a comment
There was a problem hiding this comment.
[dev-team-lead]
Code Review — CriticalPathCard.tsx ESLint purity fix
VERDICT: APPROVED ✅
Summary: This is a clean, correct structural refactor. The new Date() call is properly wrapped in useMemo and all derived state is hoisted above early returns to satisfy Rules of Hooks. Functional behaviour is preserved.
Hooks / Rules of Hooks ✅
useMemois now called unconditionally before any early returns — Rules of Hooks satisfied.@eslint-react/puritywarning resolved:new Date()no longer exists in the raw render body.
Functional Equivalence ✅
- Empty critical path (
criticalItems.length === 0): hoisted filter/sort on an empty array returns[],nextItembecomesnull,deadlinebecomesnull,useMemoshort-circuits to0— then the early return fires. Identical output. - All completed (
!nextItem):incompleteCritical[0] ?? nullgivesnullexactly when the originalincompleteCritical.length === 0check was true — equivalent guard. - Normal render path: computation of
daysRemainingis byte-for-byte identical to the original logic; only its placement anduseMemowrapper differ.
useMemo dependency array ✅
[deadline]is correct and complete.todayis computed inside the memo body (no stale closure risk).deadlineis astring | nullderived from props — it changes exactly when a recompute is needed.
Test compatibility ✅
- Existing tests use
jest.setSystemTime()with fake timers.new Date()insideuseMemocorrectly picks up the mocked system time — no test changes needed and the test file is untouched.
No regressions or new warnings identified.
steilerDev
left a comment
There was a problem hiding this comment.
[product-architect] Architecture review — APPROVED ✅ (posted as comment because GitHub blocks self-approval on this PR)
Scope is narrowly focused on the @eslint-react/purity warning in a single component, with no schema, API contract, or cross-cutting impact.
Findings
Hook correctness ✅
useMemois hoisted above both guard early returns (criticalItems.length === 0and!nextItem), so all hooks are called unconditionally on every render. Rules of Hooks honored.- Dependency array
[deadline]is correct — it's the sole reactive input to the computation.todayis intentionally recomputed inside the memo body and only stabilizes whendeadlinechanges, which is acceptable for a "days until deadline" display that doesn't need to tick in real time.
Behavioral parity ✅
incompleteCritical[0] ?? null+if (!nextItem)cleanly replaces the post-guard!non-null assertion that the previous code needed. Same semantics, less unsafe.daysRemainingdefaults to0whendeadlineis null inside the memo, matching the originallet daysRemaining = 0initialization. Health-class branching downstream is unchanged.
Pattern consistency ✅
client/src/components/GanttChart/GanttChart.tsx:183already usesconst today = useMemo(() => new Date(), [])— this PR extends an established pattern rather than inventing a new one.- Sibling timeline cards (
UpcomingMilestonesCard,WorkItemProgressCard) don't constructDateobjects in the render body, so there's no drift to harmonize.
Wiki / ADR — none required. This is internal refactoring of a single component; no architectural decision is being made or changed.
Informational (not blocking)
criticalItemson line 18 is still recomputed each render. It's a pure derivation so not a purity-rule violation, and out of scope for this PR, but worth noting if this card ever shows up in profiling.
CI signal: Static Analysis and all 6 test shards green. No concerns from an architecture standpoint — safe to merge once other required reviews land.
|
[orchestrator] Consolidated
Informational (non-blocking)
Status
|
Summary
Fixes #1471
Part of #1455
Problem
The
@eslint-react/purityrule was flaggingnew Date()called directly in the render body ofCriticalPathCard. Date construction vianew Date()is non-deterministic (reads the system clock) and is considered an impure operation in a React component's render phase.Fix
Moved the
new Date()call (and surrounding date arithmetic) into auseMemohook inCriticalPathCard:Structural Change
Because React hooks must be called unconditionally before any early returns, the component was restructured to hoist all derived state computation (
incompleteCritical,nextItem,deadline) and theuseMemoabove the two guard early-return paths. Functional behaviour is identical.Verification
Confirmed 0
@eslint-react/purityviolations across allclient/srcfiles after the fix.Files Changed
client/src/components/TimelineStatusCards/CriticalPathCard.tsx— hoist hooks, wrapnew Date()inuseMemo