Notes &
Reference
A comprehensive study guide covering JavaScript fundamentals, async patterns, Node.js backend, MongoDB, security, and React — with code examples and alternatives for every concept.
Primitives vs Reference Types
Understanding how JavaScript stores data in memory is fundamental. The difference determines how copying, comparison, and mutation work.
Types: String, Number, Boolean, null, undefined, Symbol, BigInt
Storage: Stack — fast, fixed-size memory
Behavior: Immutable. Assignment copies the actual value. Modifying creates a new value, never alters the original.
Types: Objects, Arrays, Functions
Storage: Heap (with a reference pointer in the Stack)
Behavior: Mutable. = only copies the reference pointer. Modifying the "copy" mutates the original!
Copying Objects & Arrays
Because let obj2 = obj1 only copies the reference, specific methods are needed to actually copy data.
Copies top-level elements, but nested objects/arrays still share the same reference. Changes to nested data will affect both copies.
Creates a completely independent copy including all nested objects and arrays. Changes to any level do not affect the original.
let d = { a: 10, nested: { b: 20 } };
// Alt 1: Spread Operator (Modern & Preferred)
let cd1 = { ...d };
// Alt 2: Object.assign() (Older method)
let cd2 = Object.assign({}, d);
let order = { id: "101", address: { city: "Hyd" } };
// Alt 1: structuredClone() — Modern Native (PREFERRED)
// Handles Dates, Maps, Sets, and nested objects perfectly
let copyOrder = structuredClone(order);
copyOrder.address.city = "uppal"; // Original stays "Hyd" ✓
// Alt 2: JSON.parse(JSON.stringify()) — Classic Hack
// ⚠ Loses: functions, undefined, Dates become strings
let copyOrder2 = JSON.parse(JSON.stringify(order));
Special JavaScript Operators
| Operator | Syntax | What it does | Returns |
|---|---|---|---|
| Optional Chaining | ?. | Safely accesses deeply nested properties without throwing if a reference is null/undefined | Value if exists, otherwise undefined |
| Nullish Coalescing | ?? | Checks if left side is exactly null or undefined, provides fallback if so | Left value if it exists, otherwise right-side fallback |
| Spread | ... | Expands an iterable (array or object) into its individual elements | Unpacked elements — used for shallow copies and merging |
| Destructuring | {a,b}=obj | Extracts values from arrays or properties from objects into distinct variables | Extracted values assigned to your variables |
// Optional Chaining — won't crash if city is undefined
console.log(student.city?.length);
// Nullish Coalescing — fallback if city is null/undefined
console.log(student.city?.length ?? "Not exist");
// Spread — add new course to array immutably
let addCourse = (newCourse) => [...courses, newCourse];
// Destructuring — extract from req.params
let { uid, pid } = req.params;
Array Methods
JavaScript provides powerful higher-order functions for manipulating arrays and arrays of objects. These methods are the backbone of modern JS development.
const users = [
{ id: 1, name: "Ravi", role: "student", active: true },
{ id: 2, name: "Anil", role: "admin", active: true },
];
const courses = [{ id: 101, title: "JS", price: 999 }];
// filter: Returns new array of items matching condition
let activeUsers = users.filter(user => user.active);
// map: Chain after filter to transform results
let activeUserNames = users
.filter(user => user.active)
.map(user => user.name); // ["Ravi", "Anil"]
// some: true if AT LEAST ONE matches
const adminExists = users.some(user => user.role === "admin");
// every: true only if ALL match
let allPaid = courses.every(c => c.price > 0);
// find: Returns first match
let findUserById = (id) => users.find(u => u.id === id);
// reduce: Accumulate to single value
let total = courses.reduce((sum, c) => sum + c.price, 0);
// sort: Spread first to avoid mutating original
let sortPrices = [...courses].sort((a, b) => b.price - a.price);
Rest Parameters
In most other languages, passing more arguments than a function expects causes an error. In JS it silently ignores the extras. Rest parameters let you capture all those extra arguments into a single array so nothing is lost.
function sum(a, b) called with sum(10, 20, 30, 40) — JS takes only a=10, b=20 and silently discards 30 and 40. No error, just unexpected results.
function sum(...a) — the ... packs ALL passed arguments into an array. Now you can loop over them, reduce them, or pass them elsewhere. Works with any number of arguments.
The rest parameter must always be the last parameter. You can have normal params before it: function fn(a, b, ...rest). Putting anything after the rest param is a syntax error.
// Without rest — extras are silently ignored
function sum(a, b) {
return a + b;
}
sum(10, 20, 30, 40); // returns 30 — 30 and 40 are ignored
// With rest — all arguments packed into array 'a'
function sum(...a) {
console.log(a); // [10, 20, 30, 40]
return a.reduce((x, y) => x + y);
}
sum(10, 20, 30, 40); // returns 100 ✓
// Mix of fixed + rest params — rest must be LAST
function log(label, ...values) {
console.log(label, values);
}
log("scores:", 90, 85, 78); // "scores:" [90, 85, 78]
// ✗ Syntax Error — rest param must be last
// function bad(...a, b) { }
Dates in JavaScript
Dates internally store time as a UTC timestamp in milliseconds since Jan 1, 1970 (Unix Epoch). The Date constructor accepts multiple formats.
let date = new Date(); // Current date & time
let d2 = new Date('2022-01-12'); // ISO string format
let d3 = new Date(2022, 0, 12); // Year, Month (0=Jan!), Day
console.log(date.getFullYear()); // 2024
console.log(date.getMonth()); // 0-11 (⚠ 0 = January!)
console.log(date.getDate()); // 1-31 (day of month)
console.log(date.getDay()); // 0-6 (0 = Sunday)
console.log(date.getTime()); // Milliseconds since epoch
Modules & Exports
One per module. The importer can name it whatever they want. Best for a module's primary export.
Export: export default a;
Import: import myVar from './file.js'
Multiple per module. The importer MUST use the exact name in curly braces. Best for utility functions.
Export: export { b, marks };
Import: import { b, marks } from './file.js'
First-Class Functions & Closure
In JavaScript, functions are first-class objects — they can be stored in variables, passed as arguments, and returned from other functions. This is why JS supports both OOP and functional programming styles.
Called a function expression. The function has no name of its own — it's called an anonymous function. The variable holds a reference to it.
const greet = function() { ... }
Called a callback function. You pass the function itself (not its result) to another function, which calls it later. This is the backbone of async JS and array methods like .map(), .filter().
A function that returns another function. Less common but powerful — used in factory functions, memoization, and closures. This is where closure kicks in.
How Closure Works
A closure is a function bundled together with its surrounding scope. This is the default behaviour in JS — every function "remembers" the variables from the scope it was defined in, even after that outer function has finished executing.
When a function runs, its local variables are created in the Call Stack. When the function finishes, those variables are removed from the stack. Gone.
If the function returns another function, the inner function still holds a reference to the outer variables. The JS engine sees this and moves those variables to the Heap (where reference types live) instead of deleting them — keeping them alive as long as the inner function exists.
function increment() {
let counter = 0; // local to increment()
return function() { // inner function "closes over" counter
counter++;
return counter;
};
}
let x = increment(); // increment() runs and finishes...
// but counter is NOT deleted — x holds a reference to the inner fn
console.log(x()); // 1 — counter is remembered
console.log(x()); // 2 — same counter, still alive in memory
console.log(x()); // 3
// Each call to increment() creates a SEPARATE closure
let y = increment();
console.log(y()); // 1 — y has its own private counter, separate from x
Promises & Async/Await
JavaScript is single-threaded — one task at a time. The runtime (Browser or Node.js) handles async tasks via Web APIs + Event Loop, pushing callbacks back to the engine when ready.
let futureAvailability = true;
let promiseObj = new Promise((fulfill, reject) => {
setTimeout(() => {
futureAvailability === true
? fulfill("hello kiran")
: reject("Sorry call later");
}, 5000);
});
// Classic chaining — still valid, can get "callback-hell"-y
promiseObj
.then((msg) => console.log("message: ", msg))
.catch((msg) => console.log("error: ", msg));
// Reads like synchronous code — much cleaner!
async function consumePromise() {
try {
let res = await promiseObj; // Pauses until resolved
console.log(res);
} catch (err) {
console.log(err); // Handles rejection
}
}
// All async functions implicitly return a Promise
JS Engine & Event Loop
JavaScript is single-threaded — but async operations don't block because the runtime (Browser or Node.js) offloads them to native APIs, then uses the Event Loop to bring results back when the JS engine is free.
Where JS executes code. LIFO (Last In, First Out). Functions are pushed when called, popped when they return. If a function blocks here, everything freezes.
Browser/Node handles async tasks outside the JS engine: setTimeout, HTTP requests, DOM events, file I/O. JS hands off the task and continues executing.
When a Web API finishes (e.g. timer fires), the callback is placed in this queue. It waits here until the Call Stack is empty.
Continuously checks: "Is the Call Stack empty?" If yes, it takes the next callback from the queue and pushes it onto the stack. This is how async callbacks run.
console.log("1 — synchronous, runs immediately");
setTimeout(() => {
console.log("3 — async, runs AFTER call stack clears");
}, 0); // Even 0ms delay goes to callback queue!
console.log("2 — synchronous, runs before setTimeout callback");
// Output order: 1 → 2 → 3
// The Event Loop only pushes the setTimeout callback
// after lines 1 and 2 have finished and the stack is empty.
// Node.js Runtime = JS Engine + Node APIs (libuv) + Event Loop
REST API Concepts
Express & Middlewares
Middlewares run between receiving a request and sending a response. They form a pipeline — each middleware calls next() to pass control forward.
# Initialize a Node project first (creates package.json)
npm init -y
# Install Express + cookie-parser
npm install express cookie-parser
# Add "type": "module" to package.json to use ES module imports
# (needed for: import exp from 'express')
import exp from 'express';
import cookieParser from 'cookie-parser';
const app = exp();
// Built-in Middlewares
app.use(exp.json()); // Parses JSON body → req.body
app.use(cookieParser()); // Parses cookies → req.cookies
// Custom Router
import { userApp } from "../apis/userApi.js";
app.use('/apis/user-api', userApp);
// Global Error Handler — MUST be last, needs 4 arguments
function errorHandler(err, req, res, next) {
res.json({ message: "error occured!", reason: err.message });
}
app.use(errorHandler); // ⚠ Keep at the very end
app.listen(4000, () => console.log("listening to port 4000"));
Controllers & Route Files
A Controller is just a request handler function stored in a variable instead of being written inline on the route. It keeps route files clean — the route just says what path + method, and the controller says what to do.
// ✗ Before — handler written inline, gets messy fast
app.get('/users', (req, res) => {
// all the logic crammed here
});
// ✓ After — extract to a controller variable
const getUsers = (req, res) => {
// logic lives here, named and reusable
};
app.get('/users', getUsers); // route is now clean and readable
// In larger apps, controllers go in separate files:
// userController.js → exports { getUsers, createUser, deleteUser }
// userRoutes.js → imports controllers, defines app.get/post/delete
// app.js → imports routes, mounts them with app.use()
CORS — Cross-Origin Resource Sharing
By default, browsers block frontend JS from making requests to a different origin (different domain, port, or protocol). This is the browser's Same-Origin Policy. CORS is the mechanism that lets a server say "I allow requests from this origin" — without it, your React frontend on localhost:5173 can't talk to your Express backend on localhost:4000.
Your React app (origin A) makes a fetch/axios request to your Express server (origin B). Browser sees different origins → blocks the response and throws a CORS error in the console. The request DID reach the server — the browser just refuses to hand the response to JS.
Install the cors npm package and use it as Express middleware. It adds the right headers (Access-Control-Allow-Origin) to every response so the browser allows them through.
origin: "*" allows every domain — fine for public APIs. origin: true mirrors the request origin back, allowing any origin with cookies. For production, pass a specific URL: origin: "https://yoursite.com".
# Install cors package
npm install cors
// app.js — import and use BEFORE your routes
import cors from 'cors';
import exp from 'express';
const app = exp();
app.use(cors({
origin: true, // allow the requesting origin (any)
credentials: true, // allow cookies to be sent (needed for httpOnly JWT cookies)
}));
// For specific origin only (recommended in production):
// app.use(cors({ origin: "http://localhost:5173", credentials: true }));
app.use(exp.json());
// ... rest of your middlewares and routes
MongoDB & Mongoose
# Install Mongoose (MongoDB ODM for Node.js)
npm install mongoose
# Connect to MongoDB in your server entry file
# Replace the URI with your MongoDB Atlas connection string
mongoose.connect("mongodb+srv://<user>:<pass>@cluster.mongodb.net/dbname")
.then(() => console.log("DB connected"))
.catch((err) => console.log(err));
$eq equal $neq not equal $gt greater than
$gte ≥ $lt less than $lte ≤
$in value in array $nin value NOT in array
Query: $all — doc array must contain ALL listed values (order doesn't matter)
Update:
$push — add one item to array
$each — used with $push to add multiple items at once
$pull — remove all items matching a condition
$pop — remove first (-1) or last (1) item
$addToSet — push only if value doesn't already exist (no duplicates)
$set — set a field to a value (creates it if it doesn't exist)
$unset — completely remove a field from the document
$inc — increment (or decrement with negative value) a numeric field
If you try to save a field that isn't defined in the schema, Mongoose will throw an error instead of silently ignoring it. Catches typos and accidental extra fields during development.
Automatically adds createdAt and updatedAt fields to every document. Mongoose manages these for you — no need to set them manually.
By default, Mongoose validators (like minLength, required) only run on .save(). You must pass runValidators: true in update options to enforce them on findByIdAndUpdate too.
import { Schema, model } from 'mongoose';
const productSchema = new Schema({
productName: {
type: String,
required: [true, "product name required"],
minLength: [5, 'min length is 5']
},
price: {
type: Number,
min: [10, 'min price 10']
}
}, {
strict: "throw", // Error if undefined fields are saved
timestamps: true, // Auto-adds createdAt & updatedAt
versionKey: false // Removes __v field
});
export const ProductModel = model("product", productSchema);
// Update user cart — push product and populate details
let modifiedUser = await UserModel.findByIdAndUpdate(
uid,
{ $push: { cart: { product: pid, quantity: 1 } } },
{ new: true, runValidators: true } // runValidators: false by default!
).populate("cart.product", "productName price");
// ↑ field to populate ↑ fields to select
Mongoose Query Methods
These are the core Mongoose methods for reading and updating documents. Understanding what each method accepts, returns, and how options like new: true change the behaviour is essential — both in real projects and interviews.
| Method | What it does | Arguments | Returns |
|---|---|---|---|
| find() | Returns all documents that match the filter. If no filter passed, returns the entire collection. | filter (optional), projection (optional) | Array of documents — empty array if none match, never null |
| findOne() | Returns the first document that matches the filter. Stops scanning after the first match — more efficient than find() when you only need one. | filter, projection (optional) | Single document | null if not found |
| findById() | Shorthand for findOne({ _id: id }). Looks up a document by its _id field directly. Accepts string or ObjectId. | id (string or ObjectId) | Single document | null if not found |
| findOneAndUpdate() | Finds the first matching document and applies the update. By default returns the old (pre-update) document. Pass { new: true } to get the updated version back. | filter, update, options | Old doc by default | New doc with new: true | null if no match |
| findByIdAndUpdate() | Shorthand for findOneAndUpdate({ _id: id }, ...). Same exact behaviour — returns old document by default, new document with { new: true }. | id, update, options | Old doc by default | New doc with new: true | null if not found |
| findByIdAndDelete() | Finds a document by _id, removes it from the collection entirely, and returns it — useful to confirm what was deleted. | id | The deleted document | null if not found |
find() always returns an array — even if only one document matches, you get a one-element array. findOne() always returns a single document or null. Use findOne() when you expect exactly one result (by email, username, slug, etc.).
Pass { upsert: true } to findOneAndUpdate() or findByIdAndUpdate() to create the document if it doesn't exist. Without it, if the filter matches nothing, the method just returns null and does nothing.
Mongoose validators (required, minLength) only run on .save() by default. Update methods skip validators unless you explicitly pass { runValidators: true }.
// ===== find() — always returns an Array, never null =====
let allUsers = await UserModel.find();
let activeUsers = await UserModel.find({ active: true });
let names = await UserModel.find({}, "name email"); // field projection
// find() returns [] if nothing matches — no null check needed
// ===== findOne() — returns single doc or null =====
let user = await UserModel.findOne({ email: "user@example.com" });
if (!user) return res.status(404).json({ message: "User not found" });
// ===== findById() — same as findOne({ _id: id }) =====
let userById = await UserModel.findById(req.params.id); // doc or null
// ===== findByIdAndUpdate() — default returns OLD document =====
let oldDoc = await UserModel.findByIdAndUpdate(
req.params.id,
{ $set: { name: "New Name" } }
);
// oldDoc.name is STILL the original name — the update already happened in DB
// but this variable holds the pre-update snapshot
// ✓ Pass { new: true } to get the UPDATED document back
let updatedDoc = await UserModel.findByIdAndUpdate(
req.params.id,
{ $set: { name: "New Name" } },
{ new: true, runValidators: true }
);
// updatedDoc.name is now "New Name" ✓
// ===== findOneAndUpdate() — filter by any field, not just _id =====
let updated = await ProductModel.findOneAndUpdate(
{ sku: "ABC123" },
{ $inc: { stock: -1 } },
{ new: true }
);
// ===== findByIdAndDelete() — removes doc, returns the deleted doc =====
let deleted = await UserModel.findByIdAndDelete(req.params.id);
if (!deleted) return res.status(404).json({ message: "Not found" });
// deleted holds the document that WAS in the DB before deletion
// ===== .select() — field projection on find/findOne/findById =====
let safeUser = await UserModel
.findById(id)
.select("-password -__v"); // "-" prefix = exclude these fields
• find() → always an Array (empty array if no match — never null)
• findOne() / findById() → document or null
• findByIdAndUpdate() → old doc by default | new doc with new: true | null if not found
• findOneAndUpdate() → same as above, but filter by any field
• findByIdAndDelete() → the deleted doc | null if not found
10c-DB Connection Pattern
The correct pattern is to wrap the MongoDB connection in an async function with try/catch, and only start the HTTP server inside the try block after the DB connects successfully. This ensures your server never starts with a broken database connection.
import { connect } from 'mongoose';
import 'dotenv/config';
import { app } from './app.js'; // your express app
/**
* Connects to MongoDB, then starts the HTTP server.
* Server only starts AFTER DB connects — prevents serving
* requests with no database available.
*/
const connectDB = async () => {
try {
await connect(process.env.DB_URL);
console.log("DB connection success");
// Start HTTP server only after DB is ready
app.listen(process.env.PORT, () => console.log("server started!"));
} catch (err) {
console.log("error occurred", err.message);
process.exit(1); // Exit process on DB failure — don't hang
}
};
connectDB(); // Call it to kick everything off
# Local MongoDB (MongoDB installed on your machine)
DB_URL=mongodb://localhost:27017/dbName
# ↑ ↑ ↑
# host port your database name
# MongoDB Atlas (cloud — production)
DB_URL=mongodb+srv://<username>:<password>@cluster0.xxxxx.mongodb.net/dbName
Auth, JWT & Cookies
Authentication answers: "Who are you?"
Authorization answers: "What are you allowed to do?"
In modern web apps, authentication is commonly implemented using JWT (JSON Web Tokens),
which are stored inside cookies. The browser automatically attaches these cookies to
every request, enabling secure access to protected routes.
npm install bcryptjs jsonwebtoken dotenv
The Signature proves authenticity — generated by combining the Header, Payload, and your SECRET_KEY. If even one character changes, the signature is invalid.
⚠ Never store passwords in the payload — only safe data like id, email, role.
Step-by-step authentication sequence:
1. User sends credentials (email + password)
2. Server verifies via bcrypt
3. Server generates JWT using jwt.sign()
4. JWT is stored inside an httpOnly cookie
5. Browser auto-sends cookie on every request
6. Protected routes verify token via middleware
// 1. Verify credentials against DB
let isMatch = await bcrypt.compare(password, userDB.password);
// 2. If match, sign a token (id + email + role combined with Secret)
let token = jwt.sign(
{ id: userDB._id, email: userDB.email, role: userDB.role },
process.env.JWT_SECRET,
{ expiresIn: '1d' }
);
// 3. Store token in an httpOnly cookie (best security settings)
res.cookie('token', token, {
httpOnly: true, // JS cannot read — prevents XSS theft
secure: true, // HTTPS only
sameSite: "lax", // CSRF protection
maxAge: 1000 * 60 * 60 * 24, // 1 day in ms
path: "/"
});
res.status(200).json({ message: "Valid Token Generated" });
The token is stored in a cookie. Cookies act as a container and are automatically sent with every subsequent request — no manual work needed on the frontend.
🔁 Request Flow
Every incoming request is classified as either Public or Protected. The path each takes is fundamentally different.
Public Routes
Protected Routes
The Gatekeeper — verifyToken Middleware
Public routes are served normally. Protected routes first hit this middleware. It checks if the cookie exists, verifies the signature, and checks the expiration.
export function verifyToken(req, res, next) {
const token = req.cookies.token;
// If no token, the protected request fails here → 401 Unauthorized
if (!token) return res.status(401).json({ message: "No token found" });
try {
// Verify Signature ✅ & Expiry ✅
const decoded = jwt.verify(token, process.env.JWT_SECRET);
// Store payload in req.user for cleaner downstream logic
req.user = decoded; // { id, email, role }
next(); // Proceed to Role Check or Route Handler
} catch (err) {
return res.status(401).json({ message: "Invalid or Expired Token" });
}
}
After verification, req.user contains the decoded token. This avoids repeatedly parsing the token and keeps route handlers clean.
⚠️ For sensitive operations, you may still want to validate against the DB.
After verifyToken succeeds, check whether this user actually has permission for this action. Only role: "admin" should delete other users. Authentication proves who you are — authorization decides what you can do.
No session storage on server ❌
All user info lives inside the token ✅
Server only verifies the signature.
Makes JWT fast and scalable, but requires careful handling of expiry and security.
Forgetting withCredentials on axios
Missing cookie-parser middleware
Manually sending token in headers instead of cookies
Using secure: true on localhost (breaks dev)
Not handling token expiry on the frontend
Logout — Clearing the Cookie
JWTs are stateless. Logout simply instructs the browser to empty the cookie container. Future requests arrive without a token and fail the verifyToken check. The token is not destroyed on the server — the cookie is simply removed from the browser.
res.clearCookie('token', {
httpOnly: true,
secure: true,
sameSite: "lax",
path: "/"
});
res.status(200).json({ message: "Logged out successfully" });
// Summary
JWT authentication works by generating a signed token on login, storing it in an httpOnly cookie, and verifying it on each protected request using middleware. If the token is valid and not expired, access is granted — otherwise the request is rejected. The role stored inside the token is then used for authorization, controlling what each authenticated user is permitted to do.
When using cookies, every axios request must include:
axios.get(url, { withCredentials: true })
Without this, cookies will not be sent and authentication will fail.
DOM Manipulation
Vanilla JS DOM manipulation is imperative — you tell the browser exactly what to do, step by step. React is declarative — you describe what the UI should look like and React handles the DOM.
let submitbtn = document.querySelector(".sb");
let parent = document.querySelector(".parent");
submitbtn.addEventListener('click', (e) => {
e.preventDefault(); // Stop default form submission
let newChild = document.createElement('p');
newChild.textContent = "Hello World!";
newChild.style.color = "red";
parent.appendChild(newChild);
});
CSS Fundamentals & Layouts
Default Browser CSS
Before you write a single line of CSS, the browser already applies its own built-in stylesheet called the User-Agent Stylesheet. This is why an unstyled <h1> is big and bold, <a> tags are blue and underlined, and <ul> has bullets — you didn't add any of that, the browser did.
Every browser ships with default styles. Chrome, Firefox, and Safari all have slightly different defaults — which is why sites can look inconsistent across browsers without a reset.
A CSS reset removes all browser defaults so you start from zero. The most common is * { margin:0; padding:0; box-sizing:border-box; }. Tailwind's Preflight does this automatically.
When multiple rules target the same element, the browser picks the winner by specificity: inline style > ID > class > tag. Equal specificity = last rule wins (the "cascade").
/* Browser default styles — applied before your CSS */
h1 { font-size: 2em; font-weight: bold; margin: 0.67em 0; }
p { margin: 1em 0; }
a { color: blue; text-decoration: underline; }
ul { list-style-type: disc; padding-left: 40px; }
body { margin: 8px; } /* That gap around the page edge? This. */
/* Common reset to neutralize browser defaults */
* {
margin: 0;
padding: 0;
box-sizing: border-box; /* padding/border included in width */
}
Common Properties in style.css
These are the properties you'll use constantly in every project — spacing, typography, sizing, and color.
.box {
/* SPACING */
margin: 20px; /* space OUTSIDE the element (pushes away neighbors) */
margin: 10px 20px; /* top/bottom left/right */
margin: 10px 20px 5px 0; /* top right bottom left (clockwise) */
padding: 16px; /* space INSIDE the element (between border & content) */
/* SIZING */
width: 300px;
max-width: 100%; /* never exceed this width */
height: 200px;
min-height: 100vh; /* at least full viewport height */
/* TYPOGRAPHY */
font-size: 16px;
font-weight: 700; /* 400=normal 700=bold */
line-height: 1.6; /* 1.6× the font-size, improves readability */
letter-spacing: 0.5px;
text-transform: uppercase; /* lowercase | capitalize */
text-decoration: none; /* removes underline from links */
color: #333;
/* BACKGROUND & BORDER */
background-color: #fff;
border: 1px solid #ccc; /* width style color */
border-radius: 8px; /* rounds corners */
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
text-align vs align-items
These are the most commonly confused CSS properties. They look similar but work completely differently.
Aligns inline content (text, inline elements) horizontally within its block container.
Works on: the parent that contains the text.
Values: left center right justify
e.g. centering text inside a <p> or <div>
Aligns flex/grid children along the cross axis (perpendicular to the main axis).
Works on: the flex/grid container — has no effect without display: flex.
Values: flex-start center flex-end stretch
e.g. vertically centering items inside a flex row
/* text-align: for text/inline content inside a block */
.heading {
text-align: center; /* centers the text horizontally */
}
/* align-items: for flex/grid children on the cross axis */
.navbar {
display: flex;
align-items: center; /* vertically centers children in the row */
justify-content: space-between; /* main axis (horizontal here) */
}
/* Common confusion: text-align on a flex container */
.card {
display: flex;
text-align: center; /* ✓ still works for text inside children */
align-items: center; /* ✓ vertically centers the flex children */
}
CSS Positions
Default for every element. Follows normal document flow. top / left / right / bottom have no effect whatsoever.
Offset from its own normal position. Original space is still reserved — neighbours don't move. Good for minor nudges or creating a positioned parent for absolute children.
Removed from normal flow entirely. Positioned relative to the nearest ancestor that has position: relative/absolute/fixed. Other elements act like it doesn't exist.
Positioned relative to the viewport. Doesn't move on scroll. Used for navbars, floating buttons, modals. Removed from normal flow.
Hybrid of relative + fixed. Acts relative while in view, then sticks when you scroll past its threshold. Requires top (or left/right/bottom) to activate.
/* relative: small nudge, still in flow */
.badge { position: relative; top: -2px; }
/* absolute inside relative parent — classic pattern */
.card { position: relative; }
.overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }
/* fixed navbar — always at top of viewport */
.navbar { position: fixed; top: 0; left: 0; width: 100%; }
/* sticky table header — sticks when you scroll table */
th { position: sticky; top: 0; background: #fff; }
/* ⚠ Won't work if a parent has overflow: hidden/auto */
Flex Layout
Flexbox is a one-dimensional layout system — it arranges items in a row or a column. Perfect for navbars, card rows, centering content, and any time items need to be spaced or aligned along one axis.
The main axis is the direction items flow (row = horizontal, column = vertical). The cross axis is always perpendicular to it. justify-content controls main axis, align-items controls cross axis.
By default, flex items squish to fit one line (nowrap). Set flex-wrap: wrap to let them overflow onto the next line when they run out of space — great for responsive grids.
/* === CONTAINER PROPERTIES === */
.container {
display: flex;
flex-direction: row; /* row | row-reverse | column | column-reverse */
flex-wrap: wrap; /* nowrap | wrap | wrap-reverse */
/* Main axis (horizontal by default) */
justify-content: space-between; /* flex-start | center | flex-end | space-around | space-evenly */
/* Cross axis (vertical by default) */
align-items: center; /* flex-start | center | flex-end | stretch | baseline */
gap: 16px; /* space between items (row-gap column-gap) */
}
/* === ITEM PROPERTIES === */
.item {
flex: 1; /* shorthand for flex-grow:1 flex-shrink:1 flex-basis:0 */
/* all items grow equally to fill available space */
flex-grow: 1; /* how much this item grows relative to others */
flex-shrink: 0; /* 0 = refuse to shrink below flex-basis */
flex-basis: 200px; /* starting size before grow/shrink kicks in */
align-self: flex-end; /* override align-items for THIS item only */
}
/* Common pattern: perfect centering */
.center {
display: flex;
justify-content: center;
align-items: center;
}
Grid Layout
Grid is a two-dimensional layout system — it controls both rows and columns simultaneously. Perfect for page layouts, dashboards, image galleries, and any structure that has both horizontal and vertical arrangement.
The fr (fraction) unit divides the remaining available space. 1fr 1fr 1fr = three equal columns. 2fr 1fr = first column is twice as wide as the second.
repeat(4, 1fr) is shorthand for 1fr 1fr 1fr 1fr. Use repeat(auto-fill, minmax(200px, 1fr)) for a responsive grid with no media queries.
/* === CONTAINER PROPERTIES === */
.grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr; /* 4 equal columns */
grid-template-columns: repeat(4, 1fr); /* same, shorthand */
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); /* responsive! */
grid-template-rows: 100px auto 60px; /* header auto footer */
gap: 16px; /* gap between all cells */
row-gap: 20px; /* gap between rows only */
column-gap: 10px; /* gap between columns only */
}
/* === ITEM PLACEMENT (two ways) === */
/* Longhand */
.hero {
grid-column-start: 1; /* start at column line 1 */
grid-column-end: 3; /* end at column line 3 (spans 2 columns) */
grid-row-start: 1;
grid-row-end: 2;
}
/* Shorthand (preferred) */
.hero {
grid-column: 1 / 3; /* start / end */
grid-column: 1 / span 2; /* start / span count (same result) */
grid-row: 1 / 2;
}
/* span entire row */
.full-width { grid-column: 1 / -1; } /* -1 = last line */
Breakpoints, Media Queries & Units
Responsive design means your layout adapts to different screen sizes. A breakpoint is a specific screen width at which the design needs to change. When a breakpoint is breached, media queries apply new CSS to re-arrange elements for that viewport.
Media Queries
/* @media media-type operator (media-feature) { styles } */
/* ---------- -------- --------------- */
/* screen and max-width: 768px */
/* print not min-width: 480px */
/* speech or orientation: landscape */
/* all prefers-color-scheme: dark */
/* screen — for any screen device (phone, tablet, desktop) */
@media screen and (max-width: 768px) {
.container { flex-direction: column; }
.sidebar { display: none; }
}
/* print — styles only applied when printing the page */
@media print {
.navbar, .ads { display: none; }
}
/* multiple conditions with and */
@media screen and (min-width: 768px) and (max-width: 1024px) {
/* tablet only */
}
/* dark mode media query */
@media (prefers-color-scheme: dark) {
body { background: #111; color: #fff; }
}
Common Breakpoints
/* Mobile-First: write base styles for mobile, override upward */
/* Use min-width queries — "at this size and ABOVE, apply this" */
.grid {
display: grid;
grid-template-columns: 1fr; /* mobile: single column */
gap: 16px;
}
@media (min-width: 768px) {
.grid { grid-template-columns: 1fr 1fr; } /* tablet: 2 col */
}
@media (min-width: 1024px) {
.grid { grid-template-columns: repeat(4, 1fr); } /* desktop: 4 col */
}
/* Desktop-First uses max-width (override downward) — less preferred */
@media (max-width: 768px) {
.grid { grid-template-columns: 1fr; }
}
CSS Units
The root of any HTML document is the <html> tag. This matters because rem is always calculated relative to the font size set on it. Understanding when to use which unit is critical for accessible, responsive designs.
Fixed, absolute unit. Does not scale when the user changes their browser font size in settings. Using px for text breaks accessibility — never use it for font-size.
OK for: borders, box-shadow, fine-tuned details
1em = the font-size of the parent element. Compounds up the tree — if a parent is 1.2em and a child is 1.2em, the child is actually 1.44× the root. Can cause unexpected sizes in nested elements.
Use for: padding/margin that should scale with its own component's font size
1rem = the font-size of the <html> (root) element. Defaults to 16px in all browsers. Does NOT compound. Adjusts when the user changes their browser font settings. Use this as your primary unit.
Use for: font-size, spacing, layout — almost everything
Relative to the parent element's width (for width/padding/margin). Very useful for fluid layouts. width: 50% = half of whatever the parent is.
Use for: fluid widths, responsive containers
1vw = 1% of the viewport width. 1vh = 1% of the viewport height. Independent of any parent — purely based on the current screen size.
Use for: full-screen heroes, fixed-height sections, font scaling at large sizes
/* Root font-size — 1rem = 16px by default */
/* Trick: set to 62.5% so 1rem = 10px (easy math) */
html { font-size: 62.5%; } /* now 1.6rem = 16px */
/* ✗ Bad — won't respect browser font size settings */
p { font-size: 16px; }
/* ✓ Good — scales with browser settings */
p { font-size: 1rem; } /* 16px by default */
h1 { font-size: 2.5rem; } /* 40px by default */
small{ font-size: 0.875rem;} /* 14px by default */
/* em: scales with parent — good for component-relative spacing */
.btn {
font-size: 1rem;
padding: 0.75em 1.5em; /* scales with button's own font-size */
}
/* % for fluid widths */
.container { width: 90%; max-width: 1200px; }
/* vw/vh for viewport-based sizing */
.hero { height: 100vh; } /* full screen height */
.banner { font-size: 5vw; } /* text scales with screen width */
Tailwind CSS
Tailwind is a utility-first CSS framework. Instead of writing custom CSS, you compose styles by applying small utility classes directly in your HTML. The build process scans your HTML and generates only the CSS you actually used — nothing more.
The Tailwind CLI scans your HTML, finds every utility class you used (e.g. text-red-500, flex, p-4), and writes a minimal output.css containing only those styles. The result is an extremely lean CSS file in production.
No switching between HTML and CSS files. Every style lives directly on the element as a class. Faster to build, and you can see what an element looks like just by reading its HTML markup.
Tailwind Preflight
Preflight is Tailwind's built-in CSS reset, automatically injected when you add @tailwind base to your input CSS. It removes all browser default styles so you start from a completely clean, consistent baseline across every browser.
All default margins and padding. Default h1–h6 sizes and bold weights. Bullet points on ul/ol. Blue underline on <a>. Default border on <button>. The 8px body margin.
Sets box-sizing: border-box on everything. Makes images display: block by default (removes inline bottom gap). Inherits font on form elements. Sets line-height to a sane default.
Without Preflight, a Tailwind <h1> would still be huge and bold — you'd need text-base font-normal to fight the browser. With Preflight, <h1> looks identical to <p> until YOU add classes.
/* Preflight essentially does this for you: */
*, *::before, *::after {
box-sizing: border-box; /* padding/border included in width */
border-width: 0;
border-style: solid;
}
body { margin: 0; line-height: inherit; }
/* headings reset — no size, no weight */
h1, h2, h3, h4, h5, h6 { font-size: inherit; font-weight: inherit; }
/* links reset — no color, no underline */
a { color: inherit; text-decoration: inherit; }
/* lists reset — no bullets, no padding */
ol, ul { list-style: none; margin: 0; padding: 0; }
/* images block — removes bottom whitespace gap */
img, svg, video { display: block; vertical-align: middle; }
/* buttons reset */
button { background-color: transparent; background-image: none; }
# Install Tailwind CSS as a dev dependency
npm install -D tailwindcss @tailwindcss/cli
# OR use it without installing via npx (no install needed)
npx @tailwindcss/cli --help
/* style.css — this is your INPUT file, not what you link in HTML */
/* These 3 directives import all of Tailwind's layers */
@tailwind base; /* Resets + base element styles */
@tailwind components; /* Component classes (rarely used) */
@tailwind utilities; /* All the utility classes like flex, p-4, etc. */
# -i ./style.css → your input file with @tailwind directives
# -o ./output.css → the generated file (link THIS in your HTML)
# --watch → auto-rebuilds whenever you save any file
npx @tailwindcss/cli -i ./style.css -o ./output.css --watch
# Add as an npm script in package.json for convenience:
# "scripts": { "tw": "npx @tailwindcss/cli -i ./style.css -o ./output.css --watch" }
# Then just run: npm run tw
<head>
<!-- Link output.css, NOT style.css -->
<link rel="stylesheet" href="./output.css">
</head>
<body>
<!-- Now use Tailwind classes freely -->
<div class="flex gap-4 p-6 bg-gray-900 rounded-lg">
<button class="px-4 py-2 bg-emerald-400 text-black font-bold
rounded hover:bg-emerald-300 transition-colors">
Click me
</button>
<!-- sm: md: lg: are responsive breakpoint prefixes -->
<p class="text-sm md:text-base lg:text-lg text-gray-400">
Responsive text
</p>
</div>
</body>
Tailwind Intellisense in VS Code
Without Intellisense, you're typing Tailwind classes from memory. The official VS Code extension gives you autocomplete, hover previews, and linting — essential for real development speed.
As you type inside a class="" attribute, it suggests every matching Tailwind class. Press Tab to complete. Shows the actual CSS value it maps to alongside.
Hover over any Tailwind class in your HTML and a tooltip shows you the exact CSS it generates. No more guessing what p-6 or text-xl actually outputs.
Install the Prettier plugin for Tailwind to auto-sort your classes into a consistent recommended order every time you save. Keeps code readable across teams.
# Step 1: Install the VS Code extension
# Open VS Code → Extensions (Ctrl+Shift+X)
# Search: "Tailwind CSS IntelliSense" by Tailwind Labs → Install
# Step 2 (Optional but recommended): Auto-sort classes with Prettier
npm install -D prettier prettier-plugin-tailwindcss
# Create a .prettierrc file in your project root:
# {
# "plugins": ["prettier-plugin-tailwindcss"]
# }
# Now every time you save, classes sort automatically!
React Core Concepts
React uses a Virtual DOM — a lightweight in-memory representation of the real DOM. When state changes, React diffs the virtual DOM and updates only the changed parts in the real DOM.
React is declarative: you describe what the UI should look like for a given state. React figures out the DOM operations. Vanilla JS is imperative: you write each DOM step manually.
Use .map() to loop through data and return JSX. Always provide a unique key prop to help React track elements efficiently.
// Alt 1: Ternary Operator — if/else, always use this for two outcomes
{ isActive ? <ActiveComponent /> : <InactiveComponent /> }
// Alt 2: Logical AND — render only if true (no else needed)
{ isActive && <ActiveComponent /> }
// Rendering a list with .map()
{ marks.map((m, i) => <div key={i}>{m}</div>) }
State & Mutations
useState is a React Hook that lets you add state to functional components. It returns an array with two values: the current state and a function to update it.
State allows components to re-render dynamically when data changes. Without state, components would always display static values. It also persists values across renders, unlike local variables which reset each time.
Perfect for handling user interactions (form inputs, toggles, counters) and keeping logic self-contained at the component level.
// Import useState from React
import { useState } from "react";
function Counter() {
// Declare state variable 'count' with initial value 0
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
// Update state using setCount
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
let [marks, setMarks] = useState([1, 2]);
// Add: Spread creates new array, triggers re-render
const pushed = () => {
setMarks([...marks, 123]);
};
// Delete: filter() returns a brand new array
const delMark = (index) => {
setMarks(marks.filter((_, i) => i !== index));
};
// Object State: spread existing, then override property
let [user, setUser] = useState({ email: "test@gmail.com" });
const updateCity = () => {
setUser({ ...user, city: "hyd" }); // New object, all old fields kept
};
Form Handling
# Install react-hook-form
npm install react-hook-form
Verbose. Requires separate useState for every single input field, plus manual onChange handlers and value bindings. Gets messy fast.
Designed for performance. Minimal re-renders, built-in validation, single register call per input. Preferred approach.
import { useForm } from 'react-hook-form';
function MyForm() {
const { register, handleSubmit } = useForm();
const submitForm = (data) => {
console.log(data); // { username: "...", email: "..." }
};
return (
// handleSubmit: validates then passes data to submitForm
<form onSubmit={handleSubmit(submitForm)}>
// register: injects onChange/onBlur, names the field
<input {...register("username")} />
<input {...register("email")} />
<button>Submit</button>
</form>
);
}
function MyForm() {
const { register, handleSubmit, formState: { errors } } = useForm();
return (
<form onSubmit={handleSubmit(data => console.log(data))}>
<input
{...register("username", {
required: "Username is required", // message shown on error
minLength: { value: 3, message: "Min 3 chars" },
maxLength: { value: 20, message: "Max 20 chars" },
})}
/>
{ errors.username && <span>{errors.username.message}</span> }
<input
{...register("email", {
required: "Email required",
pattern: {
value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
message: "Invalid email format"
}
})}
/>
{ errors.email && <span>{errors.email.message}</span> }
<button>Submit</button>
</form>
);
}
useEffect & Side Effects
Manages operations outside the normal render cycle — API calls, subscriptions, timers, DOM manipulation. Runs after the component renders to the screen.
Runs after every single render. Dangerous — causes infinite loops if you set state inside. Almost always wrong.
Runs only once after the initial render. Perfect for initial data fetching, subscriptions, and setup.
Runs on initial render AND whenever any listed dependency changes. Great for re-fetching on filter/search changes.
import { useEffect, useState } from 'react';
const [data, setData] = useState([]);
const [filter, setFilter] = useState('all');
// Run once on mount — initial data fetch
useEffect(() => {
fetch('https://api.example.com/data')
.then(r => r.json())
.then(d => setData(d));
}, []); // ← Empty array = mount only
// Re-run whenever 'filter' changes
useEffect(() => {
fetch(`https://api.example.com/data?type=${filter}`)
.then(r => r.json())
.then(d => setData(d));
}, [filter]); // ← Re-runs when filter changes
React Router
React has no built-in routing. React Router is the standard external library. It intercepts URL changes and renders the matching component — the browser never makes a full page reload, giving you a true Single Page Application (SPA).
# React Router v7+ (both packages merged into one)
npm install react-router
# Before v7 you needed both:
# npm install react-router react-router-dom
Core Concepts
Decide your root layout before routing. The root component (App.jsx) renders the persistent shell — Navbar, Footer, etc. The main content area is a placeholder that swaps per route.
Placed in the root layout where child routes should render. Think of it as a slot — when you navigate to /login, the Login component fills the Outlet. The rest of the layout stays unchanged.
Creates the routing configuration object. Takes an array of route objects, each with a path and element. Nested routes use a children array. Pass the result to <RouterProvider>.
Never use <a href> in React Router — it triggers a full page reload, losing all state. <NavLink> navigates without reload and automatically adds an active class to the current link.
import { createBrowserRouter, RouterProvider, Navigate } from 'react-router';
const router = createBrowserRouter([
{
path: "/",
element: <RootLayout />,
children: [
// ⚠ WHITE PAGE FIX — without this, "/" shows RootLayout with empty Outlet
// Option A: path:"" redirects "/" to first route
{ path: "", element: <Navigate to="home" /> },
// Option B: index:true — marks the default child, cleaner syntax (same result)
// { index: true, element: <Home /> }
{ path: "home", element: <Home /> },
{ path: "register", element: <Register /> },
{ path: "login", element: <Login /> },
{ path: "products", element: <Products /> },
]
}
]);
export default function App() {
return <RouterProvider router={router} />;
}
import { Outlet, NavLink } from 'react-router';
function RootLayout() {
return (
<div>
<nav>
{/* NavLink auto-adds 'active' class — use className fn for custom styling */}
<NavLink
to="home"
className={({ isActive }) => isActive ? "text-white font-bold" : "text-gray-400"}
>Home</NavLink>
<NavLink to="products"
className={({ isActive }) => isActive ? "text-white font-bold" : "text-gray-400"}
>Products</NavLink>
</nav>
{/* Child route components render here */}
<main>
<Outlet />
</main>
</div>
);
}
Programmatic Navigation
Sometimes you need to navigate from code — after a form submit, a button click, or a successful API call. Use the useNavigate hook. To pass data between routes without putting it in the URL, use the state option.
Returns a navigate() function. Call it with a path string to go there. Use navigate(-1) to go back (like browser back button). Can pass a state object as the second argument.
When you navigate with a state object, the destination component uses useLocation() to read it. Returns the current location object including pathname, search, and state.
Use <Navigate to="path" /> inside JSX for declarative redirects — e.g. redirect the bare / route to /home, or redirect unauthenticated users away from protected pages.
import { useNavigate, useLocation } from 'react-router';
// ===== SENDER COMPONENT (e.g. ProductsList) =====
function ProductsList() {
const navigate = useNavigate();
const gotoProduct = (productObj) => {
// Pass product data through state — no URL params needed
navigate('/products', { state: { product: productObj } });
};
return (
<div>
{/* Function NEEDS a parameter → wrap in arrow to pass the value */}
<div onClick={() => gotoProduct(item)}>Click me</div>
{/* Function has NO parameters → pass directly, no wrapper needed */}
<button onClick={handleClose}>Close</button>
</div>
);
}
// ===== RECEIVER COMPONENT (e.g. ProductDetail) =====
function ProductDetail() {
const { state } = useLocation(); // Read state from previous navigate()
const product = state?.product; // Optional chain — safe if navigated directly
return <h1>{product?.title}</h1>;
}
// navigate(-1) — go back to previous route (browser back)
// navigate('/login', { replace: true }) — replace history entry (no back button)
Full Pattern: Fetch + Filter + Sort + Navigate
This is the complete real-world pattern combining useEffect for fetching, useState for loading/error/data, react-hook-form for search, and useNavigate for row clicks. This comes up constantly in interviews.
Always manage 3 states for async data: loading (show spinner), error (show error message), data (show content). Return early for the first two before rendering the main JSX.
Keep the original fetched array in products and a separate filteredProducts for display. Search and sort operate on filteredProducts. Reset to products when search is cleared.
.sort() modifies the array in place. In React, mutating state directly skips re-renders. Always spread first: [...filteredProducts].sort(...) before calling setFilteredProducts.
import { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { useNavigate } from 'react-router';
function ProductsList() {
const { register, handleSubmit } = useForm();
const navigate = useNavigate();
const [products, setProducts] = useState([]);
const [filteredProducts, setFiltered] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
// 1. FETCH on mount
useEffect(() => {
setLoading(true);
async function getProducts() {
try {
let res = await fetch('https://fakestoreapi.com/products');
if (res.status !== 200) throw new Error('Failed to fetch');
let data = await res.json();
setProducts(data);
setFiltered(data); // both start as the full list
} catch (err) {
setError(err.message);
} finally {
setLoading(false); // always runs, success or fail
}
}
getProducts();
}, []);
// 2. EARLY RETURNS for loading/error states
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
// 3. SEARCH by title or exact category
const searchProduct = ({ searchInput }) => {
const term = searchInput.toLowerCase().trim();
if (!term) { setFiltered(products); return; }
setFiltered(products.filter(p =>
p.title.toLowerCase().includes(term) // substring match for title
|| p.category.toLowerCase() === term // exact match for category
));
};
// 4. SORT by price — spread first! .sort() mutates
const sortByPrice = (dir) => {
const copy = [...filteredProducts];
copy.sort((a, b) => dir === 'low' ? a.price - b.price : b.price - a.price);
setFiltered(copy);
};
return (
<div>
<form onSubmit={handleSubmit(searchProduct)}>
<input {...register('searchInput')} placeholder="Search title or category" />
<button type="submit">Search</button>
<select onChange={e => sortByPrice(e.target.value)}>
<option value="">Sort</option>
<option value="low">Low → High</option>
<option value="high">High → Low</option>
</select>
</form>
{/* Grid of product cards */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8">
{
filteredProducts.length === 0
? <p>No products found</p>
: filteredProducts.map(p => (
<div
key={p.id}
onClick={() => navigate('/products', { state: { product: p } })}
>
<img src={p.image} alt={p.title} />
<p>{p.title}</p>
<p>${p.price}</p>
</div>
))
}
</div>
</div>
);
}
// .sort() comparison: negative = a before b, positive = b before a, 0 = unchanged
// a.price - b.price → ascending (low first)
// b.price - a.price → descending (high first)
19 — API Requests from Components
There are two main ways to make HTTP requests from a React component:
the built-in fetch API and the third-party
axios library.
1. fetch (built-in)
Native browser API — no install needed. More verbose; you handle headers, body serialization, and error detection manually.
// GET request — method defaults to "GET" if omitted
const res = await fetch("https://api.example.com/data", { method: "GET" });
const data = await res.json(); // must manually extract JSON
// data → { message: "", payload: "" } — you destructure yourself
// fetch only rejects on network failure, NOT on 4xx/5xx — check manually:
if (!res.ok) throw new Error(`HTTP error: ${res.status}`);
// POST request
const resObj = await fetch("https://api.example.com/users", {
method: "POST",
headers: { "Content-Type": "application/json" }, // must set manually
body: JSON.stringify({ name: "Alice", age: 25 }), // must serialise manually
});
if (!resObj.ok) throw new Error("Request failed");
const created = await resObj.json();
res.ok (true when status 200–299) or compare
res.status manually.
2. axios (third-party)
Install: npm i axios — then import per file. Less
verbose, auto-parses JSON, and automatically throws on 4xx/5xx
responses.
import axios from "axios";
// GET request
const resObj = await axios.get("https://api.example.com/data");
const data = resObj.data; // JSON already extracted — no .json() call needed
// POST request — pass object directly, axios serialises it automatically
const resObj2 = await axios.post("https://api.example.com/users", { name: "Alice", age: 25 });
const created = resObj2.data;
// Accessing a protected route — send JWT in Authorization header
const token = localStorage.getItem("token");
const profile = await axios.get("/api/profile", {
headers: { Authorization: `Bearer ${token}` },
});
// Error handling — axios throws on 4xx/5xx automatically
try {
const res = await axios.get("/api/secret");
} catch (err) {
console.log(err.response.status); // e.g. 401
console.log(err.response.data); // server error body
console.log(err.message); // human-readable message
}
fetch vs axios — quick comparison
| Feature | fetch | axios |
|---|---|---|
| Install needed | ❌ built-in | ✅ npm i axios |
| Auto JSON parse | ❌ call .json() | ✅ resObj.data |
| Throws on 4xx/5xx | ❌ manual check | ✅ automatic |
| Set Content-Type | ❌ manual | ✅ automatic |
| Stringify body | ❌ JSON.stringify() | ✅ automatic |
| Error object | basic Error | err.response.data/status |
19b — React Fragments & Toast Notifications
React Fragment <></>
A Fragment lets you group multiple sibling elements without adding an
extra DOM node. Use it when a wrapper <div> would
break layout or add unwanted styling.
// Without fragment — adds an unnecessary div to the DOM
return (
<div>
<h1>Title</h1>
<p>Subtitle</p>
</div>
);
// With fragment — no extra DOM node
return (
<>
<h1>Title</h1>
<p>Subtitle</p>
</>
);
// Long-form (needed if you want to pass a key prop, e.g. in lists)
import { Fragment } from "react";
items.map(item => (
<Fragment key={item.id}>
<dt>{item.term}</dt>
<dd>{item.def}</dd>
</Fragment>
));
<></> instead.
Toast Notifications — react-hot-toast
Toasts are small, auto-dismissing UI messages used to give the user non-blocking feedback (success, error, loading, etc.).
// 1. Install
npm i react-hot-toast
// 2. Add <Toaster> once in your root component (App.jsx)
import { Toaster } from "react-hot-toast";
function App() {
return (
<>
<Toaster position="top-right" reverseOrder={false} />
{/* rest of your app */}
</>
);
}
// 3. Trigger toasts from any component
import { toast } from "react-hot-toast";
toast.success("Logged in successfully!"); // green ✓
toast.error("Invalid credentials"); // red ✗
toast.loading("Submitting..."); // spinner
toast("Copied to clipboard"); // neutral
// Promise toast — auto switches loading → success/error
toast.promise(
axios.post("/api/login", credentials),
{
loading: "Logging in...",
success: "Welcome back!",
error: "Login failed",
}
);
"top-left",
"top-center", "top-right",
"bottom-left", "bottom-center",
"bottom-right"reverseOrder — when
true, new toasts
stack below older ones instead of above.
State Management
As apps grow, passing state through props between many components becomes painful (prop drilling). State management solutions create a shared store outside the component tree so any component can read or update state directly. It means sharing the state across the application and achieve sync in state across the application
The Problem — Prop Drilling
Passing state through every intermediate component just to reach a deeply nested child. App → Layout → Sidebar → UserMenu → Avatar — all components in the chain have to pass user even if they don't use it. Brittle and hard to maintain.
Moving state to the nearest common ancestor of all components that need it. Works for single-level sharing or siblings. Gets impractical when the common ancestor is far up the tree.
State that lives outside the component tree entirely. Components subscribe to it directly. When the state changes, only the components that use it re-render. No prop drilling at any level.
Three Solutions
Best for: small/medium apps, theme toggling, auth state, locale settings.
Create a context, wrap the app in a Provider, consume with useContext(). No external library needed. Downside: every consumer re-renders when context value changes — not optimised for high-frequency updates.
Best for: large, complex apps with many interconnected states, time-travel debugging.
Centralised store with strict unidirectional data flow. action → reducer → store → component. Redux Toolkit (RTK) is the modern way — removes boilerplate of classic Redux.
Best for: medium/large apps that want Redux-style global state without the boilerplate.
Very minimal API. Create a store with create(), consume directly in components with a hook. No Provider needed. Much simpler than Redux but still powerful.
import { createContext, useContext, useState } from 'react';
// 1. Create the context object
const AuthContext = createContext(null);
// 2. Create a Provider component that holds the state
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
return (
{/* Wrap your app — all children can now access user + setUser */}
<AuthContext.Provider value={{ user, setUser }}>
{children}
</AuthContext.Provider>
);
}
// 3. Custom hook — cleaner than calling useContext directly
export const useAuth = () => useContext(AuthContext);
// 4. In main.jsx — wrap app with Provider
// <AuthProvider><App /></AuthProvider>
// 5. In any nested component — consume directly, no prop drilling
function Navbar() {
const { user, setUser } = useAuth();
return <p>Welcome, {user?.name}</p>;
}
# Install Zustand
npm install zustand
// store.js — create the store
import { create } from 'zustand';
export const useCartStore = create((set) => ({
cart: [],
addItem: (item) => set(state => ({ cart: [...state.cart, item] })),
removeItem: (id) => set(state => ({ cart: state.cart.filter(i => i.id !== id) })),
clearCart: () => set({ cart: [] }),
}));
// In ANY component — no Provider, no prop drilling
function CartIcon() {
const { cart, addItem } = useCartStore();
return <span>Cart: {cart.length}</span>;
}
React Hooks Deep Dive
Beyond useState and useEffect, React provides hooks for performance optimisation (useMemo, useCallback), DOM access (useRef), and reusable logic (custom hooks). These come up constantly in interviews.
useRef — Mutable Reference Without Re-render
Returns a mutable object { current: value }. Changing ref.current does NOT trigger a re-render. The value persists across renders unlike a regular variable which resets every time.
1. DOM access: Attach to JSX via ref={myRef} to directly access the DOM node (focus, measure, animate).
2. Persist values: Store values between renders without causing re-renders — e.g. previous state, timer IDs, interval references.
useRef is used to access the reference of the real DOM directly.
Reasons to use: If some action needs to happen instantly (improving user experience), to maintain previous values, or when changes occur but the component should not re-render.
ref attribute: Connects reference elements like <input ref={inputRef} type="text" />.
import { useRef, useEffect } from 'react';
// USE 1: DOM access — focus an input on mount
function SearchBox() {
const inputRef = useRef(null);
useEffect(() => {
inputRef.current.focus(); // directly call DOM method
}, []);
return <input ref={inputRef} placeholder="Auto-focused!" />;
}
// USE 2: Persist a timer ID without re-rendering
function Timer() {
const timerRef = useRef(null);
const start = () => {
timerRef.current = setInterval(() => console.log('tick'), 1000);
};
const stop = () => {
clearInterval(timerRef.current); // always have access to the ID
};
return <><button onClick={start}>Start</button><button onClick={stop}>Stop</button></>;
}
// DIFFERENCE: useState vs useRef for storing values
// useState → triggers re-render when updated
// useRef → NEVER triggers re-render, value just silently updates
Extra Info: Unlike useState, updating ref.current does not cause a re-render. This makes it ideal for storing mutable values like DOM nodes, timers, or previous state snapshots.
useMemo — Cache Expensive Computations
Every time a component re-renders, all code inside it runs again from scratch. If a computation is expensive (filtering thousands of items, complex math), useMemo caches the result and only recalculates when its dependencies change.
Heavy computation runs on every render, even when unrelated state changes. If a parent re-renders because of a different piece of state, the expensive filter runs again for no reason.
Wraps the computation. React caches the result. On re-render, React checks if the dependencies changed. If not, it returns the cached value and skips the computation entirely.
import { useMemo, useState } from 'react';
function ProductList({ products, searchTerm }) {
// ✗ Without useMemo — runs on EVERY render, even for unrelated state changes
const filteredBad = products.filter(p => p.title.includes(searchTerm));
// ✓ With useMemo — only recalculates when products or searchTerm changes
const filteredGood = useMemo(() => {
return products.filter(p => p.title.toLowerCase().includes(searchTerm));
}, [products, searchTerm]); // ← dependency array, same as useEffect
return filteredGood.map(p => <div key={p.id}>{p.title}</div>);
}
// useMemo returns the RESULT of a function (a value)
// Don't overuse it — for small arrays or cheap ops, the overhead isn't worth it
useCallback — Cache Function References
In JavaScript, every time a component re-renders, every function defined inside it is recreated as a new object. This means child components receiving that function as a prop will see it as a "new" prop and re-render unnecessarily. useCallback memoizes the function itself.
Parent re-renders → handleClick is recreated as a new function reference → Child sees a new prop → Child re-renders. This chain happens even when the function logic hasn't changed at all.
useCallback returns the same function reference across renders unless dependencies change. Combined with React.memo on the child, it prevents unnecessary re-renders.
import { useCallback, useMemo, memo } from 'react';
// useMemo → memoizes a VALUE (result of a computation)
const sortedList = useMemo(() => [...items].sort(), [items]);
// useCallback → memoizes a FUNCTION REFERENCE
const handleDelete = useCallback((id) => {
setItems(prev => prev.filter(i => i.id !== id));
}, []); // stable reference — setItems never changes
// React.memo — wraps a child component to skip re-render
// if ALL its props are the same reference (shallow equal)
const ItemRow = memo(({ item, onDelete }) => {
return <button onClick={() => onDelete(item.id)}>{item.name}</button>;
});
// Without useCallback on onDelete → ItemRow re-renders on every parent render
// With useCallback on onDelete → ItemRow only re-renders when items actually change
/*
SUMMARY:
useMemo(fn, deps) → returns fn() — cached VALUE
useCallback(fn, deps) → returns fn — cached FUNCTION
React.memo(Component) → skips render if props unchanged
*/
Custom Hooks
Custom hooks let you extract and reuse stateful logic across multiple components. Any function that starts with use and calls other hooks is a custom hook. It's not a React feature — it's a naming convention that enables the rules of hooks to work correctly.
When you find yourself copy-pasting the same useState + useEffect logic across multiple components — extract it. Common examples: useFetch, useLocalStorage, useWindowSize, useDebounce.
Only call hooks at the top level — never inside loops, conditions, or nested functions. Only call hooks from React components or other hooks — never from regular JS functions. These rules allow React to track state correctly.
// hooks/useFetch.js — reusable data fetching logic
import { useState, useEffect } from 'react';
export function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
fetch(url)
.then(r => {
if (!r.ok) throw new Error(r.status);
return r.json();
})
.then(d => setData(d))
.catch(e => setError(e.message))
.finally(() => setLoading(false));
}, [url]); // re-fetch if url changes
return { data, loading, error };
}
// Usage in ANY component — no copy-paste, just import the hook
function Users() {
const { data, loading, error } = useFetch('https://api.example.com/users');
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
return data.map(u => <div key={u.id}>{u.name}</div>);
}
function Products() {
// Same hook, different URL — logic is shared, not duplicated
const { data, loading } = useFetch('https://fakestoreapi.com/products');
}
Controlled vs Uncontrolled Components
This is one of the most common React interview topics. It describes who owns the truth of the form input value — React state or the DOM itself.
React controls the value. Input value is bound to useState, and every keystroke calls onChange to update state. React is always the single source of truth.
Pro: easy validation, instant derived state. Con: verbose for large forms.
The DOM controls the value. You access it via useRef when needed (e.g. on submit) rather than syncing on every keystroke. Less re-renders.
Pro: less code, fewer re-renders. Con: harder to do real-time validation.
Uses uncontrolled inputs under the hood (via refs) but gives you a controlled-like API. This is why it has much fewer re-renders than fully controlled forms — validation happens on submit/blur, not every keystroke.
// CONTROLLED — React owns the value via useState
function Controlled() {
const [val, setVal] = useState('');
return <input value={val} onChange={e => setVal(e.target.value)} />;
// Re-renders on EVERY keystroke — React state is always in sync
}
// UNCONTROLLED — DOM owns the value, React reads it via ref when needed
function Uncontrolled() {
const inputRef = useRef();
const handleSubmit = () => {
console.log(inputRef.current.value); // read value only when needed
};
return <input ref={inputRef} defaultValue="initial" />;
// NO re-renders on keystrokes — DOM handles it internally
}
Interview Questions
These are the most commonly asked conceptual questions in full stack interviews. Understanding the why behind each answer is what separates a candidate who has read docs from one who has built things.
JavaScript Concepts
var — function-scoped, hoisted to top of function, can be redeclared. let — block-scoped, not hoisted usably (temporal dead zone), can be reassigned. const — block-scoped, must be initialised, cannot be reassigned. Note: const objects/arrays are still mutable — only the binding is locked.
A function that remembers its outer scope even after the outer function has returned. The inner function has a reference to the variables in the enclosing scope. Used in: factory functions, memoization, module pattern, event handlers, custom hooks.
== (loose equality) — performs type coercion before comparing. 0 == "0" is true. === (strict equality) — compares value AND type, no coercion. 0 === "0" is false. Always use === in production code.
JavaScript moves declarations (not initialisations) to the top of their scope before execution. var declarations are hoisted and initialised as undefined. function declarations are fully hoisted. let/const are hoisted but not usable until declared (TDZ = Temporal Dead Zone).
undefined — a variable declared but not assigned a value. JavaScript sets this automatically. null — intentional absence of value. A developer explicitly sets this. typeof undefined is "undefined". typeof null is "object" — a famous JS bug from 1995 kept for backward compatibility.
When an event fires on a child, it bubbles up through parent elements by default. Capturing is the opposite — top-down. e.stopPropagation() stops bubbling. e.preventDefault() stops the browser's default action (e.g. form submit) but does NOT stop bubbling — they're different things.
// Classic closure — counter factory
function makeCounter() {
let count = 0; // this variable is "closed over"
return function() {
count++;
return count;
};
}
const counter1 = makeCounter();
const counter2 = makeCounter();
counter1(); // 1 — own private count
counter1(); // 2
counter2(); // 1 — separate closure, separate count
// The Temporal Dead Zone (TDZ) — let/const before declaration
console.log(x); // ReferenceError: Cannot access 'x' before initialization
let x = 5;
console.log(y); // undefined — var IS hoisted with a value
var y = 5;
React Concepts
A lightweight in-memory copy of the real DOM. When state changes, React builds a new Virtual DOM, diffs it against the previous one (reconciliation), and updates only the changed parts in the real DOM. Direct DOM manipulation is slow — React batches and optimises it.
When rendering lists, React uses key to identify which items changed, were added, or removed. Without a stable key, React re-renders the entire list. With a unique key (use id, not array index), React updates only the changed item. Never use array index as key — it breaks when items are reordered or deleted.
React uses shallow reference equality to detect state changes. If you mutate an object directly (state.name = "new"), the reference stays the same — React thinks nothing changed and skips the re-render. You must always return a new reference ({...state, name:"new"}) to trigger a re-render.
1. State changes via setState. 2. Props change (new reference passed). 3. Parent component re-renders. 4. Context value changes. Optimisations: React.memo skips render if props are shallow-equal. useMemo/useCallback stabilise values and functions.
The function returned from useEffect is the cleanup function. It runs before the component unmounts OR before the effect runs again (on next dependency change). Used to clear intervals, cancel fetch requests, remove event listeners — prevents memory leaks.
The algorithm React uses to diff two Virtual DOM trees and figure out the minimum set of DOM operations needed. React assumes: elements of different types produce different trees (replace entirely), and key prop identifies stable elements across renders. This is why keys on lists are critical for performance.
useEffect(() => {
// Setup: subscribe, start timer, add event listener
const id = setInterval(() => setCount(c => c + 1), 1000);
// Cleanup: runs when component UNMOUNTS or before effect runs again
return () => {
clearInterval(id); // prevent memory leak if component is removed
};
}, []); // runs once on mount, cleanup runs on unmount
// Without cleanup: the interval keeps running even after unmount!
// With cleanup: React calls clearInterval when component leaves the DOM
Backend / Node Concepts
REST: multiple endpoints, each returns a fixed shape. GET /users, GET /posts. Simple, cacheable, widely understood. GraphQL: single endpoint, client specifies exactly what data it needs. Solves over-fetching and under-fetching. More complex to set up but powerful for flexible clients.
Three Base64-encoded parts separated by dots: header.payload.signature. Header = algo type. Payload = data (userId, role, expiry) — NOT encrypted, just encoded. Signature = HMAC of header+payload using the secret. Anyone can read the payload, but cannot fake the signature without the secret.
XSS (Cross-Site Scripting) — attacker injects malicious JS into your page that runs in other users' browsers. Defense: httpOnly cookies (JS can't read them), sanitize input. CSRF (Cross-Site Request Forgery) — tricks the browser into making requests to your server using existing cookies. Defense: sameSite cookie flag, CSRF tokens.
Functions with signature (req, res, next) that run between request and response. Must call next() to pass control forward, or res.send() to terminate. Order matters — register error handlers with 4 parameters (err, req, res, next) last.
SQL (Relational): structured tables, predefined schema, joins, ACID transactions. Great for complex relationships. NoSQL (MongoDB): flexible schema, documents (JSON-like), horizontal scaling. Great for variable data shapes, rapid iteration. MongoDB stores data as BSON (Binary JSON).
Node.js is single-threaded but handles concurrency via the event loop. Async I/O (file reads, DB queries, HTTP requests) is handed to libuv (C++ thread pool). Node continues processing other requests. Callback fires when I/O completes. This is why Node handles 10,000 concurrent connections but would choke on heavy CPU computation.
// Q: What is the output?
console.log(typeof null); // "object" — historic JS bug
console.log(typeof undefined); // "undefined"
console.log(typeof []); // "object" — arrays are objects!
console.log(Array.isArray([])); // true — correct way to check
console.log(0 == false); // true — type coercion (both falsy)
console.log(0 === false); // false — different types
console.log("" == false); // true — coercion
console.log(null == undefined); // true — special case
console.log(null === undefined); // false — different types
console.log(NaN === NaN); // false — NaN !== NaN always!
console.log(Number.isNaN(NaN)); // true — correct way to check
// Q: What is the output? (closure in loop)
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// Output: 3 3 3 — var is function-scoped, all closures share same i
// Fix: use let (block-scoped) → each iteration gets its own i → 0 1 2
Authentication = proving who you are (verifyToken — validates the JWT). Authorization = proving what you can do (requireRole — checks req.user.role). Both are middleware, they chain together: verifyToken → requireRole('admin') → handler.
localStorage is readable by any JS on the page — an XSS attack can steal the token. httpOnly cookies are invisible to JS entirely. Only the browser and server see them. The browser also attaches cookies automatically, so no frontend code is needed to send the token.
Yes. The payload is readable (Base64, not encrypted) but unforgeable. Anyone can decode and see the userId, but changing even one character invalidates the signature. The secret key is what makes it tamper-proof. Never put passwords or sensitive data in the payload.