<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ Kelechi Apugo - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ Kelechi Apugo - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Sun, 09 Aug 2026 04:50:24 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/author/Laviedegeorge/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ How to Defend Your JavaScript App Against Unsafe Data with TypeScript Guard Utilities ]]>
                </title>
                <description>
                    <![CDATA[ Picture this: you hit an API endpoint, and you get an API response back. You pass the data straight into your application, and everything looks fine in development. Your mock data is clean, your types ]]>
                </description>
                <link>https://www.freecodecamp.org/news/defend-your-js-app-against-unsafe-data-with-typescript-guard-utilities/</link>
                <guid isPermaLink="false">6a4576be7808ec6de20de8ff</guid>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ React ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Programming Tips ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Kelechi Apugo ]]>
                </dc:creator>
                <pubDate>Wed, 01 Jul 2026 20:21:18 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/45694ec7-239d-4f58-a597-9b2145bc02f5.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Picture this: you hit an API endpoint, and you get an API response back. You pass the data straight into your application, and everything looks fine in development. Your mock data is clean, your types line up, and everything checks out.</p>
<p>Then your code hits production. A field from the API endpoint comes back as <code>null</code> instead of a string. You were expecting an array, and it comes back as <code>undefined</code>, expecting an object and receiving a <code>number</code>. Suddenly, you're faced with an error screen, a crashed UI, or worse, silent data corruption that nobody notices until a user complains.</p>
<p>This is a common and preventable bug in JavaScript development. The fix doesn't require a third-party library or a complete architecture overhaul. It requires a small set of utility functions and the discipline to use them when needed.</p>
<p>This article shows you how to build a resilient application using four TypeScript guard utilities that'll make your codebase more reliable: <code>safeArray</code>, <code>safeString</code>, <code>safeNumber</code>, and <code>safeObject</code>. The utilities are framework-agnostic, so whether you're working in React, plain JavaScript, or anything in between, you can drop them straight into your codebase.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-the-problem">The Problem</a></p>
</li>
<li><p><a href="#heading-why-this-problem-exists">Why This Problem Exists</a></p>
</li>
<li><p><a href="#heading-the-solution-safe-access-utilities">The Solution: Safe Access Utilities</a></p>
</li>
<li><p><a href="#heading-how-each-utility-works">How Each Utility Works</a></p>
</li>
<li><p><a href="#heading-how-to-use-them-in-practice">How to Use Them in Practice</a></p>
</li>
<li><p><a href="#heading-best-practices">Best Practices</a></p>
</li>
<li><p><a href="#heading-things-to-avoid">Things to Avoid</a></p>
</li>
<li><p><a href="#heading-bonus-combine-them-into-a-safedata-helper">Bonus: Combine Them into a safeData Helper</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h3 id="heading-prerequisites">Prerequisites</h3>
<p>Before diving in, you should have:</p>
<ul>
<li><p>A working knowledge of TypeScript. You don't need to be an expert, but you should be comfortable with types, interfaces, and generics</p>
</li>
<li><p>Familiarity with JavaScript and its built-in type-checking methods, like <code>typeof</code> and <code>Array.isArray</code>.</p>
</li>
</ul>
<h2 id="heading-the-problem">The Problem</h2>
<p>JavaScript is a loosely typed language. It will let you call <code>.map()</code> on something that isn't an array, access properties on <code>null</code>, and do arithmetic with <code>NaN</code>. All of this, without throwing an error until it's too late. The language doesn't push back. It just breaks quietly.</p>
<p>TypeScript helps, but only up to a point. It checks types at compile time, not at runtime. So when external data arrives from an API, a form submission, local storage, or a third-party SDK, TypeScript has already left the building. Whatever your interface says, the actual value at runtime is whatever JavaScript received.</p>
<p>Here's what that looks like in practice:</p>
<pre><code class="language-typescript">// This looks fine. It is not fine.
type User = {
  id: number;
  name: string;
  tags: string[];
};

function displayUser(user: User) {
  const upperName = user.name.toUpperCase();
  const tagList = user.tags.map((tag) =&gt; `#${tag}`);
  return { upperName, tagList };
}
</code></pre>
<p>If <code>user.name</code> comes back as <code>null</code>, calling <code>.toUpperCase()</code> crashes your application. If <code>user.tags</code> is <code>undefined</code>, calling <code>.map()</code> crashes your application, too. Both scenarios are entirely possible when you're consuming a real API, and TypeScript won't warn you because you told it to trust the type.</p>
<p>Wait! I hear you saying, "I can use <code>optional chaining</code> to stop my app from crashing". This is correct, like the example below:</p>
<pre><code class="language-typescript">// This looks better. But...
type User = {
  id: number;
  name: string;
  tags: string[];
};

function displayUser(user: User) {
  const upperName = user?.name?.toUpperCase?.();
  const tagList = (user?.tags || [])?.map((tag) =&gt; `#${tag}`); 
  return { upperName, tagList };
}
</code></pre>
<p>But there are issues with the above approach. Firstly, <code>upperName</code> will return <code>undefined</code> if <code>user.name</code> isn't a string. Secondly, the <code>user?.tag || []</code> guards for <code>undefined</code> and <code>null</code> values alone. What if an object gets returned? <code>{...}?.map(...)</code>? Do you see the real issue now?</p>
<p>So <code>user?.name?.toUpperCase?.()</code> safely handles cases where <code>user</code>, <code>name</code>, or even <code>toUpperCase</code> itself might not exist. This is handy when dealing with uncertain data shapes, but it doesn't handle data mismatch.</p>
<h2 id="heading-why-this-problem-exists">Why This Problem Exists</h2>
<p>The blame sits squarely with JavaScript's type system, or rather, its lack of one.</p>
<p>JavaScript has a handful of primitive types and a few rules that seem reasonable until you look at them closely. For example, <code>typeof null</code> returns <code>"object"</code>, <code>typeof []</code> also returns <code>"object"</code>, and <code>typeof NaN</code> returns <code>"number"</code>. These aren't edge cases. They're the language.</p>
<p>Here's a quick illustration of how easily JavaScript misleads you:</p>
<pre><code class="language-javascript">typeof null;        // "object" — not "null"
typeof [];          // "object" — not "array"
typeof NaN;         // "number" — NaN is technically a number
Array.isArray([]);  // true — this is the correct check
isNaN("hello");     // true — because "hello" coerces to NaN
Number.isNaN("hello"); // false — this is the correct check
</code></pre>
<p>TypeScript layers a static type system on top of this, catching many mistakes before your code runs. But static analysis only works on code you've already written. The moment data crosses the network boundary or comes from <code>localStorage</code>, a URL parameter, a third-party script, or any source outside your codebase, TypeScript's guarantees stop.</p>
<p>When you write something like this:</p>
<pre><code class="language-typescript">const data = await response.json() as User;
</code></pre>
<p>You're not validating anything. You're telling the TypeScript compiler, "I promise this is a <code>User</code>" . The compiler accepts that promise and stops checking. But if the API returns <code>null</code> for a field, sends a string where you expected a number, or omits a property entirely, JavaScript will proceed anyway and your code will break at the first operation that assumes otherwise.</p>
<p>This gap between "what TypeScript thinks the data is" and "what the data actually is at runtime" is where most production data bugs live. The fix is to stop trusting the type assertion and start validating the data yourself.</p>
<h2 id="heading-the-solution-safe-access-utilities">The Solution: Safe Access Utilities</h2>
<p>The fix is to validate data at the boundary. The moment the expected data enters your application, check it before you pass it anywhere else.</p>
<p>These four functions do exactly that:</p>
<pre><code class="language-typescript">export function safeArray&lt;T&gt;(prop: unknown): T[] {
  if (Array.isArray(prop)) {
    return prop as T[];
  } else {
    return [] as T[];
  }
}

