A running set of session notes — what React actually is, and the tooling that turns it into a production app. Basics kept brief, gotchas surfaced, interview Q&A in every section.
React comes as two separate packages because the core doesn't care where it draws — the same core powers the browser, mobile (React Native), and more. Each environment just swaps in its own "renderer."
Creates elements and components and works out what changed. Knows nothing about the browser or DOM.
Takes the core's output and puts it on the actual web page. The browser-specific half.
The most important mental model. createElement does not create an <h1> — it returns a plain JS object describing one. Log it and you'll see type and props, never markup.
// type, props (attributes), children const heading = React.createElement( "h1", { id: "title" }, // 2nd arg = props/attributes "Hello, world" // 3rd arg = children ); // => { type:"h1", props:{ id:"title", children:"Hello, world" } }
"h1", "div"… or a component.
An object. Anything passed lands in props.
String, one element, or an array. Also lives in props.children.
You give React one DOM node to own. It renders inside that node and nowhere else.
const root = ReactDOM.createRoot(document.getElementById("root")); root.render(heading);
#root is untouched — which is what lets React drop into one widget or card.Nest by passing a child element as arg 3. Siblings = pass an array.
React.createElement("div", { id: "parent" }, [ React.createElement("h1", {}, "Heading 1"), React.createElement("h2", {}, "Heading 2"), ]);
Two levels deep and it's already unreadable. That pain is the whole reason JSX exists — sugar that compiles straight back to these createElement calls. React never requires JSX.
Changing the real page (the DOM) is the slowest thing a browser does. So React keeps a copy of the UI as plain JS objects (the Virtual DOM), works out the smallest set of changes needed, and only then touches the real page.
Bare-bones. Drop into a small portion of an existing app — even alongside jQuery. You choose the tooling.
Opinionated and all-in. Generally expects you to build the whole app its way, batteries included.
Dependencies load before code that uses them — react & react-dom before your app, or you hit React is not defined.
Rendering a list of elements warns about a missing key. A key is a unique label that helps React tell the items apart between updates.
Once created it's a frozen snapshot. To change UI you create new elements and render again — never mutate in place.
React exposes an object literally named __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED. Real, intentionally unusable.
Library = a power tool you pick up and use where you want; framework = a factory assembly line you step into and follow. Or: with a library you call it; with a framework it calls you (inversion of control).
React.createElement return?react and react-dom?react is the brain — it creates elements and figures out what changed. react-dom is the hands for the browser — it puts that onto the actual page. They're split because React also runs elsewhere (e.g. React Native uses a different "hands" package).render() append or replace?create-react-app just hides all of this; here we wire it by hand.npm is a giant online store of reusable code packages, plus the tool that installs and version-manages them in your project. ("Package" and "dependency" mean the same thing — code your project relies on.) Fun aside: npm isn't officially short for "Node Package Manager," though that's exactly what it does.
Walks you through a few prompts and generates package.json. -y skips the questions.
npm i is shorthand for npm install. Pulls code from the registry into node_modules.
Your project's settings + the packages you want, with allowed version ranges. Commit it.
The exact version of every package actually installed (including their dependencies), with a hash to verify it. Commit it.
The real downloaded code of every package and its dependencies. Huge. Never commit — rebuild it with npm install.
Lists things git should skip: node_modules, /dist, .parcel-cache.
The symbol before a version in package.json controls auto-upgrades on the next install. Versions are MAJOR.MINOR.PATCH.
^2.8.3 → accepts 2.x.x, not 3.0.0. The safe default.
~2.8.3 → accepts 2.8.x only. More conservative.
2.8.3 → never auto-upgrades. Total control.
-DNeeded only while developing/building: bundlers, test runners. npm i -D parcel.
Needed at runtime / in production: React itself. npm i react react-dom (no flag).
Transitive dependencies: your dependency has its own dependencies, which have theirs — a whole tree. Every package carries its own package.json. That tree is why node_modules balloons.
Swapping the CDN <script> tags for installed packages, and pulling them in with import.
import React from "react"; import ReactDOM from "react-dom/client"; // note: /client for createRoot
node_modules), and version management lives in package.json instead of a hard-coded URL you'd hand-edit on every upgrade.from "react" means "the react package in node_modules" — the same role the CDN global used to play.npm installs packages; npx executes one. Parcel is a zero-config bundler.
# dev: build + serve at localhost:1234 with HMR npx parcel index.html # production: optimized build into /dist npx parcel build index.html
index.html is the source Parcel starts from.
Output written to /dist, served on a local port.
Edits hot-reload the page instantly.
Runs a local server and, when you save, updates the page instantly — it watches your files for changes (the watcher is written in C++ for speed).
Remembers previous build work, so each rebuild after the first drops to a few milliseconds.
Combines all your files into a handful (one HTML, one CSS, one JS) for production.
Removes whitespace, shortens variable names, and compresses output so it downloads faster.
If you import a library but use only part of it, the unused parts are dropped from the final bundle.
Optimizes images, splits big bundles into smaller chunks, shows friendly error messages, can serve over HTTPS.
In package.json you list which browsers your app must support. Parcel and Babel read this and add only the extra compatibility code those browsers need.
"browserslist": [ "last 2 chrome versions", "last 2 firefox versions" ]
Add type="module" to the <script> so the browser treats app.js as an ES module.
The auto-generated "main": "app.js" in package.json conflicts with Parcel's HTML entry — delete it.
Import createRoot from react-dom/client, not react-dom, or you get a warning.
Server runs its own npm install from the lock file — the integrity hash guarantees identical versions ("works on my machine" fix).
Dev build is fast + light on optimization; production build is slower but minified, tree-shaken, hashed.
It doesn't do everything itself — it orchestrates helper libs (Babel, etc.), the same way it has its own transitive deps.
package.json = the shopping list (what + acceptable versions). package-lock.json = the receipt (exact items + barcodes you actually bought). node_modules = the pantry full of the actual groceries — you never ship the pantry, you re-shop from the list with npm install.
package.json is your shopping list — it says which packages you want and roughly which versions (e.g. ^2.8.3). package-lock.json is the receipt — it records the exact version of every package that got installed, plus a hash to verify it. The lock file makes sure your machine and the server install the same thing.package.json + lock file), and npm install recreates node_modules anywhere. Simple rule: if you can regenerate it, don't put it in git — same for /dist and .parcel-cache.^ (caret) and ~ (tilde) mean?MAJOR.MINOR.PATCH. Caret ^2.8.3 = auto-update minor and patch (stays below 3.0.0). Tilde ~2.8.3 = auto-update patch only (stays below 2.9.0). Caret is the safe default: minor/patch are meant to be safe, while a major version can break things.npm i -D) are only needed while building (Parcel, test tools, linters) and aren't shipped to users. It's mainly about keeping "build-time" and "run-time" tools separate.npm installs a package; npx runs one. So npm i -D parcel downloads Parcel, and npx parcel index.html executes it.React.createElement calls. A component is just a function that returns JSX.Stop typing npx parcel by hand. Define scripts in package.json — the industry-standard way to start/build any project. Lost in a new codebase? Read its scripts to learn how to run it.
"scripts": { "start": "parcel index.html", // dev "build": "parcel build index.html" // prod }
npm run <name>. e.g. npm run start, npm run build.start is special. npm start works as a shortcut for npm run start; other scripts need the explicit run (npm build won't work).Creating elements with React.createElement is clumsy and unreadable when nested. JSX is the developer-friendly syntax Facebook built to fix that. Both produce the identical React element object — console-log them and they're the same.
// JSX const heading = <h1 id="title">Namaste React</h1>; // ...is exactly the same React element as: const heading = React.createElement("h1", { id: "title" }, "Namaste React");
createElement calls.Use className instead of class for CSS classes.
Attributes are camelCased: tabIndex, not tabindex.
Multi-line JSX must be wrapped in ( ) so Babel knows where it starts/ends.
JSX works because Babel (a JS compiler/transpiler that Parcel installs and orchestrates) converts it before it ever reaches the JS engine.
JSX
→ React.createElement
→ React element (object)
→ HTML on the DOM
babeljs.io → "Try it out."A functional component is just a JS function that returns JSX. That's the entire definition. Everything on a page — button, header, card, list — is a component.
const Title = () => ( <h1 className="head">Namaste React</h1> );
A function returning JSX. The current standard; ~99.99% of new code. Use this.
Uses JS classes. Old style, still found in legacy codebases & some interviews.
<Title />, not Title. An element renders as {element}; a component renders as a tag.<Title /> inside Heading). That's the fancy interview term for the obvious thing.<Title />, <Title></Title>, and {Title()} all work — the last because a component is, at the end of the day, just a function you can call.The superpower: inside JSX, { } runs any JavaScript expression — variables, math, function calls, even another React element or component.
const data = 10000; const Heading = () => ( <div> {data} // variable <h2>{100 + 200}</h2> // expression => 300 {title} // a React element {Title()} // or call the component </div> );
Data inside { } is escaped/sanitized before render, so malicious API data can't run as a cross-site scripting attack. Free protection.
const can't be used before initialization. Composing two components into each other = infinite loop that freezes the tab.
Arrow with ( ) implicitly returns JSX; an arrow/normal function with { } body needs an explicit return.
Arrow functions are the convention, but function Title(){ return ... } is equally valid.
Prettier auto-formats on save; Bracket Pair Colorizer, ESLint, Better Comments ease development.
Some put a placeholder in the root; if React fails to render you'll see it — a quick render-failure signal.
JSX is a translator's draft. You write in a human-friendly language (JSX); Babel is the translator that converts it to the "official" language the engine accepts (React.createElement). The engine never sees your draft — only the translation. And a component is a recipe (function); rendering <Title/> is cooking the dish (calling it).
React.createElement. JSX is simply a nicer, more readable way to write that same thing.React.createElement calls — which the browser does understand. So by the time the JS engine sees it, the JSX is already gone. Parcel runs Babel for you behind the scenes.createElement produces). A component is a function that returns elements. Element = the thing; component = the function that makes it. You use a component as <Comp /> and an element as {element}.<div>) means a normal HTML element; a Capitalized tag (<Header>) means "this is my component — call that function." Lowercase your component and React looks for an HTML tag by that name instead.{ } is treated as plain text, not code. So if bad data tries to sneak in a <script>, React shows it as harmless text instead of running it. You get this protection for free. (dangerouslySetInnerHTML turns it off — the scary name is the warning.).map() + a stable key, and let a config (data) drive what the UI shows.Senior move: design the UI before writing any JSX. Sketch a wireframe/mock, then derive the component tree from it. Good planning makes the code almost write itself.
Logo + Nav items (Home, Cart…)
Search + RestaurantContainer → RestaurantCard ×N
Copyright, links, contact
Each piece is a functional component; the app is just components nested inside components (composition).
const AppLayout = () => ( <div className="app"> <Header /> <Body /> <Footer /> </div> ); root.render(<AppLayout />);
A separate .css file + className. The default here.
style={{ }} — a JS object with camelCased properties. Not preferred.
Utility classes or component libraries (covered later).
// inline = JS object, hence the double braces <div style={{ backgroundColor: "#f0f0f0" }}> … </div>
{ } = "JS goes here", inner { } = the object literal. Properties are camelCase (backgroundColor, not background-color).Props (properties) are just the arguments you pass to a component function. React collects everything you pass into a single props object.
// pass props (like function arguments) <RestaurantCard resName="KFC" cuisine="Burgers" /> // receive them — two equivalent styles const RestaurantCard = (props) => ...props.resName... const RestaurantCard = ({ resName, cuisine }) => ... // destructure on the fly
{ resName }) is plain JavaScript object destructuring — not React magic.<Card resData={obj} /> keeps the call clean; read it as props.resData. Note the { } — you're passing a JS object, not a string.Big idea: you build the components once, and the data decides what shows up. Swiggy sends different offers and cards for Delhi vs. Mumbai — same app, different data, different page. If a section has no data, it simply doesn't appear. The data that controls this is the "config."
Config-driven UI = a stage that follows a script. The stage (your components) is built once; the script (JSON config) decides which actors appear, in what order, tonight in Delhi vs. Mumbai. Change the script, not the stage.
resData.data.name). Use optional chaining (?.) to read it safely, and watch for redundant/duplicated fields in real APIs.Don't hand-write N cards. Loop the data array with .map() inside JSX and return a component per item.
<div className="res-container"> {resList.map((restaurant) => ( <RestaurantCard key={restaurant.data.id} resData={restaurant} /> ))} </div>
for loops — functional style is the React norm and reads cleaner inside JSX.{cuisines.join(", ")} or {"₹" + costForTwo/100 + " for two"}.Every item you render in a .map() needs a unique key. It's not decoration — the key is how React identifies each item, so when the list changes it can update just the one that changed instead of redrawing all of them.
key={item.id}. React updates only what changed; inserting one item re-renders one card, not the whole list.
key={index}. Works, but if the list reorders/inserts, indexes shift and React mis-associates items — bugs + wasted renders.
Easy slip coming from HTML — React warns "Invalid DOM property `class`." Use className.
Reading a wrong path returns undefined → "Received NaN for children." Verify the data path (resData.data.x).
APIs send an image id; build the URL as {CDN_URL + imageId} (string concat inside { }).
resData?.data?.name avoids crashes when an intermediate value is missing.
resData={obj} passes the object; resData="obj" passes the literal string "obj".
Pull fields once at the top: const { name, cuisines } = resData.data; — beats repeating resData.data. everywhere.
props object. They're read-only: a component uses its props but never changes them. Data flows one way, parent → child.key when rendering lists?0 now points to a different item. React gets confused about which is which, leading to wrong updates. Only safe for a list that never changes order and has no id.className, inline styles using a JS object (style={{ }}), or a library like Tailwind or styled-components. CSS files are the usual choice; inline is fine for small one-offs.({ resName, cuisine }) => … instead of writing props.resName every time. It's just normal JavaScript — same result, cleaner to read.<Card resData={obj} />. It's cleaner than passing ten separate props. You then read it as props.resData. The curly braces matter: they pass the real object, whereas quotes would pass the text "obj".useState) can — because whenever state changes, React re-renders the component and syncs the UI to the data.Honest truth: anything React does, you could do with plain HTML/CSS/JS — React is JavaScript under the hood. The point of a library is developer experience: write less code, build more, and let React handle the hard part — keeping the screen in sync with your data, fast.
A 2,000-line App.js is unreadable. Split each component into its own file. The common convention: a src/ folder for source code, a components/ folder inside it, and a utils/ folder for shared things.
Convention, not a rule. Keeps the project root clean.
Header.js holds the Header component — file name matches the component, Capitalized.
Constants (constants.js), mock data, helpers — lowercase names, since they aren't components.
utils/ so they're reusable and the component stays clean..js vs .jsx doesn't matter. Either works; just be consistent. Don't overthink it.To use a component across files, you export it from its file, then import it where you need it (give before you take). Two flavors:
// DEFAULT — one per file export default Header; import Header from "./components/Header"; // NAMED — as many as you want, names must match export const CDN_URL = "..."; import { CDN_URL } from "../utils/constants"; // note the { }
{ }. A file can have one default but many named exports."./components/Header" resolves to the .js file automatically.Make the UI interactive by passing a function to events like onClick. The function runs when the event fires.
<button onClick={() => {
console.log("clicked");
}}>Top Rated</button>
onClick={handleClick} — React calls it on click. Every DOM event has a React equivalent (onClick, onMouseOver, onChange…), all camelCased.Say a button filters a list. You can write the filter in plain JS and the data changes — but the screen doesn't budge. A normal let variable has no way to tell React "I changed, redraw me." That gap is exactly what React's state solves.
A normal variable is a whiteboard in a locked room — you can change it, but no one outside sees. A state variable is a live scoreboard — change the number and the display updates automatically for everyone watching.
A hook is just a JavaScript function React gives you with a built-in superpower. useState creates a state variable: a value React watches, plus a setter function to change it.
import { useState } from "react"; // named import // [ current value, function to update it ] const [listOfRes, setListOfRes] = useState(resList); // arg = initial value
listOfRes is the current value; setListOfRes is the only correct way to change it. The argument to useState is the starting value.listOfRes = [...] does nothing useful — you must call setListOfRes(newValue), which is what tells React to re-render.This is the heart of React: whenever a state variable changes, React re-renders that component and updates the screen to match. Call the setter, and the UI follows automatically — you never touch the DOM yourself.
User clicks a button
You call the setter with new data
React re-runs the component
Screen matches the new state
When people say "React is fast," they mean it's efficient at updating the DOM — the slow part of any web page. Here's how.
A lightweight object representing the real DOM. (A React element is exactly this kind of object.)
On a state change React builds a new Virtual DOM and compares it to the old one — like a "git diff" between two objects.
React then changes only the real DOM nodes that actually differ, leaving the rest untouched.
It's a named import: import { useState } from "react" — braces required.
=Updating state with listOfRes = x won't re-render. Use setListOfRes(x).
[ ] is destructuringconst [a, setA] = useState() — useState returns an array; you're just array-destructuring it. Plain JS, no magic.
For listOfRes, call it setListOfRes. Not enforced, but everyone follows it.
Reused mock data can give two cards the same key — React warns. Give each a unique id.
The idea (an object representation of the DOM) existed before; React popularized it and built its diffing algorithm on top.
useState and useEffect. They're just normal JS functions with logic written inside React.useState do?[value, setValue]. The argument you pass is the starting value.useEffect runs your API call after render; you drop the data into state, and React re-renders with the real content.Two ways to structure a whole application's backend — useful context for where a React app fits.
One big project holds everything — UI, APIs, auth, DB, notifications. Change one button and you rebuild/redeploy the whole thing.
Separate small services, each with one job, its own tech stack, and its own deploy cycle. They talk to each other over the network.
:1234, backend mapped to /api. Mix stacks freely (React UI, Java backend, etc.).When should the API call happen relative to rendering? React favors the second option.
Wait for the data, then show the page. User stares at a blank/frozen screen until it arrives.
Show the page (skeleton) instantly, fetch in the background, then re-render with data. Better UX.
useEffect takes two arguments: a callback function and a dependency array. The callback runs after the component finishes rendering — the perfect place for an API call.
import { useEffect } from "react"; useEffect(() => { fetchData(); // runs AFTER render }, []); // dependency array
[] means "run once" — after the first render only. (More on dependencies in a later session.)Fetching in React is plain JavaScript — fetch() is a browser superpower, not a React one. Resolve its promise with async/await (cleaner than .then() chains).
const fetchData = async () => { const data = await fetch(SWIGGY_URL); const json = await data.json(); setListOfRes(json?.data?.cards…); // drop into state → re-render };
json.data.cards[2].data.data.cards. Use optional chaining (?.) to read it safely.Calling another site's API from localhost often fails with a CORS error. It's the browser blocking requests from one origin to a different origin — a security policy, not a React bug.
CORS is a bouncer at the API's door. The server decides which origins are on the guest list. Your localhost isn't on Swiggy's list, so the browser's bouncer turns the request away.
Toggles the check off in your browser. Fine for local learning — but your users won't have it, so it can't ship.
Route the call through a proxy (e.g. corsproxy.io) by prefixing your API URL. The proxy fetches from Swiggy server-side and returns it — no browser block.
While the API loads, don't show a blank page or a plain spinner — show a shimmer UI: fake skeleton cards that resemble the real layout. It's the modern standard (Swiggy, YouTube) and feels faster.
// conditional rendering with a ternary return listOfRes.length === 0 ? <Shimmer /> : <div className="res-container">…</div>;
if/else or a ternary (? :) is just JavaScript — the fancy interview name is the only new part.listOfRes.length === 0 is the signal to show the shimmer.A search box bound to state via value={searchText} is a controlled component — React owns its value. If you only set value and never update the state, typing appears frozen, because the box always shows the (empty) state.
<input
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
/>
onChange: on every keystroke, read e.target.value and update state. State → re-render → the box shows the new value. That two-way wiring is what makes a controlled input work.On search, filter the data and update state — React re-renders with the matches. But filtering and overwriting your one list destroys the full data, so the next search has nothing to search.
listOfRes (never mutated after fetch) and a filteredRes you render. Filter from the full list into the filtered one — so every search starts from all results..includes() (not ===) so "coffee" matches "Third Wave Coffee."Live APIs change over time. The durable skill isn't memorizing one data path — it's reading the API off the real site instead of guessing when it breaks or you don't know its URL.
Open DevTools → Network tab
Filter to API calls only
Find the call returning the data
Use it in your fetch
The response is one huge config-driven blob — offers, "what's on your mind," filters, and (somewhere) the restaurant list. Walk the tree to the part you actually need.
// the restaurants live deep inside; the exact path shifts over time const restaurants = json?.data?.cards[5]?.card?.card ?.gridElements?.infoWithStyle?.restaurants;
?.) so a missing intermediate doesn't crash the whole app.When the shape changes, the fix is small — update the keys your card reads. In this change, the per-restaurant wrapper key data became info, and a couple of fields moved/changed type.
restaurant.data.id → restaurant.info.id (same for name, image, rating…).
It's now a ready string ("₹350 for two") — stop dividing by 100, just print it.
Now info.sla.slaString (e.g. "22 mins") instead of a top-level deliveryTime.
The first call returns only ~9 restaurants. Scrolling triggers a separate POST API (passing lat/long) to load more — this is lazy loading / pagination.
fetch(url, { method: "POST", body … }) for the "load more" call.The browser blocks localhost → other-site API. A dev extension unblocks it locally; for everyone, route through a CORS proxy (or fix it server-side).
Setting value without onChange makes typing do nothing — it's a controlled input with no way to update.
Use .includes() for partial matches and lowercase both sides for case-insensitivity.
Filtering into the same list loses your data. Keep a separate filtered list.
"Cannot read properties of undefined" usually means a field moved (e.g. data → info). Log the object and re-map the path.
Skeleton cards set expectations and feel faster than a lone spinner.
The page renders once empty, then again after the fetch updates state — expected, not a bug.
Backend teams reshape responses; frontend adapts. Find the call in DevTools and trace the new JSON.
useEffect and when does it run?[], the callback runs once, right after the first render.fetch() (a browser feature) with async/await, inside a useEffect so it runs after render. Put the result into state with the setter — that triggers a re-render showing the live data.corsproxy.io): prefix your API URL with the proxy's. The browser only blocks browser cross-origin calls, so the proxy — a server — fetches the API and returns the data. Unlike an extension, this works for all your users. Public proxies are rate-limited, so production should use the API's own CORS config or your own backend proxy.value={state}) and updated via onChange. React is the single source of truth for what's in the box — you read and set it through state.if, a ternary (? :), or &&. It's just JavaScript deciding what JSX to return.a?.b?.c returns undefined instead of throwing if b is missing — so one absent field doesn't break the whole render.fetch, POST needs an options object with method and body.