Files

26 KiB
Raw Permalink Blame History

Pitfalls Research

Domain: Personal finance dashboard UI overhaul (React SPA) Researched: 2026-03-16 Confidence: HIGH (most findings verified against official docs, Recharts issues tracker, and Tailwind v4 official docs)


Critical Pitfalls

Pitfall 1: Recharts Re-renders Every Parent State Change

What goes wrong: The existing DashboardPage.tsx computes pieData and progressGroups directly in the render body — no useMemo. Every unrelated state change (e.g., a tooltip hover, a loading flag flip, QuickAddPicker opening) re-runs these array transforms and causes Recharts to diff its entire virtual DOM tree. Adding two more charts (bar chart, horizontal bar) multiplies this cost by 3x. With a large budget, each O(n) filter+reduce runs six times per render for each chart type.

Why it happens: The existing single-chart dashboard is fast enough that the missing memoization is invisible. During the overhaul, three chart instances are added to the same render tree, and the dashboard becomes visually complex enough that parent re-renders happen more frequently (collapsible state toggles, hover events on multiple chart tooltips).

How to avoid:

  • Wrap all chart data derivations in useMemo with explicit deps arrays:
    const pieData = useMemo(() =>
      EXPENSE_TYPES.map(type => { ... }).filter(d => d.value > 0),
      [items, t]
    )
    
  • Wrap formatter callbacks passed to <Tooltip formatter={...}> in useCallback. New function references on every render force Recharts to re-render tooltip internals.
  • Extract each chart into its own memoized sub-component (React.memo) so only the chart whose data changed re-renders.
  • Create a category index Map once (in a hook or useMemo) and look up by key rather than .filter() per item across three charts.

