refactor(state): clean layoutStore, add selectors, centralise STALE constants

- layoutStore: remove 4 dead field groups (pinnedPanels, selectedResultId,
  compareTrayVisible, notificationsOpen) — none were read outside the store
- CompareTray: drop dead useLayoutStore side-effect (state was write-only)
- 8 components: replace bare useStore() with explicit selectors / useShallow
  to prevent unnecessary re-renders on unrelated state mutations
- lib/constants: add STALE_MARKET_SIGNALS + STALE_REVIEW_QUEUE (30s each)
- useMarketSignals, useReviewQueue: use global constants instead of
  hook-local magic numbers
- Add STATE_MANAGEMENT.md: decision tree + rules for RQ/Zustand/local/derived

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Sutter
2026-05-24 01:30:44 +02:00
parent eedee83b49
commit 5d53f35dea
14 changed files with 263 additions and 41 deletions
@@ -3,7 +3,8 @@ import { Sparkles } from 'lucide-react'
import { useAssistantStore } from '../../stores/assistantStore'
export function GlobalAIAssistantButton() {
const { isOpen, open } = useAssistantStore()
const isOpen = useAssistantStore(s => s.isOpen)
const open = useAssistantStore(s => s.open)
return (
<Tooltip title="AI Assistent öffnen" placement="left">
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useRef, useState } from 'react'
import { Box, Divider, Drawer, IconButton, TextField, Tooltip, Typography } from '@mui/material'
import { RotateCcw, Send, Sparkles, X } from 'lucide-react'
import { useLocation } from 'react-router'
import { useShallow } from 'zustand/react/shallow'
import { useAssistantStore } from '../../stores/assistantStore'
import { useSessionStore } from '../../stores/sessionStore'
import { aiAssistantService } from '../../services/aiAssistantService'
@@ -21,7 +22,7 @@ function resolveWorkspace(pathname: string): WorkspaceType | null {
export function GlobalAIAssistantDrawer() {
const { isOpen, close, context, setContext, messages, isLoading, error, addMessage, setLoading, setError, clearConversation } =
useAssistantStore()
useAssistantStore(useShallow(s => s))
const { currentUser } = useSessionStore()
const location = useLocation()
+4 -2
View File
@@ -17,8 +17,10 @@ import { WorkspaceType } from '../../domain/enums'
// ---------------------------------------------------------------------------
export function AppShell() {
const { activeWorkspace, sidebarCollapsed, setActiveWorkspace, toggleSidebar } =
useLayoutStore()
const activeWorkspace = useLayoutStore(s => s.activeWorkspace)
const sidebarCollapsed = useLayoutStore(s => s.sidebarCollapsed)
const setActiveWorkspace = useLayoutStore(s => s.setActiveWorkspace)
const toggleSidebar = useLayoutStore(s => s.toggleSidebar)
const { currentUser } = useSessionStore()
const navigate = useNavigate()
const location = useLocation()
-7
View File
@@ -1,9 +1,7 @@
import { useEffect } from 'react'
import { useNavigate, useLocation } from 'react-router'
import { Box, Button, IconButton, Typography } from '@mui/material'
import { X } from 'lucide-react'
import { useCompareStore } from '../../stores/compareStore'
import { useLayoutStore } from '../../stores/layoutStore'
const TYPE_DOT: Record<string, string> = {
VERIFIED_PORTFOLIO: '#1e3a5f',
@@ -14,15 +12,10 @@ const TYPE_DOT: Record<string, string> = {
export function CompareTray() {
const { compareItems, removeFromCompare, clearCompare } = useCompareStore()
const { setCompareTrayVisible } = useLayoutStore()
const navigate = useNavigate()
const location = useLocation()
const isDemand = location.pathname.startsWith('/demand')
useEffect(() => {
setCompareTrayVisible(compareItems.length > 0 && isDemand)
}, [compareItems.length, setCompareTrayVisible, isDemand])
if (!isDemand) return null
const getTitle = (item: (typeof compareItems)[number]) => {
+3 -1
View File
@@ -18,7 +18,9 @@ const PANEL_PLACEHOLDERS: Record<RightPanelContentType, string> = {
}
export function RightContextPanel() {
const { isRightPanelOpen, rightPanelContentType, closeRightPanel } = useLayoutStore()
const isRightPanelOpen = useLayoutStore(s => s.isRightPanelOpen)
const rightPanelContentType = useLayoutStore(s => s.rightPanelContentType)
const closeRightPanel = useLayoutStore(s => s.closeRightPanel)
const title = rightPanelContentType ? PANEL_TITLES[rightPanelContentType] : ''
const placeholder = rightPanelContentType ? PANEL_PLACEHOLDERS[rightPanelContentType] : ''
+8 -1
View File
@@ -1,5 +1,6 @@
import { Box, Typography } from '@mui/material'
import { useReminders } from '../../hooks/useReminders'
import { useShallow } from 'zustand/react/shallow'
import { useReminderStore } from '../../stores/reminderStore'
import { ReminderListRow } from './ReminderListRow'
import { ReminderCard } from './ReminderCard'
@@ -34,7 +35,13 @@ export function ReminderFeed() {
const {
filterType, filterStatus, filterPriority, searchQuery, viewMode,
setFilterType, setFilterStatus, setFilterPriority, setSearchQuery,
} = useReminderStore()
} = useReminderStore(useShallow(s => ({
filterType: s.filterType, filterStatus: s.filterStatus,
filterPriority: s.filterPriority, searchQuery: s.searchQuery,
viewMode: s.viewMode, setFilterType: s.setFilterType,
setFilterStatus: s.setFilterStatus, setFilterPriority: s.setFilterPriority,
setSearchQuery: s.setSearchQuery,
})))
if (isLoading) return <ReminderSkeleton />
+8 -1
View File
@@ -1,5 +1,6 @@
import { Box, ToggleButtonGroup, ToggleButton, TextField, InputAdornment, Typography } from '@mui/material'
import { Search } from 'lucide-react'
import { useShallow } from 'zustand/react/shallow'
import { useReminderStore } from '../../stores/reminderStore'
import { ReminderType, ReminderStatus, ReminderPriority } from '../../domain/reminder'
import type { ReminderType as ReminderTypeType, ReminderStatus as ReminderStatusType, ReminderPriority as ReminderPriorityType } from '../../domain/reminder'
@@ -36,7 +37,13 @@ export function ReminderFilterBar() {
filterPriority, setFilterPriority,
searchQuery, setSearchQuery,
viewMode, setViewMode,
} = useReminderStore()
} = useReminderStore(useShallow(s => ({
filterType: s.filterType, setFilterType: s.setFilterType,
filterStatus: s.filterStatus, setFilterStatus: s.setFilterStatus,
filterPriority: s.filterPriority, setFilterPriority: s.setFilterPriority,
searchQuery: s.searchQuery, setSearchQuery: s.setSearchQuery,
viewMode: s.viewMode, setViewMode: s.setViewMode,
})))
return (
<Box className="flex flex-col gap-3">