export function safeString(prop: unknown, fallback = ""): string {
  if (typeof prop === "string") {
    return prop;
  } else {
    return fallback;
  }
}

export function safeNumber(prop: unknown, fallback = 0): number {
  if (typeof prop === "number" &amp;&amp; !isNaN(prop)) {
    return prop;
  } else {
    return fallback;
  }
}

export function safeObject&lt;T extends object&gt;(
  prop: unknown,
  fallback = {} as T,
): T {
  if (prop !== null &amp;&amp; typeof prop === "object" &amp;&amp; !Array.isArray(prop)) {
    return prop as T;
  }
  return fallback;
}
</code></pre>
<p>Each function accepts <code>unknown</code>, which forces you to validate the value before using it. Each one returns a safe default if the input isn't what you expected. No crashes, no silent <code>undefined</code>, and no cryptic runtime errors.</p>
<p>You can drop these into any JavaScript or TypeScript project: React app, a Node.js API, a vanilla TypeScript module, or wherever you're handling external data.</p>
<h2 id="heading-how-each-utility-works">How Each Utility Works</h2>
<h3 id="heading-safearray"><code>safeArray</code></h3>
<pre><code class="language-typescript">export function safeArray&lt;T&gt;(prop: unknown): T[] {
  if (Array.isArray(prop)) {
    return prop as T[];
  } else {
    return [] as T[];
  }
}
</code></pre>
<p>This checks whether <code>prop</code> is actually an array using <code>Array.isArray</code>. If it is, you get it back typed as <code>T[]</code>. If it's anything other than an array like <code>null</code>, <code>undefined</code>, a string, or whatever, you get back an empty array.</p>
<p>This matters because of the JavaScript quirk you saw above: <code>typeof []</code> returns <code>"object"</code>, which means a naive <code>typeof</code> check wouldn't catch this. <code>Array.isArray</code> handles it correctly.</p>
<h3 id="heading-safestring"><code>safeString</code></h3>
<pre><code class="language-typescript">export function safeString(prop: unknown, fallback = ""): string {
  if (typeof prop === "string") {
    return prop;
  } else {
    return fallback;
  }
}
</code></pre>
<p>This function uses <code>typeof</code> to confirm that the value is a string. The optional <code>fallback</code> parameter lets you specify a meaningful default. For example, <code>"Unknown"</code> instead of an empty string, when displaying a user's name.</p>
<h3 id="heading-safenumber"><code>safeNumber</code></h3>
<pre><code class="language-typescript">export function safeNumber(prop: unknown, fallback = 0): number {
  if (typeof prop === "number" &amp;&amp; !isNaN(prop)) {
    return prop;
  } else {
    return fallback;
  }
}
</code></pre>
<p>The key detail here is <code>!isNaN(prop)</code>. Because <code>typeof NaN === "number"</code> is true in JavaScript, skipping this check means you could return <code>NaN</code> and cause downstream calculation failures. This function guards against that.</p>
<h3 id="heading-safeobject"><code>safeObject</code></h3>
<pre><code class="language-typescript">export function safeObject&lt;T extends object&gt;(
  prop: unknown,
  fallback = {} as T,
): T {
  if (prop !== null &amp;&amp; typeof prop === "object" &amp;&amp; !Array.isArray(prop)) {
    return prop as T;
  }
  return fallback;
}
</code></pre>
<p>This one requires three conditions due to JavaScript's quirks. <code>typeof null === "object"</code> is true. <code>typeof [] === "object"</code> is also true. So this function explicitly excludes both. What you get back is guaranteed to be a plain object and nothing else.</p>
<h2 id="heading-how-to-use-them-in-practice">How to Use Them in Practice</h2>
<h3 id="heading-normalising-api-responses-plain-typescript">Normalising API Responses (Plain TypeScript)</h3>
<p>The best place to use these utilities is in the function that processes your API response before the data reaches any other part of your application. This works the same way, whether in a React app, a Node.js service, or a plain TypeScript module.</p>
<pre><code class="language-typescript">// lib/users.ts
import { safeArray, safeString, safeNumber, safeObject } from "@/utils/safe";

type User = {
  id: number;
  name: string;
  email: string;
  tags: string[];
};

function normaliseUser(raw: unknown): User {
  const obj = safeObject&lt;Record&lt;string, unknown&gt;&gt;(raw);

  return {
    id: safeNumber(obj.id),
    name: safeString(obj.name, "Unknown User"),
    email: safeString(obj.email),
    tags: safeArray&lt;string&gt;(obj.tags),
  };
}

async function fetchUser(id: string): Promise&lt;User&gt; {
  const response = await fetch(`/api/users/${id}`);
  const data = await response.json();
  return normaliseUser(data);
}
</code></pre>
<p>By the time your code receives the <code>User</code> object, every field is guaranteed to be the type you declared. Nothing downstream has to wonder whether <code>name</code> might be <code>null</code> or <code>tags</code> might be <code>undefined</code>.</p>
<h3 id="heading-in-a-react-component-defensive-rendering">In a React Component (Defensive Rendering)</h3>
<p>Sometimes you receive data directly in a component, from props, context, or a query result, and you don't control normalisation upstream. In that case, wrap the values at the point of use.</p>
<pre><code class="language-typescript">import { safeArray, safeString, safeNumber, safeObject } from "@/utils/safe";

type ProductProps = {
  product: unknown;
};

function ProductCard({ product }: ProductProps) {
  const p = safeObject&lt;Record&lt;string, unknown&gt;&gt;(product);
  const name = safeString(p.name, "Unnamed Product");
  const price = safeNumber(p.price);
  const tags = safeArray&lt;string&gt;(p.tags);

  return (
    &lt;div className="product-card"&gt;
      &lt;h3&gt;{name}&lt;/h3&gt;
      &lt;p&gt;${price.toFixed(2)}&lt;/p&gt;
      &lt;ul&gt;
        {tags.map((tag) =&gt; (
          &lt;li key={tag}&gt;{tag}&lt;/li&gt;
        ))}
      &lt;/ul&gt;
    &lt;/div&gt;
  );
}
</code></pre>
<p>Even if <code>product</code> arrives as <code>null</code> or a completely unexpected shape, this component will render a fallback state instead of crashing.</p>
<h3 id="heading-with-react-query">With React Query</h3>
<p>If you're using React Query, you can normalise data inside the <code>select</code> option, which transforms the raw API response before it reaches your component.</p>
<pre><code class="language-typescript">import { useQuery } from "@tanstack/react-query";
import { safeArray, safeString, safeNumber, safeObject } from "@/utils/safe";

type Order = {
  id: number;
  status: string;
  total: number;
  items: string[];
};

