Skip to main content

Command Palette

Search for a command to run...

React Debugging Tips & Tricks for Developers

Updated
•8 min read•View as Markdown
React Debugging Tips & Tricks for Developers
S

Data-Driven Fresher | Leveraging full stack technologies | Exploring tech-trends

When I first started learning React and began working on a large-scale project, I constantly found myself wondering how I could write better, more efficient code. I’d often question the logic I had implemented — was it the best way? Could it be improved? This curiosity pushed me to explore articles, best practices, and expert opinions. Over time, I gathered some valuable insights that I’m excited to share here!


1. Component Not Re-rendering?

  • Check useState and useEffect dependencies.

  • Ensure you're not mutating state directly — always use the state updater:

      // ❌ Wrong
      state.push(newItem);
      // ✅ Correct
      setState([...state, newItem]);
    

2. State Updating but Not Showing in UI?

  • You might be mutating state instead of replacing it.

    That means: don't directly modify the existing state object — always create a new object or array instead.

  • You may be logging state immediately after setState, but React batching means it won’t reflect immediately.


3. Infinite Re-renders?

  • Caused by missing dependency arrays in useEffect.

  • Fix by passing the right dependencies:

      useEffect(() => {
        fetchData();
      }, []); // ✅ EmptprevCount is the latest value (even if React batches updates).
    
      y array means run once
    

4. Stale Closures in Hooks

  • You might be using old values inside async callbacks or timers.

Fix: Use refs or move the latest state inside the callback.

useEffect(() => {
  const interval = setInterval(() => {
    setCount((prev) => prev + 1); // Here, prev is the latest value (even if React batches updates).
  }, 1000);
  return () => clearInterval(interval);
}, []);

5. Console Log Party? Use Custom Debug Logs

Instead of console.log everywhere, create a utility:

const debug = (...args) => {
  if (process.env.NODE_ENV === 'development') {
    console.log('[DEBUG]', ...args);
  }
};

6. Broken Event Handlers or Forms Not Submitting

  • Check if the default behaviour is prevented.

  • Confirm type="submit" is used for buttons in forms.


7. Styling Not Applying?

  • Conflicts due to class name collisions or CSS specificity.

  • For MUI, inspect generated classes like .MuiButton-root, use sx or styled.(I had shared another blog on MUI Blog, check out the blog.!)


8. React DevTools = Your Best Friend

"I found it really exciting to explore a developed and scaled project—especially one that's already up and running. It gave me a clear understanding of the project's overall structure and how everything fits together."

  • Inspect props, state, context, and component hierarchy live.

  • Use the “Highlight Updates” feature to debug unnecessary re-renders.


9. Unnecessary Re-renders?

  • Components re-render due to:

    • Props changing unnecessarily.

    • Functions re-created on every render.

Fixes:

  • Use React.memo, useCallback, or useMemo wisely.

  • Track renders using:

      useEffect(() => {
        console.log('Component rendered');
      });
    

10. API Calls Not Working / Doubled?

  • Caused by React StrictMode in dev which runs useEffect twice.

  • In production, this won't happen — but you can debounce or guard it.

    ⚡What is Debounce in React?

    ✅ Debounce is a performance optimization technique:

    It ensures that a function (usually an expensive one like API call or search filter) is not called too frequently. Instead, it's delayed until a user stops triggering it for a certain amount of time.

    Use Case:

    Typing in a search box shouldn't fire an API call on every keystroke — that’s where debouncing comes in.

    🔧 Example: Debounced Search Input

      import { useState, useEffect } from 'react';
    
      function useDebounce(value, delay = 500) {
        const [debounced, setDebounced] = useState(value);
    
        useEffect(() => {
          const handler = setTimeout(() => {
            setDebounced(value);
          }, delay);
    
          return () => clearTimeout(handler);
        }, [value, delay]);
    
        return debounced;
      }
    
      function SearchBox() {
        const [query, setQuery] = useState('');
        const debouncedQuery = useDebounce(query, 300);
    
        useEffect(() => {
          if (debouncedQuery) {
            fetchData(debouncedQuery); // API call happens after delay
          }
        }, [debouncedQuery]);
    
        return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
      }
    

    🛡️ What Does Guarding Mean in React?

    Guarding means putting defensive checks in place before executing logic — to prevent errors or unintended behaviour.

    Use Cases:

    • Making sure a ref is not null before accessing .current

    • Checking data exists before rendering

    • Avoiding function execution if a condition isn’t met

🔧 Example: Guarding DOM Access

    const myRef = useRef(null);

    useEffect(() => {
      if (!myRef.current) return; // 🛡️ Guard it!
      myRef.current.focus();
    }, []);

🔧 Example: Guarding Before Rendering

    if (!userData) {
      return <SkeletonLoader />; // 🛡️ Guard: wait for data before rendering
    }

TL;DR:

