Dev.to WebDev 🛠 Dev 👁 1 📖 5 min read

Understanding React Hooks: A Practical Guide to State, Effects, and Custom Hooks

React Hooks changed how developers build React applications by allowing functional components to manage state, handle side effects, and reuse logic without writing class components. Introduced in React 16.8, Hooks made c

React Hooks changed how developers build React applications by allowing functional components to manage state, handle side effects, and reuse logic without writing class components. Introduced in React 16.8, Hooks made component logic easier to organize and share across an application.

If you have worked with useState or useEffect, you have already experienced the basics of Hooks. However, understanding when to use each Hook, how dependencies work, and why the Rules of Hooks matter is what separates code that simply works from code that remains maintainable as an application grows.

In this article, we will explore the most useful React Hooks, understand their behavior through practical examples, and build a runnable JavaScript demonstration that explains state updates, effect cleanup, memoization, and reusable custom Hooks.

Understanding React Hooks Through Practical Examples

1. useState: Managing Component State

The useState Hook allows functional components to store and update information that changes over time. Common examples include form inputs, counters, dropdown visibility, and shopping cart quantities. When state changes, React schedules a render so the component can display the updated value.

When the next state depends on the previous state, use a functional updater such as setCount(previousCount => previousCount + 1). This avoids relying on a potentially outdated state value when React batches multiple updates. Remember that state updates are not immediate mutations of the current render's variables.

2. useEffect: Synchronizing with External Systems

The useEffect Hook is designed for synchronizing a component with systems outside React, such as browser event listeners, subscriptions, timers, or network connections. Its dependency array determines when React reruns the effect after a render. An empty dependency array means the effect does not rerun because of ordinary dependency changes, although development Strict Mode can perform an additional setup and cleanup cycle.

Effects can return a cleanup function that removes listeners, clears timers, or disconnects subscriptions. Cleanup runs before an effect reruns with changed dependencies and when the component unmounts. Avoid using effects for values that can be calculated directly from existing props or state, because unnecessary effects introduce extra complexity.

3. useMemo and useCallback: Avoiding Unnecessary Work

The useMemo Hook caches the result of a calculation between renders while its dependencies remain unchanged. The useCallback Hook similarly caches a function definition. These Hooks can help when expensive calculations or stable references matter, especially when working with memoized child components.

Memoization is not a replacement for good component design, and it does not automatically make every application faster. Start with clear code, measure performance, and add memoization where it provides a meaningful benefit. React Compiler can also automatically optimize many patterns in supported configurations.

4. Custom Hooks: Reusing Stateful Logic

A custom Hook is a JavaScript function whose name begins with use and which can call other Hooks. Custom Hooks allow developers to extract reusable behavior, such as form management, online status tracking, or API data synchronization, without duplicating implementation details across components.

Each invocation of a custom Hook has its own Hook state unless the logic explicitly interacts with shared external state. For example, a useCounter Hook can encapsulate counter state and update functions, allowing several components to reuse the same behavior while maintaining independent counters.

5. The Rules of Hooks

Call Hooks only at the top level of React function components or custom Hooks. Do not call them inside loops, conditions, nested functions, or ordinary JavaScript functions, because React relies on a consistent Hook call order between renders.

Hooks should also not be called from regular event handlers. Instead, call a state setter or another function from the event handler. Following these rules helps React associate state and effects with the correct component and makes the code easier to reason about.

const log = (message, value) => {
  console.log(`\n${message}`);
  if (value !== undefined) {
    console.log(value);
  }
};

// Step 1: Demonstrate the core idea behind useState.
// This standalone JavaScript simulation is runnable in Node.js.
// Real React components should use React's built-in useState Hook.
function createCounter(initialValue = 0) {
  let count = initialValue;

  return {
    getValue() {
      return count;
    },
    increment() {
      count = count + 1;
      return count;
    },
    decrement() {
      count = count - 1;
      return count;
    },
    reset() {
      count = initialValue;
      return count;
    }
  };
}

const counter = createCounter(0);
log("Step 1: Initial counter state", counter.getValue());
log("Step 2: Increment counter", counter.increment());
log("Step 3: Increment counter again", counter.increment());
log("Step 4: Decrement counter", counter.decrement());
log("Step 5: Reset counter", counter.reset());

