fix: React-Hooks in Demo-Tour und LogEntriesList bereinigen

Tour-Schritte über zentralen Effect synchronisieren, Escape-Listener per Ref stabilisieren
und Eintragsliste nur bei Logbook-Wechsel bzw. Rückkehr aus dem Editor neu laden.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-29 18:08:34 +02:00
co-authored by Cursor
parent 181cbe4895
commit 4c3f93602c
4 changed files with 47 additions and 37 deletions
+6 -6
View File
@@ -1,4 +1,4 @@
import { useState, useEffect, useCallback } from 'react' import { useState, useEffect } from 'react'
import './App.css' import './App.css'
import { DialogProvider } from './components/ModalDialog.tsx' import { DialogProvider } from './components/ModalDialog.tsx'
import AuthOnboarding from './components/AuthOnboarding.tsx' import AuthOnboarding from './components/AuthOnboarding.tsx'
@@ -141,14 +141,14 @@ function App() {
} }
}, [isAuthenticated, activeLogbookId]) }, [isAuthenticated, activeLogbookId])
const handleSelectLogbook = useCallback((id: string, title: string) => { const selectLogbook = (id: string, title: string) => {
setActiveLogbookId(id) setActiveLogbookId(id)
setActiveLogbookTitle(title) setActiveLogbookTitle(title)
setActiveTab('logs') setActiveTab('logs')
setTourSelectedEntryId(null) setTourSelectedEntryId(null)
localStorage.setItem('active_logbook_id', id) localStorage.setItem('active_logbook_id', id)
localStorage.setItem('active_logbook_title', title) localStorage.setItem('active_logbook_title', title)
}, []) }
const handleAuthenticated = async () => { const handleAuthenticated = async () => {
setIsAuthenticated(true) setIsAuthenticated(true)
@@ -156,7 +156,7 @@ function App() {
try { try {
const demo = await seedDemoLogbookIfNeeded() const demo = await seedDemoLogbookIfNeeded()
if (demo) { if (demo) {
handleSelectLogbook(demo.logbookId, demo.title) selectLogbook(demo.logbookId, demo.title)
if (demo.firstEntryId) { if (demo.firstEntryId) {
setDemoHighlightEntryId(demo.firstEntryId) setDemoHighlightEntryId(demo.firstEntryId)
} }
@@ -209,7 +209,7 @@ function App() {
onAccepted={(logbookId, title) => { onAccepted={(logbookId, title) => {
setIsAuthenticated(true) setIsAuthenticated(true)
setIsAcceptingInvite(false) setIsAcceptingInvite(false)
handleSelectLogbook(logbookId, title) selectLogbook(logbookId, title)
// Clean URL query parameters and hash anchor // Clean URL query parameters and hash anchor
window.history.replaceState({}, document.title, window.location.pathname) window.history.replaceState({}, document.title, window.location.pathname)
}} }}
@@ -237,7 +237,7 @@ function App() {
<div style={{ display: 'contents' }}> <div style={{ display: 'contents' }}>
{pwaInstallBanner} {pwaInstallBanner}
<LogbookDashboard <LogbookDashboard
onSelectLogbook={handleSelectLogbook} onSelectLogbook={selectLogbook}
onLogout={handleLogout} onLogout={handleLogout}
/> />
</div> </div>
+5 -3
View File
@@ -1,4 +1,4 @@
import { useEffect, useLayoutEffect, useState } from 'react' import { useEffect, useLayoutEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { X, ChevronLeft, ChevronRight } from 'lucide-react' import { X, ChevronLeft, ChevronRight } from 'lucide-react'
import { import {
@@ -34,6 +34,8 @@ export default function AppTourOverlay() {
} = useAppTour() } = useAppTour()
const [spotlight, setSpotlight] = useState<SpotlightRect | null>(null) const [spotlight, setSpotlight] = useState<SpotlightRect | null>(null)
const skipTourRef = useRef(skipTour)
skipTourRef.current = skipTour
useLayoutEffect(() => { useLayoutEffect(() => {
if (!isActive || !currentStepId || isCenteredTourStep(currentStepId)) { if (!isActive || !currentStepId || isCenteredTourStep(currentStepId)) {
@@ -94,11 +96,11 @@ export default function AppTourOverlay() {
useEffect(() => { useEffect(() => {
if (!isActive) return if (!isActive) return
const onKeyDown = (event: KeyboardEvent) => { const onKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') skipTour() if (event.key === 'Escape') skipTourRef.current()
} }
window.addEventListener('keydown', onKeyDown) window.addEventListener('keydown', onKeyDown)
return () => window.removeEventListener('keydown', onKeyDown) return () => window.removeEventListener('keydown', onKeyDown)
}, [isActive, skipTour]) }, [isActive])
if (!isActive || !currentStepId) return null if (!isActive || !currentStepId) return null
+17 -9
View File
@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react' import React, { useState, useEffect, useCallback, useRef } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { db } from '../services/db.js' import { db } from '../services/db.js'
import { getActiveMasterKey } from '../services/auth.js' import { getActiveMasterKey } from '../services/auth.js'
@@ -70,14 +70,9 @@ export default function LogEntriesList({
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [exporting, setExporting] = useState(false) const [exporting, setExporting] = useState(false)
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
const prevSelectedEntryIdRef = useRef<string | null | undefined>(undefined)
useEffect(() => { const loadEntries = useCallback(async () => {
if (!selectedEntryId) {
loadEntries()
}
}, [logbookId, selectedEntryId])
const loadEntries = async () => {
setLoading(true) setLoading(true)
setError(null) setError(null)
try { try {
@@ -136,7 +131,20 @@ export default function LogEntriesList({
} finally { } finally {
setLoading(false) setLoading(false)
} }
} }, [logbookId, readOnly, preloadedEntries])
useEffect(() => {
loadEntries()
}, [loadEntries])
useEffect(() => {
const prevSelectedEntryId = prevSelectedEntryIdRef.current
prevSelectedEntryIdRef.current = selectedEntryId
if (prevSelectedEntryId !== undefined && prevSelectedEntryId !== null && selectedEntryId === null) {
loadEntries()
}
}, [selectedEntryId, loadEntries])
const handleDownloadCsv = async () => { const handleDownloadCsv = async () => {
setExporting(true) setExporting(true)
+19 -19
View File
@@ -115,9 +115,7 @@ export function AppTourProvider({ children }: { children: ReactNode }) {
setStepIndex(0) setStepIndex(0)
setIsActive(true) setIsActive(true)
applyStepSideEffects(STEP_ORDER[0]) }, [])
scrollToCurrentTarget(STEP_ORDER[0])
}, [applyStepSideEffects, scrollToCurrentTarget])
const finishTour = useCallback(() => { const finishTour = useCallback(() => {
const userId = localStorage.getItem('active_userid') const userId = localStorage.getItem('active_userid')
@@ -130,24 +128,26 @@ export function AppTourProvider({ children }: { children: ReactNode }) {
const skipTour = finishTour const skipTour = finishTour
const nextStep = useCallback(() => { const nextStep = useCallback(() => {
const nextIndex = stepIndex + 1 setStepIndex((current) => {
if (nextIndex >= STEP_ORDER.length) { if (current + 1 >= STEP_ORDER.length) {
finishTour() finishTour()
return return current
} }
const nextId = STEP_ORDER[nextIndex] return current + 1
setStepIndex(nextIndex) })
applyStepSideEffects(nextId) }, [finishTour])
scrollToCurrentTarget(nextId)
}, [applyStepSideEffects, finishTour, scrollToCurrentTarget, stepIndex])
const prevStep = useCallback(() => { const prevStep = useCallback(() => {
const prevIndex = Math.max(0, stepIndex - 1) setStepIndex((current) => Math.max(0, current - 1))
const prevId = STEP_ORDER[prevIndex] }, [])
setStepIndex(prevIndex)
applyStepSideEffects(prevId) useEffect(() => {
scrollToCurrentTarget(prevId) if (!isActive) return
}, [applyStepSideEffects, scrollToCurrentTarget, stepIndex]) const stepId = STEP_ORDER[stepIndex]
if (!stepId) return
applyStepSideEffects(stepId)
scrollToCurrentTarget(stepId)
}, [isActive, stepIndex, applyStepSideEffects, scrollToCurrentTarget])
const restartTour = useCallback(() => { const restartTour = useCallback(() => {
const userId = localStorage.getItem('active_userid') const userId = localStorage.getItem('active_userid')