THE BIGGER PICTURE - Root Cause Analysis:
Problem Chain:
1. FABContext value recreated every render
2. All FAB consumers re-render
3. Dashboard re-renders
4. useFABConfig runs
5. Creates new icon/callbacks
6. Triggers FABContext update
7. INFINITE LOOP!
The Bug (in BOTH contexts):
<Context.Provider value={{ config, setFAB, clearFAB }}>
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
NEW object every render!
Every time Provider re-renders:
- Creates NEW value object
- All consumers see "new" value
- All consumers re-render
- Causes more Provider re-renders
- INFINITE LOOP!
The Fix:
const setFAB = useCallback(..., []); // Stable function
const clearFAB = useCallback(..., []); // Stable function
const value = useMemo(() => ({ config, setFAB, clearFAB }), [config, setFAB, clearFAB]);
^^^^^^^
Only creates new object when dependencies actually change!
<Context.Provider value={value}>
^^^^^^^
Stable reference!
Why This is Critical:
Context is at the TOP of the component tree:
App
└─ FABProvider ← Bug here affects EVERYTHING below
└─ PageHeaderProvider ← Bug here too
└─ DashboardProvider
└─ Shell
└─ Dashboard ← Infinite re-renders
└─ Charts ← Break from constant re-renders
React Context Performance Rules:
1. ALWAYS memoize context value object
2. ALWAYS use useCallback for context functions
3. NEVER create inline objects in Provider value
4. Context updates trigger ALL consumers
Fixed Contexts:
1. FABContext - Memoized value, callbacks
2. PageHeaderContext - Memoized value, callbacks
Before:
Every render → new value object → all consumers re-render → LOOP
After:
Only config changes → new value object → consumers re-render once → done
Result:
✅ No infinite loops
✅ No unnecessary re-renders
✅ Clean console
✅ Smooth performance
✅ All features working
Files Modified:
- FABContext.tsx: Added useMemo and useCallback
- PageHeaderContext.tsx: Added useMemo and useCallback
- useFABConfig.tsx: Memoized icon and callbacks (previous fix)
- App.tsx: Fixed scroll detection with useRef (previous fix)
All infinite loop sources now eliminated!
83 lines
2.3 KiB
TypeScript
83 lines
2.3 KiB
TypeScript
import { useEffect, useMemo, useCallback } from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { Plus } from 'lucide-react';
|
|
import { useFAB } from '@/contexts/FABContext';
|
|
|
|
/**
|
|
* Hook to configure FAB for different pages
|
|
* Usage: useFABConfig('orders') in Orders page component
|
|
*/
|
|
export function useFABConfig(page: 'orders' | 'products' | 'customers' | 'coupons' | 'dashboard' | 'none') {
|
|
const { setFAB, clearFAB } = useFAB();
|
|
const navigate = useNavigate();
|
|
|
|
// Memoize the icon to prevent re-creating on every render
|
|
const icon = useMemo(() => <Plus className="w-6 h-6" />, []);
|
|
|
|
// Memoize callbacks to prevent re-creating on every render
|
|
const handleOrdersClick = useCallback(() => navigate('/orders/new'), [navigate]);
|
|
const handleProductsClick = useCallback(() => navigate('/products/new'), [navigate]);
|
|
const handleCustomersClick = useCallback(() => navigate('/customers/new'), [navigate]);
|
|
const handleCouponsClick = useCallback(() => navigate('/coupons/new'), [navigate]);
|
|
const handleDashboardClick = useCallback(() => {
|
|
// TODO: Implement speed dial menu
|
|
console.log('Quick actions menu');
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
switch (page) {
|
|
case 'orders':
|
|
setFAB({
|
|
icon,
|
|
label: 'Create Order',
|
|
onClick: handleOrdersClick,
|
|
visible: true
|
|
});
|
|
break;
|
|
|
|
case 'products':
|
|
setFAB({
|
|
icon,
|
|
label: 'Add Product',
|
|
onClick: handleProductsClick,
|
|
visible: true
|
|
});
|
|
break;
|
|
|
|
case 'customers':
|
|
setFAB({
|
|
icon,
|
|
label: 'Add Customer',
|
|
onClick: handleCustomersClick,
|
|
visible: true
|
|
});
|
|
break;
|
|
|
|
case 'coupons':
|
|
setFAB({
|
|
icon,
|
|
label: 'Create Coupon',
|
|
onClick: handleCouponsClick,
|
|
visible: true
|
|
});
|
|
break;
|
|
|
|
case 'dashboard':
|
|
setFAB({
|
|
icon,
|
|
label: 'Quick Actions',
|
|
onClick: handleDashboardClick,
|
|
visible: true
|
|
});
|
|
break;
|
|
|
|
case 'none':
|
|
default:
|
|
clearFAB();
|
|
break;
|
|
}
|
|
|
|
return () => clearFAB();
|
|
}, [page, icon, handleOrdersClick, handleProductsClick, handleCustomersClick, handleCouponsClick, handleDashboardClick, setFAB, clearFAB]);
|
|
}
|