You are viewing a preview of this lesson. Sign in to start learning
Back to React

Advanced Topics & Ecosystem

Expand skills with routing, state management, and modern tooling

Advanced Topics & Ecosystem

Master React's advanced patterns and ecosystem with free flashcards and spaced repetition practice. This lesson covers performance optimization, code splitting, server-side rendering, state management libraries, testing strategies, and the broader React ecosystemβ€”essential concepts for building production-ready applications.

Welcome to Advanced React πŸš€

You've mastered the fundamentals of Reactβ€”components, props, state, and hooks. Now it's time to level up! This lesson explores the advanced patterns and ecosystem tools that separate hobbyist React developers from professionals building scalable, performant applications.

We'll dive deep into performance optimization techniques, explore modern state management solutions beyond useState, understand server-side rendering (SSR) and static site generation (SSG), implement code splitting for faster load times, and navigate the rich ecosystem of libraries that complement React. By the end, you'll have the knowledge to architect enterprise-level React applications.

πŸ’‘ Tip: These advanced topics build on each other. Even if you don't need all of them immediately, understanding the landscape helps you make informed architectural decisions.


Core Concepts

1. Performance Optimization ⚑

React.memo and useMemo are your first line of defense against unnecessary re-renders. React's rendering is fast, but in complex applications with deep component trees, optimization becomes critical.

React.memo is a higher-order component that memoizes your component, preventing re-renders when props haven't changed:

const ExpensiveComponent = React.memo(({ data }) => {
  // Only re-renders if 'data' prop changes
  return <div>{data.map(item => <Item key={item.id} {...item} />)}</div>;
});

useMemo memoizes computed values:

const filteredList = useMemo(() => {
  return items.filter(item => item.price < maxPrice);
}, [items, maxPrice]); // Only recalculates when dependencies change

useCallback memoizes functions, preventing child components from re-rendering due to new function references:

const handleClick = useCallback(() => {
  setCount(c => c + 1);
}, []); // Function reference stays constant

🧠 Memory Device: M.C.V. - Memo for components, Callback for functions, useMemo for Values.

Virtual DOM Optimization: React uses a virtual DOM diffing algorithm, but you can help by:

  • Using stable key props (never use array indices for dynamic lists)
  • Keeping component state as local as possible
  • Lifting computationally expensive operations outside render

Code Splitting with React.lazy loads components only when needed:

const HeavyComponent = React.lazy(() => import('./HeavyComponent'));

function App() {
  return (
    <Suspense fallback={<LoadingSpinner />}>
      <HeavyComponent />
    </Suspense>
  );
}

πŸ’‘ Did you know? React.lazy automatically splits your bundle at the component level, but you can also split at the route level using React Router for maximum efficiency!


2. Server-Side Rendering (SSR) & Static Site Generation (SSG) 🌐

Why SSR/SSG Matter:

  • SEO: Search engines can index fully-rendered HTML
  • Performance: Users see content faster (First Contentful Paint)
  • Social Sharing: Meta tags work properly for link previews

Server-Side Rendering renders React components on the server for each request:

// Next.js example
export async function getServerSideProps(context) {
  const data = await fetchDataFromAPI();
  return { props: { data } }; // Passed to component as props
}

function Page({ data }) {
  return <div>{data.title}</div>;
}

Static Site Generation pre-renders pages at build time:

// Next.js example
export async function getStaticProps() {
  const data = await fetchDataFromAPI();
  return {
    props: { data },
    revalidate: 60 // Regenerate page every 60 seconds (ISR)
  };
}

Incremental Static Regeneration (ISR) combines the best of both worldsβ€”static generation with periodic updates.

ApproachWhen to UsePerformanceFreshness
CSR (Client-Side)Authenticated dashboardsSlower initial loadReal-time
SSRDynamic, SEO-critical pagesFast FCPAlways fresh
SSGBlogs, documentationFastestBuild-time
ISRE-commerce product pagesVery fastPeriodic updates