TermPurposeCommon Use Case
DebouncePrevent calling functions too frequentlySearch input, window resize, filters
GuardingPrevent crashes by checking conditions firstRefs, optional data, lifecycle logic

11. Context Not Updating or Not Available

  • Make sure:

    • You wrapped the correct Provider.

    • You’re not accessing context before it’s initialized.


12. React Router Issues?

  • Double check that you're using correct versions (v6 vs v5).

  • useNavigate, useParams, and route matching have changed in v6.


13. React + Redux State Not Syncing?

  • Check if the component is subscribed properly.

  • what it means -

    • The component is listening to changes from a data source (like a store, WebSocket, API stream, or context)

    • And it's doing it in the right lifecycle phase (e.g., in useEffect)

    • Plus, it's cleaning up correctly to avoid memory leaks or bugs

  • What Happens If Not Subscribed Properly?

    • Data doesn’t update even though it changes

    • You get stale state.

    • Or memory leaks from unclosed listeners.

    • Or component crashes because of unguarded async updates.

  • Use useSelector with shallow equality comparison:

      const data = useSelector(state => state.data, shallowEqual);
    

14. Memory Leaks or SetState on Unmounted Component

  • Ah yes — the infamous "Memory Leak" or the dreaded "Can't perform a React state update on an unmounted component" warning 😬 — super common in real-world React apps.

  • In the start I couldn’t even understand , In simple terms:

    You're trying to call setState() (or dispatch) after the component is no longer in the DOM.

    This often happens with async operations (API calls, timers, subscriptions) that are still running even after the component is gone.

    Summary

    • If an async fetch is still running when a component unmounts, use an isMounted flag or AbortController to prevent unwanted state updates.

    • For timeouts and intervals, always remember to clearTimeout or clearInterval when the component unmounts.

    • If you're using subscriptions or sockets, make sure to removeEventListener or call off() to clean them up properly.


15. Network + API Debugging

  • Use Axios interceptors to globally log API calls and responses.

  • Use tools like Postman, Mock Service Worker (MSW) for local mocks.


16. Slow Performance or UI Lag?

  • Use React Profiler to find expensive renders.

  • Optimize large lists using react-window or react-virtualized.


17. Incorrect Props Passed to Children

  • Add TypeScript or PropTypes to catch incorrect props.

  • Better Way — Use PropTypes or TypeScript. (TypeScript will give compile-time error if the prop is wrong. and PropTypes give runtime warning (only in dev).)

  • Validate them in dev:

      if (typeof prop !== 'string') {
        console.warn('Expected prop to be a string');
      }
    

18. Check React Error Boundaries

  • Wrap your app with an Error Boundary to catch and display errors without crashing the whole app.

19. Use a Custom Hook Debugger

What is it?

A custom hook debugger is a utility that logs or inspects the internal values of your custom hooks — during development only — to help you debug and trace changes in your app.

It’s like putting a “console.log” in your hooks… but in a structured, reusable way.

Why Use It?

In large projects, you may have:

  • Complex state logic

  • Multiple hooks inside hooks

  • Unexpected re-renders

Custom hook debugger helps:

✅ Track how values change
✅ Spot stale or unexpected state
✅ Debug why a re-render happened
✅ Log hook values without polluting production logs

How to use it ?

import { useEffect, useRef } from 'react';

export function useWhyDidYouUpdate(name: string, props: Record<string, any>) {
  const previousProps = useRef(props);

  useEffect(() => {
    if (process.env.NODE_ENV !== 'development') return;

    const allKeys = Object.keys({ ...previousProps.current, ...props });
    const changesObj: Record<string, any> = {};

    allKeys.forEach((key) => {
      if (previousProps.current[key] !== props[key]) {
        changesObj[key] = {
          from: previousProps.current[key],
          to: props[key],
        };
      }
    });

    if (Object.keys(changesObj).length) {
      console.log(`[why-did-you-update] ${name}`, changesObj);
    }

    previousProps.current = props;
  });
}

Use It in Your Component

tsxCopy codefunction UserCard({ name, age }) {
  useWhyDidYouUpdate('UserCard', { name, age });

  return <div>{name} is {age} years old</div>;
}

➡️ In development, this will log what props changed and what their previous and current values are.

Wrapping Up

For me, debugging in React has become much more than just fixing bugs — it’s about building real confidence in the apps I create and what you work on.
Techniques like debouncing, guarding, safe async patterns, and even writing my own custom hook debuggers have truly changed the way I approach problems.
They've helped me avoid those hidden traps like memory leaks and stale updates — and more importantly, they've made my code more robust, maintainable, and enjoyable to work with.

Whether you’re deep into a massive project or experimenting with your next side hustle idea, a little bit of smart, proactive debugging can save you a lot of headaches down the road. 🚀

I’m still learning every day, but that’s the fun part, right? 🙂
Would love to hear how you approach debugging too! Thanks for hanging in there through this detailed post!

Happy debugging and happy coding! 🔍⚛️