<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/rss.xsl" type="text/xsl"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>rzrn.dev</title><description>rzrn - personal site that shares about software development and other topics</description><link>https://rzrn.dev</link><item><title>Hello World</title><link>https://rzrn.dev/posts/hello-world</link><guid isPermaLink="true">https://rzrn.dev/posts/hello-world</guid><pubDate>Sat, 25 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;New site, new start.&lt;/p&gt;
&lt;p&gt;Hi there! Welcome to my first post. I’ll be sharing my journey, thoughts, and experiences here, and I hope they can be helpful or inspiring to you.&lt;/p&gt;
</content:encoded><author>Ray Azrin Karim</author></item><item><title>Finally Understand Closures (for Real This Time (Hopefully))</title><link>https://rzrn.dev/posts/finally-understand-closures-for-real-this-time-hopefully</link><guid isPermaLink="true">https://rzrn.dev/posts/finally-understand-closures-for-real-this-time-hopefully</guid><pubDate>Sun, 26 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Have you ever come across the term &lt;code&gt;closure&lt;/code&gt;? Or maybe even been asked about it during an interview? You might have a rough idea of what it means, but not a deep understanding. In this post, we’ll take a closer look at what closures really are and explore several practical examples of how they’re used in real-world applications.&lt;/p&gt;
&lt;h2&gt;The Definition&lt;/h2&gt;
&lt;p&gt;Based on &lt;a href=&quot;http://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Closures&quot;&gt;MDN Docs&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;A closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment). In other words, a closure gives a function access to its outer scope. In JavaScript, closures are created every time a function is created, at function creation time.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The simple way to think about it: whenever a function uses a variable that isn’t defined inside it or passed as a parameter, that’s a closure in action. The function basically “remembers” the variables from the place it was born, even after that execution context no longer exists.&lt;/p&gt;
&lt;p&gt;The simplest example of closure would be like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function outside() {
  const num = 123; // local scope variable

  return function inner() {
    console.log(num); // accessing outer scope variable
  };
}

const printNum = outside(); // outside() finishes executing
printNum(); // output: 123 - but num is still &quot;remembered&quot;!
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this example, the variable num is defined inside the local scope of outside().
Then we return the inner() function, which references that outer variable. When we call outside(), it returns the inner function and assigns it to printNum.
Even though outside() has already finished executing, the returned function still “remembers” the value of num thanks to closure.&lt;/p&gt;
&lt;h2&gt;How It Works&lt;/h2&gt;
&lt;p&gt;Alright, let&apos;s get into the details. To really understand closures, we need to see what&apos;s actually happening inside the JavaScript engine. Don&apos;t worry though, we&apos;ll keep it practical.&lt;/p&gt;
&lt;h3&gt;Execution Contexts and Scope Chains&lt;/h3&gt;
&lt;p&gt;When your JavaScript code runs, the engine creates &lt;strong&gt;execution contexts&lt;/strong&gt;. Think of these as little worlds where your code lives. Every time you call a function, a new execution context is created. Each one has:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Variable Environment&lt;/strong&gt; - where your variables and functions hang out&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Lexical Environment&lt;/strong&gt; - basically keeps track of what variables you can access&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Scope Chain&lt;/strong&gt; - the lookup path to find variables in outer scopes&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Let&apos;s go back to our simple example and see what actually happens:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function outside() {
  const num = 123;
  return function inner() {
    console.log(num);
  };
}

const printNum = outside();
printNum();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here&apos;s what&apos;s going on behind the scenes:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Global Context
│
├─ printNum = &amp;lt;function inner&amp;gt;
│
└─ outside() Context (stays alive!)
   │
   ├─ num = 123
   └─ inner() ──┐
                │
                └─&amp;gt; [[Environment]] reference
                    (points back to outside&apos;s context)

When printNum() runs:
│
└─ looks for &apos;num&apos;
   └─&amp;gt; not found locally
       └─&amp;gt; follows [[Environment]] reference
           └─&amp;gt; finds num = 123 in outside&apos;s context ✓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 1: &lt;code&gt;outside()&lt;/code&gt; gets called&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;JavaScript creates a new execution context for &lt;code&gt;outside()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;The variable &lt;code&gt;num&lt;/code&gt; lives in this context&apos;s environment&lt;/li&gt;
