Skip to content

React Component Standards

React Component Standards

Component Structure

Follow this order within every component file:

1. Imports — external first, then internal, then types:

import { useState, useCallback } from 'react';
import { useQuery } from '@tanstack/react-query';

import { Button } from '@/components/ui/Button';
import { useAuth } from '@/hooks/useAuth';
import type { User } from '@/types';

2. Types/Interfaces — define props before the component:

interface UserCardProps {
  user: User;
  onEdit?: (user: User) => void;
  className?: string;
}

3. Component body — hooks, derived state, callbacks, then render:

export function UserCard({ user, onEdit, className }: UserCardProps) {
  // Hooks first
  const [isExpanded, setIsExpanded] = useState(false);
  const { isAdmin } = useAuth();

  // Derived state
  const canEdit = isAdmin || user.isOwner;

  // Callbacks
  const handleEdit = useCallback(() => {
    onEdit?.(user);
  }, [onEdit, user]);

  // Render
  return (
    <div className={cn('rounded-lg border p-4', className)}>
      <h3>{user.name}</h3>
      {canEdit && <Button onClick={handleEdit}>Edit</Button>}
    </div>
  );
}

Server vs Client Components

Component Type Use For
Server (default) Data fetching, static content, no interactivity
Client ('use client') Event handlers, hooks, browser APIs
// Server Component (default) - can fetch data directly
async function UserList() {
  const users = await getUsers(); // Direct async/await
  return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}

// Client Component - needs 'use client' directive
'use client';

import { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}

Props Typing

Always type props explicitly:

// Good - explicit interface
interface ButtonProps {
  variant?: 'primary' | 'secondary' | 'danger';
  size?: 'sm' | 'md' | 'lg';
  disabled?: boolean;
  children: React.ReactNode;
  onClick?: () => void;
}

// Extending HTML attributes
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
  label: string;
  error?: string;
}

// With ref forwarding
interface CardProps extends React.HTMLAttributes<HTMLDivElement> {
  variant?: 'default' | 'bordered';
}

const Card = forwardRef<HTMLDivElement, CardProps>(
  ({ variant = 'default', className, ...props }, ref) => (
    <div ref={ref} className={cn(variants[variant], className)} {...props} />
  )
);

Composition over Prop Drilling

Prefer composition patterns:

// Bad - prop drilling
function Page({ user, theme, locale }) {
  return <Layout user={user} theme={theme} locale={locale}>
    <Sidebar user={user} theme={theme} />
    <Content user={user} theme={theme} locale={locale} />
  </Layout>;
}

// Good - composition
function Page() {
  return (
    <Layout
      sidebar={<Sidebar />}
      content={<Content />}
    />
  );
}

// Good - context for truly global state
function Page() {
  return (
    <UserProvider>
      <ThemeProvider>
        <Layout />
      </ThemeProvider>
    </UserProvider>
  );
}

Named Exports

Prefer named exports over default exports:

// Good - named export
export function UserCard() { ... }
export function UserList() { ... }

// Avoid - default export
export default function UserCard() { ... }

Named exports enable: - Consistent naming across imports - Better IDE autocomplete - Easier refactoring