// Step 6: Simulate a derived value.
// In React, calculate simple derived values during rendering.
const cart = [
  { name: "Keyboard", price: 2500, quantity: 1 },
  { name: "Mouse", price: 800, quantity: 2 },
  { name: "USB Cable", price: 300, quantity: 3 }
];

const calculateCartTotal = (items) => {
  return items.reduce((total, item) => {
    return total + item.price * item.quantity;
  }, 0);
};

log("Step 6: Shopping cart items", cart);
log("Step 7: Calculated cart total", calculateCartTotal(cart));

// Step 8: Demonstrate a memoization pattern.
// React's useMemo performs dependency-aware caching automatically.
function createMemoizedTotal() {
  let previousItems = null;
  let previousTotal = 0;

  return function getTotal(items) {
    if (items === previousItems) {
      console.log("Memoization: Reusing the cached total.");
      return previousTotal;
    }

    console.log("Memoization: Calculating a new total.");
    previousItems = items;
    previousTotal = calculateCartTotal(items);
    return previousTotal;
  };
}

const getMemoizedTotal = createMemoizedTotal();
log("Step 8: First memoized calculation", getMemoizedTotal(cart));
log("Step 9: Same array reference", getMemoizedTotal(cart));

// Step 10: Create a new array to invalidate the cache.
const updatedCart = [
  ...cart,
  { name: "Mouse Pad", price: 500, quantity: 1 }
];
log("Step 10: Updated cart total", getMemoizedTotal(updatedCart));

// Step 11: Simulate effect setup and cleanup.
// In React, useEffect is appropriate for subscriptions and timers.
function createSubscription(resourceName) {
  let active = true;
  console.log(`Effect setup: Subscribed to ${resourceName}.`);

  return function cleanup() {
    if (!active) {
      return;
    }

    active = false;
    console.log(`Effect cleanup: Unsubscribed from ${resourceName}.`);
  };
}

const cleanupNotifications = createSubscription("notifications");
cleanupNotifications();
cleanupNotifications(); // Repeated cleanup is safe in this example.

// Step 12: Demonstrate a reusable custom-Hook-style abstraction.
// This is plain JavaScript, not an actual React Hook.
function createToggle(initialValue = false) {
  let enabled = initialValue;

  return {
    getValue: () => enabled,
    toggle: () => {
      enabled = !enabled;
      return enabled;
    }
  };
}

const darkMode = createToggle(false);
log("Step 12: Initial dark mode", darkMode.getValue());
log("Step 13: Toggle dark mode on", darkMode.toggle());
log("Step 14: Toggle dark mode off", darkMode.toggle());

// Step 15: Show how immutable updates work with React state.
const originalUser = { name: "Ansh", role: "Developer" };
const updatedUser = { ...originalUser, role: "Senior Developer" };

log("Step 15: Original user remains unchanged", originalUser);
log("Step 16: Updated user object", updatedUser);

// Step 17: Summarize the main concepts.
console.log("\nReact Hooks concepts demonstrated:");
console.log("- State: Store values that change over time.");
console.log("- Derived values: Calculate data from existing state.");
console.log("- Memoization: Reuse results when appropriate.");
console.log("- Effects: Synchronize with external systems.");
console.log("- Custom Hooks: Extract reusable stateful logic.");
console.log("\nDemo completed successfully.");

Conclusion

React Hooks provide a consistent way to manage state, synchronize external systems, optimize selected calculations, and reuse component logic. Understanding useState, useEffect, useMemo, and useCallback gives you a strong foundation for building maintainable React applications.

The most important lesson is to use Hooks intentionally. Keep derived values in render logic when possible, clean up effects properly, follow the Rules of Hooks, and avoid adding memoization without a clear reason. These practices reduce unnecessary complexity and make components easier to debug.

To deepen your understanding, build a small React application with a counter, a shopping cart, a form, and a custom Hook. Experiment with state updates, effect dependencies, and cleanup behavior, then use React DevTools to observe how components render. Practical experimentation is one of the fastest ways to understand how Hooks behave in real applications.

📰 Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.