Gaurav Thakur's Blog and Projects
website·Indexed 32 days ago
https://gauravthakur.comA 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.
What's inside Gaurav Thakur
- 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.
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.
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:
- 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. - 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 (
undefinedfor variables, the function body for functions). If no memory was allocated for the identifier, aReferenceErroris thrown.console.log(sayHello); // Returns the whole function definition console.log(name); // Returns undefined var name = 'Gaurav Thakur'; function sayHello() { console.log('hello'); }- Memory Creation Phase: JavaScript parses the code and allocates memory for all variables and functions. Variables are initialized with the value
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).
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.
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:
- 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. - 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
- Memory Creation Phase: The engine parses the code line by line. It allocates memory for variables and assigns them the value
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
useEffectanduseLayoutEffectto 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
useEffecthook.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> ); }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 callingheapifyon 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); } } }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 aSuspenseboundary.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>; }Use implicit binding via context objects
When a function is defined as a property of an object (attached to an object), thethiskeyword 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(); // GauravImplement 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.Deduplicate custom logic in Server Components using cache()
React provides acacheAPI 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 nativefetchAPI.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; });