function normaliseOrder(raw: unknown): Order {
  const obj = safeObject&lt;Record&lt;string, unknown&gt;&gt;(raw);
  return {
    id: safeNumber(obj.id),
    status: safeString(obj.status, "pending"),
    total: safeNumber(obj.total),
    items: safeArray&lt;string&gt;(obj.items),
  };
}

function useOrder(orderId: string) {
  return useQuery({
    queryKey: ["order", orderId],
    queryFn: () =&gt;
      fetch(`/api/orders/${orderId}`).then((res) =&gt; res.json()),
    select: normaliseOrder,
  });
}
</code></pre>
<p>The <code>select</code> callback runs after the query resolves and before the data is cached. Your <code>useOrder</code> hook always returns a properly shaped <code>Order</code>, regardless of what the API actually sent back.</p>
<h3 id="heading-with-a-react-context-provider">With a React Context Provider</h3>
<p>Context is a place where unsafe data can silently propagate through your entire component tree. Normalise it at the provider level so every consumer is protected.</p>
<pre><code class="language-typescript">import { createContext, useContext, useEffect, useState } from "react";
import { safeArray, safeString, safeObject } from "@/utils/safe";

type AppConfig = {
  theme: string;
  features: string[];
};

const defaultConfig: AppConfig = {
  theme: "light",
  features: [],
};

const ConfigContext = createContext&lt;AppConfig&gt;(defaultConfig);

function ConfigProvider({ children }: { children: React.ReactNode }) {
  const [config, setConfig] = useState&lt;AppConfig&gt;(defaultConfig);

  useEffect(() =&gt; {
    fetch("/api/config")
      .then((res) =&gt; res.json())
      .then((raw: unknown) =&gt; {
        const obj = safeObject&lt;Record&lt;string, unknown&gt;&gt;(raw);
        setConfig({
          theme: safeString(obj.theme, "light"),
          features: safeArray&lt;string&gt;(obj.features),
        });
      });
  }, []);

  return (
    &lt;ConfigContext.Provider value={config}&gt;{children}&lt;/ConfigContext.Provider&gt;
  );
}

export function useConfig() {
  return useContext(ConfigContext);
}
</code></pre>
<p>One normalisation step at the provider level protects every component that consumes the context.</p>
<h3 id="heading-in-a-nodejs-api-route">In a Node.js API Route</h3>
<p>These utilities are just as useful on the backend. If your Node.js API receives a request body, you can't trust that the client sent what you expected. Validate it at the point of entry.</p>
<pre><code class="language-typescript">// routes/orders.ts (Express)
import { safeArray, safeString, safeNumber, safeObject } from "../utils/safe";

type OrderPayload = {
  userId: number;
  notes: string;
  itemIds: number[];
};

function parseOrderPayload(raw: unknown): OrderPayload {
  const obj = safeObject&lt;Record&lt;string, unknown&gt;&gt;(raw);
  return {
    userId: safeNumber(obj.userId),
    notes: safeString(obj.notes),
    itemIds: safeArray&lt;number&gt;(obj.itemIds),
  };
}

app.post("/orders", (req, res) =&gt; {
  const payload = parseOrderPayload(req.body);

  if (!payload.userId) {
    return res.status(400).json({ error: "userId is required" });
  }

  // proceed with validated payload
});
</code></pre>
<p>The same four utilities, the same pattern. Just a different runtime environment.</p>
<h2 id="heading-best-practices">Best Practices</h2>
<p>As with anything, there are some best practices that'll help you use these utilities well and correctly.</p>
<p>First, normalise at the boundary, not inside every function. The best place to call these utilities is in your data-fetching layer, API handlers, or integration points, just once, before the data spreads. If you're calling <code>safeString</code> in five different places for the same field, that's a sign the normalisation belongs upstream.</p>
<p>Second, use meaningful fallbacks. The default fallbacks (empty string, <code>0</code>, empty array, and empty object) are safe, but sometimes misleading. For a user's display name, <code>safeString(name, "Anonymous")</code> is more informative than <code>safeString(name)</code>. Think about what makes sense for each field in your domain.</p>
<p>Third, keep your type definitions honest. If a field can realistically be <code>null</code> or <code>undefined</code> from your data source, reflect that in your types and use these utilities to handle it. Typing a field as <code>string</code> when it might be <code>null</code> just papers over the problem. These utilities work best when your types reflect the reality of what you receive.</p>
<p>Finally, create a normalisation module. Put all your normaliser functions in one place, for example, <code>src/lib/normalise.ts</code>. This keeps the defensive logic centralised, easy to test, and out of your application logic.</p>
<h2 id="heading-things-to-avoid">Things to Avoid</h2>
<p>Likewise, there are some practices you should avoid.</p>
<p>First, don't use these utilities as a substitute for a proper data contract. If your entire codebase is wrapping every value in <code>safeString</code> because your data sources are wildly inconsistent, the real fix is a contract, an OpenAPI spec, a shared schema, Zod validation, or at minimum, documented response shapes. These utilities handle edge cases and runtime surprises, not systemic chaos.</p>
<p>Second, don't skip the <code>safeObject</code> wrapper. It's tempting to cast straight to <code>any</code> and access properties directly. Avoid this. The <code>as any</code> cast defeats TypeScript entirely, and accessing properties on an <code>unknown</code> value will cause a compile error anyway. Use <code>safeObject</code> to unwrap the value first, then access its fields safely.</p>
<p>Next, don't chain these utilities without extracting intermediate values. Something like <code>safeString(safeArray(raw)[0])</code> might seem compact, but it's harder to read and debug. Extract intermediate values into clearly named variables instead.</p>
<p>And finally, don't skip validation just because you control the data source. "I wrote the API, so I know what it returns" is a reasonable position right up until a schema migration, a nullable column addition, or an unconsidered edge case proves otherwise. Trust the utilities, not your memory.</p>
<h2 id="heading-bonus-combine-them-into-a-safedata-helper">Bonus: Combine Them into a <code>safeData</code> Helper</h2>
<p>If you find yourself calling all four utilities together frequently, which you will once you start normalising API responses consistently, you can compose them into a single fluent helper.</p>
<pre><code class="language-typescript">// utils/safeData.ts
import { safeArray, safeString, safeNumber, safeObject } from "./safe";

type SafeDataAccessors = {
  string: (key: string, fallback?: string) =&gt; string;
  number: (key: string, fallback?: number) =&gt; number;
  array: &lt;T&gt;(key: string) =&gt; T[];
  object: &lt;T extends object&gt;(key: string, fallback?: T) =&gt; T;
};

export function safeData(raw: unknown): SafeDataAccessors {
  const obj = safeObject&lt;Record&lt;string, unknown&gt;&gt;(raw);

  return {
    string: (key, fallback = "") =&gt; safeString(obj[key], fallback),
    number: (key, fallback = 0) =&gt; safeNumber(obj[key], fallback),
    array: &lt;T&gt;(key: string) =&gt; safeArray&lt;T&gt;(obj[key]),
    object: &lt;T extends object&gt;(key: string, fallback = {} as T) =&gt;
      safeObject&lt;T&gt;(obj[key], fallback),
  };
}
</code></pre>
<p>Your normalisation functions then read cleanly, whether you're in a React hook, an Express route, or anywhere else:</p>
<pre><code class="language-typescript">import { safeData } from "@/utils/safeData";

