DEV Community

Cover image for Learning JS frameworks with me(part 5): React.js(2/3)- Advanced React Component Patterns & State Management
Shaman Shetty
Shaman Shetty

Posted on

Learning JS frameworks with me(part 5): React.js(2/3)- Advanced React Component Patterns & State Management

In Part 1, we covered React fundamentals. Now let's explore more advanced concepts that will elevate your React applications to professional standards. Modern React development has evolved significantly, with hooks replacing class components and custom patterns emerging to solve complex state management challenges.

Component Lifecycle and useEffect Patterns

In modern React, the class component lifecycle methods have been consolidated into the useEffect hook. This powerful hook lets you perform side effects in function components.

Basic useEffect Pattern

useEffect(() => {
  // Code to run after render
  console.log('Component rendered');

  // Optional cleanup function
  return () => {
    console.log('Component will unmount or dependencies changed');
  };
}, [/* dependencies */]);
Enter fullscreen mode Exit fullscreen mode

Common useEffect Patterns

  1. Run once on mount (equivalent to componentDidMount):
useEffect(() => {
  fetchData();
}, []); // Empty dependency array
Enter fullscreen mode Exit fullscreen mode
  1. Run when specific props/state change:
useEffect(() => {
  console.log('userId changed');
  fetchUserData(userId);
}, [userId]); // Only re-run when userId changes
Enter fullscreen mode Exit fullscreen mode
  1. Cleanup pattern (equivalent to componentWillUnmount):
useEffect(() => {
  const subscription = subscribeToData();

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

Best Practices

  • Avoid the exhaustive-deps ESLint warning by including all dependencies your effect uses
  • Split effects by concern rather than lifecycle
  • Use cleanup functions to prevent memory leaks
  • Consider using useLayoutEffect for DOM measurements that need to be synchronized

Context API for State Sharing

React Context provides a way to share values between components without having to explicitly pass props through every level of the component tree.

Creating and Using Context

// Create context
const ThemeContext = createContext('light');

// Provider component
function App() {
  const [theme, setTheme] = useState('light');

  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <MainContent />
    </ThemeContext.Provider>
  );
}

// Consumer component using useContext hook
function ThemedButton() {
  const { theme, setTheme } = useContext(ThemeContext);

  return (
    <button 
      style={{ background: theme === 'dark' ? '#333' : '#fff' }}
      onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}
    >
      Toggle Theme
    </button>
  );
}
Enter fullscreen mode Exit fullscreen mode

Context Best Practices

  • Create separate contexts for different domains of your application
  • Keep context values focused on specific concerns
  • Consider performance implications – all components using a context re-render when the context value changes
  • Use memoization with useMemo to prevent unnecessary re-renders

React Router for Navigation

React Router is the standard library for routing in React applications, allowing you to build single-page applications with navigation.

Basic Setup

import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';

function App() {
  return (
    <BrowserRouter>
      <nav>
        <Link to="/">Home</Link>
        <Link to="/about">About</Link>
        <Link to="/dashboard">Dashboard</Link>
      </nav>

      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="/dashboard" element={<Dashboard />} />
      </Routes>
    </BrowserRouter>
  );
}
Enter fullscreen mode Exit fullscreen mode

Advanced Routing Patterns

  1. Nested Routes:
<Routes>
  <Route path="/products" element={<Products />}>
    <Route path=":id" element={<ProductDetail />} />
    <Route path="new" element={<NewProduct />} />
  </Route>
</Routes>
Enter fullscreen mode Exit fullscreen mode
  1. Private Routes:
function PrivateRoute({ children }) {
  const { isAuthenticated } = useAuth();

  return isAuthenticated ? children : <Navigate to="/login" />;
}

// Usage
<Route 
  path="/dashboard" 
  element={<PrivateRoute><Dashboard /></PrivateRoute>} 
/>
Enter fullscreen mode Exit fullscreen mode
  1. useParams for Dynamic Routes:
function ProductDetail() {
  const { id } = useParams();
  const [product, setProduct] = useState(null);

  useEffect(() => {
    fetchProduct(id).then(setProduct);
  }, [id]);

  if (!product) return <div>Loading...</div>;

  return <div>{product.name}</div>;
}
Enter fullscreen mode Exit fullscreen mode

Form Handling and Validation

Forms are a core part of web applications, and React offers several approaches to handle them effectively.

Controlled Components

