The notes continue —from a single page into a real multi-page app: routing, dynamic URLs, and the hook rules that keep state predictable. Basics kept brief, gotchas surfaced, interview Q &A in every section.
The same hook from before, but the second argument (the dependency array) is what decides when the effect runs. There are exactly three cases, and interviewers love this one.
Leave the array out entirely and the effect runs after each render —usually not what you want.
Runs a single time after the initial render. This is where data fetching belongs.
Runs on first render, then again only when a listed value changes (e.g. search text).
// 1 - runs after every render
useEffect(() => { ... });
// 2 - runs once, after the first render
useEffect(() => { ... }, []);
// 3 - runs whenever btnName changes
useEffect(() => { ... }, [btnName]);
[]
:
you want the API call once, not on every keystroke or click. An empty array says "set this up after the component first appears, then leave it alone."
State variables belong inside
a component function, declared at the very top —before any logic. Putting a hook inside an if
, a loop, or a nested function breaks React.
useState
/ useEffect
calls first, before branching or returning. This keeps their order identical on every render.
if
/for
/nested functions:
a hook inside a condition might run on some renders and skip on others. React tracks hooks by call order
, so a changing order corrupts which state belongs to which variable.
Think of hooks like numbered seats on every render
. React hands out state by seat order: seat 1, seat 2, seat 3. If a hook hides behind an if
, the seats shift on the next render —and React hands the wrong state to the wrong variable. Keep them at the top, always in the same order, and the seating never changes.
React itself doesn't do URLs —it renders components. To map URLs to components you add a routing library; the standard choice is React Router DOM, installed like any package.
// install it like any npm package
npm install react-router-dom
package.json
and package-lock.json
, same as anything else.
You describe your routes as a plain list of objects —each says "for this path, render this element." Then you hand that config to a provider instead of rendering a single component directly.
import
{ createBrowserRouter, RouterProvider }
from
"react-router-dom"
;
const
appRouter = createBrowserRouter
([
{ path: "/"
, element: <AppLayout />},
{ path: "/about"
, element: <About />},
{ path: "/contact"
, element: <Contact />},
]);
// instead of rendering <AppLayout/>directly:
root.render
(<RouterProvider router={appRouter} />);
Takes an array of route objects (each a path
+ element
) and returns a router.
A component that takes your router via the router
prop and actually renders the matched route.
{ path, element }
objects —React Router reads it and decides what to show for the current URL.
createHashRouter
, memory, static) for special cases, but createBrowserRouter
is the recommended default for web apps.
Hit an unknown URL and React Router shows its own error screen. You can replace it with your own by adding an errorElement
, and read what went wrong with the useRouteError
hook.
{ path: "/"
, element: <AppLayout />,
errorElement: <Error />}
// inside the Error component:
const
err = useRouteError
();
return
<h3 >{err.status}: {err.statusText}</h3 >;
errorElement
renders whenever a route throws or the URL doesn't match —so users see a friendly page, not React's raw red crash screen.
useRouteError()
hands you an error object (status
, statusText
, etc.) so you can show "404 —Not Found" instead of a generic message.
use
is a hook —a function with a special purpose, by convention.
You usually want the header and footer to stay put while only the middle swaps per page. You do that by nesting routes as children
of a layout, and marking where the child should appear with <Outlet />
.
const
appRouter = createBrowserRouter
([
{ path: "/"
, element: <AppLayout />,
children: [
{ path: "/"
, element: <Body />},
{ path: "/about"
, element: <About />},
{ path: "/contact"
, element: <Contact />},
],
},
]);
// AppLayout renders the shell + an outlet
const
AppLayout = () => (
<div ><Header /><Outlet />{/* child route renders here */}
</div >);
<Outlet />
is a picture frame
built into your layout. The frame stays on the wall (header above, footer below); React Router just slides a different photo into it depending on the URL. The frame itself never shows up in the final HTML —it's replaced by whichever child matched.
<Outlet />
contents change per route. That's how the header survives navigation.
<Outlet >
in the DOM:
it's a placeholder —the matched child component takes its place in the rendered HTML.
An anchor tag works, but it reloads the entire app on every click —slow, and it throws away your loaded state. Inside React, navigate with <Link >
instead.
From React Router DOM. Swaps components client-side —no full reload , header stays, navigation is instant. Use this for in-app navigation.
The browser's anchor. Triggers a full page reload —refetches everything, logo flickers, state resets. Fine for external links, wrong for in-app routes.
to
instead of href
:
the only syntax difference. <Link to="/about">About </Link >
.
<Link >
still renders an <a >
—the browser only understands anchors. React Router wraps it, intercepts the click, and updates the view without a reload.
Because <Link >
only swaps components, the app is a single page application
—loaded once, then components interchange. That's client-side routing, and it's different from the old way.
All page components ship on first load. Navigating just renders a different component —no network call for the page . Calls happen only when you fetch data.
The old model: about.html
, contact.html
. Each click fetches a fresh HTML file from the server and reloads the whole page.
You don't write a route per restaurant. You write one route with a variable segment —marked with a colon —and one component that loads whatever id is in the URL.
{ path: "/restaurants/:resId"
,
element: <RestaurantMenu />}
// /restaurants/425 -> resId = "425"
// /restaurants/229 -> resId = "229"
:resId
is a placeholder:
the colon tells React Router this part of the path is dynamic. Any value slots in and the same <RestaurantMenu />
renders.
To fetch the right data, the component needs the id from the URL. The useParams
hook gives you an object of the dynamic segments; destructure the one you named.
import
{ useParams } from
"react-router-dom"
;
const
{ resId } = useParams
();
useEffect(() => { fetchMenu
(); }, []);
const
fetchMenu = async
() => {
const
data = await
fetch
(MENU_API + resId);
const
json = await
data.json
();
setResInfo
(json.data);
};
:resId
in the path, so useParams()
returns { resId }
. Destructure it on the fly.
MENU_API + resId
—same component, different restaurant, just by changing the id in the URL.
Fetch into state, show a shimmer until it arrives, then render the list. Two traps appear here —reading into null
, and rendering a list without keys.
const
[resInfo, setResInfo] = useState
(null
);
// early return - guards against null before render
if
(resInfo === null
) return
<Shimmer />;
const
{ name, cuisines } = resInfo?.cards[0]?.card?.card?.info;
const
itemCards =
resInfo?.cards[2]?.groupedCard?.cardGroupMap
?.REGULAR?.cards[1]?.card?.card?.itemCards;
return
(
<ul >{itemCards.map
((item) =><li key={item.card.info.id}>{item.card.info.name}
</li >)}
</ul >);
resInfo
is null
, return the shimmer before
any code tries to read .name
off null. A ternary in the JSX still runs the destructuring above it —and crashes.
key
in a .map()
:
without it React warns "each child in a list should have a unique key."
Use a stable id, never the array index.
?.
stops one missing field from crashing the whole render.
Calling useState
/useEffect
inside an if
, loop, or nested function corrupts hook order. Always call them at the top level.
Using <a >
for in-app links does a full page reload and resets state. Use <Link >
instead.
Destructuring before the fetch returns crashes. Guard with an early return (if (x === null) return <Shimmer/>
).
Lists without a unique key
trigger a React warning and can cause wrong re-renders. The key goes on the outermost mapped element.
In the DOM, <Link >
renders as <a >
—React Router just intercepts the click to avoid a reload.
This menu API returns far more than the UI uses. GraphQL lets the client ask for exactly the fields it needs —less over/under-fetching.
[]
it runs once, after the first render. With values inside [a, b]
it runs on the first render and again whenever one of those values changes. The array is a filter on when
to re-run.
if
or loop, that order can change between renders, and React hands the wrong state to the wrong variable. Calling them all at the top keeps the order stable.
<Link >
navigates client-side —it swaps components without a reload, so the header stays and it's instant. It renders as an <a >
underneath, but React Router intercepts the click.
/restaurants/:resId
, and point it at one component. Inside that component, call useParams()
—it returns an object with the named params —and destructure resId
to fetch the right data.
<Outlet />
contents change as the URL changes. It doesn't appear in the final DOM —the child replaces it.
errorElement
to the route —it renders whenever the route throws or the URL doesn't match. Inside that component, call useRouteError()
to read the error's status
and statusText
so you can show a real message like "404 —Not Found."
You won't reach for them in new code —functional components with hooks are the modern standard. Three reasons to know them anyway.
Many companies maintain older codebases built on class components, so interviewers test them.
You'll meet class components in existing projects and need to read and edit them.
They make React's mount/update/unmount phases explicit —which sharpens how you understand hooks.
A functional component is a function that returns JSX. A class component is a class that extends React.Component
and has a render()
method returning JSX. That's the core difference.
import
React from
"react"
;
// functional: a function returning JSX
const
User = () => return
<div >...</div >;
// class: extends React.Component, render() returns JSX
class
UserClass extends
React.Component {
render
() {
return
<div >...</div >;
}
}
extends React.Component
is what tells React "this is a class component" so it tracks and manages it. React.Component
is a base class you inherit from, imported from react
.
extends React.Component
or, with a named import, extends Component
. Same thing —don't let the two forms confuse you.
Passing props is the same. Receiving them differs: a class takes them in its constructor
, calls super(props)
, and then reads them anywhere via this.props
.
// passing - same as functional
<UserClass name="Akshay"
location="Dehradun"
/>class
UserClass extends
React.Component {
constructor
(props) {
super
(props);
}
render
() {
const
{ name, location } = this
.props;
return
<h2 >{name} —{location}</h2 >;
}
}
props
object handed to the constructor.
this.props
:
use this.props.name
anywhere in the class, or destructure for cleaner code —the same on-the-fly destructuring you use in functional components.
super(props)
:
you're extending a base class, so you must call its constructor first —and passing props
up is what makes this.props
available inside the constructor. (Classic interview "why" question.)
Where functional components use useState
, a class keeps all
its state in one object, this.state
, set up in the constructor. You update it only through this.setState
—never by assignment.
constructor
(props) {
super
(props);
this
.state = {
count: 0
,
count2: 2
, // one object holds every state variable
};
}
// update - NEVER this.state.count = ... directly
this
.setState
({ count: this
.state.count + 1
});
useState
calls, a class groups all state into the single this.state
object. (Behind the scenes, React keeps functional state as one object too.)
this.state.count = 5
won't re-render and creates inconsistencies —the class equivalent of the "don't reassign state" rule. Always go through setState
.
setState
merges, not replaces:
pass only the keys you're changing and React leaves the rest untouched. It diffs, updates those variables, and re-renders. You can batch several keys in one call.
When a class component loads (we say it mounts —an instance of the class is created), three methods run in a fixed order.
Set up state &props
Return JSX
React paints to the page
Runs after it's on screen
constructor
runs first (constructors run on instantiation, not invocation).
componentDidMount
runs last,
only after React has actually put the component on the DOM —hence the name. That timing is the whole point of the next section.
React splits mounting into two phases. This is the key to the trickiest interview question: the order of lifecycle calls when a parent has multiple children.
Runs constructor
+ render
. Pure JavaScript on the virtual DOM —computes what changed (the diff). Fast, and React batches
this across siblings.
React actually updates the real DOM, then fires componentDidMount
. DOM manipulation is expensive, so React batches all commits together.
// parent with two class children, the real order:
parent constructor
parent render
first child constructor
first child render // render phase batched
second child constructor
second child render
first child componentDidMount // commit phase batched
second child componentDidMount
parent componentDidMount
componentDidMount
does not
fire right after its render. React runs the render phase for all
children first, then the commit phase for all of them.
Think of a print shop . The render phase is laying out every page on screen —quick, all done together. The commit phase is the actual printing —slow and expensive, so you batch every page into one print run instead of walking to the printer after each page. React lays out all children first (render), then prints them all at once (commit).
In a class, you fetch data in componentDidMount
—the same role useEffect(fn, [])
plays in functional components. The reason is the two-phase timing.
async
componentDidMount
() {
const
data = await
fetch
(GITHUB_API);
const
json = await
data.json
();
this
.setState
({ userInfo: json });
}
setState
re-renders with real data. React doesn't wait on the network to paint.
componentDidMount
fires once the DOM is already on screen —the ideal moment to kick off a fetch without blocking the first paint.
After mounting, any setState
starts an update
cycle: React re-runs render
(not the constructor —that runs once), updates the DOM, then calls componentDidUpdate
.
componentDidUpdate
(prevProps, prevState) {
// the old pain: manual guards to avoid infinite loops
if
(this
.state.count !== prevState.count) {
// run effect for count change
}
}
setState
re-renders with the new values and then fires componentDidUpdate
.
prevProps
/prevState
comparisons (one per variable, easy to get wrong) collapse into one dependency array —useEffect(fn, [count])
. That's the pain the array was designed to remove.
Just before a component leaves the screen, componentWillUnmount
runs. In a single-page app the page never reloads, so anything you started —timers, subscriptions —keeps running unless you stop it here.
componentDidMount
() {
this
.timer = setInterval
(() => {
console.log
("still running…"
);
}, 1000
);
}
componentWillUnmount
() {
clearInterval
(this
.timer); // stop the leak
}
setInterval
started in componentDidMount
keeps firing after you navigate away —and starts a new
one each time you return. Several stale timers pile up invisibly and degrade performance.
this
(shared across all the class's methods) in componentDidMount
, and clearInterval
it in componentWillUnmount
. Create a mess, clean the mess.
useEffect
—it runs on unmount, doing the same job as componentWillUnmount
.
// functional cleanup - the modern equivalent
useEffect(() => {
const
timer = setInterval
(..., 1000
);
return
() => clearInterval
(timer); // runs on unmount
}, []);
this.state.count = 5
won't re-render. Always use this.setState({...})
.
Skip it and this.props
is undefined in the constructor. Always call super(props)
first.
Intervals/subscriptions outlive the component and stack up. Clear them in componentWillUnmount
(or a useEffect
cleanup).
With multiple children, all renders fire before any componentDidMount
—render phase is batched.
They overlap in use, but useEffect
is not "componentDidMount under the hood" —it's a different mechanism. Don't memorize them as equal.
Anything on this
(like this.timer
) is reachable from every method of the class.
React.Component
and returns its JSX from a render()
method. Functional components with hooks are the modern standard; class components are the older style still found in legacy code.
super(props)
and then read them anywhere via this.props
. State lives in one object, this.state
, set in the constructor, and you read it via this.state.x
. You can destructure both for cleaner code.
this.state.count = 5
) doesn't tell React anything, so nothing re-renders and you get inconsistent state. setState
updates the state, diffs it, and triggers a re-render. It also merges —it only changes the keys you pass.
componentDidMount
. Constructor and render are the render phase; the DOM update and componentDidMount
are the commit phase.
componentDidMount
in the commit phase. It does this because updating the real DOM is expensive, so it computes all changes first and commits them together.
setState
to fill in the data. Since componentDidMount
runs after the DOM is painted, it's the right moment to start a fetch without blocking the first render.
clearInterval(this.timer)
. In functional components, the cleanup function returned from useEffect
does the same job.
[]
covers the "run once after mount" role, a populated dependency array covers "run on change," and a returned cleanup function covers unmount. But don't claim they're identical —useEffect
is its own mechanism, not class lifecycle methods in disguise.
Each component (it's just a function) should do one thing well. If a component is doing several jobs, break it into smaller pieces —that's modularity.
RestaurantCard
takes props and renders a card —nothing more. A reader should grasp it at a glance.
A hook is a normal JavaScript helper function with React powers (it can hold state, run effects). You've used library hooks (useState
, useEffect
, useParams
) —you can write your own the same way.
useParams
—someone just wrote logic inside it and returned a value.
The menu component had two jobs: fetch the data and display it. Pull the fetching into a hook so the component is left with one job —displaying.
// utils/useRestaurantMenu.js
import
{ useState, useEffect } from
"react"
;
import
{ MENU_API } from
"../utils/constants"
;
const
useRestaurantMenu = (resId) => {
const
[resInfo, setResInfo] = useState
(null
);
useEffect
(() => { fetchData
(); }, []);
const
fetchData = async
() => {
const
data = await
fetch
(MENU_API + resId);
const
json = await
data.json
();
setResInfo
(json.data);
};
return
resInfo; // the contract: in resId, out resInfo
};
export default
useRestaurantMenu;
// the component is now clean - one job: display
const
resInfo = useRestaurantMenu
(resId);
useState
/useEffect
just like a component —React tracks it the same way.
utils
folder, named after the hook.
A custom hook is like a vending machine
. You put in a coin (the resId
) and a snack comes out (the resInfo
). You don't care about the motors and sensors inside —just the slot in and the tray out. The component only knows the contract, not the machinery.
A second hook to cement the pattern: report whether the user is online. Its contract takes no input and returns a boolean —useful for online/offline dots, "you're offline" screens, or an offline mini-game.
const
useOnlineStatus = () => {
const
[online, setOnline] = useState
(true
);
useEffect
(() => {
window.addEventListener
("offline"
, () => setOnline
(false
));
window.addEventListener
("online"
, () => setOnline
(true
));
}, []);
return
online;
};
// use it anywhere, in one line
const
online = useOnlineStatus
();
return
online ? <Cards />: <h1 >You're offline </h1 >;
window
fires online
and offline
events. Register them once inside a useEffect
with []
, flip the state, return it.
Start every hook's name with use
(lowercase). It isn't strictly required, but it's strongly recommended —and most projects' linters enforce it.
use
prefix tells React (and your teammates) the function follows hook rules and may hold state/effects —not a plain helper like getOnlineStatus
.
use
. Follow the convention the library recommends rather than fighting it.
A bundler (Parcel here) takes all your files and bundles them into one JavaScript file the browser loads. Fine for a small app —but as components grow into the thousands, that single file balloons and slows the first load.
Break the app into smaller bundles loaded on demand. You'll hear this called many things —they all mean the same thing.
Replace a normal import with React's lazy
, which takes a function that dynamically import
s the component. React then loads that code only when the route is visited —and you wrap it in Suspense
to show a fallback while it loads.
import
React, { lazy, Suspense } from
"react"
;
// not a normal import - loaded on demand
const
Grocery = lazy
(() => import
("./components/Grocery"
));
// wrap it so React has something to show while it loads
{ path: "/grocery"
,
element: (
<Suspense fallback={<h1 >Loading…</h1 >}><Grocery /></Suspense >) }
/grocery
triggers a separate grocery.js
request —the code arrives only on demand.
Suspense
boundary with a fallback
to show meanwhile (a "Loading…" message or a shimmer).
A lazy component with no Suspense
wrapper throws "a component suspended while responding to synchronous input." Always wrap it.
Moving fetch logic into a hook still needs the right shape —e.g. return json.data
, not json
, or you get undefined.
Skipping use
works at runtime but trips React-configured linters and hides that it's a hook.
Inside lazy
, import("./X")
is the dynamic-import function
(returns a promise) —not the static import
statement.
The offline game is just online-status detection plus a fallback UI —the exact pattern useOnlineStatus
enables.
Good chunks map to features (flights, hotels, grocery), each holding enough code to stand alone.
use
prefix, that can hold state and effects. You write one to pull logic (like data fetching) out of a component —so the component stays focused on display and the logic becomes reusable and testable.
useRestaurantMenu
the input is a restaurant id and the output is the restaurant info; for useOnlineStatus
there's no input and the output is a boolean. Once the contract is clear, the implementation follows.
lazy(() => import("./Component"))
instead of a normal import. React then loads that code only when the component is needed, producing a separate bundle.
Suspense
with a fallback
gives React something to show meanwhile, like a loading message or a shimmer.
Before picking one, know the landscape. Each approach styles the same UI —the differences are in ergonomics, scale, and team preference.
Write a class in JSX, define it in a .css
file. Simple, but doesn't scale cleanly on big apps.
Adds variables, nesting, and more to CSS. Handy, but not the common choice in modern production React.
Write styles inside JS, scoped to a component. Popular at large companies (e.g. used at Uber).
Material UI, Bootstrap, Chakra UI, Ant Design —import a ready-made <Button >
that's already beautiful, no CSS needed.
Compose tiny single-purpose classes in your markup. Trending, and the pick here.
All do the same job. Try a few and form your own opinion —there's no universally "correct" answer.
Tailwind's pitch: style your components without leaving your JSX. Rather than naming a class and writing its CSS elsewhere, you apply many small classes —each does one CSS thing —directly in the markup.
Add class="logo"
in JSX, switch to index.css
, write .logo { width: 14rem; }
. Two files, back and forth.
Write className="w-56"
right where you build the element. The styling lives with the markup —no file switching.
flex
→display:flex
, w-56
→width:14rem
, p-4
→padding:1rem
. You compose the look from small pieces.
Tailwind isn't React-specific —it works with any framework or plain HTML. Setup with Parcel is a few steps; follow the framework guide that matches your bundler.
npm i tailwindcss postcss
npx tailwindcss init
.postcssrc + content paths
@tailwind in index.css
/* index.css - the only CSS you write */
@tailwind base;
@tailwind components;
@tailwind utilities;
// tailwind.config.js - where Tailwind looks for classes
content: ["./src/**/*.{html,js,jsx,ts,tsx}"
],
.postcssrc
file tells your bundler to read Tailwind —you don't have to learn PostCSS separately.
content
array
lists where your classes live so Tailwind knows which files to scan. List only the file types you actually use.
@tailwind
lines
are the entire stylesheet —after adding them, you may never touch the CSS file again.
Tailwind's class names follow consistent shorthand. Learn the pattern once and most classes become predictable.
m-4
margin, p-4
padding, px-4
left+right, py-2
top+bottom, mb-2
margin-bottom. Numbers map to rem (4 = 1rem).
flex
, flex-wrap
, justify-between
, items-center
—flexbox without the verbosity.
w-56
sets width (14rem). Sizes come from a scale.
bg-pink-100
, bg-gray-400
—shades run 50 →950; higher = bolder/darker.
font-bold
, text-lg
, shadow-lg
, rounded-lg
. Sizes use sm/md/lg/xl/2xl.
hover:bg-gray-400
applies on hover. Same idea for focus, responsive breakpoints, etc.
You build a layout by stacking utilities. And when the scale doesn't have the exact value you need, escape to a custom one with square brackets.
// a header laid out with utilities
<div className="flex justify-between items-center shadow-lg"
><img className="w-56"
/><ul className="flex"
><li className="px-4"
>Home </li ></ul ></div >// arbitrary value when no class fits exactly
<div className="w-[200px]"
>...</div >
m-4 p-4 w-[250px] rounded-lg shadow-lg
—assembled from utilities, no separate CSS rule.
w-[200px]
sets an exact size the scale doesn't offer. Use the built-in scale by default; reach for brackets only when you truly need a specific value.
The Tailwind CSS IntelliSense VS Code extension is what makes utility-first practical —you don't memorize classes, you get suggestions as you type.
pink
or shadow
and it lists matching classes —no trips to the Tailwind website.
w-56
→width: 14rem
).
Tailwind is a tradeoff. The wins are speed and a tiny CSS bundle; the costs are a learning curve and busier markup.
Style right in the JSX —faster iteration, no jumping between markup and stylesheets.
The build includes only
the classes you actually used —reuse m-4
100 times and it ships once. Unused classes (e.g. shadow) never reach the browser.
Remembering the class names takes time up front; it gets fast once the patterns stick.
Heavily-styled elements grow long className
strings, which can make JSX harder to read.
Utility classes are like LEGO bricks . Plain CSS is sculpting a custom piece in another room (the stylesheet); Tailwind hands you a bin of standard bricks you snap together right where you're building. Faster to assemble —but pour out enough bricks and the table (your markup) gets cluttered.
If a file isn't in the content
array, its Tailwind classes are purged out and won't apply. Keep the globs accurate.
p-4
pads every side. For a button you usually want px-4 py-2
—mixing axes, not all-around.
A block element stretches to its parent. Wrap it in a div
(or set width) to size a button naturally.
The spacing scale is rem-based: 4
= 1rem, 2
= 0.5rem (8px). Consistent across m/p/w/h.
Tailwind works with Angular, plain HTML, anything —it's a generic CSS framework.
Tailwind UI offers ready-made component markup to copy —like a component library, but as utility classes.
.postcssrc
so your bundler understands Tailwind —but you don't need to learn PostCSS itself to use Tailwind.
shadow
, that CSS never ships; if you use m-4
a hundred times, it's included once. That keeps the CSS bundle small.
w-[200px]
. Prefer the built-in scale for consistency, and reach for brackets only when you genuinely need an exact, off-scale value.
hover:bg-gray-400
applies on hover, and breakpoint prefixes apply at certain widths. The same utility just runs conditionally.
A useful mental model: the UI layer is the static JSX you render, and the data layer (state, props, local variables) is what drives it. The UI is powered by the data —get the data layer right and the UI follows.
A higher order component is a function that takes a component and returns a new, enhanced component . It doesn't change the original —it wraps it to add features (a label, extra props, behavior).
// contract: in a component, out an enhanced component
const
withPromotedLabel = (RestaurantCard) => {
return
(props) => (
<div ><label className="absolute"
>Promoted </label ><RestaurantCard {...props} />// pass props through
</div >);
};
// build the enhanced version, use it conditionally
const
RestaurantCardPromoted = withPromotedLabel
(RestaurantCard);
{resData.info.promoted
? <RestaurantCardPromoted resData={resData} />: <RestaurantCard resData={resData} />}
{...props}
) down so the wrapped component still receives everything it needs.
A component is uncontrolled when it manages its own state, and controlled when its parent drives it through props. There's no strict definition —it's about where the source of truth lives.
The component holds its own state (e.g. its own showItems
) and decides for itself. The parent can't coordinate it.
The parent passes the value in as a prop (e.g. showItems={...}
) and the component just follows. The parent is the source of truth.
To let a parent coordinate its children, move the state out of the children and up into the parent. The parent holds one piece of state and passes both the value and a setter down.
// parent (RestaurantMenu) owns which one is open
const
[showIndex, setShowIndex] = useState
(null
);
{categories.map
((cat, index) => (
<RestaurantCategory
data={cat}
showItems={index === showIndex} // controlled
setShowIndex={() => setShowIndex
(index)} // callback down
/>))}
showItems
from props and calls the setShowIndex
callback on click —so the parent's single showIndex
decides which accordion is open.
Think of a row of light switches wired to one central dimmer . If each switch controlled only its own bulb (state in the child), no switch could turn the others off. Lifting state up moves the wiring to a single panel (the parent): press any switch and it tells the panel, which lights that bulb and dims the rest.
The React DevTools browser extension adds two tabs to your console —Components and Profiler —that make the data layer visible.
showItems
flip live.
RouterProvider
in the tree come from libraries (React Router), not your code —don't panic when you see them.
React data flows one way: parent →child →grandchild. To get data to a deeply nested component, you pass it through every component in between —even ones that don't use it. That's prop drilling .
has the data
doesn't need it —just passes it
actually wants it
Context is React's built-in fix for prop drilling: a shared space any component can read from, no matter how deeply nested —ideal for data needed in many places, like the logged-in user or the theme.
// utils/UserContext.js
import
{ createContext } from
"react"
;
const
UserContext = createContext
({ loggedInUser: "Default User"
});
export default
UserContext;
// read it in a functional component with a hook
const
{ loggedInUser } = useContext
(UserContext);
// read it in a class component with Consumer
<UserContext.Consumer >{(data) => <h1 >{data.loggedInUser}</h1 >}
</UserContext.Consumer >
createContext
makes the context with a default value. Two ways to consume it: the useContext
hook (functional components) or Context.Consumer
with a callback (class components, which can't use hooks).
To replace the default value, wrap part of your tree in Context.Provider
and pass a value
. Bind that value to state, and the whole subtree updates live when the state changes.
const
[userName, setUserName] = useState
();
// tie context to state - everything inside re-reads on change
<UserContext.Provider value={{ loggedInUser: userName, setUserName }}><AppLayout /></UserContext.Provider >
value
. Now any component can read the user and call setUserName
—type in an input anywhere and every consumer updates instantly.
Both give you a central store readable from anywhere. The difference is what they are and when you reach for them.
No install. Great for small and mid-size apps —and can scale to large ones with multiple contexts (user, theme, cart…).
Installed separately. Adds structure for very large apps —middleware, thunks, devtools —at the cost of more setup.
An HOC should wrap, not modify, the component passed in. Keep it pure and spread props through.
Pass a setter function down as a prop; the child calls it. Data down, events up.
Threading the same prop through 5 –10 layers is a smell —use context for widely-shared data.
Context isn't a dumping ground. Local parent-to-child data should stay as props.
Wrap the whole app or just a branch; nested providers override outer values for their subtree.
JSON keys like @type
aren't valid identifiers —read them with bracket notation: card["@type"]
.
createContext
and read it with useContext
(or Context.Consumer
in class components).
Context.Provider
and pass a value
; everything inside reads that instead of the default. Bind the value to state and include the state setter in it —then any consumer can update the context, and the whole subtree re-renders live.
Redux is a standalone state-management library, popularly paired with React but not built into it —and not required. You built this whole app for 11 sessions without it.
Old "vanilla" Redux was complex, needed many extra packages, and demanded lots of boilerplate. Redux Toolkit (RTK) is the standard way to write Redux today —less code, less setup.
Builds the store, slices, reducers. The odd @
in the name is just branding —it's a normal package.
Connects Redux to React —gives you Provider
, useSelector
, useDispatch
.
@reduxjs/toolkit
for Redux logic and react-redux
to wire it into React.
The store is one big object kept in a central place that any component can read or write. To stay organized, you split it into slices (logical partitions: a cart slice, a user slice, a theme slice…).
click fires an action
action calls a reducer fn
reducer updates the slice
subscribed component re-renders
Create the store with configureStore
(from RTK), then make it available to React by wrapping the app in <Provider >
(from react-redux).
// utils/appStore.js
import
{ configureStore } from
"@reduxjs/toolkit"
;
import
cartReducer from
"./cartSlice"
;
const
appStore = configureStore
({
reducer: { // the app's ONE big reducer
cart: cartReducer, // each slice plugs in here
},
});
// App.js - provide the store to the whole app
<Provider store={appStore}><AppLayout /></Provider >
configureStore
comes from RTK (a Redux job); Provider
comes from react-redux (a React-bridge job). Knowing which is which is the whole point of two libraries.
A slice bundles a name, its initial state, and its reducers. createSlice
returns an object with auto-generated actions
and a reducer
—you export both.
import
{ createSlice } from
"@reduxjs/toolkit"
;
const
cartSlice = createSlice
({
name: "cart"
,
initialState: { items: [] },
reducers: {
addItem: (state, action) => {
state.items.push
(action.payload); // mutate directly
},
removeItem: (state) => { state.items.pop
(); },
clearCart: (state) => { state.items.length = 0
; },
},
});
export const
{ addItem, removeItem, clearCart } = cartSlice.actions;
export default
cartSlice.reducer;
reducers
(e.g. addItem
) auto-generates a matching action you dispatch. A reducer receives (state, action)
and changes state based on action.payload
.
actions
(to dispatch) and the default reducer
(to plug into the store). This syntax is what RTK expects —it's not arbitrary.
reducer
vs reducers
:
the store takes one big reducer
(singular, a combination of slice reducers); a slice defines many reducers
(plural). A classic mix-up —watch the s.
To change the store, get the dispatch
function from useDispatch
and dispatch an action with your data. Whatever you pass becomes action.payload
in the reducer.
import
{ useDispatch } from
"react-redux"
;
import
{ addItem } from
"../utils/cartSlice"
;
const
dispatch = useDispatch
();
const
handleAddItem = (item) => {
dispatch(addItem
(item)); // item becomes action.payload
};
{ type, payload }
and hands it to the reducer as the second arg —so action.payload
is whatever you dispatched.
onClick={() => handleAddItem(item)}
, not onClick={handleAddItem(item)}
—the second calls it immediately on render. (Plain JS, but a common bug.)
To read from the store, use the useSelector
hook and select the exact slice portion you need. Doing so subscribes
the component —it re-renders automatically when that data changes.
import
{ useSelector } from
"react-redux"
;
// GOOD - subscribe to the smallest slice you need
const
cartItems = useSelector
((store) => store.cart.items);
// BAD - subscribing to the whole store
const
store = useSelector
((store) => store);
const
cartItems = store.cart.items;
store.cart.items
, not the whole store
.
The biggest shift from old Redux: in RTK reducers you mutate
state directly (state.items.push(...)
). Old Redux forbade this and forced you to return a new copy.
// OLD vanilla Redux - never mutate, return a new state
addItem: (state, action) => {
const
newState = { ...state };
newState.items.push
(action.payload);
return
newState;
}
// RTK - just mutate; no return needed
addItem: (state, action) => { state.items.push
(action.payload); }
return
a brand-new state. RTK accepts both —but don't half-do it.
state = []
fails:
reassigning the state
parameter just repoints a local variable —it doesn't mutate the real state Immer is tracking. Use state.items.length = 0
(mutate) or return
a new object instead.
Immer is like editing a tracked Google Doc
. You just type your changes into the working draft (mutate freely), and the system quietly records a clean version history —you never manually photocopy the document first. Reassigning state = []
is like writing your edits on a sticky note instead of the doc: the note (local variable) changes, the real document doesn't.
A browser extension that activates on any Redux app —a major reason to use Redux at scale, since it makes a busy store debuggable.
useSelector(s => s)
re-renders on every change anywhere. Select the smallest slice you need.
Store config uses reducer
(one big); a slice uses reducers
(many). Easy typo, real bug.
state = []
won't clear the cart —it repoints a local var. Mutate (state.items.length = 0
) or return a new state.
onClick={fn(item)}
runs on render. Use onClick={() => fn(item)}
.
It logs an Immer proxy. Wrap it: console.log(current(state))
(current
from RTK).
RTK includes RTK Query for data fetching —the modern replacement for old middleware/thunks.
configureStore
and createSlice
out of the box.
useSelector
; writes use useDispatch
.
createSlice
(giving it a name, initial state, and reducers), and plug its reducer into the store's reducer object.
useSelector
hook, selecting the specific slice you need (which subscribes the component). Write with useDispatch
to get dispatch
, then dispatch an action —whatever you pass becomes action.payload
in the reducer. Both hooks come from react-redux.
store.cart.items
) keeps it efficient.
state.items.push(...)
), and RTK uses Immer behind the scenes to produce a correct immutable copy. Vanilla Redux was the opposite: never mutate, always return a new state. You can also return a new state in RTK if you prefer.
Any line of code can introduce a bug —including a bug in a feature you weren't even touching. Automated tests catch those regressions before users do, which is why testing is part of writing the code, not a chore bolted on after.
Test a single component (or pure function) on its own —does it render, does it return the right value? The bulk of what you write.
Test a flow that spans multiple components —type in a box, click search, see the list change. Several components must cooperate.
Drive the real app like a user, start to finish. Done with tools like Cypress, Puppeteer, or Selenium —usually owned by QA, not the dev.
React testing leans on a small stack. Each piece has one job; templates like Create React App wire it up for you, but doing it by hand once shows what each part is actually for.
Built on top of DOM Testing Library, tuned for React. Gives you render
, screen
, fireEvent
. It's a library, not a runner.
Runs the tests and provides test/it
, describe
, and expect
. RTL works with it. Runs your code through Babel.
A browser-like environment in Node so tests can "render" DOM with no real browser. For Jest 28+, install jest-environment-jsdom
separately.
Babel transpiles modern JS/JSX so Jest can read it. @babel/preset-react
(runtime automatic
) turns JSX into elements.
// scaffolding the setup (Parcel project, by hand)
npm i -D @testing-library/react jest babel-jest @babel/core
npx jest --init // no TS, env: jsdom, coverage: yes, clearMocks: yes
npm i -D jest-environment-jsdom // Jest 28+ needs this separately
// package.json
"scripts"
: {
"test"
: "jest"
,
"watch-test"
: "jest --watch"
// re-runs on save, like HMR
}
@babel/preset-react
with { runtime: "automatic" }
. "toBeInTheDocument is not a function
" →import @testing-library/jest-dom
, which adds the DOM matchers.
.parcelrc
(or scope the Babel config) so the two don't fight over your files.
/coverage
directory —add it to .gitignore
so it doesn't pollute your history.
Before any React, the shape of a test is clearest on a plain function. A test is test("description", callback)
; inside, you expect(value).toBe(expected)
. That's the entire skeleton.
// sum.js
export const
sum = (a, b) => a + b;
// sum.test.js
import
{ sum } from
"./sum"
;
test
("adds two numbers"
, () => {
const
result = sum
(3, 4);
expect
(result).toBe
(7); // assertion
});
expect
is the assertion:
it wraps a value, then a matcher (.toBe
, .toEqual
, .toBeTruthy
…) states what should be true. .toBe
uses Object.is
under the hood.
__tests__
folder (the double underscore is "dunder") or any file ending .test.js
/ .spec.js
(and the .ts
variants).
To test a UI component you first render
it into jsdom, then reach into what was rendered through the screen
object, then assert. screen
exposes the queries; the most useful is getByRole
.
import
{ render, screen } from
"@testing-library/react"
;
import
"@testing-library/jest-dom"
; // gives toBeInTheDocument
import
Contact from
"../Contact"
;
test
("should load Contact component"
, () => {
render
(<Contact />); // 1. render to jsdom
const
heading = screen.getByRole
("heading"
); // 2. query
expect
(heading).toBeInTheDocument
(); // 3. assert
});
Find elements by their accessibility role —heading
, button
, textbox
(the role for inputs). The recommended, robust query.
By visible text, by placeholder, by an explicit data-testid
. getByTestId
always works when nothing else fits —it's the escape hatch.
<input >
's role is textbox
, not input
. A wrong role won't crash silently —Jest lists the roles it actually found, which is how you discover the right one.
getByRole("button", { name: "Login" })
—when several elements share a role.
HTMLInputElement
-like object with its props.
Use getBy*
when you expect exactly one match —more than one is an error
. Use getAllBy*
when you expect several; it returns an array you can count.
// expecting many - returns an array
const
inputs = screen.getAllByRole
("textbox"
);
expect
(inputs.length).toBe
(2);
expect
(inputs.length).not
.toBe
(3); // .not inverts any matcher
// text queries accept a regex - looser matching
screen.getByText
(/cart/i); // matches "Cart - 0 items"
getByRole("textbox")
with two text boxes throws. Switch to getAllByRole
—the error message tells you to.
toBe
, toBeTruthy/Falsy
, toBeGreaterThan
, toBeNull
, toBeNaN
, and the .not
inverter. Type a .
after expect(x)
and the editor lists them.
getByText("Cart - 0 items")
is brittle; getByText(/cart/i)
survives small copy changes.
jsdom understands JSX, but it doesn't know about Redux or React Router. Render a component that uses useSelector
or <Link >
on its own and it crashes —you must wrap it in the same providers the real app gives it.
import
{ render, screen, fireEvent } from
"@testing-library/react"
;
import
{ Provider } from
"react-redux"
;
import
{ BrowserRouter } from
"react-router-dom"
;
import
appStore from
"../utils/appStore"
;
import
Header from
"../Header"
;
test
("Header has a login button"
, () => {
render
(
<BrowserRouter ><Provider store={appStore}><Header /></Provider ></BrowserRouter >);
const
btn = screen.getByRole
("button"
, { name: "Login"
});
expect
(btn).toBeInTheDocument
();
});
<Provider >
. "error in Link component" →wrap in <BrowserRouter >
. Each missing context fails with its own clear message.
appStore
so selectors resolve against actual initial state.
Testing a component is like auditioning an actor on a bare stage
. jsdom is the empty stage —it has the floor and the lights (JSX, the DOM), but not the props the actor reaches for. If the role calls for them to pick up a phone (useSelector
) or walk through a door (<Link >
), you have to set those props on stage first (<Provider >
, <BrowserRouter >
) —otherwise they grab at thin air and the scene falls apart.
To simulate a user, import fireEvent
from RTL. fireEvent.click
triggers a click; fireEvent.change
simulates typing —and you hand it the event object the browser would normally build.
// click toggles Login -> Logout
fireEvent
.click
(loginButton);
expect
(screen.getByRole
("button"
, { name: "Logout"
}))
.toBeInTheDocument
();
// typing = a change event with a faked e.target.value
fireEvent
.change
(searchInput, {
target: { value: "burger"
},
});
e.target.value
comes from the DOM; in jsdom you supply { target: { value: "burger" } }
yourself to mimic what an onChange
handler receives.
A component that takes props (like a restaurant card) needs data supplied in the test. You don't hit the network —you create mock data
: a JSON snapshot of a real response, kept in a /mocks
folder and imported.
import
MOCK_DATA from
"../mocks/resCardMock.json"
;
test
("renders card from props"
, () => {
render
(<RestaurantCard resData={MOCK_DATA} />);
const
name = screen.getByText
("Leon's Burgers &Wings"
);
expect
(name).toBeInTheDocument
();
});
resData
), so the component renders exactly as it would live.
An integration test renders a whole feature —e.g. the Body with its search box, cards, and API call. Because jsdom has no fetch
(it's a browser superpower, not core JS), you replace global.fetch
with a mock that resolves your mock data in the same promise shape.
import
MOCK_RESLIST from
"../mocks/resListMock.json"
;
import
{ act } from
"react-dom/test-utils"
;
// fetch returns a promise -> json() returns a promise -> data
global
.fetch = jest.fn
(() => {
return
Promise.resolve
({
json: () => Promise.resolve
(MOCK_RESLIST),
});
});
test
("search filters the list to 4 cards"
, async
() => {
await
act
(async
() =>
render
(<BrowserRouter ><Body /></BrowserRouter >)
);
const
input = screen.getByTestId
("searchInput"
);
fireEvent
.change
(input, { target: { value: "burger"
} });
fireEvent
.click
(screen.getByRole
("button"
, { name: "Search"
}));
const
cards = screen.getAllByTestId
("resCard"
);
expect
(cards.length).toBe
(4);
});
fetch
resolves to a response whose .json()
also
returns a promise. Your mock must nest Promise.resolve({ json: () =>Promise.resolve(data) })
or the component's await res.json()
breaks.
act
:
when rendering triggers state updates (a fetch then setState
), Jest warns unless you await act(async () =>render(...))
. act
comes from react-dom/test-utils
.
data-testid
for counting:
tag each card with data-testid="resCard"
in the component, then getAllByTestId
counts them —assert 20 before search, 4 after. Snapshotting before-and-after proves the filter actually ran.
Wrap related tests in describe("name", () =>{...})
to group them. Purely organizational —can be nested, changes nothing about how tests run.
it
is just an alias of test
. Many prefer it("should ...")
because it reads as a sentence. Pick one and stay consistent.
beforeAll
runs once before the whole file; beforeEach
runs before every test. Use for shared setup.
afterEach
runs after each test, afterAll
once at the end. Use for cleanup. Order: beforeAll →(beforeEach →test →afterEach)…→afterAll.
toBeTruthy
shows what it does (e.g. it fails on JavaScript's six falsy values). Cheap way to learn the API as you write.
test
, describe
, and expect
. React Testing Library is a library built on DOM Testing Library that renders components and queries the output (render
, screen
, fireEvent
). They're used together.
screen
, then assert something with expect
. Render, query, assert —every test repeats that rhythm.
fetch
, which is why those have to be mocked.
getByRole
finds elements by their accessibility role and is the most robust query. getByText
is a fallback. getByTestId
is the escape hatch —you add a data-testid
attribute and query it when nothing else works, useful for counting elements like cards.
getBy*
when exactly one element should match —multiple matches throw an error. Use getAllBy*
when several match; it returns an array, so you can assert on its length (e.g. two input boxes, four cards).
useSelector
and <Link >
fail with "no context" errors. Wrap the rendered component in the same providers the app uses —<Provider store={appStore }>
for Redux and <BrowserRouter >
for routing.
global.fetch
with a Jest mock that returns a promise resolving to an object whose json()
also returns a promise resolving to your mock data, matching real fetch exactly.
fireEvent
from RTL. fireEvent.click(element)
simulates a click. fireEvent.change(input, { target: { value: "burger" } })
simulates typing —you supply the event object's target.value
that the browser would normally provide.
it
is just an alias for test
—identical behavior, often chosen because it("should ...")
reads as a sentence. describe
groups related tests into a block for organization; it can be nested and doesn't change how tests run.