function normaliseUser(raw: unknown) {
  const d = safeData(raw);
  return {
    id: d.number("id"),
    name: d.string("name", "Unknown User"),
    email: d.string("email"),
    tags: d.array&lt;string&gt;("tags"),
  };
}
</code></pre>
<p>This is a thin abstraction: no magic, just less repetition. Use it if your normalisation functions are getting verbose. Skip it if the direct utility calls are clear enough for your team.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>JavaScript's loose type system and TypeScript's compile-time-only guarantees leave a gap at every data boundary. External data — from APIs, request bodies, local storage, third-party scripts — arrives at runtime with no guarantee it matches the shape you declared. These four utilities close that gap.</p>
<p><code>safeArray</code>, <code>safeString</code>, <code>safeNumber</code>, and <code>safeObject</code> each accept <code>unknown</code>, validate the actual type, and return a safe fallback if the value isn't what you expected. They work in React components, Node.js routes, custom hooks, context providers, and any other JavaScript or TypeScript context where data enters your application.</p>
<p>The pattern is simple: validate at the boundary, trust inside. Normalise your data once, at the point it enters your codebase, and everything downstream can focus on its actual job instead of defending against bad inputs.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Debug React State Updates Like a Pro (Without Polluting Production) ]]>
                </title>
                <description>
                    <![CDATA[ When you’re debugging a large React codebase, you might start to feel like a detective. Especially when you are looking for unexpected state changes, components that re-render when they like, or Context values that disappear into thin air without any... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-debug-react-state-updates-like-a-pro-without-polluting-production/</link>
                <guid isPermaLink="false">698115a622cd39b64c5fac49</guid>
                
                    <category>
                        <![CDATA[ React ]]>
                    </category>
                
                    <category>
                        <![CDATA[ debugging ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Frontend Development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ React state management ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Kelechi Apugo ]]>
                </dc:creator>
                <pubDate>Mon, 02 Feb 2026 21:22:46 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1770067225475/d0910306-5756-465a-8b6f-adf839fe004a.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>When you’re debugging a large React codebase, you might start to feel like a detective. Especially when you are looking for unexpected state changes, components that re-render when they like, or Context values that disappear into thin air without any prior warning sign.</p>
<p>And the main difficulty isn’t necessarily what went wrong – it’s pinpointing where it went wrong.</p>
<p>React offers powerful ways to change state, but it doesn’t specify who or what caused those changes. In large apps with many layers of components, hooks, and contexts, this lack of insight can turn simple bugs into frustrating, time-consuming puzzles.</p>
<p>This is where more innovative debugging methods become crucial. Before now, the go-to solution was to sprinkle <code>console.log</code> calls at key points or to fall back to DevTools.</p>
<p>But these days, you can write a small but powerful utility function that can catch the criminal involved in the crimes against your codebase. This utility function can log changes, display meaningful stack traces, and work smoothly with <code>useState</code>, <code>useReducer</code>, Context providers, and custom hooks. And all of the above can occur while remaining invisible in production.</p>
<p>This article guides you through how to use this helper function to improve clarity, minimise guesswork, and debug efficiently without affecting performance or code cleanliness in your live environment.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-the-problem">The Problem</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-why-this-problem-exists">Why This Problem Exists</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-my-solution-createdebugsetter">My Solution: <code>createDebugSetter</code></a></p>
<ul>
<li><a class="post-section-overview" href="#heading-practical-examples-of-createdebugsetter">Practical Examples of <code>createDebugSetter</code></a></li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-best-practices-for-using-createdebugsetter">Best Practices for using <code>createDebugSetter</code></a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-things-to-avoid">Things to Avoid</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-convert-createdebugsetter-to-a-hook">Bonus: How to Convert <code>createDebugSetter</code> to a Hook</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-the-problem">The Problem</h2>
<p>React’s state system is powerful, but it hides too much information when something goes wrong – for example, when an unexpected update happens or a component re-renders endlessly. React doesn’t tell you <em>what</em> triggered the update, <em>what</em> changed, or <em>why</em> it happened. This lack of visibility creates several challenges.</p>
<p>The first is that you can’t easily see which component, function, or effect initiated a state update. In large applications, where the same state may be modified from multiple places, this quickly turns debugging into guesswork. Without clear traces, developers often sprinkle <code>console.log</code> throughout their code to find the source of a single update.</p>
<p>Secondly, React lacks a built-in method for directly comparing previous and current values. This complicates diagnosing whether a bug stems from an incorrect calculation, a faulty API response, or erroneous business logic. The challenge increases with nested objects, arrays, or shared context.</p>
<p>Thirdly, Context updates can trigger re-renders across the entire tree, even for components wrapped in memoisation. But React doesn’t explain <em>why</em> a particular provider changed, leaving teams to wonder what triggered the cascade.</p>
<p>Finally, infinite loops caused by effects, unstable dependencies, or repeated setState calls provide no clues in the console. You only see symptoms like “loading…” repeating endlessly, with no indication of the source.</p>
<p>All of this makes debugging complex React apps frustrating, slow, and often misleading without additional tools or structured techniques.</p>
<h2 id="heading-why-this-problem-exists">Why This Problem Exists</h2>
<p>React intentionally conceals its internal update process to keep the framework fast and predictable.</p>
<p>Because of this:</p>
<ul>
<li><p><code>setState()</code> doesn’t report where it was called from</p>
</li>
<li><p>Context re-renders can originate from anywhere.</p>
</li>
<li><p>State overrides can happen silently.</p>
</li>
<li><p>Debugging often relies on manually adding console logs.</p>
</li>
</ul>
<p>In large applications, this lack of visibility makes it nearly impossible to trace unexpected state changes.</p>
<h2 id="heading-my-solution-createdebugsetter">My Solution: <code>createDebugSetter</code></h2>
<p>A small helper function, <code>createDebugSetter</code>, wraps your state setter and logs:</p>
<ul>
<li><p>The label of the state</p>
</li>
<li><p>The new value</p>
</li>
<li><p>A complete stack trace showing exactly where the update originated</p>
</li>
</ul>
<p>And best of all, it automatically disables itself in production using <code>NODE_ENV</code> so there’s no impact on your live app.</p>
<h3 id="heading-createdebugsetter">createDebugSetter</h3>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">createDebugSetter</span>(<span class="hljs-params">
  label: <span class="hljs-built_in">string</span>,
  setter:
    | React.Dispatch&lt;React.SetStateAction&lt;unknown&gt;&gt;
    | React.Dispatch&lt;unknown&gt;
</span>): <span class="hljs-title">React</span>.<span class="hljs-title">Dispatch</span>&lt;<span class="hljs-title">React</span>.<span class="hljs-title">SetStateAction</span>&lt;<span class="hljs-title">unknown</span>&gt;&gt; | <span class="hljs-title">React</span>.<span class="hljs-title">Dispatch</span>&lt;<span class="hljs-title">unknown</span>&gt; </span>{
  <span class="hljs-comment">// In production, return the original setter unchanged</span>
  <span class="hljs-keyword">if</span> (<span class="hljs-keyword">import</span>.meta.env.PROD <span class="hljs-comment">/* vite-react */</span>) {
    <span class="hljs-keyword">return</span> setter;
  }

  <span class="hljs-comment">// Create a wrapper that logs before calling the original setter</span>
  <span class="hljs-keyword">return</span> <span class="hljs-function">(<span class="hljs-params">value: React.SetStateAction&lt;unknown&gt; | unknown</span>) =&gt;</span> {
    <span class="hljs-comment">// Log the state change</span>
    <span class="hljs-built_in">console</span>.groupCollapsed(
      <span class="hljs-string">`%c🔄 [<span class="hljs-subst">${label}</span>] State Update`</span>,
      <span class="hljs-string">"color: #adad01; font-weight: bold;"</span>
    );
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"🆕 New value:"</span>, value);
    <span class="hljs-built_in">console</span>.trace(<span class="hljs-string">"📍 Update triggered from:"</span>);
    <span class="hljs-built_in">console</span>.groupEnd();

    <span class="hljs-comment">// Call the original setter</span>
    setter(value);
  };
}
</code></pre>
<p>The function above is a <strong>debugging wrapper for React state setters</strong> that logs during development.</p>
<p>It takes two parameters: a label for identification and the <code>setState</code> function you want to debug. In development mode, every state update triggers a collapsible console log that shows the new value and the stack trace of the update's origin. In production, the hook skips entirely and returns the original setter unchanged, ensuring zero runtime overhead in deployed applications.</p>
<p>How it does it:</p>
<pre><code class="lang-typescript"> <span class="hljs-comment">// In production, return the original setter unchanged</span>
  <span class="hljs-keyword">if</span> (<span class="hljs-keyword">import</span>.meta.env.PROD <span class="hljs-comment">/* vite-react */</span>) {
    <span class="hljs-keyword">return</span> setter;
  }