🌍 Real-world analogy: Think of SSG as a restaurant with a pre-made buffet (instant service), SSR as cooking each dish to order (fresh but takes time), and CSR as giving customers ingredients to cook themselves (flexibility but slowest).


3. State Management Ecosystem πŸ—‚οΈ

When Context API Isn't Enough: While React's Context API works for simple global state, large applications need more robust solutions.

Redux remains the most popular state management library:

// Redux Toolkit (modern Redux)
import { configureStore, createSlice } from '@reduxjs/toolkit';

const counterSlice = createSlice({
  name: 'counter',
  initialState: { value: 0 },
  reducers: {
    increment: state => { state.value += 1; },
    decrement: state => { state.value -= 1; }
  }
});

const store = configureStore({
  reducer: { counter: counterSlice.reducer }
});

Zustand offers a simpler API with less boilerplate:

import create from 'zustand';

const useStore = create(set => ({
  count: 0,
  increment: () => set(state => ({ count: state.count + 1 })),
  decrement: () => set(state => ({ count: state.count - 1 }))
}));

function Counter() {
  const { count, increment } = useStore();
  return <button onClick={increment}>{count}</button>;
}

Recoil provides atom-based state management from Facebook:

import { atom, useRecoilState } from 'recoil';

const countState = atom({
  key: 'countState',
  default: 0
});

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

Jotai is even lighter than Recoil:

import { atom, useAtom } from 'jotai';

const countAtom = atom(0);

