All posts

4 min readProgramming

Building Modern React Components: Patterns and Best Practices

Learn how to create scalable, maintainable React components using modern patterns, hooks, and TypeScript for better developer experience and performance.

  • #react
  • #typescript
  • #components
  • #hooks
  • #frontend

Building Modern React Components

Creating well-structured, maintainable React components is crucial for building scalable applications. Here are modern patterns and best practices for better React code.

Component Architecture Principles

Single Responsibility

Each component should have one clear purpose:

// Good: Single responsibility
function UserAvatar({ user, size = 'medium' }: UserAvatarProps) {
  return (
    <img
      src={user.avatar}
      alt={user.name}
      className={`avatar avatar-${size}`}
    />
  );
}

// Good: Separate concerns
function UserProfile({ user }: UserProfileProps) {
  return (
    <div className="user-profile">
      <UserAvatar user={user} size="large" />
      <UserDetails user={user} />
      <UserActions user={user} />
    </div>
  );
}

Composition Over Inheritance

Use composition for flexible components:

// Compound component pattern
function Card({ children, className = '' }: CardProps) {
  return <div className={`card ${className}`}>{children}</div>;
}

function CardHeader({ children }: CardHeaderProps) {
  return <header className="card-header">{children}</header>;
}

function CardBody({ children }: CardBodyProps) {
  return <div className="card-body">{children}</div>;
}

function CardFooter({ children }: CardFooterProps) {
  return <footer className="card-footer">{children}</footer>;
}

// Usage
<Card>
  <CardHeader>
    <h2>Article Title</h2>
  </CardHeader>
  <CardBody>
    <p>Article content goes here...</p>
  </CardBody>
  <CardFooter>
    <button>Read More</button>
  </CardFooter>
</Card>;

TypeScript Integration

Strong Typing for Props

Define clear interfaces for your components:

interface ButtonProps {
  variant: 'primary' | 'secondary' | 'danger';
  size: 'small' | 'medium' | 'large';
  disabled?: boolean;
  loading?: boolean;
  onClick?: () => void;
  children: React.ReactNode;
}

function Button({
  variant,
  size,
  disabled = false,
  loading = false,
  onClick,
  children,
}: ButtonProps) {
  const className = `btn btn-${variant} btn-${size}`;

  return (
    <button
      className={className}
      disabled={disabled || loading}
      onClick={onClick}
    >
      {loading ? <Spinner /> : children}
    </button>
  );
}

Generic Components

Create reusable components with generics:

interface ListProps<T> {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
  keyExtractor: (item: T) => string | number;
}

function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
  return (
    <ul>
      {items.map((item) => (
        <li key={keyExtractor(item)}>{renderItem(item)}</li>
      ))}
    </ul>
  );
}

// Usage
<List
  items={users}
  renderItem={(user) => <UserCard user={user} />}
  keyExtractor={(user) => user.id}
/>;

Modern Hook Patterns

Custom Hooks for Business Logic

Extract complex logic into custom hooks:

function useApi<T>(url: string) {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    async function fetchData() {
      try {
        setLoading(true);
        const response = await fetch(url);
        const result = await response.json();
        setData(result);
      } catch (err) {
        setError(err as Error);
      } finally {
        setLoading(false);
      }
    }

    fetchData();
  }, [url]);

  return { data, loading, error };
}

// Usage in component
function UserList() {
  const { data: users, loading, error } = useApi<User[]>('/api/users');

  if (loading) return <LoadingSpinner />;
  if (error) return <ErrorMessage error={error} />;
  if (!users) return <EmptyState />;

  return (
    <List
      items={users}
      renderItem={(user) => <UserCard user={user} />}
      keyExtractor={(user) => user.id}
    />
  );
}

State Management with useReducer

For complex state logic, prefer useReducer:

type FormState = {
  values: Record<string, string>;
  errors: Record<string, string>;
  isSubmitting: boolean;
};

