Skip to content

Commit 21d3993

Browse files
m4dm4rtig4nClément VALENTINclaude
authored
fix(simulator): resolve React hooks violations (#61)
* fix(simulator): remove competing setFetchDataFunction calls Remove useEffect hooks that were registering handleSimulation in the global dataFetchStore, which conflicted with PageHeader's unified fetch registration. This fixes React error #310 (infinite loop). The Simulator page now uses its own local state and button for triggering simulations instead of conflicting with the global store. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> * fix(simulator): move hooks before early returns Fix React error #310 and "rendered more hooks than before" by moving all hooks (useEffect, useMemo) before conditional early returns. This ensures hooks are called in the same order every render, respecting React's rules of hooks. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> * fix(web): regenerate package-lock.json for CI Restore complete package-lock.json to fix npm ci sync error in CI. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> --------- Co-authored-by: Clément VALENTIN <[email protected]> Co-authored-by: Claude <[email protected]>
1 parent 3d5809f commit 21d3993

File tree

2 files changed

+150
-165
lines changed

2 files changed

+150
-165
lines changed

.claude/commands/sync.md

Lines changed: 23 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,41 @@
11
---
22
description: Synchronise le worktree avec le projet root
3-
allowed-tools: Bash(git:*), Bash(rsync:*), Bash(ls:*), Bash(pwd:*)
3+
allowed-tools: Bash(rsync:*), Bash(ls:*), Bash(pwd:*)
44
---
55

66
# Objectif
77

8-
Synchroniser le worktree Conductor en cours avec le projet root (`/Users/cvalentin/Git/myelectricaldata_new`) pour récupérer les dernières modifications.
8+
Copier les fichiers modifiés du worktree Conductor vers le projet root (`/Users/cvalentin/Git/myelectricaldata_new`) pour tester en live.
99

1010
## Workflow de synchronisation
1111

12-
### 1. Vérifier l'état du worktree actuel
12+
### 1. Copier les fichiers vers le projet root
1313

14-
```bash
15-
git status
16-
```
17-
18-
S'il y a des modifications locales non commitées, avertir l'utilisateur et demander s'il veut continuer (les modifications locales pourraient être écrasées).
19-
20-
### 2. Récupérer les derniers changements depuis main
14+
Utiliser rsync pour copier les fichiers modifiés (en excluant les dossiers git, node_modules, etc.) :
2115

2216
```bash
23-
# Fetch depuis origin
24-
git fetch origin main
25-
26-
# Rebase sur origin/main pour récupérer les dernières modifications
27-
git rebase origin/main
17+
rsync -av --progress \
18+
--exclude='.git' \
19+
--exclude='node_modules' \
20+
--exclude='.conductor' \
21+
--exclude='dist' \
22+
--exclude='build' \
23+
--exclude='.venv' \
24+
--exclude='__pycache__' \
25+
--exclude='.pytest_cache' \
26+
--exclude='.mypy_cache' \
27+
--exclude='*.pyc' \
28+
/Users/cvalentin/Git/myelectricaldata_new/.conductor/santo/ \
29+
/Users/cvalentin/Git/myelectricaldata_new/
2830
```
2931

30-
### 3. En cas de conflit
32+
### 2. Confirmer la synchronisation
3133

32-
Si le rebase échoue à cause de conflits :
33-
1. Lister les fichiers en conflit avec `git status`
34-
2. Informer l'utilisateur des conflits
35-
3. Proposer soit de résoudre les conflits, soit d'abandonner avec `git rebase --abort`
36-
37-
### 4. Vérification finale
38-
39-
Après la synchronisation :
40-
```bash
41-
# Afficher les derniers commits pour confirmer la sync
42-
git log --oneline -5
43-
44-
# Afficher l'état actuel
45-
git status
46-
```
34+
Afficher les fichiers copiés et informer l'utilisateur que la synchronisation est terminée.
4735

4836
## Notes importantes
4937

50-
- Cette commande utilise `git rebase` pour garder un historique linéaire
51-
- Les modifications locales non commitées peuvent être écrasées
52-
- En cas d'erreur, utiliser `git rebase --abort` pour revenir à l'état précédent
53-
- Le worktree reste sur sa branche actuelle, seuls les commits de main sont intégrés
38+
- Cette commande copie les fichiers du worktree vers le projet root
39+
- Les dossiers `node_modules`, `.git`, `dist`, etc. sont exclus
40+
- Le projet root doit avoir ses services en cours d'exécution pour voir les changements (hot-reload)
41+
- Pour annuler, utiliser git dans le projet root : `git checkout -- .`

apps/web/src/pages/Simulator.tsx

Lines changed: 127 additions & 130 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import { logger } from '@/utils/logger'
1313
import { ModernButton } from './Simulator/components/ModernButton'
1414
import { useIsDemo } from '@/hooks/useIsDemo'
1515
import { usePdlStore } from '@/stores/pdlStore'
16-
import { useDataFetchStore } from '@/stores/dataFetchStore'
1716

1817
// Helper function to check if a date is weekend (Saturday or Sunday)
1918
function isWeekend(dateString: string): boolean {
@@ -180,8 +179,9 @@ export default function Simulator() {
180179
const [isInitializing, setIsInitializing] = useState(true)
181180
// const [isClearingCache, setIsClearingCache] = useState(false) // Unused for now
182181

183-
// Register fetch function in store for PageHeader button
184-
const { setFetchDataFunction, setIsLoading } = useDataFetchStore()
182+
// Note: We DO NOT register handleSimulation in the global store
183+
// because PageHeader already handles the unified fetch function.
184+
// The Simulator uses its own button to trigger simulation.
185185

186186
// Filters and sorting state
187187
const [filterType, setFilterType] = useState<string>('all')
@@ -361,17 +361,6 @@ export default function Simulator() {
361361
}
362362
}, [selectedPdl, pdlsData, offersData, providersData, queryClient])
363363

364-
// Register fetch function in store for PageHeader button
365-
useEffect(() => {
366-
setFetchDataFunction(handleSimulation)
367-
return () => setFetchDataFunction(null)
368-
}, [handleSimulation, setFetchDataFunction])
369-
370-
// Sync loading state with store
371-
useEffect(() => {
372-
setIsLoading(isSimulating)
373-
}, [isSimulating, setIsLoading])
374-
375364
const calculateSimulationsForAllOffers = (consumptionData: any[], offers: EnergyOffer[], providers: EnergyProvider[], tempoColors: TempoDay[], pdl?: PDL) => {
376365
// Create a map of date -> TEMPO color for fast lookup
377366
const tempoColorMap = new Map<string, 'BLUE' | 'WHITE' | 'RED'>()
@@ -773,6 +762,128 @@ export default function Simulator() {
773762
return results
774763
}
775764

765+
// Auto-launch simulation if cache data exists
766+
// IMPORTANT: This hook must be before any early returns to respect React's rules of hooks
767+
useEffect(() => {
768+
logger.log('[Auto-launch] useEffect triggered', {
769+
selectedPdl,
770+
isSimulating,
771+
hasSimulationResult: !!simulationResult,
772+
hasAutoLaunched,
773+
isDemo,
774+
pdlsDataLoaded: !!pdlsData,
775+
offersDataLoaded: !!offersData,
776+
providersDataLoaded: !!providersData,
777+
})
778+
779+
// Don't auto-launch if already launched, simulating, or have results
780+
if (!selectedPdl || isSimulating || simulationResult || hasAutoLaunched) {
781+
logger.log('[Auto-launch] Skipping auto-launch due to conditions')
782+
return
783+
}
784+
785+
// Don't auto-launch if PDL data, offers, or providers are not loaded yet
786+
if (!pdlsData || !Array.isArray(offersData) || offersData.length === 0 || !providersData) {
787+
logger.log('[Auto-launch] Skipping auto-launch - data not loaded yet', {
788+
pdlsData: !!pdlsData,
789+
offersData: Array.isArray(offersData) ? offersData.length : 'not array',
790+
providersData: !!providersData
791+
})
792+
return
793+
}
794+
795+
// Check if we have cached data for this PDL (uses new single cache key format)
796+
const cachedData = queryClient.getQueryData(['consumptionDetail', selectedPdl]) as any
797+
798+
if (!cachedData?.data?.meter_reading?.interval_reading?.length) {
799+
logger.log('[Auto-launch] ❌ No cached data found for PDL:', selectedPdl)
800+
return
801+
}
802+
803+
const readings = cachedData.data.meter_reading.interval_reading
804+
const totalPoints = readings.length
805+
806+
// Check if we have enough data (at least 30 days worth = ~1440 points at 30min intervals)
807+
const minRequiredPoints = 30 * 48 // 30 days * 48 half-hours
808+
const hasEnoughData = totalPoints >= minRequiredPoints
809+
810+
logger.log(`[Auto-launch] Cache check: ${totalPoints} points (need ${minRequiredPoints} minimum)`)
811+
812+
if (hasEnoughData) {
813+
logger.log(`✅ Auto-launching simulation with ${totalPoints} cached points`)
814+
setHasAutoLaunched(true)
815+
// Show loading overlay while preparing simulation
816+
setIsInitialLoadingFromCache(true)
817+
818+
// Use setTimeout to avoid calling during render
819+
setTimeout(() => {
820+
handleSimulation()
821+
}, 100)
822+
} else {
823+
logger.log(`❌ Not enough cached data (${totalPoints} points), skipping auto-launch`)
824+
}
825+
}, [selectedPdl, isSimulating, simulationResult, hasAutoLaunched, isDemo, pdlsData, offersData, providersData, queryClient, handleSimulation])
826+
827+
// Filter and sort simulation results
828+
// IMPORTANT: This hook must be before any early returns to respect React's rules of hooks
829+
const filteredAndSortedResults = useMemo(() => {
830+
if (!simulationResult || !Array.isArray(simulationResult)) return []
831+
832+
let filtered = [...simulationResult]
833+
834+
// Filter by type
835+
if (filterType !== 'all') {
836+
filtered = filtered.filter((result) => result.offerType === filterType)
837+
}
838+
839+
// Filter by provider
840+
if (filterProvider !== 'all') {
841+
filtered = filtered.filter((result) => result.providerName === filterProvider)
842+
}
843+
844+
// Filter by recency (tariffs < 6 months old)
845+
if (showOnlyRecent) {
846+
filtered = filtered.filter((result) => !isOldTariff(result.validFrom))
847+
}
848+
849+
// Sort by selected criteria
850+
filtered.sort((a, b) => {
851+
let comparison = 0
852+
switch (sortBy) {
853+
case 'subscription':
854+
comparison = a.subscriptionCost - b.subscriptionCost
855+
break
856+
case 'energy':
857+
comparison = a.energyCost - b.energyCost
858+
break
859+
case 'total':
860+
default:
861+
comparison = a.totalCost - b.totalCost
862+
break
863+
}
864+
return sortOrder === 'asc' ? comparison : -comparison
865+
})
866+
867+
return filtered
868+
}, [simulationResult, filterType, filterProvider, showOnlyRecent, sortBy, sortOrder])
869+
870+
// Get unique providers and types for filter options
871+
// IMPORTANT: These hooks must be before any early returns to respect React's rules of hooks
872+
const availableProviders = useMemo(() => {
873+
if (!simulationResult || !Array.isArray(simulationResult)) return []
874+
const providers = new Set(simulationResult.map((r) => r.providerName))
875+
return Array.from(providers).sort()
876+
}, [simulationResult])
877+
878+
const availableTypes = useMemo(() => {
879+
if (!simulationResult || !Array.isArray(simulationResult)) return []
880+
const types = new Set(simulationResult.map((r) => r.offerType))
881+
return Array.from(types).sort()
882+
}, [simulationResult])
883+
884+
// ==================== EARLY RETURNS (after all hooks) ====================
885+
// These must come AFTER all hooks to respect React's rules of hooks
886+
776887
if (pdlsLoading) {
777888
return (
778889
<div className="flex items-center justify-center py-12">
@@ -814,6 +925,8 @@ export default function Simulator() {
814925
)
815926
}
816927

928+
// ==================== HELPER FUNCTIONS (after early returns) ====================
929+
817930
const getTypeColor = (type: string) => {
818931
switch (type) {
819932
case 'BASE':
@@ -862,67 +975,6 @@ export default function Simulator() {
862975
}
863976
}
864977

865-
// Auto-launch simulation if cache data exists
866-
useEffect(() => {
867-
logger.log('[Auto-launch] useEffect triggered', {
868-
selectedPdl,
869-
isSimulating,
870-
hasSimulationResult: !!simulationResult,
871-
hasAutoLaunched,
872-
isDemo,
873-
pdlsDataLoaded: !!pdlsData,
874-
offersDataLoaded: !!offersData,
875-
providersDataLoaded: !!providersData,
876-
})
877-
878-
// Don't auto-launch if already launched, simulating, or have results
879-
if (!selectedPdl || isSimulating || simulationResult || hasAutoLaunched) {
880-
logger.log('[Auto-launch] Skipping auto-launch due to conditions')
881-
return
882-
}
883-
884-
// Don't auto-launch if PDL data, offers, or providers are not loaded yet
885-
if (!pdlsData || !Array.isArray(offersData) || offersData.length === 0 || !providersData) {
886-
logger.log('[Auto-launch] Skipping auto-launch - data not loaded yet', {
887-
pdlsData: !!pdlsData,
888-
offersData: Array.isArray(offersData) ? offersData.length : 'not array',
889-
providersData: !!providersData
890-
})
891-
return
892-
}
893-
894-
// Check if we have cached data for this PDL (uses new single cache key format)
895-
const cachedData = queryClient.getQueryData(['consumptionDetail', selectedPdl]) as any
896-
897-
if (!cachedData?.data?.meter_reading?.interval_reading?.length) {
898-
logger.log('[Auto-launch] ❌ No cached data found for PDL:', selectedPdl)
899-
return
900-
}
901-
902-
const readings = cachedData.data.meter_reading.interval_reading
903-
const totalPoints = readings.length
904-
905-
// Check if we have enough data (at least 30 days worth = ~1440 points at 30min intervals)
906-
const minRequiredPoints = 30 * 48 // 30 days * 48 half-hours
907-
const hasEnoughData = totalPoints >= minRequiredPoints
908-
909-
logger.log(`[Auto-launch] Cache check: ${totalPoints} points (need ${minRequiredPoints} minimum)`)
910-
911-
if (hasEnoughData) {
912-
logger.log(`✅ Auto-launching simulation with ${totalPoints} cached points`)
913-
setHasAutoLaunched(true)
914-
// Show loading overlay while preparing simulation
915-
setIsInitialLoadingFromCache(true)
916-
917-
// Use setTimeout to avoid calling during render
918-
setTimeout(() => {
919-
handleSimulation()
920-
}, 100)
921-
} else {
922-
logger.log(`❌ Not enough cached data (${totalPoints} points), skipping auto-launch`)
923-
}
924-
}, [selectedPdl, isSimulating, simulationResult, hasAutoLaunched, isDemo, pdlsData, offersData, providersData, queryClient, handleSimulation])
925-
926978
const toggleRowExpansion = (offerId: string) => {
927979
setExpandedRows((prev) => {
928980
const newSet = new Set(prev)
@@ -953,61 +1005,6 @@ export default function Simulator() {
9531005
return sortOrder === 'asc' ? <ArrowUp size={14} /> : <ArrowDown size={14} />
9541006
}
9551007

956-
// Filter and sort simulation results
957-
const filteredAndSortedResults = useMemo(() => {
958-
if (!simulationResult || !Array.isArray(simulationResult)) return []
959-
960-
let filtered = [...simulationResult]
961-
962-
// Filter by type
963-
if (filterType !== 'all') {
964-
filtered = filtered.filter((result) => result.offerType === filterType)
965-
}
966-
967-
// Filter by provider
968-
if (filterProvider !== 'all') {
969-
filtered = filtered.filter((result) => result.providerName === filterProvider)
970-
}
971-
972-
// Filter by recency (tariffs < 6 months old)
973-
if (showOnlyRecent) {
974-
filtered = filtered.filter((result) => !isOldTariff(result.validFrom))
975-
}
976-
977-
// Sort by selected criteria
978-
filtered.sort((a, b) => {
979-
let comparison = 0
980-
switch (sortBy) {
981-
case 'subscription':
982-
comparison = a.subscriptionCost - b.subscriptionCost
983-
break
984-
case 'energy':
985-
comparison = a.energyCost - b.energyCost
986-
break
987-
case 'total':
988-
default:
989-
comparison = a.totalCost - b.totalCost
990-
break
991-
}
992-
return sortOrder === 'asc' ? comparison : -comparison
993-
})
994-
995-
return filtered
996-
}, [simulationResult, filterType, filterProvider, showOnlyRecent, sortBy, sortOrder])
997-
998-
// Get unique providers and types for filter options
999-
const availableProviders = useMemo(() => {
1000-
if (!simulationResult || !Array.isArray(simulationResult)) return []
1001-
const providers = new Set(simulationResult.map((r) => r.providerName))
1002-
return Array.from(providers).sort()
1003-
}, [simulationResult])
1004-
1005-
const availableTypes = useMemo(() => {
1006-
if (!simulationResult || !Array.isArray(simulationResult)) return []
1007-
const types = new Set(simulationResult.map((r) => r.offerType))
1008-
return Array.from(types).sort()
1009-
}, [simulationResult])
1010-
10111008
// Clear cache function (admin only) - Unused for now as cache clearing is in the sidebar
10121009
/*
10131010
const confirmClearCache = async () => {

0 commit comments

Comments
 (0)