function Counter() {
  const [count, setCount] = useAtom(countAtom);
  return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
STATE MANAGEMENT DECISION TREE

     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
     β”‚ Need global state?      β”‚
     β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
              β”‚
       β”Œβ”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”
       β”‚              β”‚
     β”Œβ”€β”΄β”€β”          β”Œβ”€β”΄β”€β”
     β”‚YESβ”‚          β”‚NO β”‚
     β””β”€β”¬β”€β”˜          β””β”€β”¬β”€β”˜
       β”‚              β”‚
       β–Ό              β–Ό
 β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
 β”‚ Complex app β”‚  β”‚ Use useState β”‚
 β”‚ with many   β”‚  β”‚ and props    β”‚
 β”‚ features?   β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
 β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜
        β”‚
   β”Œβ”€β”€β”€β”€β”΄β”€β”€β”€β”€β”
   β”‚         β”‚
 β”Œβ”€β”΄β”€β”     β”Œβ”€β”΄β”€β”
 β”‚YESβ”‚     β”‚NO β”‚
 β””β”€β”¬β”€β”˜     β””β”€β”¬β”€β”˜
   β”‚         β”‚
   β–Ό         β–Ό
β”Œβ”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚Reduxβ”‚  β”‚ Context β”‚
β”‚Zustandβ”‚ β”‚   or    β”‚
β””β”€β”€β”€β”€β”€β”˜  β”‚ Zustand β”‚
         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

4. Testing React Applications πŸ§ͺ

Testing Philosophy: Test behavior, not implementation. Users don't care about state variablesβ€”they care about what they see and can do.

React Testing Library is the modern standard:

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

test('increments counter on button click', () => {
  render(<Counter />);
  const button = screen.getByRole('button', { name: /increment/i });
  const count = screen.getByText(/count: 0/i);
  
  fireEvent.click(button);
  
  expect(screen.getByText(/count: 1/i)).toBeInTheDocument();
});

userEvent simulates real user interactions more accurately:

import userEvent from '@testing-library/user-event';

test('types into input field', async () => {
  render(<SearchForm />);
  const input = screen.getByRole('textbox');
  
  await userEvent.type(input, 'React');
  
  expect(input).toHaveValue('React');
});

Testing Hooks with renderHook:

import { renderHook, act } from '@testing-library/react';
import useCounter from './useCounter';

test('increments counter', () => {
  const { result } = renderHook(() => useCounter());
  
  act(() => {
    result.current.increment();
  });
  
  expect(result.current.count).toBe(1);
});

Integration Testing with MSW (Mock Service Worker):

import { rest } from 'msw';
import { setupServer } from 'msw/node';

const server = setupServer(
  rest.get('/api/user', (req, res, ctx) => {
    return res(ctx.json({ name: 'John' }));
  })
);

beforeAll(() => server.listen());
afterAll(() => server.close());

πŸ”§ Try this: Write tests that describe user stories: "As a user, when I click the submit button, I should see a success message."


5. Advanced Patterns 🎨

Higher-Order Components (HOCs) wrap components to add functionality:

function withAuth(Component) {
  return function AuthenticatedComponent(props) {
    const { user } = useAuth();
    
    if (!user) return <LoginPrompt />;
    
    return <Component {...props} user={user} />;
  };
}

const ProtectedPage = withAuth(Dashboard);

Render Props pattern:

function MouseTracker({ render }) {
  const [position, setPosition] = useState({ x: 0, y: 0 });
  
  useEffect(() => {
    const handleMove = (e) => setPosition({ x: e.clientX, y: e.clientY });
    window.addEventListener('mousemove', handleMove);
    return () => window.removeEventListener('mousemove', handleMove);
  }, []);
  
  return render(position);
}

// Usage
<MouseTracker render={({ x, y }) => (
  <h1>Mouse at ({x}, {y})</h1>
)} />

Compound Components pattern (like HTML's <select> and <option>):

const TabContext = createContext();

function Tabs({ children }) {
  const [activeTab, setActiveTab] = useState(0);
  return (
    <TabContext.Provider value={{ activeTab, setActiveTab }}>
      {children}
    </TabContext.Provider>
  );
}

function TabList({ children }) {
  return <div className="tab-list">{children}</div>;
}

function Tab({ index, children }) {
  const { activeTab, setActiveTab } = useContext(TabContext);
  return (
    <button
      className={activeTab === index ? 'active' : ''}
      onClick={() => setActiveTab(index)}
    >
      {children}
    </button>
  );
}

function TabPanel({ index, children }) {
  const { activeTab } = useContext(TabContext);
  return activeTab === index ? <div>{children}</div> : null;
}

Tabs.List = TabList;
Tabs.Tab = Tab;
Tabs.Panel = TabPanel;

// Usage
<Tabs>
  <Tabs.List>
    <Tabs.Tab index={0}>Home</Tabs.Tab>
    <Tabs.Tab index={1}>Profile</Tabs.Tab>
  </Tabs.List>
  <Tabs.Panel index={0}>Home content</Tabs.Panel>
  <Tabs.Panel index={1}>Profile content</Tabs.Panel>
</Tabs>

Error Boundaries catch JavaScript errors in component trees:

class ErrorBoundary extends React.Component {
  state = { hasError: false };
  
  static getDerivedStateFromError(error) {
    return { hasError: true };
  }
  
  componentDidCatch(error, errorInfo) {
    console.error('Error caught:', error, errorInfo);
  }
  
  render() {
    if (this.state.hasError) {
      return <h1>Something went wrong.</h1>;
    }
    return this.props.children;
  }
}

// Usage
<ErrorBoundary>
  <MyComponent />
</ErrorBoundary>

⚠️ Note: Error Boundaries only catch errors in rendering, lifecycle methods, and constructors. They don't catch errors in event handlersβ€”use try/catch for those.


6. React Ecosystem Tools πŸ› οΈ

Build Tools:

  • Vite: Lightning-fast dev server using native ES modules
  • Create React App: Official starter (being phased out for Next.js)
  • Webpack: Powerful bundler with extensive plugin ecosystem

Styling Solutions:

  • Tailwind CSS: Utility-first CSS framework
  • Styled-Components: CSS-in-JS with tagged templates
  • Emotion: Another CSS-in-JS library with better performance
  • CSS Modules: Scoped CSS without runtime overhead

UI Component Libraries:

  • Material-UI (MUI): Comprehensive Material Design components
  • Chakra UI: Accessible, composable components
  • Ant Design: Enterprise-grade UI library
  • Radix UI: Unstyled, accessible primitives
  • Headless UI: Unstyled components from Tailwind team

Form Libraries:

  • React Hook Form: Performant forms with minimal re-renders
  • Formik: Popular form library with validation
import { useForm } from 'react-hook-form';

function LoginForm() {
  const { register, handleSubmit, formState: { errors } } = useForm();
  
  const onSubmit = (data) => console.log(data);
  
  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register('email', { required: true, pattern: /^\S+@\S+$/i })} />
      {errors.email && <span>Valid email required</span>}
      <button type="submit">Submit</button>
    </form>
  );
}

