Jobnik

Dss

React Zero to Hero: Part 2 – Advanced Hooks and React Router

Welcome back to our “React Zero to Hero” series! In Part 1, we laid the groundwork by understanding React’s core concepts: JSX, functional components, props, and the fundamental `useState` hook. Now, it’s time to elevate our React skills by diving into more powerful hooks and exploring how to handle navigation in single-page applications with React Router.

React Hooks: Beyond useState

React Hooks are functions that let you “hook into” React features from your functional components. They allow you to use state and other React features without writing a class.

1. useEffect: The Side Effect Hook

The useEffect hook lets you perform side effects in functional components. Data fetching, subscriptions, and manually changing the DOM are all examples of side effects. It runs after every render of the component, but you can control when it runs.

Basic Data Fetching Example

Let’s simulate fetching data when the component mounts.


import React, { useState, useEffect } from 'react';

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

  useEffect(() => {
    // This function runs after the component renders
    const fetchData = async () => {
      try {
        setLoading(true);
        // Simulate API call
        const response = await new Promise(resolve => setTimeout(() => {
          resolve({ message: 'Data fetched successfully!' });
        }, 2000));
        setData(response.message);
      } catch (err) {
        setError(err);
      } finally {
        setLoading(false);
      }
    };

    fetchData();

    // Cleanup function (optional): runs when the component unmounts or before re-running the effect
    return () => {
      console.log('Cleanup function ran');
      // For example, cancel network requests or clear timers
    };
  }, []); // Empty dependency array: runs only once after the initial render (like componentDidMount)

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

  return <h2>Fetched Data: {data}</h2>;
}

export default DataFetcher;

Cleanup Example: Subscriptions

If your effect subscribes to an external data source, it’s important to clean up that subscription when the component unmounts to prevent memory leaks.


import React, { useState, useEffect } from 'react';

// Simulate an external chat API
const ChatAPI = {
  subscribeToMessages: (userId, callback) => {
    console.log(`Subscribing to messages for user ${userId}`);
    // In a real app, this would establish a WebSocket connection or similar.
    const intervalId = setInterval(() => {
      callback(`New message from user ${userId} at ${new Date().toLocaleTimeString()}`);
    }, 3000);
    return intervalId;
  },
  unsubscribeFromMessages: (intervalId) => {
    console.log(`Unsubscribing from messages (interval ID: ${intervalId})`);
    clearInterval(intervalId);
  }
};

function ChatRoom({ userId }) {
  const [message, setMessage] = useState('No messages yet.');

  useEffect(() => {
    const interval = ChatAPI.subscribeToMessages(userId, (msg) => {
      setMessage(msg);
    });

    // Return a cleanup function
    return () => {
      ChatAPI.unsubscribeFromMessages(interval);
    };
  }, [userId]); // Dependency array: re-run effect if userId changes

  return (
    <div>
      <h3>Chat for User {userId}</h3>
      <p>{message}</p>
    </div>
  );
}

// Usage in App.js
// <ChatRoom userId="Alice" />
// <ChatRoom userId="Bob" />

2. useContext: Simplifying Prop Drilling

useContext provides a way to pass data through the component tree without having to pass props down manually at every level (prop drilling).

Creating and Using Context

Let’s create a simple theme context.


// ThemeContext.js
import React, { createContext, useContext, useState } from 'react';

// 1. Create the Context
const ThemeContext = createContext(null);

// 2. Create a Provider Component
export function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');

  const toggleTheme = () => {
    setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light'));
  };

  const contextValue = { theme, toggleTheme };

  return (
    <ThemeContext.Provider value={contextValue}>
      {children}
    </ThemeContext.Provider>
  );
}

// 3. Create a Custom Hook to Consume the Context (optional but recommended)
export function useTheme() {
  const context = useContext(ThemeContext);
  if (context === undefined) {
    throw new Error('useTheme must be used within a ThemeProvider');
  }
  return context;
}

// ThemeToggler.js
import React from 'react';
import { useTheme } from './ThemeContext';

function ThemeToggler() {
  const { theme, toggleTheme } = useTheme();

  return (
    <button onClick={toggleTheme}>
      Toggle to {theme === 'light' ? 'Dark' : 'Light'} Theme
    </button>
  );
}

export default ThemeToggler;

// ThemedParagraph.js
import React from 'react';
import { useTheme } from './ThemeContext';

function ThemedParagraph() {
  const { theme } = useTheme();
  const style = {
    background: theme === 'light' ? '#eee' : '#333',
    color: theme === 'light' ? '#333' : '#eee',
    padding: '10px',
    borderRadius: '5px'
  };
  return <p style={style}>This paragraph changes theme!</p>;
}

export default ThemedParagraph;

// App.js (Putting it all together)
import React from 'react';
import { ThemeProvider } from './ThemeContext';
import ThemeToggler from './ThemeToggler';
import ThemedParagraph from './ThemedParagraph';

function App() {
  return (
    <ThemeProvider>
      <div>
        <h1>Context API Example</h1>
        <ThemeToggler />
        <ThemedParagraph />
      </div>
    </ThemeProvider>
  );
}

export default App;

3. useRef: Accessing the DOM Directly or Storing Mutable Values

The useRef hook returns a mutable ref object whose .current property is initialized to the passed argument (initialValue). The returned object will persist for the full lifetime of the component.

Focusing an Input with useRef


import React, { useRef } from 'react';

function InputFocus() {
  const inputRef = useRef(null); // Create a ref object

  const handleFocusClick = () => {
    inputRef.current.focus(); // Access the DOM element and call its focus method
  };

  return (
    <div>
      <input type="text" ref={inputRef} /> {/* Attach the ref to the input element */}
      <button onClick={handleFocusClick}>Focus Input</button>
    </div>
  );
}

