admond@portfolio:~/blog
← all posts
$ cat frontend-best-practices.md

Frontend Best Practices: A Complete Guide

2024-03-11 · 1 min read

Frontend Best Practices: A Complete Guide

Building maintainable frontend applications requires more than just writing code—it demands thoughtful architecture, consistent patterns, and the right tools. This guide covers the practices we've refined through real-world projects.

Project Structure

A well-organized folder structure is the foundation of a scalable application. Here's what we recommend:

src
├── assets              # Static files: images, fonts, icons
├── components          # Shared components across the application
├── config              # Global configuration and env variables
├── constants           # Application constants
├── hoc                 # Higher-order components
├── hooks               # Shared custom hooks
├── lib                 # Pre-configured library exports
├── mock                # Mock data for static UI development
├── providers           # Application providers (QueryClient, Auth, etc.)
├── api                 # REST API integration
│   ├── queries         # GET requests
│   └── mutations       # POST, PUT, PATCH, DELETE requests
├── routes              # Route configuration
├── stores              # Global state management (Zustand, etc.)
├── test                # Test utilities and mock server
├── types               # TypeScript base types
└── utils               # Shared utility functions

This structure scales with your application. Each folder has a single responsibility, making it easy to locate code and onboard new team members.

Tech Stack Essentials

Choose tools that complement each other and solve real problems. Here's our recommended stack:

| Use Case | Package | Notes | |----------|---------|-------| | Framework | Next.js, Vite | Next.js for full-stack; Vite for standalone React | | API Client | Axios + TanStack React Query | Query handles caching, synchronization, and state | | Forms | React Hook Form + Zod | Minimal re-renders, type-safe validation | | State Management | Zustand | Lightweight, TypeScript-first, minimal boilerplate | | UI Components | shadcn/ui | Unstyled, composable, Tailwind-ready | | Tables | TanStack React Table | Headless, powerful, zero dependencies | | Charts | Recharts | React-native, responsive, production-ready | | Notifications | Sonner | Toast notifications, minimal setup | | Carousel | Swiper | swiperjs.com/react | | Animation | Framer Motion | Declarative animations, great DX | | Dates | date-fns | Tree-shakeable, functional API | | Markdown Editor | react-quill | Rich text editing without complexity |

Pro tip: Avoid over-engineering. Start with TanStack React Query + Zustand for most apps. Add tools only when you have the problem they solve.

Core Patterns

The cn() Utility: Twin Merge

Tailwind's utility-first approach creates naming conflicts. Use tailwind-merge with clsx to intelligently resolve them:

import { type ClassValue, clsx } from 'clsx';
import tailwindConfig from '../../tailwind.config';
import { extendTailwindMerge } from 'tailwind-merge';

export function cn(...inputs: ClassValue[]) {
  const twMerge = extendTailwindMerge({
    classGroups: {
      'font-size': [{ text: Object.keys(tailwindConfig.theme?.extend?.fontSize || []) }],
    },
  });

  return twMerge(clsx(inputs));
}

Use it to merge conditional styles without duplication:

<div className={cn('p-4 bg-white', isActive && 'bg-blue-500')} />

Block Heading Component

A flexible heading component that handles typography hierarchy and visual styling:

import * as React from 'react';
import { cn } from '@/utils/shade-cn';

type HeadingType = 'h1' | 'h2' | 'h3' | 'h4' | 'h5';

interface BlockHeadingProps extends React.ComponentPropsWithRef<HeadingType> {
  className?: string;
  type: HeadingType;
  hasLeftBorder?: boolean;
  hasBottomBorder?: boolean;
}

const BlockHeading = React.forwardRef<HTMLHeadingElement, BlockHeadingProps>(
  (
    {
      type: Tag,
      hasLeftBorder = true,
      hasBottomBorder = false,
      className,
      ...props
    },
    ref,
  ) => {
    return (
      <Tag
        ref={ref}
        className={cn([
          'relative text-content-heading',
          Tag === 'h1' && 'text-h2 md:pl-8 md:text-h3 lg:text-h1',
          Tag === 'h2' && 'text-h5 md:text-h4 lg:text-h2',
          Tag === 'h3' && 'text-h5 md:text-h4 lg:text-h3',
          Tag === 'h4' && 'text-h5 lg:text-h4',
          Tag === 'h5' && 'text-h6 lg:text-h5',
          hasLeftBorder &&
            'pl-6 before:absolute before:left-0 before:top-0 before:h-full before:w-1 before:bg-accent-400',
          hasBottomBorder && 'border-b border-[#E6E9E7] pb-5',
          className,
        ])}
      >
        {props.children}
      </Tag>
    );
  },
);

BlockHeading.displayName = 'BlockHeading';
export default BlockHeading;

Container: Responsive Layout

Centralize layout constraints and responsive behavior:

import * as React from 'react';
import { cn } from '@/utils/shade-cn';

interface ContainerProps extends React.ComponentPropsWithRef<'div'> {
  gridLayout?: boolean;
  fluid?: boolean;
}

const Container = React.forwardRef<HTMLDivElement, ContainerProps>(
  ({ className, gridLayout, fluid, children, ...props }, ref) => {
    return (
      <div
        className={cn([
          !fluid &&
            'mx-auto w-full max-w-[calc(1320px+32px)] px-layout-margin-sm md:px-layout-margin-md lg:px-layout-margin-lg 2xl:max-w-[77rem] 3xl:max-w-[89.5rem] 4xl:max-w-[1832px]',
          gridLayout && 'grid grid-cols-6 gap-x-5 md:grid-cols-12 lg:gap-x-4.5',
          className,
        ])}
        ref={ref}
        {...props}
      >
        {children}
      </div>
    );
  },
);