Data Fetching:

  • TanStack Query (React Query): Caching, synchronization, and more
  • SWR: React Hooks for data fetching by Vercel
  • Apollo Client: GraphQL client
import { useQuery } from '@tanstack/react-query';

function UserProfile({ userId }) {
  const { data, isLoading, error } = useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetch(`/api/users/${userId}`).then(r => r.json())
  });
  
  if (isLoading) return <Spinner />;
  if (error) return <Error message={error.message} />;
  
  return <div>{data.name}</div>;
}

Animation Libraries:

  • Framer Motion: Declarative animations
  • React Spring: Physics-based animations
  • GSAP: Professional-grade animation library
import { motion } from 'framer-motion';

function AnimatedBox() {
  return (
    <motion.div
      initial={{ opacity: 0, y: -50 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ duration: 0.5 }}
    >
      Hello!
    </motion.div>
  );
}

Examples with Explanations

Example 1: Performance Optimization with React.memo and useCallback πŸ’¨

Problem: A parent component re-renders frequently, causing unnecessary re-renders of child components.

// ❌ Inefficient: Child re-renders every time Parent renders
function Parent() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('');
  
  const handleClick = () => {
    console.log('Button clicked');
  };
  
  return (
    <div>
      <input value={name} onChange={e => setName(e.target.value)} />
      <ExpensiveChild onClick={handleClick} />
      <button onClick={() => setCount(count + 1)}>Count: {count}</button>
    </div>
  );
}

function ExpensiveChild({ onClick }) {
  console.log('ExpensiveChild rendered');
  // Expensive computation
  const result = complexCalculation();
  return <button onClick={onClick}>{result}</button>;
}

Why it's inefficient: Every time you type in the input (changing name), ExpensiveChild re-renders even though its props appear unchanged. This is because handleClick is recreated on every render, giving it a new reference.

Solution:

// βœ… Optimized: Child only re-renders when necessary
function Parent() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('');
  
  // Memoize the callback so it doesn't change on every render
  const handleClick = useCallback(() => {
    console.log('Button clicked');
  }, []); // Empty dependency array means it never changes
  
  return (
    <div>
      <input value={name} onChange={e => setName(e.target.value)} />
      <ExpensiveChild onClick={handleClick} />
      <button onClick={() => setCount(count + 1)}>Count: {count}</button>
    </div>
  );
}

// Memoize the entire component
const ExpensiveChild = React.memo(({ onClick }) => {
  console.log('ExpensiveChild rendered');
  const result = complexCalculation();
  return <button onClick={onClick}>{result}</button>;
});

Key Takeaway: Use React.memo for components and useCallback for functions passed as props. Now ExpensiveChild only re-renders when its props actually change, not when Parent's other state updates.


Example 2: Code Splitting with React.lazy and Route-Based Splitting πŸ—‚οΈ

Problem: Your app's initial bundle is 2MB, making the first load painfully slow.

// ❌ Everything loads upfront
import Dashboard from './Dashboard';
import Settings from './Settings';
import Profile from './Profile';
import AdminPanel from './AdminPanel';