</code></pre>
<p>In production, the <code>createDebugSetter</code> function returns the React <code>setState</code> as-is. This is because we don’t want to log anything when our code is running in a production environment. Here, we’re using the <code>import.meta.env.PROD</code> from React-vite. It returns a <code>Boolean</code> value that tells us if it’s in the production environment or not.</p>
<p>If it’s not in production, we return the modified setter below.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// Create a wrapper that logs before calling the original setter</span>
  <span class="hljs-keyword">return</span> <span class="hljs-function">(<span class="hljs-params">value: React.SetStateAction&lt;unknown&gt; | unknown</span>) =&gt;</span> {
    <span class="hljs-comment">// Log the state change</span>
    <span class="hljs-built_in">console</span>.groupCollapsed(
      <span class="hljs-string">`%c🔄 [<span class="hljs-subst">${label}</span>] State Update`</span>,
      <span class="hljs-string">"color: #adad01; font-weight: bold;"</span>
    );
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"🆕 New value:"</span>, value);
    <span class="hljs-built_in">console</span>.trace(<span class="hljs-string">"📍 Update triggered from:"</span>);
    <span class="hljs-built_in">console</span>.groupEnd();

    <span class="hljs-comment">// Call the original setter</span>
    setter(value);
  };
</code></pre>
<p>This new setter function first logs a collapsible console group with the label and emoji. Then it shows the new value being set. After that, it displays a stack trace showing where the update was triggered. Lastly, it calls the original setter to actually update the state.</p>
<h3 id="heading-practical-examples-of-createdebugsetter">Practical Examples of createDebugSetter</h3>
<p>Let’s now see how <code>createDebugSetter</code> can be used in several places within a codebase.</p>
<h4 id="heading-context-providers">Context Providers</h4>
<p>You can use <code>createDebugSetter</code> within a Context provider to log state changes when <code>setState</code> is called. This can help log and trace state changes whenever <code>setState</code> is called in a Context Provider anywhere in the application.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { createContext, useContext, useState, <span class="hljs-keyword">type</span> ReactNode } <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
<span class="hljs-keyword">import</span> { createDebugSetter } <span class="hljs-keyword">from</span> <span class="hljs-string">"../utils/createDebugSetter"</span>;

<span class="hljs-keyword">interface</span> User {
  name: <span class="hljs-built_in">string</span>;
  email: <span class="hljs-built_in">string</span>;
  role: <span class="hljs-built_in">string</span>;
}

<span class="hljs-keyword">interface</span> UserContextType {
  user: User | <span class="hljs-literal">null</span>;
  setUser: React.Dispatch&lt;React.SetStateAction&lt;User | <span class="hljs-literal">null</span>&gt;&gt;;
  login: <span class="hljs-function">(<span class="hljs-params">name: <span class="hljs-built_in">string</span>, email: <span class="hljs-built_in">string</span></span>) =&gt;</span> <span class="hljs-built_in">void</span>;
  logout: <span class="hljs-function">() =&gt;</span> <span class="hljs-built_in">void</span>;
}

<span class="hljs-keyword">const</span> UserContext = createContext&lt;UserContextType | <span class="hljs-literal">undefined</span>&gt;(<span class="hljs-literal">undefined</span>);