function SignupForm() {
  const [formData, setFormData] = useState({
    email: '',
    password: '',
  });
  const [errors, setErrors] = useState({});

  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormData(prev => ({
      ...prev,
      [name]: value
    }));
  };

  const validate = () => {
    const newErrors = {};

    if (!formData.email) {
      newErrors.email = 'Email is required';
    } else if (!/\S+@\S+\.\S+/.test(formData.email)) {
      newErrors.email = 'Email is invalid';
    }

    if (!formData.password) {
      newErrors.password = 'Password is required';
    } else if (formData.password.length < 8) {
      newErrors.password = 'Password must be at least 8 characters';
    }

    setErrors(newErrors);
    return Object.keys(newErrors).length === 0;
  };

  const handleSubmit = (e) => {
    e.preventDefault();

    if (validate()) {
      // Submit form data
      console.log('Form submitted:', formData);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label>Email</label>
        <input
          type="email"
          name="email"
          value={formData.email}
          onChange={handleChange}
        />
        {errors.email && <span>{errors.email}</span>}
      </div>

      <div>
        <label>Password</label>
        <input
          type="password"
          name="password"
          value={formData.password}
          onChange={handleChange}
        />
        {errors.password && <span>{errors.password}</span>}
      </div>

      <button type="submit">Sign Up</button>
    </form>
  );
}
Enter fullscreen mode Exit fullscreen mode

Form Libraries

For complex forms, libraries like Formik and React Hook Form can simplify development:

Using React Hook Form:

import { useForm } from 'react-hook-form';

function SignupForm() {
  const { register, handleSubmit, formState: { errors } } = useForm();

  const onSubmit = (data) => {
    console.log('Form submitted:', data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div>
        <label>Email</label>
        <input
          {...register('email', { 
            required: 'Email is required',
            pattern: {
              value: /\S+@\S+\.\S+/,
              message: 'Email is invalid'
            }
          })}
        />
        {errors.email && <span>{errors.email.message}</span>}
      </div>

      <div>
        <label>Password</label>
        <input
          type="password"
          {...register('password', {
            required: 'Password is required',
            minLength: {
              value: 8,
              message: 'Password must be at least 8 characters'
            }
          })}
        />
        {errors.password && <span>{errors.password.message}</span>}
      </div>

      <button type="submit">Sign Up</button>
    </form>
  );
}
Enter fullscreen mode Exit fullscreen mode

Custom Hooks

Custom hooks allow you to extract component logic into reusable functions, promoting code reuse and separation of concerns.

Creating a Custom Hook

function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const controller = new AbortController();

    setLoading(true);
    fetch(url, { signal: controller.signal })
      .then(response => {
        if (!response.ok) {
          throw new Error('Network response was not ok');
        }
        return response.json();
      })
      .then(data => {
        setData(data);
        setLoading(false);
      })
      .catch(error => {
        if (error.name !== 'AbortError') {
          setError(error);
          setLoading(false);
        }
      });

    return () => controller.abort();
  }, [url]);

  return { data, loading, error };
}

// Usage
function UserProfile({ userId }) {
  const { data, loading, error } = useFetch(`/api/users/${userId}`);

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <div>
      <h2>{data.name}</h2>
      <p>Email: {data.email}</p>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Common Custom Hook Patterns

  1. useLocalStorage - Persist state to localStorage:
function useLocalStorage(key, initialValue) {
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.error(error);
      return initialValue;
    }
  });

  const setValue = (value) => {
    try {
      const valueToStore = value instanceof Function ? value(storedValue) : value;
      setStoredValue(valueToStore);
      window.localStorage.setItem(key, JSON.stringify(valueToStore));
    } catch (error) {
      console.error(error);
    }
  };

  return [storedValue, setValue];
}
Enter fullscreen mode Exit fullscreen mode
  1. useMediaQuery - Respond to media queries:
function useMediaQuery(query) {
  const [matches, setMatches] = useState(false);

  useEffect(() => {
    const media = window.matchMedia(query);
    setMatches(media.matches);

    const listener = (event) => setMatches(event.matches);
    media.addEventListener('change', listener);

    return () => media.removeEventListener('change', listener);
  }, [query]);

  return matches;
}

