DEV Community

Cover image for React Concepts Explained: Part 2 - Advanced Patterns
Dehemi Fabio
Dehemi Fabio

Posted on

React Concepts Explained: Part 2 - Advanced Patterns

Welcome back! In Part 1, we covered React's core fundamentals—components, props, state, and the basics of hooks. Now it's time to level up!

In Part 2, we'll explore advanced patterns that help you build production-ready React applications. These concepts might seem complex at first, but they solve real problems you'll encounter as your apps grow.

What you'll learn:

  • How to work with external systems using effects
  • When and how to access the DOM directly
  • Solving prop drilling with Context
  • Advanced rendering patterns with Portals
  • Handling loading and error states gracefully

Let's dive in!


1. Effects: Stepping Outside React

What are effects?

Effects (or side effects) are operations that interact with systems outside of React—like browser APIs, HTTP requests, timers, or third-party libraries.

Examples of side effects:

  • Fetching data from an API
  • Setting up subscriptions (WebSockets, event listeners)
  • Manually changing the DOM
  • Logging to analytics
  • Setting timers (setTimeout, setInterval)

Where to Run Effects

Option 1: Event Handlers (Preferred)

Whenever possible, run side effects in event handlers:

function SearchButton() {
  const handleClick = () => {
    // Side effect in response to user action
    fetch('/api/search')
      .then(res => res.json())
      .then(data => console.log(data));
  };

  return <button onClick={handleClick}>Search</button>;
}
Enter fullscreen mode Exit fullscreen mode

Why prefer event handlers?

They run only when the user takes action, making your app more predictable and easier to debug.

Option 2: The useEffect Hook

When you need to run effects outside of user interactions (like loading data when a component appears), use the useEffect hook:

import { useState, useEffect } from 'react';

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    // This runs after the component renders
    setLoading(true);

    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(data => {
        setUser(data);
        setLoading(false);
      });
  }, [userId]); // Re-run when userId changes

  if (loading) return <div>Loading...</div>;
  return <div>{user?.name}</div>;
}
Enter fullscreen mode Exit fullscreen mode

How useEffect works:

useEffect(() => {
  // Effect code runs after render

  return () => {
    // Cleanup code (optional)
    // Runs before the component unmounts
    // Or before the effect runs again
  };
}, [dependencies]); // When to re-run the effect
Enter fullscreen mode Exit fullscreen mode

Understanding Dependencies

The dependency array controls when your effect runs:

Empty array [] - Run once when component mounts:

useEffect(() => {
  console.log('Component mounted!');
}, []); // Only runs once
Enter fullscreen mode Exit fullscreen mode

With dependencies [value] - Run when dependencies change:

useEffect(() => {
  console.log('Count changed:', count);
}, [count]); // Runs when count changes
Enter fullscreen mode Exit fullscreen mode

No array - Run after every render (usually not what you want):

useEffect(() => {
  console.log('Every render!'); // Be careful with this!
});
Enter fullscreen mode Exit fullscreen mode

Cleanup Functions

Some effects need cleanup to prevent memory leaks:

useEffect(() => {
  // Set up subscription
  const subscription = subscribeToChat(roomId);

  // Cleanup function
  return () => {
    subscription.unsubscribe();
  };
}, [roomId]);
Enter fullscreen mode Exit fullscreen mode

Common cleanup scenarios:

  • Canceling network requests
  • Removing event listeners
  • Clearing timers
  • Unsubscribing from services

Complete example with cleanup:

function Timer() {
  const [seconds, setSeconds] = useState(0);

  useEffect(() => {
    const interval = setInterval(() => {
      setSeconds(s => s + 1);
    }, 1000);

    // Cleanup: clear interval when component unmounts
    return () => clearInterval(interval);
  }, []); // Empty array = run once on mount

  return <div>Seconds: {seconds}</div>;
}
Enter fullscreen mode Exit fullscreen mode

2. Refs: Accessing the Real DOM

What are refs?

Refs let you reference actual DOM elements or store mutable values that persist between renders without causing re-renders.

Using useRef for DOM Access

Basic example:

import { useRef } from 'react';