type FormAction =
  | { type: 'SET_FIELD'; field: string; value: string }
  | { type: 'SET_ERROR'; field: string; error: string }
  | { type: 'SET_SUBMITTING'; isSubmitting: boolean }
  | { type: 'RESET' };

function formReducer(state: FormState, action: FormAction): FormState {
  switch (action.type) {
    case 'SET_FIELD':
      return {
        ...state,
        values: { ...state.values, [action.field]: action.value },
        errors: { ...state.errors, [action.field]: '' },
      };
    case 'SET_ERROR':
      return {
        ...state,
        errors: { ...state.errors, [action.field]: action.error },
      };
    case 'SET_SUBMITTING':
      return { ...state, isSubmitting: action.isSubmitting };
    case 'RESET':
      return { values: {}, errors: {}, isSubmitting: false };
    default:
      return state;
  }
}

function useForm(initialValues: Record<string, string> = {}) {
  const [state, dispatch] = useReducer(formReducer, {
    values: initialValues,
    errors: {},
    isSubmitting: false,
  });

  const setField = (field: string, value: string) => {
    dispatch({ type: 'SET_FIELD', field, value });
  };

  const setError = (field: string, error: string) => {
    dispatch({ type: 'SET_ERROR', field, error });
  };

  return { state, setField, setError, dispatch };
}

Performance Optimization

Memoization Strategies

Use React.memo, useMemo, and useCallback strategically:

// Memo for expensive components
const ExpensiveComponent = React.memo(function ExpensiveComponent({
  data,
  onUpdate,
}: ExpensiveComponentProps) {
  // Expensive computation
  const processedData = useMemo(() => {
    return data.map((item) => ({
      ...item,
      computed: expensiveCalculation(item),
    }));
  }, [data]);

  // Stable callback reference
  const handleClick = useCallback(
    (id: string) => {
      onUpdate(id);
    },
    [onUpdate],
  );

  return (
    <div>
      {processedData.map((item) => (
        <ItemComponent key={item.id} item={item} onClick={handleClick} />
      ))}
    </div>
  );
});

Lazy Loading

Implement lazy loading for better performance:

import { lazy, Suspense } from 'react';

const LazyModal = lazy(() => import('./Modal'));
const LazyChart = lazy(() => import('./Chart'));

function Dashboard() {
  const [showModal, setShowModal] = useState(false);

  return (
    <div>
      <h1>Dashboard</h1>

      {showModal && (
        <Suspense fallback={<div>Loading modal...</div>}>
          <LazyModal onClose={() => setShowModal(false)} />
        </Suspense>
      )}

      <Suspense fallback={<div>Loading chart...</div>}>
        <LazyChart data={chartData} />
      </Suspense>
    </div>
  );
}

Testing Patterns

Component Testing

Write comprehensive tests for your components:

import { render, screen, fireEvent } from '@testing-library/react';
import Button from './Button';

describe('Button', () => {
  it('renders with correct text', () => {
    render(
      <Button variant="primary" size="medium">
        Click me
      </Button>,
    );
    expect(screen.getByText('Click me')).toBeInTheDocument();
  });

  it('calls onClick when clicked', () => {
    const handleClick = jest.fn();
    render(
      <Button variant="primary" size="medium" onClick={handleClick}>
        Click me
      </Button>,
    );

    fireEvent.click(screen.getByText('Click me'));
    expect(handleClick).toHaveBeenCalledTimes(1);
  });

  it('shows loading state', () => {
    render(
      <Button variant="primary" size="medium" loading>
        Click me
      </Button>,
    );

    expect(screen.getByRole('button')).toBeDisabled();
  });
});

Conclusion

Building modern React components requires attention to:

  • Architecture: Clear separation of concerns and composition
  • TypeScript: Strong typing for better developer experience
  • Performance: Strategic memoization and lazy loading
  • Testing: Comprehensive test coverage

By following these patterns, you'll create components that are maintainable, reusable, performant, and testable.


AI has been used to correct sentence flow and improve readability.

Share this post