// Usage
function ResponsiveComponent() {
  const isMobile = useMediaQuery('(max-width: 768px)');

  return (
    <div>
      {isMobile ? <MobileView /> : <DesktopView />}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Modern React Development in 2025

The React ecosystem has continued to evolve, with several key trends dominating the landscape:

Server Components and React Server Components (RSC)

React Server Components allow rendering components on the server, with zero JavaScript sent to the client for server-only components. This provides:

  • Improved performance with reduced client-side JavaScript
  • Better SEO as content is rendered server-side
  • Access to backend resources directly from components
// A server component
import { db } from '../db'; // Direct backend access

// This component never sends JS to the client
async function ProductList() {
  const products = await db.query('SELECT * FROM products');

  return (
    <div>
      {products.map(product => (
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Meta-Frameworks

Most React applications now use meta-frameworks that build on top of React:

  1. Next.js - The most popular React framework offering:

    • Server components
    • File-based routing
    • API routes
    • Server-side rendering and static generation
    • Built-in image optimization
  2. Remix - Focused on web fundamentals:

    • Nested routing
    • Server-rendered UI
    • Progressive enhancement
    • Error boundaries at route level

State Management Evolution

While Redux was once the standard, state management has diversified:

  1. React Query / TanStack Query - For server state management:

    • Caching
    • Background refetching
    • Mutation handling
    • Pagination and infinite scrolling
  2. Zustand - A lightweight alternative to Redux:

    • Simple API with hooks
    • No boilerplate
    • Middleware support
  3. Jotai/Recoil - Atomic state management:

    • Granular updates
    • Derived state
    • Shared state without context providers

Styling Approaches

Several approaches have gained prominence:

  1. Tailwind CSS - Utility-first CSS:

    • Component-level styling
    • No naming required
    • Consistency through constraints
  2. CSS Modules - Scoped CSS with no runtime cost:

    • Local scope by default
    • Composition
    • TypeScript support
  3. CSS-in-JS - Libraries like styled-components and emotion:

    • Dynamic styling
    • Theming
    • Component-based styling

TypeScript Integration

TypeScript has become the standard for React development:

type UserProps = {
  id: string;
  name: string;
  email: string;
  role: 'admin' | 'user';
  onDelete?: (id: string) => void;
};

function User({ id, name, email, role, onDelete }: UserProps) {
  return (
    <div>
      <h3>{name}</h3>
      <p>{email}</p>
      <span>Role: {role}</span>
      {onDelete && (
        <button onClick={() => onDelete(id)}>Delete</button>
      )}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Testing Best Practices

Modern React testing focuses on user behavior:

  1. React Testing Library - Testing from the user's perspective:

    • Queries that mirror how users find elements
    • Event firing
    • Accessibility checks
  2. Vitest - Fast testing framework:

    • Compatible with Jest API
    • Faster execution
    • Better HMR support

Performance Optimization

  1. Suspense and Concurrent Rendering:
   <Suspense fallback={<LoadingSpinner />}>
     <ProductDetails id={productId} />
   </Suspense>
Enter fullscreen mode Exit fullscreen mode
  1. Memoization with useMemo and useCallback:
   const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
   const memoizedCallback = useCallback(() => {
     doSomething(a, b);
   }, [a, b]);
Enter fullscreen mode Exit fullscreen mode
  1. Virtualization for long lists:
   import { useVirtualizer } from '@tanstack/react-virtual';

   function VirtualList({ items }) {
     const rowVirtualizer = useVirtualizer({
       count: items.length,
       getScrollElement: () => parentRef.current,
       estimateSize: () => 35,
     });

     return (
       <div ref={parentRef} style={{ height: '500px', overflow: 'auto' }}>
         <div
           style={{
             height: `${rowVirtualizer.getTotalSize()}px`,
             width: '100%',
             position: 'relative',
           }}
         >
           {rowVirtualizer.getVirtualItems().map((virtualRow) => (
             <div
               key={virtualRow.index}
               style={{
                 position: 'absolute',
                 top: 0,
                 left: 0,
                 width: '100%',
                 height: `${virtualRow.size}px`,
                 transform: `translateY(${virtualRow.start}px)`,
               }}
             >
               {items[virtualRow.index]}
             </div>
           ))}
         </div>
       </div>
     );
   }
Enter fullscreen mode Exit fullscreen mode

Conclusion

React continues to evolve with a focus on performance, developer experience, and component composition. By mastering these advanced patterns and staying current with ecosystem trends, you'll be well-equipped to build modern, maintainable React applications that scale with your needs.

The fundamental React philosophy remains: compose your UI from small, focused components and manage state predictably. However, the tools and techniques to achieve this have become more sophisticated, allowing for increasingly powerful and performant web applications.

Tiugo image

Modular, Fast, and Built for Developers

CKEditor 5 gives you full control over your editing experience. A modular architecture means you get high performance, fewer re-renders and a setup that scales with your needs.

Start now

Top comments (0)

👋 Kindness is contagious

Engage with a wealth of insights in this thoughtful article, cherished by the supportive DEV Community. Coders of every background are encouraged to bring their perspectives and bolster our collective wisdom.

A sincere “thank you” often brightens someone’s day—share yours in the comments below!

On DEV, the act of sharing knowledge eases our journey and forges stronger community ties. Found value in this? A quick thank-you to the author can make a world of difference.

Okay