function App() {
  return (
    <Routes>
      <Route path="/dashboard" element={<Dashboard />} />
      <Route path="/settings" element={<Settings />} />
      <Route path="/profile" element={<Profile />} />
      <Route path="/admin" element={<AdminPanel />} />
    </Routes>
  );
}

Solution with lazy loading:

// βœ… Load components only when routes are visited
import { lazy, Suspense } from 'react';
import { Routes, Route } from 'react-router-dom';

const Dashboard = lazy(() => import('./Dashboard'));
const Settings = lazy(() => import('./Settings'));
const Profile = lazy(() => import('./Profile'));
const AdminPanel = lazy(() => import('./AdminPanel'));

function App() {
  return (
    <Suspense fallback={<LoadingSpinner />}>
      <Routes>
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/settings" element={<Settings />} />
        <Route path="/profile" element={<Profile />} />
        <Route path="/admin" element={<AdminPanel />} />
      </Routes>
    </Suspense>
  );
}

function LoadingSpinner() {
  return (
    <div className="spinner">
      <div className="bounce1"></div>
      <div className="bounce2"></div>
      <div className="bounce3"></div>
    </div>
  );
}

Result: Initial bundle drops to ~200KB. Each route's code loads on-demand. Users on the dashboard never download admin panel code!

Advanced: Prefetching:

// Prefetch a route when user hovers over a link
function NavigationLink({ to, children }) {
  const prefetch = () => {
    // Trigger the lazy import without rendering
    import('./Dashboard');
  };
  
  return (
    <Link to={to} onMouseEnter={prefetch}>
      {children}
    </Link>
  );
}

Example 3: Custom Hook for Data Fetching with TanStack Query πŸ“Š

Problem: Every component fetches data differently, with inconsistent loading states and caching.

// ❌ Manual data fetching in every component
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(data => {
        setUser(data);
        setLoading(false);
      })
      .catch(err => {
        setError(err);
        setLoading(false);
      });
  }, [userId]);
  
  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  
  return <div>{user.name}</div>;
}

Solution with TanStack Query:

// βœ… Declarative, cached, auto-refetching data
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';

// Custom hook for user data
function useUser(userId) {
  return useQuery({
    queryKey: ['user', userId],
    queryFn: async () => {
      const response = await fetch(`/api/users/${userId}`);
      if (!response.ok) throw new Error('Failed to fetch user');
      return response.json();
    },
    staleTime: 5 * 60 * 1000, // Data fresh for 5 minutes
    cacheTime: 10 * 60 * 1000, // Cache for 10 minutes
  });
}

// Custom hook for updating user
function useUpdateUser() {
  const queryClient = useQueryClient();
  
  return useMutation({
    mutationFn: async ({ userId, data }) => {
      const response = await fetch(`/api/users/${userId}`, {
        method: 'PATCH',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(data)
      });
      return response.json();
    },
    onSuccess: (data, variables) => {
      // Invalidate and refetch user query
      queryClient.invalidateQueries({ queryKey: ['user', variables.userId] });
    }
  });
}

// Component using the hooks
function UserProfile({ userId }) {
  const { data: user, isLoading, error } = useUser(userId);
  const updateUser = useUpdateUser();
  
  const handleNameChange = (newName) => {
    updateUser.mutate({ userId, data: { name: newName } });
  };
  
  if (isLoading) return <Spinner />;
  if (error) return <ErrorMessage error={error} />;
  
  return (
    <div>
      <h1>{user.name}</h1>
      <button onClick={() => handleNameChange('New Name')}>Update</button>
    </div>
  );
}

Benefits:

  • βœ… Automatic caching: Navigate away and backβ€”no refetch needed
  • βœ… Background refetching: Data stays fresh
  • βœ… Deduplication: Multiple components requesting same data make one request
  • βœ… Optimistic updates: UI updates before server responds
  • βœ… Error retry logic built-in

Example 4: Server-Side Rendering with Next.js 🌐

Problem: Your React SPA has poor SEO and slow initial load times.

