# React Debugging Tips & Tricks for Developers

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:
    
    ```js
    // ❌ 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:
    
    ```js
    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.

```js
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:

```js
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:
    
    ```js
    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
    
    ```js
    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;
    }
    ```
    
    ```jsx
    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
    
    ```js
    const myRef = useRef(null);
    
    useEffect(() => {
      if (!myRef.current) return; // 🛡️ Guard it!
      myRef.current.focus();
    }, []);
    ```
    
    ### 🔧 Example: Guarding Before Rendering
    
    ```jsx
    if (!userData) {
      return <SkeletonLoader />; // 🛡️ Guard: wait for data before rendering
    }
    ```
    
    ---
    
    ### TL;DR:
    
    | Term | Purpose | Common Use Case |
    | --- | --- | --- |
    | **Debounce** | Prevent calling functions too frequently | Search input, window resize, filters |
    | **Guarding** | Prevent crashes by checking conditions first | Refs, 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:
    
    ```js
    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:
    
    ```js
    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 ?

```js
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

```javascript
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!** 🔍⚛️
