fix: resolve infinite re-render loop by optimizing Zustand selectors - #1248
Conversation
Apply the same ref + JSON.stringify pattern to prevent infinite re-renders caused by React Query returning new object references even when data is identical. This is an additional fix to address the remaining React error #185 (Maximum update depth exceeded) that was not fully resolved by the previous fix in useFinancial.ts and useDepositCalculations.ts. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Replace shallow with useShallow hook wrapper (Zustand v5 requirement) - Select primitives instead of arrays/objects where possible: - TurnkeyProvider: select credentialId string only - app/_layout: select hasSelectedUser boolean - useCardStatus: select userId string only - Fix optional chaining in useActivity.ts (transactionsRef.current?.withdraws) - Remove anti-pattern ref mutation in usePostSignupInit useMemo Root cause: Immer (used by Zustand persist) creates new object references on every state update, causing React to re-render components that select arrays/objects without proper equality functions.
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
Pull Request Review: Fix Infinite Re-render LoopSummaryThis PR addresses a critical performance issue caused by Zustand + Immer creating new object references on every state update, triggering unnecessary re-renders. The solution correctly migrates to useShallow and optimizes selectors to extract primitives instead of objects/arrays. Strengths1. Root Cause AnalysisThe PR description clearly identifies the problem: Immer (used by Zustand persist) creates new object references, breaking React shallow equality checks. This is excellent documentation. 2. Correct Use of useShallowAll migrations to useShallow are properly implemented:
3. Smart Stabilization Pattern in useActivity.tsThe ref-based stabilization using JSON comparison (lines 109-120) is a clever solution for unstable React Query references. This prevents infinite loops while maintaining data freshness. 4. Anti-pattern RemovalRemoving the ref mutation in usePostSignupInit.ts useMemo is correct - refs should not be mutated during render. Issues and Concerns1. CRITICAL: Verify Zustand v5 Migration CompleteThe PR mentions Zustand v5 requirement. Verify no other files still use the old v4 shallow import pattern:
Fixed in: useUser.ts, TurnkeyProvider.tsx, app/(protected)/_layout.tsx, app/signup/creating.tsx 2. Potential Bug: Optional Chaining Inconsistency (useActivity.ts:196)Implementation is inconsistent with optional chaining. If transactionsRef.current can be undefined, always use optional chaining consistently throughout. 3. Performance: JSON.stringify in Hot Path (useActivity.ts:113)Using JSON.stringify on potentially large transaction objects in a useMemo dependency can be expensive. Monitor performance with realistic data volumes. Consider using a hash function or deep-equal library if performance becomes an issue. 4. Test Coverage GapNo test changes accompany this critical performance fix. Recommended additions:
SecurityNo security concerns identified. Changes are performance optimizations without security implications. Performance ImpactExpected Improvements:
Potential Regressions:
RecommendationsHigh Priority:
Medium Priority: Low Priority: Final Verdict: Approve with Minor FixesThis is a well-researched and correctly implemented fix for a critical performance issue. The changes demonstrate strong understanding of React rendering, Zustand internals, and Immer behavior. Required before merge:
Recommended before merge:
Great work identifying and solving this subtle but impactful bug! |
Pull Request Review: Fix infinite re-render loop by optimizing Zustand selectorsOverviewThis PR addresses a critical performance issue - infinite re-render loops caused by unstable object/array references in Zustand selectors when using Immer with the persist middleware. The approach is solid and follows React best practices. ✅ Strengths1. Root Cause AnalysisThe PR description correctly identifies that Immer (used by Zustand persist) creates new object references on every state update, causing components selecting arrays/objects to re-render unnecessarily. This is a well-understood performance issue. 2. Correct Use of
|
Add useShallow wrapper to prevent infinite re-renders (React error #185) caused by Zustand returning new object references on every state update. Fixed components: - Modal providers (8 files): currentModal, transaction objects - Token selectors (4 files): selectedToken object - BankTransfer components (7 instances): kyc, bankTransfer objects - Hooks: useSendOption, useWithdrawOption with multiple objects - Other: country_selection, CheckConnectionWrapper, DepositDirectly
PR Review: Fix infinite re-render loop by optimizing Zustand selectorsSummaryThis PR addresses React Error #185 (Maximum update depth exceeded) by optimizing Zustand selectors to prevent unnecessary re-renders caused by Immer creating new object references on every state update. The changes are well-targeted and follow best practices for Zustand v5. ✅ Code Quality & Best PracticesStrengths:
Suggestions for improvement:
🐛 Potential IssuesMinor concerns:
⚡ Performance ConsiderationsExcellent improvements:
Suggestions:
🔒 Security ConcernsNo significant security issues identified. The changes are purely performance-related and don't introduce new attack vectors. Minor note:
🧪 Test CoverageCritical gap identified:
📋 Additional Recommendations
✨ SummaryVerdict: ✅ Approve with minor suggestions This is a high-quality PR that systematically addresses a critical performance issue. The changes are:
Before merging:
Estimated impact:
Great work on identifying and systematically fixing this issue! 🎉 Files reviewed: 36 files changed (+389/-112) |
…nders Wrap all Zustand store object destructuring with useShallow to fix React error #185 (infinite re-render loops). When destructuring like const { a, b } = useStore(), a new object reference is created on every state change, causing components to re-render infinitely. Changes: - Add useShallow wrapper to ~55 files (components, hooks, app routes) - Simplify single-property selectors to direct selectors - Add ESLint rule to enforce useShallow for object-returning selectors - Add useRenderMonitor hook for production re-render detection - Export selector functions from useUserStore for reusability
PR Review: Fix Infinite Re-render Loop by Optimizing Zustand SelectorsOverviewThis PR addresses a critical performance issue (React error #185 - infinite re-render loops) by properly implementing Zustand v5's ✅ Strengths1. Correct Root Cause Analysis
2. Comprehensive Coverage
3. Performance Monitoring
4. ESLint Rule Addition
5. Selector Function Exports
🔍 Issues & ConcernsCRITICAL: useRenderMonitor Implementation BugLocation: if (isActive) {
renderCountRef.current += 1;
}Issue: Mutating refs during render phase is an anti-pattern and violates React's rules. While refs are mutable, incrementing during render can cause issues with:
Fix: Move the increment to useEffect(() => {
if (!isActive) return;
renderCountRef.current += 1;
});SECURITY: Missing Input ValidationLocation: The export const useRenderMonitor = ({
componentName,
warnThreshold = 10,
trackInDev = false,
}: RenderMonitorOptions) => {
// Add validation
if (!componentName || typeof componentName !== 'string') {
console.error('[useRenderMonitor] Invalid componentName');
return;
}MODERATE: ESLint Rule LimitationsLocation: The current ESLint rule only catches inline object expressions: selector: 'CallExpression[callee.name=/^use.*Store$/] > ArrowFunctionExpression > ObjectExpression'Misses:
Recommendation: Consider adding additional rules or document these edge cases. MODERATE: Incomplete MigrationLocation: const userEventsFromStore = useActivityStore(
useShallow(state => (user?.userId ? state.events[user.userId] : undefined)),Issue: Selecting arrays with Question: Should this follow the same pattern as in the commit MINOR: Inconsistent Pattern UsageSome files use primitives (optimal): // ✅ Good - app/_layout.tsx:34-35
const { usersCount, _hasHydrated } = useUserStore(
useShallow(state => ({ usersCount: state.users.length, _hasHydrated: state._hasHydrated })),
);Others use objects even for single properties: // ⚠️ Could be simpler - components/coins/[id].tsx:44-50
const { selectedTime, selectedPrice, selectedPriceChange } = useCoinStore(
useShallow(state => ({
selectedTime: state.selectedTime,
selectedPrice: state.selectedPrice,
selectedPriceChange: state.selectedPriceChange,
})),
);Recommendation: Document when to use useShallow vs direct selection in a style guide. MINOR: usePostSignupInit RefactorLocation: Good removal of the useMemo anti-pattern! However: useEffect(() => {
if (!user || user.userId === lastUserId.current) return;
lastUserId.current = user.userId;
// ...
}, [user?.userId]);Issue: The dependency array Consider: Moving ref update to end of async operation or using a proper loading flag. 🧪 Testing ConcernsMissing Tests
Recommendation: Add tests for:
Manual Testing
📊 Performance ConsiderationsPositive Impact
Potential Concerns
🔒 Security AssessmentLow Risk
Considerations
📝 Best PracticesFollows:
Could Improve:
📋 RecommendationsBefore Merge (Required):
Before Merge (Nice to Have):
Post-Merge:
🎯 Overall AssessmentRecommendation: APPROVE with minor changes required This is a well-executed fix for a critical performance issue. The systematic approach, monitoring addition, and ESLint rule make this a solid foundation. The Impact: 🟢 High positive impact - Fixes critical UX issue Great work on tackling this complex refactor! The addition of proactive monitoring shows excellent engineering judgment. 🚀 Generated with Claude Code |
Root cause: Immer (used by Zustand persist) creates new object references
on every state update, causing React to re-render components that select
arrays/objects without proper equality functions.