export default InputFocus;

Storing a Mutable Value (without causing re-renders)

Unlike state, updating a ref’s .current value does not trigger a re-render.


import React, { useRef, useState } from 'react';

function Stopwatch() {
  const [time, setTime] = useState(0);
  const intervalRef = useRef(null); // To store the interval ID

  const startStopwatch = () => {
    if (!intervalRef.current) {
      intervalRef.current = setInterval(() => {
        setTime(prevTime => prevTime + 1);
      }, 1000);
    }
  };

  const stopStopwatch = () => {
    if (intervalRef.current) {
      clearInterval(intervalRef.current);
      intervalRef.current = null; // Clear the ref
    }
  };

  const resetStopwatch = () => {
    stopStopwatch();
    setTime(0);
  };

  return (
    <div>
      <h2>Stopwatch: {time}s</h2>
      <button onClick={startStopwatch}>Start</button>
      <button onClick={stopStopwatch}>Stop</button>
      <button onClick={resetStopwatch}>Reset</button>
    </div>
  );
}

export default Stopwatch;

4. useReducer: For Complex State Logic

useReducer is an alternative to useState for more complex state logic, especially when the next state depends on the previous one or when you have multiple sub-values in the state. It’s often preferred for state logic that involves multiple actions or transitions between different states.

Counter Example with useReducer

A simple counter can illustrate `useReducer`.


import React, { useReducer } from 'react';

// 1. Define a reducer function
const counterReducer = (state, action) => {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    case 'reset':
      return { count: action.payload }; // Use payload for dynamic initial value
    default:
      return state;
  }
};

function ComplexCounter() {
  // 2. Initialize useReducer: [state, dispatch] = useReducer(reducer, initialState)
  const [state, dispatch] = useReducer(counterReducer, { count: 0 });

  return (
    <div>
      <h2>Count: {state.count}</h2>
      <button onClick={() => dispatch({ type: 'increment' })}>Increment</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>Decrement</button>
      <button onClick={() => dispatch({ type: 'reset', payload: 0 })}>Reset</button>
      <button onClick={() => dispatch({ type: 'reset', payload: 100 })}>Reset to 100</button>
    </div>
  );
}

export default ComplexCounter;

React Router: Navigation in Single Page Applications

Modern web applications are often Single Page Applications (SPAs), meaning they load a single HTML page and dynamically update its content as the user interacts. React Router is a popular library for managing navigation within React SPAs, allowing you to map URLs to specific components without a full page reload.

Installation (Conceptual)

To use React Router, you’d typically install it via npm or yarn:


npm install react-router-dom
# or
yarn add react-router-dom

Core Components of React Router

  • BrowserRouter: Uses the HTML5 history API to keep your UI in sync with the URL. It should wrap your entire application.
  • Routes: Renders the first child &lt;Route&gt; that matches the current URL.
  • Route: Defines a mapping between a URL path and a component to render.
  • Link: A component for declarative navigation. It renders as an &lt;a&gt; tag but prevents full page reloads.
  • useNavigate: A hook for programmatic navigation.
  • useParams: A hook for accessing URL parameters.

Basic Routing Example

Let’s create a simple application with Home, About, and Contact pages.


// App.js
import React from 'react';
import { BrowserRouter as Router, Routes, Route, Link, useNavigate, useParams } from 'react-router-dom';

// --- Page Components ---
const Home = () => <h2>Welcome to the Home Page!</h2>;
const About = () => <h2>Learn About Us!</h2>;
const Contact = () => <h2>Get in Touch!</h2>;

const UserProfile = () => {
  const { id } = useParams(); // Get URL parameter
  return <h2>User Profile for ID: {id}</h2>;
};

const NotFound = () => <h2>404 - Page Not Found</h2>;

function Navbar() {
  const navigate = useNavigate(); // Hook for programmatic navigation

  const goToHome = () => {
    navigate('/');
  };

  return (
    <nav>
      <ul>
        <li>
          <Link to="/">Home</Link>
        </li>
        <li>
          <Link to="/about">About</Link>
        </li>
        <li>
          <Link to="/contact">Contact</Link>
        </li>
        <li>
          <Link to="/user/123">User 123</Link>
        </li>
        <li>
          <button onClick={goToHome}>Go Home (Programmatic)</button>
        </li>
      </ul>
    </nav>
  );
}

function App() {
  return (
    <Router>
      <div>
        <h1>React Router Example</h1>
        <Navbar />

        <hr />

        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/about" element={<About />} />
          <Route path="/contact" element={<Contact />} />
          {/* Dynamic route with a parameter */}
          <Route path="/user/:id" element={<UserProfile />} />
          {/* Catch-all route for 404 */}          
          <Route path="*" element={<NotFound />} />
        </Routes>
      </div>
    </Router>
  );
}

export default App;

Best Practices Reminder from Part 1

  • Component Reusability: Always think about how you can make your components generic and reusable.
  • Props are Read-Only: Never modify props directly within a child component.
  • State Immutability: When updating state that is an object or array, always create a new copy rather than mutating the original. For example, use spread syntax (...) for arrays/objects or array methods that return new arrays (map, filter).
  • Key Prop for Lists: Always provide a unique `key` prop when rendering lists of elements to help React efficiently update the DOM.

What’s Next?

In this second part, we’ve significantly expanded our React toolkit by exploring `useEffect` for side effects, `useContext` for global state, `useRef` for direct DOM access, and `useReducer` for complex state logic. We also introduced React Router, which is essential for building multi-page feel in single-page applications.

In the upcoming parts, we will delve into more advanced state management patterns, especially focusing on Redux, and explore more best practices for building robust, scalable, and maintainable React applications. Stay tuned for deeper dives and more exciting examples!

Leave a Reply

Your email address will not be published. Required fields are marked *