This document describes how React components in this codebase are structured and written. It is derived from the patterns in app/_components/PomodoroPanel.tsx. Agents and contributors should follow it for new or heavily edited components unless a file already establishes a different, consistent local convention.
- Use the
"use client";directive at the top of the file (first line) when the module uses browser APIs, React state/effects, or other client-only behavior.
File layout is one of the most important rules in this guide. It should match the following structure whenever practical.
After imports, the first function in the file must be the exported main component (the public entry point for the module). Then private subcomponents, then custom hooks, then pure helpers at the bottom.
A single file may contain:
| Order in file | Section | Role |
|---|---|---|
| 1 | Exported main component | export function MyPanel() — appears immediately after imports. Keeps JSX shallow: compose children, avoid deep nesting in one function. |
| 2 | Private subcomponents | not exported. Each handles one visual or logical chunk. |
| 3 | Custom hooks | function useThingMechanics() — see below: init and subscriptions live here; use an explicit tuple return type when returning a tuple. Order: declare the hook the exported component calls first; place helper hooks used only by that hook after it in the file (function declarations hoist, so the helper may appear below the consumer). |
| 4 | Pure helpers | function formatX(), function getYFromZ() at the bottom — no hooks, easy to test and reuse. |
Prefer many small functions over one large component with long JSX.
Import order is mandatory. Keep the groups below in this exact order, each group separated by a blank line. Do not merge groups or reorder them (e.g. do not put React before Mantine).
- UI library — e.g.
@mantine/corecomponents, alphabetized within the block. - Icons — e.g.
@tabler/icons-react. - React — hooks from
react(alphabetize:useCallback,useEffect,useRef,useState, …). - App modules — use the
@/path alias. Puttypeimports on the same line as related value imports when they come from the same module. - Relative imports — same-folder or nearby files (e.g.
./FlipClockJsCountdown).
Use named imports; avoid default imports for components unless the dependency only exposes a default.
Aim to keep each component function under 100 lines (excluding blank lines is a reasonable interpretation; the goal is readability, not a hard linter rule). When a component grows past that:
- Extract subcomponents — especially along layout boundaries: one subcomponent per major
Group,Stack,Tabs/Tabs.Tab, or similar layout wrapper when that block is self-contained. - Layout containers — when the shell is stable (e.g. a
Box+ innerGroupwith fixed scroll/alignment styles) and only the inner body varies, extract a privateSomethingContainerthat takeschildrenand owns the outer chrome. The exported component then composes:<MyContainer>{…}</MyContainer>. - Expandable search / add slot — when a column toggles between a compact trigger (e.g. icon button) and an expanded inline control (e.g.
Autocompletewith cancel, blur rules, portal), extract a subcomponent (e.g.AddClockButton) with a small, named props type for parent context only (e.g.implicitZoneSet,clocks). Keep local state, refs, and effects inside that subcomponent, or in a private hook called from that subcomponent (e.g.useWorldClockAddColumn) when the logic is large—do not thread those through the page-level hook. - Extract a custom hook that returns a tuple (or small object) so the main component mostly wires props and JSX instead of owning all effects and state.
Combine subcomponents, containers, expandable slots, and hooks as needed.
- The main custom hook (the one the exported component calls) should hold cross-cutting concerns: store
init, shared timers, and state that several children need (e.g. onenowtick for every clock card). Do not fold every subtree’s local UI state into that hook—doing so produces a large return object and forces long prop lists into children. - Do not put state, refs, or effects that serve only one subcomponent in the main custom hook. Keep them inside that subcomponent (
useState/useRef/useEffectin the component body). If the logic is large, extract a separate custom hook and call it from that subcomponent (e.g.AddClockButtoncallsuseWorldClockAddColumn). Colocate such hooks in the file’s Custom hooks section (typically after the primary hook and its direct helpers);functiondeclarations hoist so the hook may appear below the component that uses it. - When the hook returns a tuple, declare it with an explicit tuple return type so call sites and refactors stay clear:
function usePomodoroMechanics(): [PomodoroPhase, boolean, boolean] { ... }. Avoid relying on inference alone for multi-value tuple returns.
- Export the main component as a named function:
export function PomodoroPanel() { ... }. - Keep subcomponents and hooks file-private unless another module genuinely needs them (then consider moving to a separate file).
-
For small prop sets (few primitives, no per-field JSDoc), an inline object type on the parameter list is fine:
function TabPanel({ phase, running }: { phase: PomodoroPhase, running: boolean }). -
When props are non-trivial (many fields, optional callbacks/refs, or JSDoc on individual props), declare a named
typeorinterfaceimmediately above the component (e.g.type WorldClockCardProps = { … }thenfunction WorldClockCard(props: WorldClockCardProps)). -
Reuse domain types from stores or shared modules instead of duplicating unions (e.g.
PomodoroPhasefrom@/lib/layout, re-exported from@/app/_stores/pomodoroStorefor store-centric imports).
- You may use object spread to forward a small bundle without repeating names:
<TabPanel {...{ phase, running }} />
Use this when it stays readable; switch to explicit props if the list grows or names are unclear.
- Prefer Mantine primitives for layout and controls:
Paper,Stack,Group,Box,Tabs,Button,ActionIcon,Text, etc. - When markup for a
Group,Stack,Tabs, orTabs.Tabgrows heavy, treat that wrapper as a candidate for a dedicated subcomponent (see File layout → Component size (~100 lines)). - Express layout with Mantine props first (
gap,align,justify,wrap,w,py,radius,variant,size,c, …). - Use
style={{ ... }}for one-off values (e.g. fixed widths, rgba backgrounds, flex quirks) that are not covered by props. - Use
classNamewith Tailwind utilities where they are concise and stable (e.g.flex,min-w-0,invisible,pointer-events-none). Mixing Mantine + Tailwind in one tree is acceptable here. - Primary action color (pomodoro phase): for the main
Buttonvariant="filled"in the pomodoro panel and for primary-style controls elsewhere (e.g.ActionIconvariant="light"used as the main “add” affordance), setcolor={getColorFromPhase(phase)}from@/lib/layout, withphasefromuseCurrentPhase()in@/app/_stores/pomodoroStore. Related icon-only controls in the same control group (e.g. cancel next to an expanded search) may use the samecolorso accents stay aligned with the timer phase.
- Prefer running subscriptions, timers, and one-off initialization from the file’s main custom hook (see File layout → Custom hooks) so the exported component stays mostly declarative.
- Subscribe to global state with small selector hooks from stores (e.g.
useCurrentPhase(),useIsRunning()). - Mutations go through a stable actions object (e.g.
pomodoroActions.pause()), not ad-hoc store access in JSX click handlers when an actions API exists. useEffect: guard early (if (!running) return), always return a cleanup for timers/subscriptions, and list complete dependency arrays.- When a callback is referenced inside an effect, stabilize it with
useCallbackand include its dependencies. - For
window/Notification/AudioContext, guard withtypeof window === "undefined"(or equivalent) where setup must not assume a browser during SSR/build if the code path can run on the server.
- Give interactive controls
aria-label(andtitleon icons when it helps). - Use
aria-live/aria-atomicon regions that update for assistive tech (e.g. a timer). - Use
rolewhen it clarifies semantics (role="timer"). - Use
aria-hiddenon decorative or duplicate-visual slots (e.g. invisible layout spacers). - Motion: this app does not implement
prefers-reduced-motionbranching. Do not adduseSyncExternalStore/matchMedia("(prefers-reduced-motion: reduce)")(or similar) to toggle animation unless product requirements change. Use full motion where the underlying component supports it (e.g. always render a running second hand on analog clocks).
- Escape apostrophes in text with the entity
'(e.g.Today's work). - Conditional UI: prefer
condition ? <Node /> : nullover&&when the condition is not strictly boolean, to avoid leaking0/""into the tree. - Casts: after runtime checks, narrow with
asonly when necessary (e.g. tab value to a union type); prefer validation when values come from untrusted input. - Handlers in JSX: do not paste large anonymous functions into props (e.g.
onKeyDown={(e) => { …many branches… }}or a longonChange). Extract them: useuseCallback(often inside a custom hook colocated with the subtree), or a namedfunction handleXin the module, then passonKeyDown={handleKeyDown}. Keeps JSX readable and matches how event logic is tested and reviewed.
- Use short
/** ... */blocks above non-obvious UI rules (e.g. when a control is shown or hidden), not on every line.
- Never leave
catchblocks empty. Log a warning at minimum (e.g.console.warn).
- Use Tabler icons from
@tabler/icons-reactwith explicitsizeandstrokewhere the design calls for it.
Keep semicolons and trailing commas aligned with the surrounding file. When editing a file, match its existing style.