// ❌ Client-side only: Bad for SEO
function BlogPost() {
  const [post, setPost] = useState(null);
  const { id } = useParams();
  
  useEffect(() => {
    fetch(`/api/posts/${id}`)
      .then(r => r.json())
      .then(setPost);
  }, [id]);
  
  if (!post) return <div>Loading...</div>;
  
  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </article>
  );
}

Solution with Next.js SSR:

// βœ… Server-side rendered: Great for SEO
// File: pages/posts/[id].js

export async function getServerSideProps(context) {
  const { id } = context.params;
  
  // This runs on the server for every request
  const response = await fetch(`https://api.example.com/posts/${id}`);
  const post = await response.json();
  
  return {
    props: { post } // Passed to component
  };
}

function BlogPost({ post }) {
  // post is already available on first render!
  return (
    <article>
      <Head>
        <title>{post.title}</title>
        <meta name="description" content={post.excerpt} />
        <meta property="og:title" content={post.title} />
        <meta property="og:image" content={post.image} />
      </Head>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </article>
  );
}

export default BlogPost;

Even better with Static Generation:

// βœ… Static generation with ISR: Best of both worlds

export async function getStaticPaths() {
  // Generate paths for the 100 most popular posts at build time
  const response = await fetch('https://api.example.com/posts/popular');
  const posts = await response.json();
  
  const paths = posts.map(post => ({
    params: { id: post.id.toString() }
  }));
  
  return {
    paths,
    fallback: 'blocking' // Other posts generated on first request
  };
}

export async function getStaticProps(context) {
  const { id } = context.params;
  const response = await fetch(`https://api.example.com/posts/${id}`);
  const post = await response.json();
  
  return {
    props: { post },
    revalidate: 60 // Regenerate page every 60 seconds if requested
  };
}

function BlogPost({ post }) {
  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </article>
  );
}

export default BlogPost;

Result:

  • βœ… Pages served as static HTML (instant load)
  • βœ… Automatic regeneration keeps content fresh
  • βœ… Perfect for SEO (search engines see full HTML)
  • βœ… Social media preview cards work correctly

Common Mistakes ⚠️

1. Over-optimizing Too Early

// ❌ Premature optimization
const MyComponent = React.memo(({ text }) => {
  const memoizedValue = useMemo(() => text.toUpperCase(), [text]);
  const handleClick = useCallback(() => console.log(text), [text]);
  return <div onClick={handleClick}>{memoizedValue}</div>;
});

Why it's wrong: This component is so simple that the optimization overhead costs more than the re-render. React is fastβ€”optimize when you measure a problem, not preemptively.

Better:

// βœ… Keep it simple until you need optimization
function MyComponent({ text }) {
  return <div onClick={() => console.log(text)}>{text.toUpperCase()}</div>;
}

2. Forgetting Dependencies in useMemo/useCallback

// ❌ Stale closure: count is always 0
function Counter() {
  const [count, setCount] = useState(0);
  
  const logCount = useCallback(() => {
    console.log(count);
  }, []); // Missing dependency!
  
  return <button onClick={logCount}>Log Count: {count}</button>;
}

Fix: Always include all dependencies from the component scope:

// βœ… Proper dependencies
const logCount = useCallback(() => {
  console.log(count);
}, [count]);

3. Not Handling Loading and Error States in Data Fetching

// ❌ Incomplete: What if the fetch fails?
function UserList() {
  const [users, setUsers] = useState([]);
  
  useEffect(() => {
    fetch('/api/users')
      .then(r => r.json())
      .then(setUsers);
  }, []);
  
  return users.map(u => <div key={u.id}>{u.name}</div>);
}

Fix: Always handle loading, error, and empty states:

// βœ… Complete state handling
function UserList() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    fetch('/api/users')
      .then(r => r.json())
      .then(data => {
        setUsers(data);
        setLoading(false);
      })
      .catch(err => {
        setError(err.message);
        setLoading(false);
      });
  }, []);
  
  if (loading) return <Spinner />;
  if (error) return <div>Error: {error}</div>;
  if (users.length === 0) return <div>No users found</div>;
  
  return users.map(u => <div key={u.id}>{u.name}</div>);
}

