Skip to content

Latest commit

 

History

History
196 lines (151 loc) · 5.77 KB

File metadata and controls

196 lines (151 loc) · 5.77 KB
paths
**/*.tsx

React Rules

One component per file

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

React Compiler — no manual memoization

  • NEVER use useMemo or useCallback
  • 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)

Critical exception: objects/callbacks consumed as useEffect deps

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:

  1. Grep for the variable name in useEffect/useLayoutEffect dep arrays
  2. If found, check what the effect does — if it creates/destroys resources, keep the memoization
  3. Add a comment explaining WHY memoization is needed: // useMemo required: used in useEffect dep that creates chart widget

Component autonomy

  • 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} />
}

Colocate state with consumers

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 />
  </>
)

Custom hook conventions

  • Always prefix with use
  • useXxxQuery — returns React Query object (with loading/error state)
  • useXxx — returns resolved data (calls ensurePresent, 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')
}

Event handler naming

  • 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)} />
}

MatchQuery for loading/error/success — never manual checks

// 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} />}
/>

Keys in lists — never use index

// Bad
{items.map((item, i) => <Item key={i} />)}

// Good — use stable unique identifier
{items.map((item) => <Item key={item.id} />)}

No derived state — use computed values

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)

Composition over configuration

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 }} />

Pattern matching over switch/case in JSX

  • Simple value mapping → Record<Key, Value>
  • Repetitive conditionals → config arrays with .every() / .filter()
  • Reset operations → config-derived maps
  • See pattern-matching.md for full guide