| paths | |
|---|---|
|
Each .tsx file exports ONE React component. Exceptions:
- Styled components used only by that component can stay inline
- Small helpers (< 10 lines) used only within the file
- NEVER use
useMemooruseCallback - Write natural code and let the compiler optimize it
// Bad
const formattedPrice = useMemo(() => formatCurrency(price), [price])
const handleClick = useCallback(() => setOpen(true), [])
// Good — React Compiler handles this
const formattedPrice = formatCurrency(price)
const handleClick = () => setOpen(true)Before removing useMemo/useCallback, always check if the value is used in a useEffect dependency array that triggers expensive side effects (widget creation, DOM manipulation, subscriptions, external library init).
An unstable reference in a useEffect dep array causes the effect to re-run every render — destroying and recreating external resources (e.g. charting widget teardown, loader flash, full recreation).
// DANGEROUS — removing useMemo here causes widget recreation every render
const datafeed = useChartDatafeed(params) // returns new object each render
useEffect(() => {
createWidget({ datafeed }) // runs every render!
return () => widget.destroy() // destroys widget every render!
}, [datafeed]) // unstable reference triggers effect
// SAFE — useMemo keeps reference stable, effect only runs when params change
const datafeed = useMemo(() => createDatafeed(params), [params])Checklist before removing useMemo/useCallback:
- Grep for the variable name in useEffect/useLayoutEffect dep arrays
- If found, check what the effect does — if it creates/destroys resources, keep the memoization
- Add a comment explaining WHY memoization is needed:
// useMemo required: used in useEffect dep that creates chart widget
- Domain components access state directly via hooks — not props
- Only pass props to truly reusable/generic components (design system, shared UI)
// Good — domain component gets its own state
export const OrderHeader = () => {
const [orderId, setOrderId] = useOrderId()
return <OrderSelector currentOrderId={orderId} onSelectOrder={setOrderId} />
}
// Bad — parent fetches state and drills it down
export const OrderHeader = ({ orderId, onSelectOrder }: Props) => {
return <OrderSelector currentOrderId={orderId} onSelectOrder={onSelectOrder} />
}Call hooks in the component that needs the data, not a parent. Prevents unnecessary re-renders of siblings.
// Bad — parent re-renders both children when price changes
const TradingPanel = () => {
const price = useTokenPrice()
return (
<>
<PriceDisplay price={price} />
<OrderForm /> {/* re-renders unnecessarily */}
</>
)
}
// Good — only PriceDisplay re-renders
const TradingPanel = () => (
<>
<PriceDisplay /> {/* owns its own price hook */}
<OrderForm />
</>
)- Always prefix with
use useXxxQuery— returns React Query object (with loading/error state)useXxx— returns resolved data (callsensurePresent, assumes parent renders via MatchQuery)- Hooks that return a tuple:
const [value, setValue] = useXxx() - Hooks that return an object:
const { data, isLoading } = useXxxQuery()
// Query hook — returns full query object
export const useTokenQuery = () =>
useQuery({ queryKey: ['token', id], queryFn: () => getToken(id) })
// Resolved data hook — used inside MatchQuery success branch
export const useToken = () => {
const { data } = useTokenQuery()
return ensurePresent(data, 'token data')
}- Props:
onXxx(what happened) - Implementations:
handleXxx(how to respond)
// Good
type Props = { onSelect: (id: string) => void }
const TokenList = ({ onSelect }: Props) => {
const handleTokenClick = (id: string) => {
trackAnalytics('token_selected')
onSelect(id)
}
return <Token onClick={() => handleTokenClick(token.id)} />
}// Bad
if (query.isLoading) return <Spinner />
if (query.error) return <ErrorMessage />
return <Content data={query.data!} />
// Good
<MatchQuery
value={query}
pending={() => <Spinner />}
error={(e) => <ErrorMessage error={e} />}
success={(data) => <Content data={data} />}
/>// Bad
{items.map((item, i) => <Item key={i} />)}
// Good — use stable unique identifier
{items.map((item) => <Item key={item.id} />)}Never store in state what can be computed from existing state or props.
// Bad — redundant state that can go stale
const [items, setItems] = useState(initialItems)
const [filteredItems, setFilteredItems] = useState(initialItems)
useEffect(() => {
setFilteredItems(items.filter(i => i.active))
}, [items])
// Good — derived inline
const [items, setItems] = useState(initialItems)
const filteredItems = items.filter(i => i.active)Prefer children/render props over large config objects for flexible UI.
// Good — composable
<Dialog title="Confirm">
<ConfirmationContent />
</Dialog>
// Bad — monolithic config
<Dialog config={{ title: 'Confirm', content: ConfirmationContent, footer: Footer }} />- Simple value mapping →
Record<Key, Value> - Repetitive conditionals → config arrays with
.every()/.filter() - Reset operations → config-derived maps
- See
pattern-matching.mdfor full guide