4. Improper Error Boundary Placement

// ❌ Single error boundary catches too much
function App() {
  return (
    <ErrorBoundary>
      <Navbar />
      <MainContent />
      <Sidebar />
      <Footer />
    </ErrorBoundary>
  );
}

Problem: If Navbar crashes, the entire app disappears!

Fix: Use granular error boundaries:

// βœ… Isolated error boundaries
function App() {
  return (
    <>
      <ErrorBoundary fallback={<SimpleNav />}>
        <Navbar />
      </ErrorBoundary>
      <ErrorBoundary fallback={<ErrorMessage />}>
        <MainContent />
      </ErrorBoundary>
      <ErrorBoundary fallback={<div>Sidebar unavailable</div>}>
        <Sidebar />
      </ErrorBoundary>
      <Footer />
    </>
  );
}

5. Mixing SSR and Client-Only APIs

// ❌ Crashes during SSR: window is undefined on server
function Component() {
  const width = window.innerWidth;
  return <div>Width: {width}</div>;
}

Fix: Check if you're on the client:

// βœ… Safe for SSR
function Component() {
  const [width, setWidth] = useState(0);
  
  useEffect(() => {
    setWidth(window.innerWidth);
    
    const handleResize = () => setWidth(window.innerWidth);
    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);
  
  return <div>Width: {width}</div>;
}

Key Takeaways 🎯

  1. Performance: Don't optimize prematurely. Profile first, then use React.memo, useMemo, and useCallback strategically.

  2. Code Splitting: Use React.lazy for route-based splitting. Your users shouldn't download code they'll never use.

  3. SSR/SSG: Choose based on your content:

    • SSG for static content (blogs, docs)
    • SSR for dynamic, SEO-critical pages
    • CSR for authenticated dashboards
    • ISR for the best of both worlds
  4. State Management: Start simple (useState/Context), upgrade when needed (Zustand/Redux). Don't reach for Redux on day one.

  5. Testing: Test behavior, not implementation. Use React Testing Library and write tests users would understand.

  6. Error Handling: Always use Error Boundaries, and make them granular. One component's crash shouldn't take down your entire app.

  7. Ecosystem: The React ecosystem is vast. Choose tools that solve your actual problems, not what's trendy.

  8. Data Fetching: Consider TanStack Query or SWR instead of managing loading/error states manually everywhere.

πŸ’‘ Final Tip: The React ecosystem moves fast. Focus on understanding the underlying patterns (composition, unidirectional data flow, declarative UI) rather than memorizing specific APIs. Patterns stay relevant; APIs change.


πŸ“š Further Study


πŸ“‹ Quick Reference Card: Advanced React Essentials

Concept Tool/Pattern When to Use
Performance React.memo, useMemo, useCallback Expensive renders, deep trees, frequent updates
Code Splitting React.lazy + Suspense Large bundles, route-based splitting
SSR Next.js getServerSideProps SEO-critical, dynamic content
SSG Next.js getStaticProps Blogs, docs, mostly static content
State Management Zustand / Redux Toolkit Complex global state, large apps
Data Fetching TanStack Query REST APIs, caching, background sync
Testing React Testing Library Unit & integration tests
Error Handling Error Boundaries Graceful failure, error recovery
Forms React Hook Form Complex forms, validation
Animation Framer Motion UI transitions, micro-interactions

Optimization Decision Tree:

1. Is there a performance problem? β†’ NO β†’ Don't optimize
   ↓ YES
2. Profile with React DevTools
   ↓
3. Identify expensive components
   ↓
4. Apply targeted optimization:
   - React.memo for component
   - useMemo for expensive calculations
   - useCallback for stable function references

Memory Aid - M.C.V.: Memo components, Callback functions, useMemo Values