Gaurav Thakur's Blog and Projects

website·Indexed 32 days ago

https://gauravthakur.com

A collection of technical articles and projects by Gaurav Thakur. Content covers JavaScript internals (execution context, hoisting, the 'this' keyword, and array implementation), React patterns (Server Components, useEffect, and data fetching), and data structures like heaps and binary trees. Also features projects such as SplitMaadi, a group expense-splitting app, and PerfKit.

Tokens
5.8K
Snippets
25
Records
35
Agent score
58%

What's inside Gaurav Thakur

  1. SplitMaadi Cross-Platform Architecture

    SplitMaadi uses a unified codebase to ensure a consistent experience across all devices. It is built as a Progressive Web App (PWA), allowing it to run on any device with a web browser (Mac, Windows, Linux, tablets). For mobile users, dedicated iPhone and Android apps are provided using Capacitor, which acts as a native wrapper around the web application.
  2. Implement Max Heap and Min Heap

    A Heap is a complete binary tree. There are two primary types:

    • Max Heap: Every node's value is greater than or equal to all its descendants. The root contains the maximum value.
    • Min Heap: Every node's value is less than or equal to all its descendants. The root contains the minimum value.
  3. Understand how hoisting works in JavaScript

    Hoisting is the behavior in JavaScript that allows variables and functions to be accessed before they are explicitly initialized in the code. This occurs because of the two-phase process of creating an Execution Context:

    1. Memory Creation Phase: JavaScript parses the code and allocates memory for all variables and functions. Variables are initialized with the value undefined, while function declarations are stored in their entirety.
    2. Code Execution Phase: The code is executed line by line.

    When a variable or function is accessed before its declaration, JavaScript checks the memory allocated during Phase 1. If the identifier exists, it returns the stored value (undefined for variables, the function body for functions). If no memory was allocated for the identifier, a ReferenceError is thrown.

    console.log(sayHello); // Returns the whole function definition
    console.log(name);     // Returns undefined
    
    var name = 'Gaurav Thakur';
    function sayHello() {
      console.log('hello');
    }
  4. Understand Binary Tree types: Perfect vs Complete

    Binary trees are categorized by their structure:

    • Perfect Binary Tree: A tree where every level is completely filled with the maximum number of nodes.
    • Complete Binary Tree: A tree where all levels are fully filled except possibly the last level, which is filled from left to right. In an array representation, there are no empty gaps between the first and last elements. The height of a complete binary tree is always log(n).
  5. SplitMaadi Product Overview and Core Features

    SplitMaadi is a free expense-sharing application designed to provide a frictionless experience without arbitrary limits. Key product guarantees include:

    • No daily caps on adding expenses.
    • No limits on the number of groups or people added to a group.
    • No advertisements.
    • Full support for receipts and media attachments as a core part of the expense record.
    • Permanent preservation of expense history.
    • Free access to all basic product functionality without premium gates.
  6. Understand the JavaScript Execution Context lifecycle

    An execution context is the environment in which JavaScript code is executed. It consists of two components: a Memory Component (storing variable and function references as key-value pairs) and a Code Component (responsible for executing the code). Every execution context is created in two distinct phases:

    1. Memory Creation Phase: The engine parses the code line by line. It allocates memory for variables and assigns them the value undefined. For functions, the entire function body is stored in memory.
    2. Code Execution Phase: The engine executes the code line by line. It assigns actual values to variables and invokes functions when encountered.
    var num1 = 5;
    var num2 = 7;
    function add(num1, num2) {
      return num1 + num2;
    }
    var result = add(num1, num2);

    // Memory Phase Result: // num1: undefined // num2: undefined // add: function(num1, num2) { ... } // result: undefined

  7. Handle React Strict Mode effects running twice in development

    In React 18+, when Strict Mode is enabled in development, React will intentionally unmount and remount components upon their first mount. This causes useEffect and useLayoutEffect to run twice. This behavior is designed to help developers identify bugs where effects are not properly cleaned up, which could lead to issues in production (e.g., when a user navigates away from and back to a screen quickly).

    To prevent bugs such as duplicate API connections or memory leaks when an effect runs multiple times, always provide a cleanup function in the useEffect hook.

    function Chat() {
      useEffect(() => {
        const { connect, disconnect, connection } = dummyServer();
        connect();
        connection.on(() => {
          console.log("connected");
        });
    
        // The cleanup function ensures that previous connections 
        // are closed before a new one is created
        return disconnect; 
      }, []);
    
      return (
        <div>
          <p>Chat Screen</p>
          <a href="/">Go to home page</a>
        </div>
      );
    }
  8. Construct a heap from an array using the heapify algorithm

    To create a heap from an existing array of values efficiently, use the heapify algorithm. This process involves populating the heap array and then calling heapify on all non-leaf nodes starting from the bottom and moving upwards to the root. This approach is more efficient than traditional insertion, taking O(n) time instead of O(n log n).
    void Heap::constructHeap(const vector<int> &values) {
        if (size == 0) {
            for (int value: values) {
                heap[size++] = value;
            }
            for (int i = size - 1; i >= 0; i--) {
                heapify(i);
            }
        }
    }
  9. Stream promises from Server Components to Client Components

    To avoid render-time waterfalls and prevent blocking the server render, you can initiate a fetch in a Server Component without awaiting it, and pass the resulting promise to a Client Component. The Client Component then unwraps the promise using the use() hook inside a Suspense boundary.

    Constraints:

    • The promise must resolve to serializable data (plain objects, arrays, or primitives).
    • Non-serializable data such as class instances, Mongoose documents, or functions will break this pattern.

    Use this pattern when:

    • Data is needed in a client component.
    • The fetch is slow or expensive.
    • The consuming component is deep in the component tree.
    • You want to start network work as early as possible while the server continues rendering the rest of the tree.
    // app/page.tsx (Server Component)
    import { Suspense } from "react";
    import PageWrapper from "./page-wrapper";
    import ClientPanel from "./client-panel";
    import { getUserPreferences } from "./api";
    
    export default function Page() {
      // Start the fetch early — do not await here
      const prefsPromise = getUserPreferences();
    
      return (
        <PageWrapper>
          <Suspense fallback={<div>Loading preferences...</div>}>
            <ClientPanel prefsPromise={prefsPromise} />
          </Suspense>
        </PageWrapper>
      );
    }
    
    // app/client-panel.tsx (Client Component)
    "use client";
    import { use } from "react";
    
    export default function ClientPanel({ prefsPromise }) {
      // Unwrap the promise using the 'use' hook
      const prefs = use(prefsPromise);
      return <div>Max discount: {prefs.maxDiscount}</div>;
    }
  10. Use implicit binding via context objects

    When a function is defined as a property of an object (attached to an object), the this keyword inside that function refers to the object it is attached to. The object serves as the context for the function call.
    const obj = {
        name: 'Gaurav',
        foo: function () {
            console.log(this.name);
        }
    };
    
    obj.foo(); // Gaurav
  11. Implement a Priority Queue using Heaps

    A priority queue is a data structure that always pops the value with the highest priority. While a normal array can be used, it results in O(n) time for the pop operation. Using a Max Heap or Min Heap allows the highest priority element to be returned in O(log n) time, though push operations also take O(log n) time.
  12. Deduplicate custom logic in Server Components using cache()

    React provides a cache API to ensure that a function is called only once per request cycle. This is essential for deduplicating custom logic (such as database calls via Prisma or API calls with dynamic headers) that cannot be automatically deduplicated by the native fetch API.
    import { cache } from "react";
    
    // Deduplicate an API call with dynamic headers
    const getUserPreferences = cache(async () => {
      const response = await client("https://api.gauravthakur.com/v1/user-config");
      return response.json();
    });
    
    // Deduplicate a database call
    const getOrderStatus = cache(async (id: string) => {
      const order = await prisma.order.findFirst({
        where: { id },
      });
      return order?.status ?? null;
    });