Warning signs:

  • React DevTools Profiler shows DashboardContent re-rendering on tooltip mouseover
  • Multiple chart tooltips fighting each other (Recharts issues #281 / #1770)
  • "ResizeObserver loop limit exceeded" console errors when multiple ResponsiveContainer instances mount simultaneously

Phase to address: Dashboard redesign phase (charts implementation)


Pitfall 2: CSS Variable Scope — Recharts Cannot Read Tailwind @theme inline Variables

What goes wrong: The existing palette.ts already uses var(--color-income) etc. as chart fill values. This works correctly because Tailwind v4's @theme inline inlines these as true CSS custom properties on :root. However, the risk is subtle: if any chart color is set via a Tailwind utility class string (e.g., fill-[var(...)]) rather than through the style prop or a direct CSS variable reference, Recharts SVG elements — which render outside the standard DOM paint context — may not resolve them correctly in all browsers.

Additionally, dark mode: the current CSS has no dark-mode overrides for --color-income through --color-investment. If the overhaul adds a dark mode toggle, all chart colors will remain light-mode pastel on dark backgrounds, creating very poor contrast.

Why it happens: Tailwind v4's @theme inline block defines variables at :root scope, but the dark mode variant uses .dark class scoping. Theme variables defined only in @theme inline are not automatically duplicated under .dark {}. Dark-mode overrides for semantic colors (background, foreground) exist in shadcn's generated block, but category colors were custom-added without dark variants.

How to avoid:

  • Pass all Recharts color values via JavaScript (the fill={categoryColors[type]} pattern is correct — maintain it).
  • If dark mode is added in the overhaul: add a .dark {} block in index.css that overrides --color-income, --color-bill, etc. with darker/brighter variants appropriate for dark backgrounds.
  • Never attempt to pass Tailwind utility class strings as Recharts fill props. Recharts Cell, Bar, Line props require resolved color values (hex, oklch string, or var() reference).
  • Test chart colors in both light and dark modes before marking a phase complete.

Warning signs:

  • Chart fills show as oklch(...) literal text in DOM attributes instead of resolved colors
  • Colors are invisible or white-on-white on one theme variant
  • Browser DevTools shows SVG fill as unresolved var(--color-income) with no computed value

Phase to address: Design token / theming phase (early); dashboard charts phase (verification)


Pitfall 3: Collapsible Sections Causing Layout Shift and CLS Jank

What goes wrong: The planned hybrid dashboard includes collapsible inline sections for each category group (income, bills, variable expenses, debt, savings). If these are implemented by toggling display: none / display: block or by animating the height CSS property from 0 to auto, the result is either: (a) instant snap with no animation, or (b) jank where the browser triggers full layout recalculations on every animation frame. With five collapsible sections and charts above them, collapsing a section causes the charts to resize (their ResponsiveContainer detects parent width change), triggering a cascade of resize events.

Why it happens: Animating height from 0 to auto is a known browser limitation — you cannot CSS-transition to height: auto. Common naive workarounds (JavaScript measuring scrollHeight on every frame) cause layout thrashing. Radix UI's Collapsible component handles this correctly via CSS custom properties for height, but requires the data-state attribute pattern and the correct CSS transition on the inner CollapsibleContent element.

How to avoid:

  • Use Radix UI Collapsible (already available via shadcn) — it sets --radix-collapsible-content-height as a CSS variable during open/close, enabling smooth CSS-only transition:
    [data-state='open'] > .collapsible-content {
      animation: slideDown 200ms ease-out;
    }
    [data-state='closed'] > .collapsible-content {
      animation: slideUp 200ms ease-out;
    }
    @keyframes slideDown {
      from { height: 0 }
      to { height: var(--radix-collapsible-content-height) }
    }
    
  • Add debounce={50} to all ResponsiveContainer instances to prevent rapid resize recalculations when parent containers animate.
  • Use padding instead of margin inside collapsible children — margin collapsing causes jump artifacts on some browsers.
  • Never animate height, padding, or margin directly with JavaScript setInterval; use CSS animations or Radix primitives.

Warning signs:

  • Charts visually "snap" to a narrower width and back when a section above them is toggled
  • Frame rate drops to under 30fps when expanding/collapsing sections (visible in DevTools Performance panel)
  • ResizeObserver loop limit exceeded errors spike after section toggle

Phase to address: Dashboard collapsible sections phase


Pitfall 4: Color Accessibility Failures in Financial Semantic Colors

What goes wrong: The most dangerous pattern in the existing dashboard is using pure green / red to signal positive / negative balance:

const balanceColor = availableBalance >= 0
  ? "text-green-600 dark:text-green-400"
  : "text-red-600 dark:text-red-400"

And in the progress bar:

const barColor = group.overBudget
  ? "bg-red-500 dark:bg-red-400"
  : "bg-green-500 dark:bg-green-400"

Pure green (#00FF00) has a contrast ratio of only 1.4:1 against white — catastrophically below WCAG AA's 4.5:1 minimum for text. Even green-600 (#16a34a) must be verified. Additionally, pie chart adjacent slice colors must maintain 3:1 contrast against each other (WCAG 2.1 SC 1.4.11 Non-text Contrast) — the existing six category colors are all similar lightness (OKLCH L ≈ 0.650.72), meaning they may be hard to distinguish for colorblind users or when printed.

Why it happens: Designers focus on making the palette "look rich" during an overhaul without running contrast checks. The redesign goal is "rich, colorful visual style" — this is a direct risk factor for accessibility regressions. Color-alone encoding (green = good, red = bad) violates WCAG 1.4.1 Use of Color, which requires that color not be the sole means of conveying information.

How to avoid:

  • Run every text color against its background through a WCAG contrast checker (target 4.5:1 for normal text, 3:1 for large text and UI components). Use the Tailwind oklch values — compute with tools like https://webaim.org/resources/contrastchecker/
  • For semantic colors (balance positive/negative, over-budget): supplement color with an icon or text label, not color alone. Example: a checkmark icon + green for positive balance; an exclamation icon + red for over-budget.
  • For pie/donut chart slices: ensure adjacent colors have at least 3:1 contrast, or add a visible stroke separator (1px white/black stroke between slices provides a natural contrast boundary).
  • For the 6 category colors: vary hue and lightness, not just hue. Consider making the OKLCH lightness values more spread (e.g., 0.55 to 0.80 range) so colors are distinguishable at reduced color sensitivity.
  • Do not rely on dark:text-green-400 having passed contrast automatically — verify each dark mode color pair independently.

Warning signs:

  • All six category pie slices are clearly distinguishable to the developer but indistinguishable when the browser's "Emulate vision deficiency" filter is applied in DevTools
  • Running window.getComputedStyle on a colored element and checking its OKLCH L value — if multiple semantic colors cluster at the same lightness, colorblind users see identical grays

Phase to address: Design token / visual language phase (establish accessible palette before building components); then verify in each component phase


Pitfall 5: i18n Key Regressions When Renaming or Adding UI Sections

What goes wrong: The current translation files (en.json, de.json) have flat keys per page section. The overhaul adds new dashboard sections (bar chart, horizontal bar chart, collapsible income/bill/expense groups, richer donut legend). Each new label, section header, tooltip, and ARIA label needs a corresponding key in both files. During rapid UI iteration, developers commonly add t("dashboard.incomeSection") to the JSX, forget to add it to de.json, and only notice when the German locale shows the raw key string — or worse, the i18next fallback silently shows the English value and the German regression goes undetected.

Why it happens: i18next with fallbackLng: 'en' (the existing config) silently falls back to English when a German key is missing. There is no visible failure. The project has no i18n linting step and no build-time key extraction check. The debug: false production config makes this invisible.

How to avoid:

  • When adding a new UI section, add its keys to both en.json and de.json in the same commit — never split across commits.
  • Use i18next-cli or i18next-scanner (npm package) to extract all t("...") call keys from source and diff against the JSON files. Run this as a pre-commit check or part of the build.
  • In development, consider setting debug: true in i18n.ts — i18next logs missingKey warnings to console for every untranslated key in the non-fallback language.
  • Use a TypeScript-typed i18n setup (e.g., i18next-typescript) so that t("dashboard.nonExistentKey") produces a type error at compile time.
  • Before marking any phase complete, manually switch locale to German and click through every changed screen.

Warning signs:

  • German UI text contains raw dot-notation strings (e.g., dashboard.barChart.title)
  • console.warn messages containing i18next::translator: missingKey de
  • de.json has fewer top-level keys than en.json after a phase's changes

Phase to address: Every phase that adds new UI text; establish the key-parity check process in the first phase of the overhaul


Pitfall 6: Design Inconsistency Across Page Refreshes (The "Island Redesign" Problem)

What goes wrong: The overhaul covers all pages, but if phases are structured page-by-page, early pages get the richest design attention and later pages get inconsistency or fatigue-driven shortcuts. The most common failure mode: the dashboard uses a specific card style (colored header accent, icon in corner), but the Categories page — redesigned two weeks later — uses a subtly different card variant. Buttons on the Budget Detail page use different spacing than on the Template page. The result is a design that looks cohesive in screenshots of individual pages but feels broken when navigating between them.

Why it happens: There is no design system enforced at the component level. shadcn/ui components are used directly in pages without project-specific wrappers. When the overhaul introduces new visual patterns (e.g., a colored icon badge, a section divider style), they are implemented inline in the first page that needs them and then drift in subsequent pages as developers make minor "feels close enough" adaptations.

How to avoid:

  • Before redesigning any pages, establish a small shared component library of new visual primitives (e.g., <SectionHeader>, <StatCard>, <CategoryBadge>) with fixed prop interfaces. All page redesigns consume these components — they never re-implement them inline.
  • Define the full color palette and spacing scale in the first phase, not incrementally. The index.css @theme block is the single source of truth — no hardcoded hex values anywhere else.
  • Create a visual spec (even a quick screenshot grid) of what each page will look like before coding begins, so inconsistencies are caught at design time not code review time.
  • In code review: any new Tailwind classes that don't use design tokens (e.g., text-[#6b21a8], p-[13px]) should be flagged as violations.

Warning signs:

  • Two pages use different visual patterns for "section with a list of items" (one uses a table, one uses cards)
  • Color values appear as raw hex or oklch literals in component files instead of semantic tokens
  • shadcn Card component is used in 3 different ways across 3 pages (no wrapper abstraction)

Phase to address: Design foundation phase (first phase of overhaul) before any page work begins


Technical Debt Patterns

Shortcut Immediate Benefit Long-term Cost When Acceptable
Inline chart data transforms in render Faster to write Re-renders on every state change; adding more charts multiplies the cost Never — always useMemo for chart data
Hardcoded color classes text-green-600 for semantic states Familiar Tailwind pattern Dark mode variants need to be manually maintained in two places; fails contrast checks Only for truly non-semantic colors (e.g., a green checkmark icon that is always green)
Copying shadcn component patterns across pages without abstraction Faster per-page development Design drift guarantees inconsistency; changes to shared patterns require hunting all usages MVP only, with explicit note to abstract before more pages are built
Adding English i18n keys without German equivalent Unblocks development Silent regression in German locale; accumulates debt that is hard to audit later Never in this project — add both languages in the same commit
Using ResponsiveContainer without debounce Default behavior, no extra code Multiple containers trigger simultaneous resize cascades when sections open/close Acceptable for single-chart pages; set debounce={50} on all multi-chart layouts
Implementing collapsible with display: none toggle Simplest implementation No animation; abrupt layout shift; screen readers cannot detect intermediate states Never for primary dashboard sections; acceptable for debug/dev-only UI

Integration Gotchas

Integration Common Mistake Correct Approach
Recharts + Tailwind CSS variables Passing Tailwind utility strings (e.g., "text-blue-500") as chart fill or stroke props Pass var(--color-income) or a resolved hex/oklch string via JS. Recharts SVG does not process Tailwind class strings.
Recharts + ResponsiveContainer Placing ResponsiveContainer inside a flex/grid parent without an explicit height The container measures 0px height. Always wrap in a <div style={{ height: 240 }}> or give the parent an explicit height class.
shadcn Collapsible + Recharts Placing charts inside collapsible sections without debounce When the collapsible opens, the chart container resizes and triggers ResizeObserver rapidly. Add debounce={50} to ResponsiveContainer.
Tailwind v4 @theme inline + dark mode Defining category colors only in @theme inline without a .dark {} override block Category colors remain light-mode pastels on dark backgrounds. Define dark variants in a .dark {} block in index.css.
i18next + new UI sections Adding t("new.key") JSX without adding the key to de.json Silently falls back to English in German locale. Always update both files simultaneously.

Performance Traps

Trap Symptoms Prevention When It Breaks
Unmemoized chart data on a multi-chart dashboard Tooltip hover causes all three charts to re-render simultaneously; visible lag useMemo for all data transforms, useCallback for formatter props, React.memo on chart sub-components Noticeable with 50+ budget items; subtle with 1020
O(n) category lookup per budget item across 3 charts CPU spike when dashboard loads; slow initial paint Build a Map<id, Category> once in the hook layer, O(1) lookup in components Becomes visible at ~100 budget items
Multiple ResponsiveContainer without debounce Rapid resize loop on section toggle; "ResizeObserver loop" errors Add debounce={50} to all ResponsiveContainer instances on the dashboard Any time two or more containers are mounted simultaneously
Animating collapsible height with JavaScript Dropped frames during expand/collapse; chart reflow cascade Use Radix Collapsible with CSS @keyframes on --radix-collapsible-content-height Every interaction on the dashboard
TanStack Query unbounded cache growth Long browser session consumes excessive memory Set gcTime and staleTime on QueryClient config After ~30 minutes of active use without page reload

UX Pitfalls

Pitfall User Impact Better Approach
Color as the only signal for budget overrun (red progress bar) Colorblind users cannot distinguish over-budget from on-track Add an icon (exclamation mark) or text indicator alongside the color change
Hiding line items behind collapsible sections with no affordance Users don't discover that sections are expandable; they think the dashboard is incomplete Use an explicit chevron icon with visible state change; consider first-time-open hint
Chart tooltips showing raw numbers without currency formatting Users see "1234.5" instead of "$1,234.50" — especially jarring in a financial app Always pass formatCurrency(value, currency) in the Recharts formatter prop
Summary cards showing totals without context (no comparison to last month or budget) Numbers without context are harder to interpret; users don't know if $1,200 expenses is good or bad Show budget vs actual delta or a "vs budget" sub-label on cards
Five-section collapsible dashboard that defaults to all-collapsed Users land on a nearly empty dashboard and are confused Default the primary sections (income, bills) to open on first load
Progress bar clamped to 100% without showing actual overage amount Users cannot see how much they are over budget from the bar alone Show the actual percentage (e.g., "132%") in the label even when the bar is clamped

"Looks Done But Isn't" Checklist

  • Charts: Verify all three chart types (donut, bar, horizontal bar) render correctly when items is empty — Recharts renders a blank SVG that can overlap other content if dimensions are not handled
  • Dark mode: Switch to dark theme (if implemented) and verify all category colors, chart fills, and semantic state colors (green/red) have sufficient contrast — light-mode testing only is the most common gap
  • German locale: Navigate every redesigned page in German and verify no raw key strings appear — de.json parity check
  • Collapsible sections: Toggle each collapsible section open and closed rapidly three times — verify no layout shift in charts above/below, no "ResizeObserver loop" errors in console
  • Empty states: Load the dashboard with a budget that has zero actual amounts — verify charts handle pieData.length === 0 and empty bar data without rendering broken empty SVGs
  • Long category names: Enter a category named "Wiederkehrende monatliche Haushaltsausgaben" (long German string) — verify it doesn't overflow card boundaries or truncate without tooltip
  • Currency formatting: Verify EUR formatting (€1.234,56) and USD formatting ($1,234.56) both display correctly in chart tooltips and summary cards when locale is switched in Settings
  • Responsive at 1024px: View the dashboard at exactly 1024px viewport width — the breakpoint where desktop layout switches — verify no horizontal overflow or chart sizing issues

Recovery Strategies

Pitfall Recovery Cost Recovery Steps
Chart re-render performance discovered in production LOW Add useMemo wrappers to chart data transforms; wrap chart components in React.memo; requires no visual changes
Color accessibility failures discovered post-launch MEDIUM Audit all semantic color uses with WCAG checker; update index.css CSS variable values; may require minor component changes for icon-supplement approach
i18n key regressions across multiple pages MEDIUM Run i18next-scanner to enumerate all missing German keys; systematically add translations; no code changes needed
Design inconsistency across pages after all pages are shipped HIGH Requires extracting shared component abstractions retroactively and refactoring all pages — avoid by establishing components in first phase
height: 0 → auto collapsible causing jank discovered mid-phase LOW Replace toggle logic with Radix Collapsible and add CSS keyframe animation — isolated to the collapsible component, no broader refactor needed
ResponsiveContainer ResizeObserver loop in multi-chart layout LOW Add debounce={50} prop to each ResponsiveContainer — one-line fix per chart instance

Pitfall-to-Phase Mapping

Pitfall Prevention Phase Verification
Recharts re-render on every parent state change Dashboard charts phase — add memoization before wiring data React DevTools Profiler: chart sub-components should not appear in flame graph on tooltip hover
CSS variable scope / dark mode chart color gaps Design tokens phase (first) Inspect SVG fill in DevTools; toggle dark mode and visually verify all chart colors resolve
Collapsible layout shift and ResizeObserver cascade Dashboard collapsible sections phase Toggle all sections rapidly; check console for ResizeObserver errors; check DevTools Performance for layout thrash
Color accessibility failures Design tokens phase — define accessible palette upfront Run each color pair through WCAG contrast checker; use DevTools "Emulate vision deficiency" filter
i18n key regressions Every phase — enforce dual-language commit rule from the start Run i18next-scanner before marking any phase complete; switch to German locale and navigate all changed pages
Design inconsistency across page refreshes Design foundation phase — create shared components before any page work Code review: flag any Tailwind color/spacing classes that are not semantic tokens; check that all pages use shared <SectionHeader> / <StatCard> etc.

Sources


Pitfalls research for: SimpleFinanceDash UI overhaul (React + Recharts + Tailwind v4 + shadcn/ui) Researched: 2026-03-16