Back to Blog

Modern React Patterns for Production Applications

6 min read
Chivan Visal
ReactPerformanceArchitectureBest Practices

Modern React Patterns for Production Applications

After years of building and maintaining large-scale React applications, I've learned that the difference between a good app and a great one often lies in the patterns and practices we choose. Here are some of the most effective patterns I use in production applications.

1. Compound Components Pattern

The Compound Components pattern provides a flexible and expressive way to build complex components while maintaining a clean API:

// A flexible and reusable select component
const Select = {
  Root: ({ children, value, onChange }: SelectRootProps) => {
    const [isOpen, setIsOpen] = useState(false);
    return (
      <SelectContext.Provider value={{ isOpen, setIsOpen, value, onChange }}>
        <div className="relative">{children}</div>
      </SelectContext.Provider>
    );
  },
  
  Trigger: ({ children }: { children: React.ReactNode }) => {
    const { isOpen, setIsOpen, value } = useSelectContext();
    return (
      <button 
        onClick={() => setIsOpen(!isOpen)}
        className="flex items-center gap-2 px-3 py-2 border rounded-md"
      >
        {children}
        <ChevronDown className={cn("h-4 w-4 transition-transform", {
          "transform rotate-180": isOpen
        })} />
      </button>
    );
  },
  
  Content: ({ children }: { children: React.ReactNode }) => {
    const { isOpen } = useSelectContext();
    if (!isOpen) return null;
    
    return (
      <div className="absolute top-full mt-1 w-full border rounded-md bg-background shadow-lg">
        {children}
      </div>
    );
  },
  
  Item: ({ value, children }: SelectItemProps) => {
    const { onChange, setIsOpen } = useSelectContext();
    return (
      <button
        onClick={() => {
          onChange(value);
          setIsOpen(false);
        }}
        className="w-full px-3 py-2 text-left hover:bg-accent transition-colors"
      >
        {children}
      </button>
    );
  }
};

// Usage
function UserRoleSelect() {
  const [role, setRole] = useState<string>('user');
  
  return (
    <Select.Root value={role} onChange={setRole}>
      <Select.Trigger>
        {role.charAt(0).toUpperCase() + role.slice(1)}
      </Select.Trigger>
      <Select.Content>
        <Select.Item value="user">User</Select.Item>
        <Select.Item value="admin">Admin</Select.Item>
        <Select.Item value="moderator">Moderator</Select.Item>
      </Select.Content>
    </Select.Root>
  );
}

2. Custom Hooks for Complex Logic

I often extract complex logic into custom hooks, making components cleaner and logic reusable:

function useDebounceCallback<T extends (...args: any[]) => any>(
  callback: T,
  delay: number
): T {
  const timeoutRef = useRef<NodeJS.Timeout>();
  const callbackRef = useRef(callback);
  callbackRef.current = callback;

  return useCallback(
    ((...args) => {
      if (timeoutRef.current) {
        clearTimeout(timeoutRef.current);
      }

      timeoutRef.current = setTimeout(() => {
        callbackRef.current(...args);
      }, delay);
    }) as T,
    [delay]
  );
}

function useIntersectionObserver(
  elementRef: RefObject<Element>,
  options: IntersectionObserverInit = {}
): boolean {
  const [isIntersecting, setIsIntersecting] = useState(false);

  useEffect(() => {
    const element = elementRef.current;
    if (!element) return;

    const observer = new IntersectionObserver(([entry]) => {
      setIsIntersecting(entry.isIntersecting);
    }, options);

    observer.observe(element);
    return () => observer.disconnect();
  }, [elementRef, options]);

  return isIntersecting;
}

// Usage example
function SearchInput() {
  const debouncedSearch = useDebounceCallback((query: string) => {
    // API call here
  }, 300);

  return (
    <input
      type="text"
      onChange={(e) => debouncedSearch(e.target.value)}
      className="px-3 py-2 border rounded-md"
    />
  );
}

function LazyImage({ src, alt }: { src: string; alt: string }) {
  const imgRef = useRef<HTMLImageElement>(null);
  const isVisible = useIntersectionObserver(imgRef);
  
  return (
    <img
      ref={imgRef}
      src={isVisible ? src : undefined}
      alt={alt}
      className="w-full h-full object-cover"
    />
  );
}

3. Performance Optimization Patterns

Here are some patterns I use to optimize React performance:

