React and Next.js Best Practices for 2024

December 10, 2024 (9mo ago)

Building React applications with Next.js has become the go-to choice for many developers. After working with these technologies extensively, I've compiled some best practices that can significantly improve your development experience and application performance.

Component Architecture

1. Keep Components Small and Focused

// ❌ Avoid: Large, complex components
const UserProfile = () => {
  // 200+ lines of mixed concerns
  return <div>...</div>;
};
 
// ✅ Better: Focused, single-responsibility components
const UserProfile = () => {
  return (
    <div className="user-profile">
      <UserAvatar />
      <UserInfo />
      <UserActions />
    </div>
  );
};

2. Use Custom Hooks for Logic Reuse

// Custom hook for data fetching
const useUserData = (userId: string) => {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
 
  useEffect(() => {
    const fetchUser = async () => {
      try {
        setLoading(true);
        const userData = await api.getUser(userId);
        setUser(userData);
      } catch (err) {
        setError(err);
      } finally {
        setLoading(false);
      }
    };
 
    fetchUser();
  }, [userId]);
 
  return { user, loading, error };
};

Next.js Specific Patterns

1. Leverage App Router Features

// app/dashboard/page.tsx
import { Suspense } from 'react';
 
export default function DashboardPage() {
  return (
    <div>
      <h1>Dashboard</h1>
      <Suspense fallback={<DashboardSkeleton />}>
        <DashboardContent />
      </Suspense>
    </div>
  );
}
 
// app/dashboard/loading.tsx
export default function Loading() {
  return <DashboardSkeleton />;
}

2. Optimize Images and Fonts

import Image from "next/image";
import { Inter } from "next/font/google";
 
const inter = Inter({ subsets: ["latin"] });
 
const OptimizedComponent = () => {
  return (
    <div className={inter.className}>
      <Image
        src="/hero-image.jpg"
        alt="Hero image"
        width={800}
        height={600}
        priority
        placeholder="blur"
        blurDataURL="data:image/jpeg;base64,..."
      />
    </div>
  );
};

Performance Optimization

1. Implement Proper Caching

// app/api/users/route.ts
export async function GET() {
  const users = await db.users.findMany();
 
  return Response.json(users, {
    headers: {
      "Cache-Control": "public, s-maxage=60, stale-while-revalidate=300",
    },
  });
}

2. Use Dynamic Imports for Code Splitting

import dynamic from "next/dynamic";
 
const HeavyComponent = dynamic(() => import("./HeavyComponent"), {
  loading: () => <p>Loading...</p>,
  ssr: false,
});

State Management

1. Choose the Right Tool for the Job

// For simple state: useState
const [count, setCount] = useState(0);
 
// For complex state: useReducer
const [state, dispatch] = useReducer(reducer, initialState);
 
// For server state: React Query or SWR
const { data, error, isLoading } = useQuery({
  queryKey: ["users"],
  queryFn: fetchUsers,
});

Error Handling

1. Implement Error Boundaries

"use client";
 
import { ErrorBoundary } from "react-error-boundary";
 
function ErrorFallback({ error, resetErrorBoundary }) {
  return (
    <div role="alert">
      <h2>Something went wrong:</h2>
      <pre>{error.message}</pre>
      <button onClick={resetErrorBoundary}>Try again</button>
    </div>
  );
}
 
export default function App() {
  return (
    <ErrorBoundary FallbackComponent={ErrorFallback}>
      <MyApp />
    </ErrorBoundary>
  );
}

Testing Strategies

1. Component Testing with Testing Library

import { render, screen, fireEvent } from "@testing-library/react";
import { Button } from "./Button";
 
test("button calls onClick when clicked", () => {
  const handleClick = jest.fn();
  render(<Button onClick={handleClick}>Click me</Button>);
 
  fireEvent.click(screen.getByText("Click me"));
  expect(handleClick).toHaveBeenCalledTimes(1);
});

Conclusion

These practices have helped me build more maintainable and performant React applications. The key is to start with the fundamentals and gradually adopt more advanced patterns as your application grows.

Remember, best practices are guidelines, not rules. Always consider your specific use case and team context when implementing these patterns.