<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">UserProvider</span>(<span class="hljs-params">{ children }: { children: ReactNode }</span>) </span>{
  <span class="hljs-keyword">const</span> [user, setUserOriginal] = useState&lt;User | <span class="hljs-literal">null</span>&gt;(<span class="hljs-literal">null</span>);

  <span class="hljs-comment">// Wrap setter with debug functionality</span>
  <span class="hljs-keyword">const</span> setUser = createDebugSetter(
    <span class="hljs-string">"UserContext"</span>,
    setUserOriginal
  ) <span class="hljs-keyword">as</span> React.Dispatch&lt;React.SetStateAction&lt;User | <span class="hljs-literal">null</span>&gt;&gt;;

  <span class="hljs-keyword">const</span> login = <span class="hljs-function">(<span class="hljs-params">name: <span class="hljs-built_in">string</span>, email: <span class="hljs-built_in">string</span></span>) =&gt;</span> {
    setUser({
      name,
      email,
      role: <span class="hljs-string">"user"</span>,
    });
  };

  <span class="hljs-keyword">const</span> logout = <span class="hljs-function">() =&gt;</span> {
    setUser(<span class="hljs-literal">null</span>);
  };

  <span class="hljs-keyword">return</span> (
    &lt;UserContext.Provider value={{ user, setUser, login, logout }}&gt;
      {children}
    &lt;/UserContext.Provider&gt;
  );
}
</code></pre>
<p>In the above code sample, we create a modified <code>setUserOriginal</code> called <code>setUser</code> that uses <code>createDebugSetter</code> under the hood. We then expose it to the context value instead of <code>setUserOriginal</code> .</p>
<p>Whenever <code>setUser</code> is called, it triggers <code>createDebugSetter</code> which does its job of checking the environment the code is running in, and returns a modified setter that will call <code>setUserOriginal</code> after the logging process, or will return <code>setUserOriginal</code> as-is.</p>
<p>This is useful because Context updates can trigger many re-renders. This reveals exactly who changed the shared state.</p>
<h4 id="heading-usestate">useState</h4>
<p>As you saw in the Context provider example above, we can use the same technique in regular components that use React state setters (just as in Context providers). We log and trace the value. It also shows where it was triggered from within the component or application.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { useState } <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
<span class="hljs-keyword">import</span> { useDebugSetter } <span class="hljs-keyword">from</span> <span class="hljs-string">"../hooks/useDebugSetter"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">UseStateExample</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> [count, setCountOriginal] = useState(<span class="hljs-number">0</span>);
  <span class="hljs-keyword">const</span> [name, setNameOriginal] = useState(<span class="hljs-string">"React"</span>);

  <span class="hljs-comment">// Wrap setters with debug functionality</span>
  <span class="hljs-keyword">const</span> setCount = useDebugSetter(<span class="hljs-string">"Counter"</span>, setCountOriginal);
  <span class="hljs-keyword">const</span> setName = useDebugSetter(<span class="hljs-string">"Name"</span>, setNameOriginal);

  <span class="hljs-keyword">const</span> handleIncrement = <span class="hljs-function">() =&gt;</span> {
    setCount(count + <span class="hljs-number">1</span>);
  };

  <span class="hljs-keyword">const</span> handleDecrement = <span class="hljs-function">() =&gt;</span> {
    setCount(count - <span class="hljs-number">1</span>);
  };

  <span class="hljs-keyword">const</span> handleNameChange = <span class="hljs-function">() =&gt;</span> {
    setName(name === <span class="hljs-string">"React"</span> ? <span class="hljs-string">"Vite"</span> : <span class="hljs-string">"React"</span>);
  };

  <span class="hljs-keyword">return</span> (
    &lt;div
      style={{
        padding: <span class="hljs-string">"20px"</span>,
        border: <span class="hljs-string">"1px solid #ccc"</span>,
        borderRadius: <span class="hljs-string">"8px"</span>,
        margin: <span class="hljs-string">"10px"</span>,
      }}
    &gt;
      &lt;h2&gt;useState Example&lt;/h2&gt;
      &lt;p&gt;Open the <span class="hljs-built_in">console</span> to see debug logs when state changes.&lt;/p&gt;

      &lt;div style={{ marginTop: <span class="hljs-string">"15px"</span> }}&gt;
        &lt;p&gt;
          Count: &lt;strong&gt;{count}&lt;/strong&gt;
        &lt;/p&gt;
        &lt;button onClick={handleIncrement} style={{ marginRight: <span class="hljs-string">"10px"</span> }}&gt;
          Increment
        &lt;/button&gt;
        &lt;button onClick={handleDecrement}&gt;Decrement&lt;/button&gt;
      &lt;/div&gt;

      &lt;div style={{ marginTop: <span class="hljs-string">"15px"</span> }}&gt;
        &lt;p&gt;
          Name: &lt;strong&gt;{name}&lt;/strong&gt;
        &lt;/p&gt;
        &lt;button onClick={handleNameChange}&gt;Toggle Name&lt;/button&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  );
}
</code></pre>
<p>This works exactly like the Context providers example. The only differences are that the component uses the <code>setCount</code> and <code>setName</code> functions out of the box in buttons and related components. Also, unlike the Context provider, this component has local state that can be passed to its child components if needed.</p>
<p>This is ideal for monitoring unforeseen local state changes or loops triggered by effects.</p>
<h4 id="heading-usereducer">useReducer</h4>
<p>React reducers are used to calculate complex logic before updating the state. This can introduce unwanted side effects during the complex phase. <code>createDebugSetter</code> can help in debugging, as shown below:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { useReducer } <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
<span class="hljs-keyword">import</span> { createDebugSetter } <span class="hljs-keyword">from</span> <span class="hljs-string">"../utils/createDebugSetter"</span>;

<span class="hljs-keyword">interface</span> CounterState {
  count: <span class="hljs-built_in">number</span>;
  step: <span class="hljs-built_in">number</span>;
}

<span class="hljs-keyword">type</span> CounterAction =
  | { <span class="hljs-keyword">type</span>: <span class="hljs-string">"increment"</span> }
  | { <span class="hljs-keyword">type</span>: <span class="hljs-string">"decrement"</span> }
  | { <span class="hljs-keyword">type</span>: <span class="hljs-string">"reset"</span> }
  | { <span class="hljs-keyword">type</span>: <span class="hljs-string">"setStep"</span>; step: <span class="hljs-built_in">number</span> };

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">counterReducer</span>(<span class="hljs-params">
  state: CounterState,
  action: CounterAction
</span>): <span class="hljs-title">CounterState</span> </span>{
  <span class="hljs-keyword">switch</span> (action.type) {
    <span class="hljs-keyword">case</span> <span class="hljs-string">"increment"</span>:
      <span class="hljs-keyword">return</span> { ...state, count: state.count + state.step };
    <span class="hljs-keyword">case</span> <span class="hljs-string">"decrement"</span>:
      <span class="hljs-keyword">return</span> { ...state, count: state.count - state.step };
    <span class="hljs-keyword">case</span> <span class="hljs-string">"reset"</span>:
      <span class="hljs-keyword">return</span> { ...state, count: <span class="hljs-number">0</span> };
    <span class="hljs-keyword">case</span> <span class="hljs-string">"setStep"</span>:
      <span class="hljs-keyword">return</span> { ...state, step: action.step };
    <span class="hljs-keyword">default</span>:
      <span class="hljs-keyword">return</span> state;
  }
}

<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">UseReducerExample</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> [state, dispatchOriginal] = useReducer(counterReducer, {
    count: <span class="hljs-number">0</span>,
    step: <span class="hljs-number">1</span>,
  });

  <span class="hljs-comment">// Wrap dispatch with debug functionality</span>
  <span class="hljs-keyword">const</span> dispatch = createDebugSetter(<span class="hljs-string">"CounterReducer"</span>, dispatchOriginal);

  <span class="hljs-keyword">return</span> (
    &lt;div
      style={{
        padding: <span class="hljs-string">"20px"</span>,
        border: <span class="hljs-string">"1px solid #ccc"</span>,
        borderRadius: <span class="hljs-string">"8px"</span>,
        margin: <span class="hljs-string">"10px"</span>,
      }}
    &gt;
      &lt;h2&gt;useReducer Example&lt;/h2&gt;
      &lt;p&gt;Open the <span class="hljs-built_in">console</span> to see debug logs <span class="hljs-keyword">for</span> reducer actions.&lt;/p&gt;

      &lt;div style={{ marginTop: <span class="hljs-string">"15px"</span> }}&gt;
        &lt;p&gt;
          Count: &lt;strong&gt;{state.count}&lt;/strong&gt;
        &lt;/p&gt;
        &lt;p&gt;
          Step: &lt;strong&gt;{state.step}&lt;/strong&gt;
        &lt;/p&gt;

        &lt;div style={{ marginTop: <span class="hljs-string">"10px"</span> }}&gt;
          &lt;button
            onClick={<span class="hljs-function">() =&gt;</span> dispatch({ <span class="hljs-keyword">type</span>: <span class="hljs-string">"increment"</span> })}
            style={{ marginRight: <span class="hljs-string">"10px"</span> }}
          &gt;
            Increment (+{state.step})
          &lt;/button&gt;
          &lt;button
            onClick={<span class="hljs-function">() =&gt;</span> dispatch({ <span class="hljs-keyword">type</span>: <span class="hljs-string">"decrement"</span> })}
            style={{ marginRight: <span class="hljs-string">"10px"</span> }}
          &gt;
            Decrement (-{state.step})
          &lt;/button&gt;
          &lt;button
            onClick={<span class="hljs-function">() =&gt;</span> dispatch({ <span class="hljs-keyword">type</span>: <span class="hljs-string">"reset"</span> })}
            style={{ marginRight: <span class="hljs-string">"10px"</span> }}
          &gt;
            Reset
          &lt;/button&gt;
          &lt;button
            onClick={<span class="hljs-function">() =&gt;</span>
              dispatch({ <span class="hljs-keyword">type</span>: <span class="hljs-string">"setStep"</span>, step: state.step === <span class="hljs-number">1</span> ? <span class="hljs-number">5</span> : <span class="hljs-number">1</span> })
            }
          &gt;
            Toggle Step ({state.step === <span class="hljs-number">1</span> ? <span class="hljs-string">"1→5"</span> : <span class="hljs-string">"5→1"</span>})
          &lt;/button&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  );
}
</code></pre>
<p><code>dispatchOriginal</code>, which is the main dispatch function, is replaced with a custom function called <code>dispatch</code> that uses <code>createDebugSetter</code>. When the custom <code>dispatch</code> function is called, it does the job of <code>createDebugSetter</code> and by extension, the job of <code>dispatchOriginal</code> .</p>
<p>This is perfect for logging reducer actions and understanding complex state transitions.</p>
<h4 id="heading-custom-hooks">Custom Hooks</h4>
<p>Custom hooks are not left out of the equation, as they can use <code>setState</code> in some cases. They’re also capable of running complex logic that could backfire when updating <code>state</code>.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { useState, useEffect } <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
<span class="hljs-keyword">import</span> { useDebugSetter } <span class="hljs-keyword">from</span> <span class="hljs-string">"../hooks/useDebugSetter"</span>;