// 1. Memoization with proper dependencies
const MemoizedExpensiveComponent = memo(
  function ExpensiveComponent({ data, onAction }: Props) {
    return (
      // Component implementation
    );
  },
  (prevProps, nextProps) => {
    return (
      prevProps.data.id === nextProps.data.id &&
      prevProps.onAction === nextProps.onAction
    );
  }
);

// 2. Virtualized Lists for Large Datasets
function VirtualizedList({ items }: { items: Item[] }) {
  return (
    <VirtualList
      height={400}
      itemCount={items.length}
      itemSize={50}
      width="100%"
    >
      {({ index, style }) => (
        <div style={style}>
          {items[index].name}
        </div>
      )}
    </VirtualList>
  );
}

// 3. Lazy Loading with Suspense
const LazyComponent = lazy(() => import('./HeavyComponent'));

function App() {
  return (
    <Suspense 
      fallback={
        <div className="flex items-center justify-center min-h-screen">
          <Spinner />
        </div>
      }
    >
      <LazyComponent />
    </Suspense>
  );
}

4. Error Boundaries with Recovery UI

I always implement error boundaries with recovery options:

class ErrorBoundary extends React.Component<
  { children: React.ReactNode },
  { hasError: boolean; error: Error | null }
> {
  state = { hasError: false, error: null };

  static getDerivedStateFromError(error: Error) {
    return { hasError: true, error };
  }

  componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
    // Log to error reporting service
    console.error('Error boundary caught an error:', error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return (
        <div className="p-4 rounded-lg bg-destructive/10 text-destructive">
          <h2 className="text-lg font-semibold mb-2">Something went wrong</h2>
          <p className="mb-4">{this.state.error?.message}</p>
          <button
            onClick={() => this.setState({ hasError: false, error: null })}
            className="px-4 py-2 bg-primary text-primary-foreground rounded-md"
          >
            Try again
          </button>
        </div>
      );
    }

    return this.props.children;
  }
}

5. State Management Patterns

Here's how I organize application state:

// 1. Context + Reducer for Complex State
interface State {
  user: User | null;
  theme: 'light' | 'dark';
  notifications: Notification[];
}

type Action =
  | { type: 'SET_USER'; payload: User | null }
  | { type: 'SET_THEME'; payload: 'light' | 'dark' }
  | { type: 'ADD_NOTIFICATION'; payload: Notification }
  | { type: 'REMOVE_NOTIFICATION'; payload: string };

const AppContext = createContext<{
  state: State;
  dispatch: Dispatch<Action>;
} | null>(null);

// 2. Custom Hook for State Management
function useAppState() {
  const context = useContext(AppContext);
  if (!context) {
    throw new Error('useAppState must be used within AppProvider');
  }
  return context;
}

// 3. Selector Pattern to Prevent Unnecessary Rerenders
function useUser() {
  const { state } = useAppState();
  return state.user;
}

function useTheme() {
  const { state } = useAppState();
  return state.theme;
}

// Usage
function UserProfile() {
  const user = useUser();
  const { dispatch } = useAppState();

  if (!user) return null;

  return (
    <div>
      <h1>{user.name}</h1>
      <button
        onClick={() => dispatch({ type: 'SET_USER', payload: null })}
      >
        Logout
      </button>
    </div>
  );
}

Best Practices for Production

  1. Component Organization:
// Feature-based structure
src/
  features/
    auth/
      components/
      hooks/
      utils/
      types.ts
      index.ts
    dashboard/
      components/
      hooks/
      utils/
      types.ts
      index.ts
  1. Testing Strategy:
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

test('user can submit form with valid data', async () => {
  const onSubmit = vi.fn();
  render(<LoginForm onSubmit={onSubmit} />);

  await userEvent.type(
    screen.getByLabelText(/email/i),
    'test@example.com'
  );
  await userEvent.type(
    screen.getByLabelText(/password/i),
    'password123'
  );
  await userEvent.click(screen.getByRole('button', { name: /submit/i }));

  await waitFor(() => {
    expect(onSubmit).toHaveBeenCalledWith({
      email: 'test@example.com',
      password: 'password123'
    });
  });
});

Conclusion

These patterns have proven invaluable in building robust React applications. They help maintain code quality, improve performance, and make the codebase more maintainable as it grows.

Key takeaways:

  • Use compound components for flexible, reusable UI components
  • Extract complex logic into custom hooks
  • Implement proper performance optimizations
  • Handle errors gracefully with error boundaries
  • Organize state management based on application needs

Remember, patterns are tools - choose them based on your specific requirements and constraints. Stay tuned for more deep dives into React patterns and best practices!