function TextInput() {
  const inputRef = useRef(null);

  const focusInput = () => {
    inputRef.current.focus(); // Access the real DOM element
  };

  return (
    <>
      <input ref={inputRef} type="text" />
      <button onClick={focusInput}>Focus Input</button>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

How it works:

  1. useRef(null) creates a ref object
  2. ref={inputRef} attaches the ref to the DOM element
  3. inputRef.current contains the actual DOM element

Common Use Cases for Refs

1. Managing focus:

function SearchForm() {
  const searchRef = useRef(null);

  useEffect(() => {
    // Focus search input when component mounts
    searchRef.current?.focus();
  }, []);

  return <input ref={searchRef} type="search" />;
}
Enter fullscreen mode Exit fullscreen mode

2. Measuring elements:

function MeasureComponent() {
  const divRef = useRef(null);

  const getSize = () => {
    const width = divRef.current.offsetWidth;
    const height = divRef.current.offsetHeight;
    console.log('Size:', width, height);
  };

  return (
    <>
      <div ref={divRef}>Measure me!</div>
      <button onClick={getSize}>Get Size</button>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

3. Integrating third-party libraries:

function VideoPlayer({ src }) {
  const videoRef = useRef(null);

  useEffect(() => {
    // Use video.js or another library
    const player = videojs(videoRef.current);

    return () => player.dispose();
  }, []);

  return <video ref={videoRef} src={src} />;
}
Enter fullscreen mode Exit fullscreen mode

Using Refs to Store Values

Refs can also store mutable values that don't cause re-renders:

function Stopwatch() {
  const [time, setTime] = useState(0);
  const intervalRef = useRef(null);

  const start = () => {
    intervalRef.current = setInterval(() => {
      setTime(t => t + 1);
    }, 1000);
  };

  const stop = () => {
    clearInterval(intervalRef.current);
  };

  return (
    <div>
      <p>Time: {time}s</p>
      <button onClick={start}>Start</button>
      <button onClick={stop}>Stop</button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Refs vs State:

  • State: Triggers re-renders when updated
  • Refs: Don't trigger re-renders, persist between renders

When NOT to Use Refs

❌ Don't use refs for things React can handle normally:

  • Updating text content (use state instead)
  • Showing/hiding elements (use conditional rendering)
  • Handling events (use event handlers)

✅ Use refs when you need to:

  • Focus inputs
  • Measure DOM elements
  • Integrate with non-React libraries
  • Store mutable values without re-rendering

3. Context: Avoiding Prop Drilling

The problem: Prop Drilling

In large apps, passing props through many nested components gets tedious:

function App() {
  const user = { name: 'John', theme: 'dark' };

  return <Layout user={user} theme={user.theme} />;
}

function Layout({ user, theme }) {
  return <Header user={user} theme={theme} />;
}

function Header({ user, theme }) {
  return <Navigation user={user} theme={theme} />;
}

function Navigation({ user, theme }) {
  return <UserMenu user={user} theme={theme} />;
}

function UserMenu({ user, theme }) {
  // Finally used here!
  return <div className={theme}>{user.name}</div>;
}
Enter fullscreen mode Exit fullscreen mode

See how user and theme pass through components that don't even use them? This is prop drilling.

The Solution: Context API

Context lets you share data across your component tree without passing props manually at every level.

Step 1: Create Context

import { createContext } from 'react';

const ThemeContext = createContext('light'); // Default value
Enter fullscreen mode Exit fullscreen mode

Step 2: Provide the Context

function App() {
  const [theme, setTheme] = useState('dark');

  return (
    <ThemeContext.Provider value={theme}>
      <Layout />
    </ThemeContext.Provider>
  );
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Consume the Context

import { useContext } from 'react';

function UserMenu() {
  const theme = useContext(ThemeContext);

  return <div className={theme}>Menu</div>;
}
Enter fullscreen mode Exit fullscreen mode

Complete Context Example

// 1. Create context with default value
const UserContext = createContext(null);

// 2. Create provider component (optional but recommended)
function UserProvider({ children }) {
  const [user, setUser] = useState(null);

  const login = (userData) => setUser(userData);
  const logout = () => setUser(null);

  return (
    <UserContext.Provider value={{ user, login, logout }}>
      {children}
    </UserContext.Provider>
  );
}

// 3. Custom hook for easier usage (optional)
function useUser() {
  const context = useContext(UserContext);
  if (!context) {
    throw new Error('useUser must be used within UserProvider');
  }
  return context;
}

// 4. Use in your app
function App() {
  return (
    <UserProvider>
      <Header />
      <MainContent />
    </UserProvider>
  );
}

function Header() {
  const { user, logout } = useUser();

  return (
    <header>
      {user ? (
        <>
          <span>Welcome, {user.name}!</span>
          <button onClick={logout}>Logout</button>
        </>
      ) : (
        <span>Please log in</span>
      )}
    </header>
  );
}
Enter fullscreen mode Exit fullscreen mode

When to Use Context

Good use cases:

  • Theme/dark mode
  • User authentication data
  • Language/localization
  • Global app settings

When NOT to use Context:

  • Don't use it just to avoid passing props one level down
  • For frequently changing values, consider state management libraries
  • Context re-renders all consumers when value changes

4. Portals: Breaking Out of the Component Tree

What are portals?

Portals let you render a component into a different part of the DOM tree, outside its parent component's DOM hierarchy.

Why use them?

Some components (modals, tooltips, dropdowns) need to "escape" their parent's CSS constraints (like overflow: hidden or z-index).

Creating a Portal

import { createPortal } from 'react-dom';

function Modal({ isOpen, children }) {
  if (!isOpen) return null;

  return createPortal(
    <div className="modal-overlay">
      <div className="modal-content">
        {children}
      </div>
    </div>,
    document.getElementById('modal-root') // Target element
  );
}
Enter fullscreen mode Exit fullscreen mode

HTML setup:

<body>
  <div id="root"></div>
  <div id="modal-root"></div> <!-- Portal target -->
</body>
Enter fullscreen mode Exit fullscreen mode

Complete Modal Example

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

  return (
    <div style={{ overflow: 'hidden', position: 'relative' }}>
      <h1>My App</h1>
      <button onClick={() => setShowModal(true)}>
        Open Modal
      </button>

      <Modal isOpen={showModal} onClose={() => setShowModal(false)}>
        <h2>Modal Title</h2>
        <p>Modal content goes here</p>
        <button onClick={() => setShowModal(false)}>Close</button>
      </Modal>
    </div>
  );
}

function Modal({ isOpen, onClose, children }) {
  if (!isOpen) return null;

  return createPortal(
    <div className="modal-backdrop" onClick={onClose}>
      <div 
        className="modal-content" 
        onClick={(e) => e.stopPropagation()}
      >
        {children}
      </div>
    </div>,
    document.body // Render to body instead of modal-root
  );
}
Enter fullscreen mode Exit fullscreen mode

Common Portal Use Cases

  1. Modals/Dialogs
  2. Tooltips
  3. Dropdown menus
  4. Notifications/Toast messages
  5. Floating UI elements

Key benefit: Even though the modal renders outside the component tree in the DOM, it still behaves like a normal React component (receives props, can use context, etc.).


5. Suspense: Handling Loading States

What is Suspense?

Suspense is a component that helps you handle loading states elegantly while components or data are loading.

Basic Suspense Usage

import { Suspense, lazy } from 'react';

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <UserProfile />
    </Suspense>
  );
}
Enter fullscreen mode Exit fullscreen mode

How it works:

  • While UserProfile is loading, Suspense shows the fallback component
  • Once loaded, Suspense displays the actual component
  • Provides a better user experience than showing nothing

Lazy Loading Components

One of the most common uses of Suspense is lazy loading—loading components only when they're needed:

import { lazy, Suspense } from 'react';

// Instead of: import HeavyComponent from './HeavyComponent'
const HeavyComponent = lazy(() => import('./HeavyComponent'));

function App() {
  const [show, setShow] = useState(false);

  return (
    <div>
      <button onClick={() => setShow(true)}>
        Load Heavy Component
      </button>

      {show && (
        <Suspense fallback={<div>Loading component...</div>}>
          <HeavyComponent />
        </Suspense>
      )}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Benefits of lazy loading:

  • Smaller initial bundle size
  • Faster initial page load
  • Load features only when users need them

Multiple Suspense Boundaries

You can have multiple Suspense boundaries for fine-grained loading states:

function Dashboard() {
  return (
    <div>
      <Suspense fallback={<div>Loading header...</div>}>
        <Header />
      </Suspense>

      <Suspense fallback={<div>Loading main content...</div>}>
        <MainContent />
      </Suspense>

      <Suspense fallback={<div>Loading sidebar...</div>}>
        <Sidebar />
      </Suspense>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Nested Suspense

function App() {
  return (
    <Suspense fallback={<PageLoader />}>
      <Layout>
        <Suspense fallback={<SidebarLoader />}>
          <Sidebar />
        </Suspense>

        <Suspense fallback={<ContentLoader />}>
          <Content />
        </Suspense>
      </Layout>
    </Suspense>
  );
}
Enter fullscreen mode Exit fullscreen mode

Best practices:

  • Use specific fallbacks that match the content being loaded
  • Consider skeleton screens instead of spinners
  • Place Suspense boundaries at logical component boundaries

6. Error Boundaries: Catching React Errors

The problem:

If a component throws an error during rendering, it can crash your entire app and show a blank screen!

function BrokenComponent() {
  const user = null;
  return <div>{user.name}</div>; // 💥 Error! Cannot read 'name' of null
}
Enter fullscreen mode Exit fullscreen mode

The Solution: Error Boundaries

Error boundaries are special components that catch JavaScript errors anywhere in their child component tree and display a fallback UI.

Important: Error boundaries must currently be class components. React doesn't yet support error boundaries with hooks.

import React from 'react';

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null };
  }

  static getDerivedStateFromError(error) {
    // Update state so next render shows fallback UI
    return { hasError: true, error };
  }

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

  render() {
    if (this.state.hasError) {
      // Fallback UI
      return (
        <div className="error-container">
          <h1>Something went wrong</h1>
          <p>{this.state.error?.message}</p>
          <button onClick={() => window.location.reload()}>
            Reload Page
          </button>
        </div>
      );
    }

    return this.props.children;
  }
}
Enter fullscreen mode Exit fullscreen mode

Using Error Boundaries

function App() {
  return (
    <ErrorBoundary>
      <Header />
      <ErrorBoundary>
        <MainContent />
      </ErrorBoundary>
      <Footer />
    </ErrorBoundary>
  );
}
Enter fullscreen mode Exit fullscreen mode

Multiple error boundaries:

function Dashboard() {
  return (
    <div>
      {/* If Sidebar crashes, only it shows error */}
      <ErrorBoundary fallback={<div>Sidebar failed to load</div>}>
        <Sidebar />
      </ErrorBoundary>

      {/* MainContent has its own error boundary */}
      <ErrorBoundary fallback={<div>Content failed to load</div>}>
        <MainContent />
      </ErrorBoundary>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

What Error Boundaries Catch

Do catch:

  • Errors during rendering
  • Errors in lifecycle methods
  • Errors in constructors

Don't catch:

  • Event handler errors (use try/catch instead)
  • Asynchronous code (setTimeout, promises)
  • Server-side rendering errors
  • Errors in the error boundary itself

Handling event handler errors:

function MyComponent() {
  const handleClick = () => {
    try {
      // Risky code
      doSomethingRisky();
    } catch (error) {
      console.error('Button click error:', error);
      // Handle error appropriately
    }
  };

  return <button onClick={handleClick}>Click Me</button>;
}
Enter fullscreen mode Exit fullscreen mode

Reusable Error Boundary with Custom Fallback

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError() {
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    console.error('Error:', error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      // Use custom fallback if provided
      return this.props.fallback || <h1>Something went wrong.</h1>;
    }

    return this.props.children;
  }
}

// Usage with custom fallback
<ErrorBoundary fallback={<CustomErrorPage />}>
  <App />
</ErrorBoundary>
Enter fullscreen mode Exit fullscreen mode

Best Practices

  1. Place error boundaries strategically:

    • Around route components
    • Around independent features
    • At the top level of your app
  2. Log errors to monitoring services:

   componentDidCatch(error, errorInfo) {
     logErrorToMyService(error, errorInfo);
   }
Enter fullscreen mode Exit fullscreen mode
  1. Provide helpful fallback UIs:

    • Explain what happened
    • Offer a way to recover (reload, go back)
    • Include contact/support information
  2. Don't overuse:

    • Don't wrap every single component
    • Group related components under one boundary

7. Advanced Hooks: useReducer, useMemo, useCallback

useReducer: Managing Complex State

When state logic becomes complex, useReducer is often clearer than useState:

import { useReducer } from 'react';

// Reducer function
function cartReducer(state, action) {
  switch (action.type) {
    case 'ADD_ITEM':
      return [...state, action.item];
    case 'REMOVE_ITEM':
      return state.filter(item => item.id !== action.id);
    case 'CLEAR_CART':
      return [];
    default:
      return state;
  }
}

function ShoppingCart() {
  const [cart, dispatch] = useReducer(cartReducer, []);

  const addItem = (item) => {
    dispatch({ type: 'ADD_ITEM', item });
  };

  const removeItem = (id) => {
    dispatch({ type: 'REMOVE_ITEM', id });
  };

  return (
    <div>
      <h2>Cart ({cart.length} items)</h2>
      <button onClick={() => addItem({ id: 1, name: 'Product' })}>
        Add Item
      </button>
      <button onClick={() => dispatch({ type: 'CLEAR_CART' })}>
        Clear Cart
      </button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

When to use useReducer:

  • Complex state logic with multiple sub-values
  • Next state depends on previous state
  • Need to manage related state together
  • Want to optimize performance for deep component trees

useMemo: Memoizing Expensive Calculations

useMemo caches the result of expensive calculations:

import { useMemo, useState } from 'react';

function ExpensiveComponent({ items }) {
  const [filter, setFilter] = useState('');

  // Only recalculate when items or filter changes
  const filteredItems = useMemo(() => {
    console.log('Filtering items...');
    return items.filter(item => 
      item.name.toLowerCase().includes(filter.toLowerCase())
    );
  }, [items, filter]);

  return (
    <div>
      <input 
        value={filter} 
        onChange={(e) => setFilter(e.target.value)} 
      />
      <ul>
        {filteredItems.map(item => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Without useMemo:

The filtering happens on every render, even if items and filter haven't changed.

With useMemo:

Filtering only happens when dependencies (items or filter) change.

useCallback: Memoizing Functions

useCallback caches function instances between renders:

import { useCallback, useState } from 'react';

function TodoApp() {
  const [todos, setTodos] = useState([]);

  // This function is recreated on every render without useCallback
  const addTodo = useCallback((text) => {
    setTodos(prev => [...prev, { id: Date.now(), text }]);
  }, []); // Empty deps = function never changes

  const removeTodo = useCallback((id) => {
    setTodos(prev => prev.filter(todo => todo.id !== id));
  }, []);

  return (
    <div>
      <TodoForm onSubmit={addTodo} />
      <TodoList todos={todos} onRemove={removeTodo} />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Why use useCallback?

  • Prevent unnecessary re-renders of child components
  • Useful when passing callbacks to optimized child components using React.memo

Example with React.memo:

const TodoItem = React.memo(({ todo, onRemove }) => {
  console.log('TodoItem rendered:', todo.text);
  return (
    <li>
      {todo.text}
      <button onClick={() => onRemove(todo.id)}>Remove</button>
    </li>
  );
});
Enter fullscreen mode Exit fullscreen mode

Performance tip: Don't use useMemo and useCallback everywhere! Only use them when:

  • You have performance issues
  • Calculations are genuinely expensive
  • Child components are wrapped in React.memo

Putting It All Together: Advanced Patterns Summary

Let's see how these concepts work together in a real-world example:

// User context for app-wide user data
const UserContext = createContext(null);

// Error boundary at the app level
class AppErrorBoundary extends React.Component {
  state = { hasError: false };

  static getDerivedStateFromError() {
    return { hasError: true };
  }

  render() {
    if (this.state.hasError) {
      return <ErrorPage />;
    }
    return this.props.children;
  }
}

function App() {
  const [user, setUser] = useState(null);

  // Effect: Load user on mount
  useEffect(() => {
    fetchUser().then(setUser);
  }, []);

  return (
    <AppErrorBoundary>
      <UserContext.Provider value={user}>
        <Suspense fallback={<PageLoader />}>
          <Router />
        </Suspense>
      </UserContext.Provider>
    </AppErrorBoundary>
  );
}

function Dashboard() {
  const user = useContext(UserContext);
  const [showModal, setShowModal] = useState(false);
  const modalRef = useRef(null);

  // Effect: Focus modal when it opens
  useEffect(() => {
    if (showModal && modalRef.current) {
      modalRef.current.focus();
    }
  }, [showModal]);

  return (
    <div>
      <h1>Welcome, {user?.name}</h1>
      <button onClick={() => setShowModal(true)}>
        Open Settings
      </button>

      {/* Portal: Render modal outside component tree */}
      {showModal && createPortal(
        <div className="modal" ref={modalRef}>
          <ErrorBoundary fallback={<div>Settings failed</div>}>
            <Suspense fallback={<div>Loading settings...</div>}>
              <SettingsPanel onClose={() => setShowModal(false)} />
            </Suspense>
          </ErrorBoundary>
        </div>,
        document.body
      )}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

This example demonstrates:

  • ✅ Context for global state
  • ✅ Error boundaries for resilience
  • ✅ Suspense for loading states
  • ✅ Portals for modals
  • ✅ useEffect for side effects
  • ✅ useRef for DOM access

Quick Reference: Advanced Concepts Checklist

Here's what we covered in Part 2:

useEffect - Side effects and lifecycle

useRef - DOM access and mutable values

Context - Global state without prop drilling

Portals - Rendering outside component tree

Suspense - Elegant loading states

Error Boundaries - Catching and handling errors

useReducer - Complex state management

useMemo - Memoizing expensive calculations

useCallback - Memoizing functions


Practice Challenge 🎯

Build a complete feature using these advanced patterns:

User Settings Panel

Requirements:

  • Use Context for theme (light/dark mode)
  • Render settings modal with a Portal
  • Use Suspense to lazy load settings content
  • Wrap in an Error Boundary
  • Use useEffect to save settings to localStorage
  • Use useRef to focus first input when modal opens
  • Use useCallback for event handlers passed to child components

Bonus:

  • Add a custom hook useLocalStorage
  • Implement undo/redo with useReducer
  • Add loading states with Suspense
  • Measure modal performance with useMemo

The Complete React Mental Model

After completing both Part 1 and Part 2, you now have a complete understanding of React:

Foundation (Part 1)

  • Components → Building blocks
  • Props → Data flow down
  • State → Dynamic data
  • Hooks → React features
  • Events → User interactions

Advanced (Part 2)

  • Effects → External systems
  • Refs → DOM access
  • Context → Data sharing
  • Portals → Flexible rendering
  • Error Boundaries → Resilience

The React Philosophy

  1. Declarative: Describe what you want, React figures out how
  2. Component-Based: Build encapsulated, reusable pieces
  3. Learn Once, Write Anywhere: React, React Native, React Server Components
  4. Unidirectional Data Flow: Props flow down, events bubble up
  5. Composition: Build complex UIs from simple components

Next Steps in Your React Journey

Congratulations! You've completed the foundational knowledge of React. Here's what to explore next:

Intermediate Topics

  • Custom Hooks - Create your own reusable hooks
  • React Router - Client-side routing
  • Form Libraries - React Hook Form, Formik
  • State Management - Redux, Zustand, Jotai

Advanced Topics

  • Performance Optimization - React.memo, code splitting
  • TypeScript with React - Type-safe components
  • Testing - Jest, React Testing Library
  • Server Components - React Server Components (RSC)

Frameworks

  • Next.js - Full-stack React framework
  • Remix - Web standards-focused framework
  • Gatsby - Static site generator

Build Projects

The best way to solidify your learning:

  1. Todo App (classic for a reason!)
  2. Weather Dashboard (API integration)
  3. E-commerce Store (complex state)
  4. Real-time Chat (websockets, effects)
  5. Your Own Idea! (most important)

Final Thoughts

React's advanced patterns might seem complex, but they solve real problems:

  • Effects connect to the outside world
  • Refs give you an escape hatch when needed
  • Context eliminates prop drilling
  • Portals solve layout constraints
  • Suspense improves user experience
  • Error boundaries keep your app stable

The key is knowing when to use each pattern. Don't reach for Context when props are fine. Don't use useEffect when an event handler works. React gives you powerful tools—use them wisely!

Remember:

  • 🎯 Start simple, add complexity when needed
  • 📚 Read the official React docs (they're excellent!)
  • 🛠️ Build projects to reinforce learning
  • 💬 Join React communities for help and inspiration
  • 🚀 Keep experimenting and have fun!

You've completed the series! 🎉

Check out these related posts:

Now go build something amazing! 🚀

Top comments (0)