Container.displayName = 'Container';
export default Container;

Providers: Query Client Setup

Wrap your app with providers to enable caching, background sync, and data management:

'use client';

import React from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

interface ProvidersProps {
  children?: React.ReactNode;
}

const queryClient = new QueryClient();

const Providers: React.FC<ProvidersProps> = (props) => {
  return (
    <QueryClientProvider client={queryClient}>
      {props.children}
    </QueryClientProvider>
  );
};

export default Providers;

Responsive Images

Always use aspect ratio containers to prevent layout shift:

<div className='aspect-h-[619] aspect-w-[1350] relative mt-10 w-full rounded-lg'>
  <Image
    src={coverImage}
    alt='Church Overview Image'
    className='flex-shrink-0 rounded-lg object-cover'
    fill
  />
</div>

Dividers: Simple but Essential

A reusable divider component for visual separation:

import * as React from 'react';
import { cn } from '@/lib/cn';

interface SeparatorProps {
  orientation?: 'horizontal' | 'vertical';
  className?: string;
}

const Separator = React.forwardRef<HTMLDivElement, SeparatorProps>(
  ({ orientation, className, ...props }, ref) => {
    return (
      <div
        ref={ref}
        className={cn([
          'inline-block whitespace-pre shrink-0 bg-stroke',
          orientation === 'horizontal' ? 'h-[0.063rem] w-full' : 'h-full w-[0.063rem]',
          className,
        ])}
        {...props}
      />
    );
  },
);

Separator.displayName = 'Separator';
export default Separator;

Usage:

<Separator orientation='horizontal' className='my-5' />

Design System Configuration

Tailwind Configuration

A comprehensive design system in Tailwind config enables consistency and scalability:

/** @type {import('tailwindcss').Config} */
export default {
  darkMode: ['class'],
  content: [
    './pages/**/*.{ts,tsx}',
    './components/**/*.{ts,tsx}',
    './app/**/*.{ts,tsx}',
    './src/**/*.{ts,tsx}',
  ],
  theme: {
    extend: {
      boxShadow: {
        round: '0px 2px 2px 0px rgba(0, 0, 0, 0.10)',
        popup: '1rem 1rem 2rem 0 rgba(0, 0, 0, 0.24)',
      },
      fontFamily: {
        default: ['var(--font-inter)', 'sans-serif'],
        heading: ['var(--font-figtree)', 'sans-serif'],
      },
      spacing: {
        4.5: '1.125rem',
        5.5: '1.375rem',
        7.5: '1.875rem',
        // ... additional spacing tokens
      },
      colors: {
        content: {
          heading: 'var(--text-heading)',
          subtitle: 'var(--text-subtitle)',
          body: 'var(--text-body)',
          placeholder: 'var(--text-placeholder)',
          disabled: 'var(--text-disabled)',
        },
        state: {
          success: { base: 'var(--state-success-base)' },
          error: { base: 'var(--state-error-base)' },
          warning: { base: 'var(--state-warning-base)' },
        },
        // ... extensive color palette
      },
      fontSize: {
        h1: ['2.625rem', { lineHeight: '1.2', letterSpacing: '-2', fontWeight: '700' }],
        h2: ['2.188rem', { lineHeight: '1.2', fontWeight: '500', letterSpacing: '-2' }],
        // ... typography scale
      },
    },
  },
  plugins: [require('tailwindcss-animate'), require('@tailwindcss/aspect-ratio')],
};

Global CSS Variables

Define your design tokens at the root level:

@layer base {
  :root {
    /* Semantic colors */
    --accent-purple: #9f1eba;
    --accent-red: #d4145a;
    --accent-green: #22b573;

    /* State colors */
    --state-success-base: #3cc9ae;
    --state-error-base: #e64c4c;
    --state-warning-base: #fdc854;

    /* Text colors */
    --text-heading: #010101;
    --text-body: #010101bd;
    --text-placeholder: #8a8a8a;

    /* Stroke & spacing */
    --stroke-default: #d9e2e8;
    --black-80: #000000cc;
    --white-100: #ffffff;
  }
}

Responsive Email Templates

When building email templates, prioritize mobile-first responsive design:

<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <style>
    @media only screen and (max-width: 600px) {
      table { width: 100% !important; }
      td { padding: 10px !important; }
      img { max-width: 100% !important; height: auto !important; }
    }
  </style>
</head>
<body style="margin: 0; padding: 0; font-family: 'Open Sans', sans-serif;">
  <table align="center" border="0" cellpadding="0" cellspacing="0" style="border-collapse: collapse; max-width: 700px;">
    <!-- Email content here -->
  </table>
</body>
</html>

Key principles:

Useful Tools & Resources

Design & Assets:

Architecture & Best Practices:

Common Issues & Solutions

| Issue | CSS Solution | Tailwind Class | |-------|--------------|-----------------| | Image stretching | object-fit: cover; | object-cover | | Layout shift from images | Use aspect ratio containers | aspect-h-[619] aspect-w-[1350] | | Text overflow | text-overflow: ellipsis; | truncate or line-clamp-3 |

Debugging Git

When you need to reset to remote:

git reset --hard origin/dev

Key Takeaways

  1. Structure matters. A consistent folder layout scales with your team.
  2. Choose your tools wisely. Prefer proven, minimal libraries over everything-at-once frameworks.
  3. Design systems are infrastructure. Invest in Tailwind config and CSS variables early.
  4. Components are documentation. Build reusable patterns that teach your team how to build.
  5. Test responsiveness early. Mobile-first design catches layout issues before they ship.

Build with intention. Your future self will thank you.

← all posts
admond tamang · portfoliotheme: mono