<span class="hljs-comment">// Custom hook that manages a timer</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">useTimer</span>(<span class="hljs-params">initialSeconds: <span class="hljs-built_in">number</span> = 0</span>) </span>{
  <span class="hljs-keyword">const</span> [seconds, setSecondsOriginal] = useState(initialSeconds);
  <span class="hljs-keyword">const</span> [isRunning, setIsRunningOriginal] = useState(<span class="hljs-literal">false</span>);

  <span class="hljs-comment">// Wrap setters with debug functionality</span>
  <span class="hljs-keyword">const</span> setSeconds = useDebugSetter(<span class="hljs-string">"Timer.seconds"</span>, setSecondsOriginal);
  <span class="hljs-keyword">const</span> setIsRunning = useDebugSetter(<span class="hljs-string">"Timer.isRunning"</span>, setIsRunningOriginal);

  useEffect(<span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">if</span> (!isRunning) <span class="hljs-keyword">return</span>;

    <span class="hljs-keyword">const</span> interval = <span class="hljs-built_in">setInterval</span>(<span class="hljs-function">() =&gt;</span> {
      setSeconds(<span class="hljs-function">(<span class="hljs-params">prev</span>) =&gt;</span> prev + <span class="hljs-number">1</span>);
    }, <span class="hljs-number">1000</span>);

    <span class="hljs-keyword">return</span> <span class="hljs-function">() =&gt;</span> <span class="hljs-built_in">clearInterval</span>(interval);
  }, [isRunning, setSeconds]);

  <span class="hljs-keyword">const</span> start = <span class="hljs-function">() =&gt;</span> setIsRunning(<span class="hljs-literal">true</span>);
  <span class="hljs-keyword">const</span> stop = <span class="hljs-function">() =&gt;</span> setIsRunning(<span class="hljs-literal">false</span>);
  <span class="hljs-keyword">const</span> reset = <span class="hljs-function">() =&gt;</span> {
    setSeconds(<span class="hljs-number">0</span>);
    setIsRunning(<span class="hljs-literal">false</span>);
  };

  <span class="hljs-keyword">return</span> {
    seconds,
    isRunning,
    start,
    stop,
    reset,
  };
}

<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">CustomHookExample</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> timer = useTimer(<span class="hljs-number">0</span>);

  <span class="hljs-keyword">const</span> formatTime = <span class="hljs-function">(<span class="hljs-params">seconds: <span class="hljs-built_in">number</span></span>) =&gt;</span> {
    <span class="hljs-keyword">const</span> mins = <span class="hljs-built_in">Math</span>.floor(seconds / <span class="hljs-number">60</span>);
    <span class="hljs-keyword">const</span> secs = seconds % <span class="hljs-number">60</span>;
    <span class="hljs-keyword">return</span> <span class="hljs-string">`<span class="hljs-subst">${mins.toString().padStart(<span class="hljs-number">2</span>, <span class="hljs-string">"0"</span>)}</span>:<span class="hljs-subst">${secs
      .toString()
      .padStart(<span class="hljs-number">2</span>, <span class="hljs-string">"0"</span>)}</span>`</span>;
  };

  <span class="hljs-keyword">return</span> (
    &lt;div
      style={{
        padding: <span class="hljs-string">"20px"</span>,
        border: <span class="hljs-string">"1px solid #ccc"</span>,
        borderRadius: <span class="hljs-string">"8px"</span>,
        margin: <span class="hljs-string">"10px"</span>,
      }}
    &gt;
      &lt;h2&gt;Custom Hook Example&lt;/h2&gt;
      &lt;p&gt;Open the <span class="hljs-built_in">console</span> to see debug logs <span class="hljs-keyword">for</span> internal hook state changes.&lt;/p&gt;

      &lt;div style={{ marginTop: <span class="hljs-string">"15px"</span> }}&gt;
        &lt;p style={{ fontSize: <span class="hljs-string">"24px"</span>, fontWeight: <span class="hljs-string">"bold"</span> }}&gt;
          {formatTime(timer.seconds)}
        &lt;/p&gt;
        &lt;p&gt;
          Status: &lt;strong&gt;{timer.isRunning ? <span class="hljs-string">"Running"</span> : <span class="hljs-string">"Stopped"</span>}&lt;/strong&gt;
        &lt;/p&gt;

        &lt;div style={{ marginTop: <span class="hljs-string">"15px"</span> }}&gt;
          &lt;button
            onClick={timer.start}
            disabled={timer.isRunning}
            style={{ marginRight: <span class="hljs-string">"10px"</span> }}
          &gt;
            Start
          &lt;/button&gt;
          &lt;button
            onClick={timer.stop}
            disabled={!timer.isRunning}
            style={{ marginRight: <span class="hljs-string">"10px"</span> }}
          &gt;
            Stop
          &lt;/button&gt;
          &lt;button onClick={timer.reset}&gt;Reset&lt;/button&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  );
}
</code></pre>
<p>As shown in previous examples, <code>setSecondsOriginal</code> and <code>setIsRunningOriginal</code> are replaced with <code>setSeconds</code> and <code>setIsRunning</code>. The latter uses the <code>createDebugSetter</code> helper function. This enables console log statements to be printed every second for better visualisation.</p>
<p>Custom hooks often hide multiple internal updates, making it hard to see exactly where each begins.</p>
<h2 id="heading-best-practices-for-using-createdebugsetter">Best Practices for Using <code>createDebugSetter</code></h2>
<p>When using helper functions like <code>createDebugSetter</code>, it’s best to keep in mind why you’re actually using them. For our purpose here, we’re using it to debug a React application. So I’ll share some tips that will help with this debugging process.</p>
<h3 id="heading-use-clear-labels">Use Clear Labels</h3>
<p>Using labels that can say where <code>createDebugSetter</code> was triggered from is a step in the right direction. Detailed labels will help you better understand where and why the issue may be occurring. Also, keep in mind that the <code>createDebugSetter</code> utility function could be used in several places in your application, and improper labelling could make debugging difficult.  </p>
<p>Using the name of the component or area that calls it as the label for <code>createDebugSetter</code> can also be a good pointer for clear labelling, as shown below.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// Bad</span>
createDebugSetter(<span class="hljs-string">"aaa"</span>, setUser)
createDebugSetter(<span class="hljs-string">"1"</span>, setUser)