&lt;li&gt;When &lt;code&gt;inner&lt;/code&gt; is created, here&apos;s the key part: it &lt;strong&gt;captures a reference&lt;/strong&gt; to &lt;code&gt;outside()&lt;/code&gt;&apos;s environment&lt;/li&gt;
&lt;li&gt;That reference? That&apos;s your closure right there&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Step 2: &lt;code&gt;outside()&lt;/code&gt; finishes and returns&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Normally when a function finishes, its execution context gets destroyed&lt;/li&gt;
&lt;li&gt;All those variables? Gone. Garbage collected&lt;/li&gt;
&lt;li&gt;But wait, &lt;code&gt;inner&lt;/code&gt; is still holding onto a reference to &lt;code&gt;outside()&lt;/code&gt;&apos;s environment&lt;/li&gt;
&lt;li&gt;So JavaScript keeps that environment alive in memory instead of throwing it away&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Step 3: &lt;code&gt;printNum()&lt;/code&gt; runs&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;New execution context gets created for &lt;code&gt;inner&lt;/code&gt; (which we&apos;re calling as &lt;code&gt;printNum&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;It tries to find &lt;code&gt;num&lt;/code&gt;, not in the local scope&lt;/li&gt;
&lt;li&gt;JavaScript follows the scope chain to the captured environment&lt;/li&gt;
&lt;li&gt;Finds &lt;code&gt;num = 123&lt;/code&gt; sitting there waiting&lt;/li&gt;
&lt;li&gt;Logs it out like nothing ever happened&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;The [[Environment]] Hidden Property&lt;/h3&gt;
&lt;p&gt;Here&apos;s something cool: every function in JavaScript has a secret internal property called &lt;code&gt;[[Environment]]&lt;/code&gt; (sometimes you&apos;ll see it as &lt;code&gt;[[Scope]]&lt;/code&gt;). This thing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Gets set when the function is &lt;strong&gt;created&lt;/strong&gt; (not when you call it)&lt;/li&gt;
&lt;li&gt;Points back to where the function was born&lt;/li&gt;
&lt;li&gt;Sticks around for as long as the function exists&lt;/li&gt;
&lt;li&gt;Gets used to look up variables that aren&apos;t in the function&apos;s local scope&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So when we say closures &quot;remember&quot; stuff - they literally do. They carry around a reference to their birthplace.&lt;/p&gt;
&lt;h3&gt;Memory Stuff You Should Know&lt;/h3&gt;
&lt;p&gt;Because closures keep environments alive, there&apos;s a memory trade-off:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function createCounter() {
  let count = 0;
  const history = []; // imagine this is huge

  return function increment() {
    count++;
    return count;
  };
}

const counter = createCounter();
// &apos;history&apos; is just sitting there in memory
// even though we never touch it in increment()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The whole environment gets preserved, not just the variables you actually use. Modern JavaScript engines are smart about this and do some optimization, but it&apos;s worth keeping in mind if you&apos;re working with large datasets.&lt;/p&gt;
&lt;h3&gt;Why Does JavaScript Even Do This?&lt;/h3&gt;
&lt;p&gt;JavaScript has closures because of &lt;strong&gt;lexical scoping&lt;/strong&gt;. Basically, where you write a function in your code determines what variables it can see, not where you call it from.&lt;/p&gt;
&lt;p&gt;Closures happen naturally because JavaScript has:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;First-class functions (you can pass functions around like any other value)&lt;/li&gt;
&lt;li&gt;Lexical scoping (scope is determined by where code is written)&lt;/li&gt;
&lt;li&gt;Functions that can reach into outer scopes&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Put these together, and you get closures. When you pass a function somewhere else or return it, it needs to bring its scope along for the ride. That&apos;s all a closure really is.&lt;/p&gt;
&lt;h2&gt;Practical Example of Closures in Real Apps&lt;/h2&gt;
&lt;p&gt;Closures aren’t just a tricky interview question, they’re a core part of how modern JavaScript works. You may already use them all the time without realizing it. Let’s see various examples of where closure is used.&lt;/p&gt;
&lt;h3&gt;Logger&lt;/h3&gt;
&lt;p&gt;Closure makes it easy to create a logger for each context, making it easy to track the flow of your application. It saves the context reference between calls.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function createLogger(context: string) {
  function log(message: string) {
    const now = new Date().toISOString();
    console.log(`[${now}] [${context}] ${message}`);
  }

  return log;
}

const wsLogger = createLogger(&apos;WebSocket&apos;);
const dbLogger = createLogger(&apos;Database&apos;);

wsLogger(&apos;Subscribed&apos;);
dbLogger(`Query successful, takes ${10}ms`);
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;[2025-10-26T05:44:42.437Z] [WebSocket] Subscribed
[2025-10-26T05:44:42.437Z] [Database] Query successful, takes 10ms
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Debounce&lt;/h3&gt;
&lt;p&gt;Closure helps us to create a function that will only run once after a certain amount of time has passed since the last call. It saves the timeoutId reference between calls, so it &quot;remembers&quot; when is the last time the function was called.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function debounce(func: Function, delay: number) {
  let timeoutId: NodeJS.Timeout | null = null;

  return function (...args: any[]) {
    if (timeoutId) clearTimeout(timeoutId);
    timeoutId = setTimeout(() =&amp;gt; func(...args), delay);
  };
}

const debouncedSearch = debounce((query: string) =&amp;gt; {
  console.log(&apos;Searching for&apos;, query);
}, 500);

debouncedSearch(&apos;a&apos;);
debouncedSearch(&apos;ap&apos;);
debouncedSearch(&apos;app&apos;);
debouncedSearch(&apos;appl&apos;);
debouncedSearch(&apos;apple&apos;); // only runs once, after 500ms
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;Searching for apple
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Caching/Memoization&lt;/h3&gt;
&lt;p&gt;Closures fit naturally for caching previously computed results, as it allows us to save the result of a function call and reuse it later without recomputing it.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function memoized(fn: Function) {
  const cache = new Map(); // beware of memory leaks, read the Memory section above

  return function (...args: any[]) {
    const key = JSON.stringify(args);
    if (cache.has(key)) {
      const val = cache.get(key);
      console.log(`Cache hit for ${key}: ${val}`);
      return val;
    }

    const result = fn(...args);
    cache.set(key, result);
    console.log(`Cache miss for ${key}: ${result}`);
    return result;
  };
}

const memoizedAdd = memoized((a: number, b: number) =&amp;gt; a + b);

memoizedAdd(1, 2);
memoizedAdd(3, 4);
memoizedAdd(1, 2);
memoizedAdd(3, 4);
memoizedAdd(3, 4);
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;Cache miss for [1,2]: 3
Cache miss for [3,4]: 7
Cache hit for [1,2]: 3
Cache hit for [3,4]: 7
Cache hit for [3,4]: 7
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Wrapping Up&lt;/h2&gt;
&lt;p&gt;And there you have it, closures explained! They might seem like this abstract concept that only comes up in interviews, but as we&apos;ve seen, they&apos;re everywhere in real JavaScript code. From loggers to debounce functions to memoization, closures are quietly doing the heavy lifting in patterns you probably use every day. The key takeaway? A closure is just a function that remembers where it came from. Once you get that, everything else clicks into place. Now go use/explain closures with confidence!&lt;/p&gt;
</content:encoded><author>Ray Azrin Karim</author></item><item><title>Building a Poor Man&apos;s React Query from Scratch</title><link>https://rzrn.dev/posts/building-a-poor-mans-react-query-from-scratch</link><guid isPermaLink="true">https://rzrn.dev/posts/building-a-poor-mans-react-query-from-scratch</guid><pubDate>Thu, 30 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Do you ever use &lt;a href=&quot;https://tanstack.com/query/latest&quot;&gt;TanStack Query&lt;/a&gt; and think, &quot;Wow, this library is so cool &amp;amp; convenient, they really thought about every edge case and made it easy to use. I wish I could build something like this myself.&quot; If so, let&apos;s try to build a poor man&apos;s version of React Query from scratch.&lt;/p&gt;
&lt;h2&gt;What is React Query?&lt;/h2&gt;
&lt;p&gt;From their site, TanStack Query (formerly known as React Query) is often described as the missing data-fetching library for web applications, but in more technical terms, it makes fetching, caching, synchronizing and updating server state in your web applications a breeze. The library really makes it easy to manage data fetching and state management in our web applications.&lt;/p&gt;
&lt;p&gt;The usage example is as follows:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { QueryClient, QueryClientProvider, useQuery } from &apos;@tanstack/react-query&apos;;

const queryClient = new QueryClient();

export default function App() {
  return (
    &amp;lt;QueryClientProvider client={queryClient}&amp;gt;
      &amp;lt;Example /&amp;gt;
    &amp;lt;/QueryClientProvider&amp;gt;
  );
}

function Example() {
  const { isPending, error, data } = useQuery({
    queryKey: [&apos;repoData&apos;],
    staleTime: 10 * 1000, // 10 seconds
    queryFn: () =&amp;gt;
      fetch(&apos;https://api.github.com/repos/TanStack/query&apos;).then((res) =&amp;gt; res.json()),
  });

  if (isPending) return &apos;Loading...&apos;;

  if (error) return &apos;An error has occurred: &apos; + error.message;

  return (
    &amp;lt;div&amp;gt;
      &amp;lt;h1&amp;gt;{data.name}&amp;lt;/h1&amp;gt;
      &amp;lt;p&amp;gt;{data.description}&amp;lt;/p&amp;gt;
      &amp;lt;strong&amp;gt;👀 {data.subscribers_count}&amp;lt;/strong&amp;gt;{&apos; &apos;}
      &amp;lt;strong&amp;gt;✨ {data.stargazers_count}&amp;lt;/strong&amp;gt; &amp;lt;strong&amp;gt;🍴 {data.forks_count}&amp;lt;/strong&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We see that the library provides loading state, error state, and data state. It really helps to tackle a common use case of data fetching. And we also see that the API requires a key to identify the query and a function that will be used to fetch the data.&lt;/p&gt;
&lt;p&gt;The key will be used to keep track of the query results, making other queries with the same key share the same data. This is useful for caching and reusing data across different components. It helps to reduce multiple requests for the same data if requested within the &lt;code&gt;staleTime&lt;/code&gt; window and improves the performance of our application by caching the data in memory.&lt;/p&gt;
&lt;p&gt;To keep this post short, we will only cover the core functionality of TanStack Query, such as:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Fetching states (isLoading, isError, isSuccess)&lt;/li&gt;
&lt;li&gt;Simple caching mechanism (cache key, staleTime, invalidation)&lt;/li&gt;
&lt;li&gt;Shared state across components&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Alright, let&apos;s roll up our sleeves and build this thing step by step!&lt;/p&gt;
&lt;h2&gt;Step 1: Setting Up the Foundation&lt;/h2&gt;
&lt;p&gt;Before we dive into the fancy stuff, we need to define what data we&apos;re actually tracking. Think of this as the blueprint for our query state.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type QueryStatus = &apos;idle&apos; | &apos;loading&apos; | &apos;success&apos; | &apos;error&apos;;

interface QueryState&amp;lt;T = any&amp;gt; {
  data: T | undefined;
  status: QueryStatus;
  error: Error | null;
  lastFetchedAt: number;
  staleTime: number;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is pretty straightforward - we&apos;re tracking:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;data&lt;/code&gt;: The actual data we fetched (or undefined if we haven&apos;t fetched yet)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;status&lt;/code&gt;: Where we are in the fetch lifecycle&lt;/li&gt;
&lt;li&gt;&lt;code&gt;error&lt;/code&gt;: Any error that occurred during fetching&lt;/li&gt;
&lt;li&gt;&lt;code&gt;lastFetchedAt&lt;/code&gt;: Timestamp of when we last successfully fetched data&lt;/li&gt;
&lt;li&gt;&lt;code&gt;staleTime&lt;/code&gt;: How long (in milliseconds) the data stays &quot;fresh&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This follows the &lt;strong&gt;State Pattern&lt;/strong&gt; - instead of having a bunch of boolean flags scattered around (&lt;code&gt;isLoading&lt;/code&gt;, &lt;code&gt;hasError&lt;/code&gt;, &lt;code&gt;hasData&lt;/code&gt;), we have one clear status that tells us exactly what&apos;s happening at any moment.&lt;/p&gt;
&lt;h2&gt;Step 2: Building the QueryClient&lt;/h2&gt;
&lt;p&gt;Now let&apos;s create the &lt;code&gt;QueryClient&lt;/code&gt; class. This is where all the magic happens - it&apos;s going to store our queries, manage subscriptions, and handle fetching.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Listener = () =&amp;gt; void;

class QueryClient {
  private queries: Map&amp;lt;string, QueryState&amp;gt;;
  private queryFns: Map&amp;lt;string, () =&amp;gt; Promise&amp;lt;any&amp;gt;&amp;gt;;
  private listeners: Map&amp;lt;string, Set&amp;lt;Listener&amp;gt;&amp;gt;;

  constructor() {
    this.queries = new Map();
    this.queryFns = new Map();
    this.listeners = new Map();
  }

  getQueryState&amp;lt;T&amp;gt;(key: string): QueryState&amp;lt;T&amp;gt; | undefined {
    return this.queries.get(key);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here&apos;s what we&apos;re storing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;queries&lt;/code&gt;: Our cache! Maps query keys to their current state&lt;/li&gt;
&lt;li&gt;&lt;code&gt;queryFns&lt;/code&gt;: Remembers the fetch functions so we can re-run them when needed&lt;/li&gt;
&lt;li&gt;&lt;code&gt;listeners&lt;/code&gt;: Components that want to know when data changes (we&apos;ll get to this in a sec)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You typically create one &lt;code&gt;QueryClient&lt;/code&gt; instance for your entire app and share it everywhere (the &lt;strong&gt;Singleton pattern&lt;/strong&gt;). It acts as a centralized store for all your query data.&lt;/p&gt;
&lt;h2&gt;Step 3: The Observable Pattern&lt;/h2&gt;
&lt;p&gt;Alright, here&apos;s where it gets interesting. How do we tell React components that data has changed? We use the &lt;strong&gt;Observer Pattern&lt;/strong&gt; (also called Pub/Sub).&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class QueryClient {
  // ... previous code

  subscribe(key: string, listener: Listener): () =&amp;gt; void {
    if (!this.listeners.has(key)) {
      this.listeners.set(key, new Set());
    }
    this.listeners.get(key)!.add(listener);

    // Return unsubscribe function
    return () =&amp;gt; {
      const listeners = this.listeners.get(key);
      if (listeners) {
        listeners.delete(listener);
        if (listeners.size === 0) {
          this.listeners.delete(key);
        }
      }
    };
  }

  private notify(key: string) {
    const listeners = this.listeners.get(key);
    if (listeners) {
      listeners.forEach((listener) =&amp;gt; listener());
    }
  }

  private setQueryState(key: string, state: QueryState) {
    this.queries.set(key, state);
    this.notify(key); // Tell everyone who&apos;s listening!
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here&apos;s how this works:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Components call &lt;code&gt;subscribe()&lt;/code&gt; and say &quot;Hey, let me know when this query changes&quot;&lt;/li&gt;
&lt;li&gt;We store their callback in a Set (multiple components can watch the same query)&lt;/li&gt;
&lt;li&gt;Whenever we update the query state, we &lt;code&gt;notify()&lt;/code&gt; all listeners&lt;/li&gt;
&lt;li&gt;We return an unsubscribe function so components can clean up when they unmount&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The QueryClient is the &quot;subject&quot; that maintains a list of &quot;observers&quot; (React components). When the subject&apos;s state changes, it notifies all observers automatically. Think of it like subscribing to a newsletter - you sign up (subscribe), get updates when new content arrives (notify), and can unsubscribe anytime you want.&lt;/p&gt;
&lt;p&gt;This pattern shows up everywhere in UI frameworks - it&apos;s literally how React itself works under the hood!&lt;/p&gt;
&lt;h2&gt;Step 4: The Caching Logic&lt;/h2&gt;
&lt;p&gt;Now for the fun part - the actual fetching and caching logic. This is what makes React Query so powerful.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class QueryClient {
  // ... previous code

  async fetchQuery&amp;lt;T&amp;gt;(
    key: string,
    queryFn: () =&amp;gt; Promise&amp;lt;T&amp;gt;,
    options: { staleTime?: number } = {},
  ): Promise&amp;lt;T&amp;gt; {
    const { staleTime = 0 } = options;

    this.queryFns.set(key, queryFn);

    const existingQuery = this.queries.get(key);

    // Check if we have fresh cached data
    if (existingQuery &amp;amp;&amp;amp; existingQuery.status === &apos;success&apos;) {
      const age = Date.now() - existingQuery.lastFetchedAt;
      if (age &amp;lt; existingQuery.staleTime) {
        console.log(`✅ [Cache Hit] key: &quot;${key}&quot;`);
        return existingQuery.data;
      }
    }

    // Request deduplication - if already loading, wait for it
    if (existingQuery?.status === &apos;loading&apos;) {
      console.log(`⏳ [Deduplication] key: &quot;${key}&quot;`);
      return new Promise((resolve, reject) =&amp;gt; {
        const unsubscribe = this.subscribe(key, () =&amp;gt; {
          const state = this.queries.get(key);
          if (state &amp;amp;&amp;amp; state.status !== &apos;loading&apos;) {
            unsubscribe();
            if (state.status === &apos;success&apos;) {
              resolve(state.data);
            } else if (state.status === &apos;error&apos;) {
              reject(state.error);
            }
          }
        });
      });
    }

    // Actually fetch the data
    console.log(`🚀 [Fetching] key: &quot;${key}&quot;`);
    this.setQueryState(key, {
      data: existingQuery?.data ?? undefined,
      status: &apos;loading&apos;,
      error: null,
      lastFetchedAt: existingQuery?.lastFetchedAt || 0,
      staleTime,
    });

    try {
      const data = await queryFn();
      console.log(`✨ [Success] key: &quot;${key}&quot;`);
      this.setQueryState(key, {
        data,
        status: &apos;success&apos;,
        error: null,
        lastFetchedAt: Date.now(),
        staleTime,
      });
      return data;
    } catch (error) {
      const err = error instanceof Error ? error : new Error(String(error));
      console.log(`❌ [Error] key: &quot;${key}&quot;`, err.message);
      this.setQueryState(key, {
        data: existingQuery?.data,
        status: &apos;error&apos;,
        error: err,
        lastFetchedAt: existingQuery?.lastFetchedAt || 0,
        staleTime,
      });
      throw err;
    }
  }

  invalidateQuery(key: string) {
    const queryFn = this.queryFns.get(key);
    const queryState = this.queries.get(key);

    if (queryState &amp;amp;&amp;amp; queryFn) {
      console.log(`🔄 [Invalidate] key: &quot;${key}&quot;`);
      this.queries.delete(key);
      this.fetchQuery(key, queryFn, { staleTime: queryState.staleTime });
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let&apos;s break down what&apos;s happening here:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. Cache Check (The Fast Path)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (existingQuery &amp;amp;&amp;amp; existingQuery.status === &apos;success&apos;) {
  const age = Date.now() - existingQuery.lastFetchedAt;
  if (age &amp;lt; existingQuery.staleTime) {
    return existingQuery.data; // Return cached data!
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Before we make any network request, we check if we already have fresh data. If the data is younger than &lt;code&gt;staleTime&lt;/code&gt;, we just return it immediately. No network request needed!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. Request Deduplication (The Smart Path)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (existingQuery?.status === &apos;loading&apos;) {
  // Wait for the existing request instead of making a new one
  return new Promise((resolve, reject) =&amp;gt; {
    const unsubscribe = this.subscribe(key, () =&amp;gt; {
      // When the request finishes, resolve/reject this promise
    });
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If a request is already in flight, we don&apos;t start a new one. Instead, we subscribe to the existing request and wait for it to finish. This prevents duplicate requests when multiple components mount at the same time.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. The Actual Fetch (The Slow Path)&lt;/strong&gt;
If we don&apos;t have fresh cached data and nothing is currently loading, we actually fetch the data, update the state, and notify all listeners.&lt;/p&gt;
&lt;p&gt;What we&apos;ve built here is the &lt;strong&gt;Cache-Aside pattern&lt;/strong&gt; - check the cache first, and only hit the API if we don&apos;t have fresh data. We also avoid duplicate work by sharing in-flight requests (like memoization). This is exactly how databases, CDNs, and browser caches work too.&lt;/p&gt;
&lt;p&gt;Here&apos;s a visual of how the caching flow works:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Component calls fetchQuery
           |
           v
   Have cached data?
      /          \
    Yes           No
     |             |
     v             |
Data fresh?        |
   /    \          |
 Yes    No         |
  |      |         |
  v      +---------+
Return             |
cached             v
data ✅      Already loading?
                /         \
              Yes         No
               |           |
               v           v
           Wait for    Fetch from API
              it           |
              ⏳           v
               |      Success/Error
               |           |
               +-----------+
                           v
                    Update cache +
                    notify listeners ✨
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./console-output-1.png&quot; alt=&quot;Console Output&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Step 5: Building the useQuery Hook&lt;/h2&gt;
&lt;p&gt;Now we need to create a React hook that uses our &lt;code&gt;QueryClient&lt;/code&gt;. This is where we bring everything together and make it easy to use in components.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const queryClient = new QueryClient();

interface UseQueryOptions&amp;lt;T = any&amp;gt; {
  queryKey: string;
  queryFn: () =&amp;gt; Promise&amp;lt;T&amp;gt;;
  staleTime?: number;
}

function useQuery&amp;lt;T&amp;gt;(params: UseQueryOptions&amp;lt;T&amp;gt;) {
  const { queryKey, queryFn, staleTime = 0 } = params;

  // Store the latest queryFn in a ref to avoid triggering effects on every render
  const queryFnRef = useRef(queryFn);
  queryFnRef.current = queryFn;

  useEffect(() =&amp;gt; {
    queryClient.fetchQuery(queryKey, queryFnRef.current, { staleTime });
  }, [queryKey, staleTime]); // Only re-fetch when key or staleTime changes

  const state = useSyncExternalStore(
    (callback) =&amp;gt; queryClient.subscribe(queryKey, callback),
    () =&amp;gt; queryClient.getQueryState&amp;lt;T&amp;gt;(queryKey),
  );

  return {
    data: state?.data,
    isLoading: state?.status === &apos;loading&apos;,
    isError: state?.status === &apos;error&apos;,
    isSuccess: state?.status === &apos;success&apos;,
    error: state?.error,
    refetch: () =&amp;gt; queryClient.invalidateQuery(queryKey),
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let&apos;s break down the key parts:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. The useRef Trick&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const queryFnRef = useRef(queryFn);
queryFnRef.current = queryFn;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We store the &lt;code&gt;queryFn&lt;/code&gt; in a ref so we always have the latest version, but it doesn&apos;t trigger the useEffect when it changes. This prevents unnecessary re-fetches when the component re-renders with a new function reference.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. The Fetch Effect&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;useEffect(() =&amp;gt; {
  queryClient.fetchQuery(queryKey, queryFnRef.current, { staleTime });
}, [queryKey, staleTime]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When the component mounts (or when the key/staleTime changes), we trigger a fetch. The &lt;code&gt;fetchQuery&lt;/code&gt; method is smart enough to return cached data if it&apos;s still fresh.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. The useSyncExternalStore Magic&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const state = useSyncExternalStore(
  (callback) =&amp;gt; queryClient.subscribe(queryKey, callback),
  () =&amp;gt; queryClient.getQueryState&amp;lt;T&amp;gt;(queryKey),
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is a React 18 hook that&apos;s perfect for our use case! It does the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Subscribes&lt;/strong&gt; to our QueryClient (first argument)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Gets&lt;/strong&gt; the current state snapshot (second argument)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Re-renders&lt;/strong&gt; the component when the state changes&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Handles&lt;/strong&gt; all the edge cases around concurrent rendering&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We&apos;re basically adapting our QueryClient (which knows nothing about React) to work seamlessly with React&apos;s rendering model. &lt;code&gt;useSyncExternalStore&lt;/code&gt; is the bridge - the &lt;strong&gt;Adapter pattern&lt;/strong&gt; in action - connecting our external state management to React&apos;s internal state system.&lt;/p&gt;
&lt;h2&gt;Step 6: Putting It All Together&lt;/h2&gt;
&lt;p&gt;Now let&apos;s see our poor man&apos;s React Query in action:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export function App() {
  const [count, setCount] = useState(1);
  return (
    &amp;lt;div&amp;gt;
      &amp;lt;h2&amp;gt;App Content&amp;lt;/h2&amp;gt;
      &amp;lt;button onClick={() =&amp;gt; setCount(count + 1)}&amp;gt;Add new component&amp;lt;/button&amp;gt;
      &amp;lt;div style={{ display: &apos;flex&apos;, flexWrap: &apos;wrap&apos; }}&amp;gt;
        {new Array(count).fill(0).map((_, i) =&amp;gt; (
          &amp;lt;CompA key={i} /&amp;gt;
        ))}
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}

function CompA() {
  const query = useQuery({
    queryKey: &apos;compa-query&apos;,
    queryFn: async () =&amp;gt; {
      await new Promise((resolve) =&amp;gt; setTimeout(resolve, 1000));
      return Math.random();
    },
    staleTime: 10000, // 10 seconds
  });

  return (
    &amp;lt;div&amp;gt;
      &amp;lt;pre&amp;gt;{JSON.stringify(query, (k, v) =&amp;gt; (v === undefined ? &apos;undefined&apos; : v), 2)}&amp;lt;/pre&amp;gt;
      &amp;lt;button onClick={() =&amp;gt; query.refetch()}&amp;gt;refetch&amp;lt;/button&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here&apos;s what happens when you run this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;First mount&lt;/strong&gt;: Component fetches data, shows loading state, then shows the result&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Add more components&lt;/strong&gt;: They all share the same cached data instantly (cache hit!)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;After 10 seconds&lt;/strong&gt;: Data becomes stale, next component mount triggers a new fetch&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Click refetch&lt;/strong&gt;: Invalidates the cache and fetches fresh data&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;./console-output-2.png&quot; alt=&quot;Console Output 2&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The magic here is that all &lt;code&gt;CompA&lt;/code&gt; instances share the same data because they use the same &lt;code&gt;queryKey&lt;/code&gt;. When one fetches, they all get updated. When the data is still fresh (within 10 seconds), new components just read from the cache.&lt;/p&gt;
&lt;h2&gt;What We&apos;ve Built&lt;/h2&gt;
&lt;p&gt;Let&apos;s recap what our poor man&apos;s React Query can do:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Caching&lt;/strong&gt;: Stores data and reuses it within the staleTime window&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Automatic Loading States&lt;/strong&gt;: Provides &lt;code&gt;isLoading&lt;/code&gt;, &lt;code&gt;isError&lt;/code&gt;, &lt;code&gt;isSuccess&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Shared State&lt;/strong&gt;: Multiple components using the same key share the same data&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Request Deduplication&lt;/strong&gt;: Prevents duplicate requests for the same data&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Manual Refetching&lt;/strong&gt;: Invalidate and refetch data on demand&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Observer Pattern&lt;/strong&gt;: Components automatically re-render when data changes&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;What&apos;s Missing (Compared to the Real Thing)&lt;/h2&gt;
&lt;p&gt;Of course, the real TanStack Query has way more features that we didn&apos;t implement:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Background refetching&lt;/strong&gt;: Automatically refetch when window regains focus or network reconnects&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Retry logic&lt;/strong&gt;: Automatically retry failed requests with exponential backoff&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Garbage collection&lt;/strong&gt;: Clean up unused query data after a timeout&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Optimistic updates&lt;/strong&gt;: Update UI before the server responds&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Infinite queries&lt;/strong&gt;: Load more data patterns (pagination, infinite scroll)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Query dependencies&lt;/strong&gt;: Make queries depend on other queries&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Suspense integration&lt;/strong&gt;: Work with React Suspense for better loading states&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DevTools&lt;/strong&gt;: Visual debugging of all your queries&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;But for understanding the core concepts? We&apos;ve got the essentials down!&lt;/p&gt;
&lt;h2&gt;Wrapping Up&lt;/h2&gt;
&lt;p&gt;And there you have it - a poor man&apos;s React Query built from scratch! We&apos;ve covered a ton of ground: the Observer pattern for subscriptions, cache-aside pattern for performance, request deduplication to avoid waste, and how to connect external state to React with &lt;code&gt;useSyncExternalStore&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;What&apos;s cool is that by building this, you&apos;ve learned the fundamental patterns that power not just React Query, but tons of other libraries too. The Observer pattern shows up everywhere (Redux, MobX, RxJS). The caching strategy is used in databases, CDNs, and browsers. The subscription model is how WebSockets, event emitters, and even vanilla JavaScript events work.&lt;/p&gt;
&lt;p&gt;Next time you use TanStack Query, you&apos;ll have a much deeper appreciation for what&apos;s happening under the hood. You&apos;ll understand why query keys need to be unique, why staleTime is so powerful, and how components magically stay in sync. And who knows, maybe you&apos;ll even find yourself reaching for these patterns in your own code!&lt;/p&gt;
</content:encoded><author>Ray Azrin Karim</author></item></channel></rss>