<span class="hljs-comment">// Good </span>
createDebugSetter(<span class="hljs-string">"UserContextProvider"</span>, setUser)
createDebugSetter(<span class="hljs-string">"From UserContextProvider"</span>, setUser)
<span class="hljs-comment">// Too long but can still work</span>
createDebugSetter(<span class="hljs-string">"From UserContextProvider in user-context.tsx file"</span>, setUser)
</code></pre>
<h3 id="heading-use-createdebugsetter-only-in-dev-mode">Use <code>createDebugSetter</code> Only in Dev Mode</h3>
<p>Using <code>createDebugSetter</code> only in a development environment can prevent many headaches. It’s not a good practice to mistakenly expose or log sensitive data in production. Also, logging in production can cause cluttering.</p>
<h3 id="heading-use-createdebugsetter-with-react-devtools">Use <code>createDebugSetter</code> with React DevTools</h3>
<p><strong>The</strong> <code>createDebugSetter</code> may not be enough for some complex bugs. You can use <code>createDebugSetter</code> and React DevTools for a more powerful/thorough debugging session<strong>.</strong> Although <code>createDebugSetter</code> cannot be directly integrated with React DevTools, it shows who triggered the update, whereas React DevTools displays what was re-rendered.</p>
<h3 id="heading-place-createdebugsetter-in-utils">Place <code>createDebugSetter</code> in <code>utils</code></h3>
<p><code>createDebugSetter</code> is a utility function, as I have mentioned above. This means you should place it in a <code>utils</code> folder so any team member can access and use it when needed across your React application.</p>
<h2 id="heading-things-to-avoid">Things to Avoid</h2>
<ol>
<li><p>Avoid using debug setters in production builds. While they are safe, unnecessary logs can slow down debugging tools. Also, sensitive credentials could be logged mistakenly. There are professional tools you can use, such as Sentry, that let you trace errors and debug your app effortlessly.</p>
</li>
<li><p>Don’t conditionally wrap setter functions within components. Perform wrapping outside renders to prevent the creation of new setter identities.</p>
</li>
<li><p>Don’t rely on it to replace proper state architecture. This tool helps identify issues, but doesn’t fix poor state design.</p>
</li>
<li><p>Don’t depend solely on console logs. Use it as part of a broader debugging workflow, not as the only strategy.</p>
</li>
</ol>
<h2 id="heading-bonus-how-to-convert-createdebugsetter-to-a-hook">Bonus: How to Convert <code>createDebugSetter</code> to a Hook</h2>
<h3 id="heading-converting-to-a-hook">Converting to a Hook</h3>
<p>The plain <code>createDebugSetter</code> function works, but it creates a new wrapper function on every render when used inside React components. By converting it into a custom hook with <code>useCallback</code>, we can ensure that the wrapper function maintains a stable reference across re-renders, preventing unnecessary performance overhead and making it safe to use in dependency arrays.</p>
<p>Here’s the hook version:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { useCallback } <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">useDebugSetter</span>&lt;<span class="hljs-title">T</span>&gt;(<span class="hljs-params">
  label: <span class="hljs-built_in">string</span>,
  setState: React.Dispatch&lt;React.SetStateAction&lt;T&gt;&gt;
</span>): <span class="hljs-title">React</span>.<span class="hljs-title">Dispatch</span>&lt;<span class="hljs-title">React</span>.<span class="hljs-title">SetStateAction</span>&lt;<span class="hljs-title">T</span>&gt;&gt; </span>{
  <span class="hljs-keyword">const</span> debugSetter = useCallback(
    <span class="hljs-function">(<span class="hljs-params">newValue: React.SetStateAction&lt;T&gt;</span>) =&gt;</span> {
      <span class="hljs-comment">// Only log in development</span>
      <span class="hljs-keyword">if</span> (!<span class="hljs-keyword">import</span>.meta.env.PROD) {
        <span class="hljs-built_in">console</span>.groupCollapsed(
          <span class="hljs-string">`%c🔄 State Update: <span class="hljs-subst">${label}</span>`</span>,
          <span class="hljs-string">"color: #2fa; font-weight: bold;"</span>
        );
        <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"🆕 New value:"</span>, newValue);
        <span class="hljs-built_in">console</span>.trace(<span class="hljs-string">"📍 Update triggered from:"</span>);
        <span class="hljs-built_in">console</span>.groupEnd();
      }

      setState(newValue);
    },
    [label, setState]
  );

  <span class="hljs-comment">// In production, return the original setter (no wrapping overhead)</span>
  <span class="hljs-comment">// In development, return the debug wrapper</span>
  <span class="hljs-keyword">return</span> <span class="hljs-keyword">import</span>.meta.env.PROD ? setState : debugSetter;
}
</code></pre>
<h3 id="heading-how-the-hook-version-works">How the Hook Version Works</h3>
<p>The core difference between the <code>useDebugSetter</code> hook and <code>createDebugSetter</code> is that the function is wrapped in a <code>useCallback</code> that logs debug information before calling the original setter. Apart from this, all other components of the functions remain the same.</p>
<h3 id="heading-why-the-hook-version-is-better">Why the Hook Version is Better</h3>
<p>The hook version is superior for component usage because it leverages <code>useCallback</code> memoisation of the debug wrapper. This means the function reference stays the same across renders, avoiding potential re-render cascades when the setter is passed to child components or used in <code>useEffect</code> dependencies.</p>
<p>The plain function, by contrast, generates a brand new wrapper on every render, which can break React's optimisation strategies and cause subtle bugs. In production, both versions simply return the original setter, so there's no performance difference there – but in development, the hook prevents unnecessary work.</p>
<h3 id="heading-when-to-use-the-hook">When to Use the Hook</h3>
<p>Use <code>useDebugSetter</code> whenever you're inside a React component and need to debug state updates. This covers the vast majority of cases: wrapping <code>useState</code> setters, passing debug setters to child components, or including them in effect dependencies.</p>
<p>Only reach for the plain <code>createDebugSetter</code> function when you're working outside React components entirely, such as in utility modules, global stores, or configuration files where hooks can't be used. For day-to-day component debugging, the hook is the right choice.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Debugging React state doesn’t have to be guesswork. With a simple helper, you can instantly see what changed, who changed it, where the change originated, and how your app reached that state – all without touching your production environment.</p>
<p>This small utility function can save hours spent searching through your codebase, making you faster, more precise, and more confident in your React application’s behaviour.</p>
<p>Once you adopt this approach, you’ll never debug state the old way again. 🚀</p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
