Overview
Modern CSS brings powerful new features that make styling more intuitive, maintainable, and responsive.
Key Ideas
- Logical Properties: Write direction-agnostic CSS that works globally
- Layout Systems: Flexbox and Grid provide powerful, flexible layouts
- Visual Effects: Masks, filters, and backdrop effects without JavaScript
- Modern Selectors: More precise targeting with :is(), :where(), and :has()
Logical Properties
Logical properties adapt to writing direction (LTR/RTL) automatically, making your CSS more flexible and internationalization-friendly.
padding-inline/padding-block- Horizontal and vertical paddingmargin-inline/margin-block- Horizontal and vertical marginsinset- Shorthand for top, right, bottom, left positioninginline-size/block-size- Width and height that respect writing mode
.card {
padding-block: 1rem; /* top & bottom */
padding-inline: 2rem; /* left & right */
margin-block: 1.5rem;
inline-size: min(100%, 40rem);
}
.positioned {
inset: 0; /* same as top: 0; right: 0; bottom: 0; left: 0; */
inset-inline: 1rem; /* left & right */
inset-block-start: 2rem; /* top */
}
HTML5 Semantic Layout
This structure ensures correct document outline and accessibility. Use <section> for
thematic grouping and <article> for independent content.
<html>
<body>
<header> <!-- Logo, Navigation --> </header>
<main>
<!-- Container for centering/width control -->
<div class="container">
<article>
<h1>Blog Post Title</h1>
<section>
<h2>Chapter 1</h2>
<p>Content...</p>
</section>
<section>
<h2>Chapter 2</h2>
<p>Content...</p>
</section>
</article>
</div>
</main>
<footer> <!-- Copyright --> </footer>
</body>
</html>
Flexbox
Flexbox provides a one-dimensional layout system perfect for distributing space and aligning items.
| Property | Values |
|---|---|
display |
flex
inline-flex
|
flex-direction |
row
row-reverse
column
column-reverse
Defines the main axis direction.
|
flex-wrap |
nowrap
wrap
wrap-reverse
Controls whether items wrap to new lines.
|
justify-content |
flex-start
flex-end
center
space-between
space-around
space-evenly
Aligns items along the main axis.
|
align-items |
stretch
flex-start
flex-end
center
baseline
Aligns items along the cross axis (single line).
|
align-content |
stretch
flex-start
flex-end
center
space-between
space-around
Aligns lines along the cross axis (multi-line).
|
gap |
10px
1rem
10px 20px
Spacing between items (row / column).
|
.flex-container {
display: flex;
flex-direction: row; /* or column */
flex-wrap: wrap; /* allow wrapping */
justify-content: space-between;
align-items: center;
gap: 1rem;
}
.flex-item {
flex: 1 1 auto; /* grow | shrink | basis */
}
Grid
CSS Grid provides a two-dimensional layout system for creating complex, responsive layouts with ease.
| Property | Values |
|---|---|
display |
grid
inline-grid
|
grid-template-columnsgrid-template-rows |
100px
1fr
20%
auto
min-content
max-content
repeat(3, 1fr)
minmax(100px, 1fr)
Defines the line names and track sizing functions.
|
gap |
20px
1rem
10px 20px
Shorthand for row-gap and column-gap.
|
justify-items |
start
end
center
stretch
Aligns grid items along the inline (row) axis.
|
align-items |
start
end
center
stretch
Aligns grid items along the block (column) axis.
|
place-items |
center
start end
Shorthand for align-items and justify-items.
|
auto-fit / auto-fill |
repeat(auto-fit, minmax(200px, 1fr))
Keywords for responsive tracks without media queries.
|
.grid-container {
display: grid;
/* Responsive columns without media queries! */
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 2rem;
align-items: start;
}
/* Named grid areas */
.layout {
display: grid;
grid-template-areas:
"header header"
"sidebar main"
"footer footer";
gap: 1rem;
}
.header { grid-area: header; }
.sidebar { grid-area: sidebar; }
.main { grid-area: main; }
Subgrid
Subgrid allows nested grids to inherit and align with parent grid tracks, solving the problem of aligning content across sibling elements.
Why Use Subgrid?
- Align nested content across sibling elements (e.g., all card titles at same height)
- Perfect for card layouts where content heights vary
- No more JavaScript height calculations!
- Maintains grid alignment through nested levels
- grid-template-rows: subgrid - Inherit parent's row tracks
- grid-template-columns: subgrid - Inherit parent's column tracks
- grid-row: span N - Span multiple parent rows
- Perfect for card grids - All images, titles, and buttons align
/* Parent grid */
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 2rem;
}
/* Child inherits parent's row tracks */
.card {
display: grid;
grid-template-rows: subgrid;
grid-row: span 4; /* image, title, description, button */
}
/* All images align, all titles align, all buttons align! */
.card-image { grid-row: 1; }
.card-title { grid-row: 2; }
.card-description { grid-row: 3; }
.card-button { grid-row: 4; }
/* Real-world example: Product grid */
.products {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 2rem;
}
.product-card {
display: grid;
grid-template-rows: subgrid;
grid-row: span 5;
border: 1px solid #ddd;
border-radius: 8px;
padding: 1rem;
}
/* Perfect alignment across all cards */
.product-image { grid-row: 1; }
.product-name { grid-row: 2; }
.product-price { grid-row: 3; }
.product-rating { grid-row: 4; }
.product-buy-button { grid-row: 5; }
Alignment (place-*)
The place-* properties provide shorthand for aligning content in both flex and grid
layouts.
| Property | Values |
|---|---|
place-items |
center
start end
stretch
Shorthand for align-items + justify-items.
|
place-content |
center
space-between
start end
Shorthand for align-content +
justify-content.
|
place-self |
auto
center
start
end
stretch
center start
Shorthand for align-self + justify-self.
|
.centered {
display: grid;
place-items: center; /* centers both axes */
min-block-size: 100vh;
}
.grid {
display: grid;
place-content: center space-between;
/* align-content: center; justify-content: space-between; */
}
Modern Backgrounds
Modern CSS provides advanced background features including layering, blending modes, and text clipping for creative effects.
| Property | Values |
|---|---|
background-image |
url('img.jpg')
linear-gradient()
radial-gradient()
conic-gradient()
|
background-size |
auto
cover
contain
100% 100%
Specifies the size of the background images.
|
background-position |
top
bottom
left
right
center
50% 50%
Sets the starting position of a background image.
|
background-repeat |
repeat
no-repeat
repeat-x
repeat-y
space
round
|
background-clip |
border-box
padding-box
content-box
text
Specifies how far the background (color or image) should extend.
|
background-attachment |
scroll
fixed
local
Sets whether a background image scrolls with the rest of the page.
|
background-blend-mode |
normal
multiply
screen
overlay
darken
lighten
Sets how the background image should blend with the background color.
|
/* Gradient text */
.gradient-text {
background: linear-gradient(45deg, #667eea, #764ba2);
background-clip: text;
-webkit-background-clip: text;
color: transparent;
font-size: 4rem;
font-weight: bold;
}
/* Image text */
.image-text {
background: url('pattern.jpg');
background-clip: text;
-webkit-background-clip: text;
color: transparent;
}
/* Multiple layered backgrounds */
.hero {
background:
linear-gradient(to bottom, transparent 0%, rgba(0,0,0,0.7) 100%),
url('hero.jpg') center/cover no-repeat;
color: white;
min-height: 100vh;
}
/* Duotone effect with blend mode */
.duotone {
background:
linear-gradient(#667eea, #764ba2),
url('photo.jpg') center/cover;
background-blend-mode: screen;
}
Border Features
Modern border properties including logical properties, gradient borders, and advanced border-radius for creative shapes.
| Property | Values |
|---|---|
border-width |
thin
medium
thick
1px
|
border-style |
none
hidden
dotted
dashed
solid
double
groove
ridge
inset
outset
|
border-radius |
50%
10px 20px
9999px
Rounded corners (pill, circle, organic shapes).
|
border-image |
url() 30 round
linear-gradient() 1
Allows specifying an image to be used instead of the normal border.
|
box-shadow |
none
inset 0 0 10px #000
0 4px 6px rgba(0,0,0,0.1)
|
/* Logical border-radius */
.box {
border-start-start-radius: 10px; /* top-left (LTR) */
border-start-end-radius: 20px; /* top-right (LTR) */
border-end-end-radius: 30px; /* bottom-right (LTR) */
border-end-start-radius: 40px; /* bottom-left (LTR) */
}
/* Pill shape */
.pill {
border-radius: 9999px;
}
/* Organic shape */
.organic {
border-radius: 30% 70% 70% 30% / 30% 30% 70% 70%;
}
/* Gradient border with border-radius */
.gradient-border {
position: relative;
background: white;
border-radius: 12px;
padding: 2rem;
}
.gradient-border::before {
content: '';
position: absolute;
inset: -3px;
border-radius: 12px;
background: linear-gradient(45deg, #667eea, #764ba2);
z-index: -1;
}
Text Decoration
Modern text decoration properties provide granular control over underlines, overlines, and strikethroughs.
| Property | Values |
|---|---|
text-decoration-line |
none
underline
overline
line-through
|
text-decoration-style |
solid
double
dotted
dashed
wavy
|
text-decoration-color |
currentColor
#ff0000
transparent
|
text-decoration-thickness |
auto
from-font
2px
0.1em
|
text-underline-offset |
auto
3px
0.2em
Distance between the text and the underline.
|
/* Modern link styling */
a {
color: #667eea;
text-decoration: underline;
text-decoration-color: transparent;
text-decoration-thickness: 2px;
text-underline-offset: 4px;
transition: text-decoration-color 0.3s;
}
a:hover {
text-decoration-color: #667eea;
text-decoration-style: wavy;
}
/* Shorthand */
.fancy-link {
text-decoration: underline wavy blue 2px;
text-underline-offset: 5px;
}
/* Different decoration styles */
.solid { text-decoration-style: solid; }
.double { text-decoration-style: double; }
.dotted { text-decoration-style: dotted; }
.dashed { text-decoration-style: dashed; }
.wavy { text-decoration-style: wavy; }
/* Strike-through with style */
.sale-price {
text-decoration: line-through;
text-decoration-color: #e74c3c;
text-decoration-thickness: 2px;
}
Visual Effects
Modern CSS provides powerful visual effects without needing JavaScript or images.
| Property | Values |
|---|---|
opacity |
0
0.5
1
|
filter |
blur(5px)
brightness(1.5)
contrast(2)
grayscale(100%)
hue-rotate(90deg)
drop-shadow()
Applies graphical effects to the element.
|
backdrop-filter |
blur(10px)
brightness(0.5)
Applies effects to the area behind the element
(glassmorphism).
|
clip-path |
circle(50%)
polygon(0 0, 100% 0, 50% 100%)
inset(10px)
Clips the element to a specific shape.
|
mask |
url(mask.png)
linear-gradient(black, transparent)
Hides parts of an element using an image or gradient.
|
mix-blend-mode |
multiply
screen
overlay
difference
Sets how an element's content blends with its parent.
|
/* Glassmorphism effect */
.glass {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
/* Image effects */
.image {
filter: brightness(1.1) contrast(1.2);
transition: filter 0.3s;
}
.image:hover {
filter: brightness(1.3) saturate(1.5);
}
Transforms (2D & 3D)
Modify the coordinate space of the CSS visual formatting model.
| Property | Values |
|---|---|
transform |
none
matrix()
translate(x,y)
scale(x,y)
rotate(angle)
skew(x-angle,y-angle)
|
transform-origin |
center
top left
50% 50%
The point around which a transformation is applied.
|
perspective |
none
1000px
Distance between the Z=0 plane and the user (3D effect).
|
.card:hover {
transform: scale(1.05) rotate(2deg);
}
/* 3D Flip Card */
.flip-card-inner {
transform-style: preserve-3d;
transition: transform 0.6s;
}
.flip-card:hover .flip-card-inner {
transform: rotateY(180deg);
}
Transitions & Animations
Smoothly change property values or create keyframe animations.
| Property | Values |
|---|---|
transition |
all 0.3s ease
color 200ms linear
Shorthand for property, duration, timing-function, delay.
|
animation |
spin 1s linear infinite
Shorthand structure.
|
timing-function |
ease
linear
ease-in
ease-out
cubic-bezier(n,n,n,n)
|
/* Simple Transition */
.btn {
transition: all 0.2s ease-in-out;
}
/* Keyframe Animation */
@keyframes bounce {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-20px); }
}
.loading {
animation: bounce 1s infinite;
}
Modern Selectors
New CSS selectors provide more powerful and concise ways to target elements.
:is()- Matches any of the selectors in the list:where()- Like :is() but with zero specificity:has()- Parent selector! Style based on children:not()- Exclude elements from selection
/* :is() - reduces repetition */
:is(h1, h2, h3) {
margin-block: 1em;
}
/* :has() - parent selector */
.card:has(img) {
display: grid;
grid-template-columns: 200px 1fr;
}
/* :not() - exclude elements */
button:not(.primary) {
background: transparent;
}
Responsive Units
Modern CSS provides powerful responsive units including new viewport units and container query units for truly responsive designs.
Key Unit Types
- New Viewport Units: dvh, svh, lvh (mobile-friendly!)
- Container Query Units: cqi, cqb, cqw, cqh
- Typography Units: ch, lh, rlh
- Relative Units: rem, em, %
- dvh (dynamic viewport height) - Accounts for mobile browser UI
- svh/lvh - Small/large viewport (browser UI visible/hidden)
- cqi/cqb - Container inline/block size
- ch - Character width (optimal reading: 65ch)
/* New viewport units - mobile-friendly! */
.hero {
min-height: 100dvh; /* Dynamic - adjusts with mobile UI */
/* Better than 100vh on mobile! */
}
.sidebar {
height: 100svh; /* Small viewport (UI visible) */
}
.fullscreen {
height: 100lvh; /* Large viewport (UI hidden) */
}
/* Container query units */
.container {
container-type: inline-size;
}
.title {
font-size: clamp(1rem, 5cqi, 3rem);
/* cqi = container inline (width in LTR) */
padding: 2cqb; /* cqb = block (height) */
}
/* Typography units */
.readable {
max-width: 65ch; /* Optimal reading width */
line-height: 1.6;
margin-block: 2lh; /* 2x line height */
}
Aspect Ratio & Object-fit
Modern properties for maintaining aspect ratios and controlling how images and videos scale within their containers.
- aspect-ratio - Maintain proportions without padding hacks
- object-fit - Control image/video scaling (cover, contain, fill)
- object-position - Position content within frame
- Common ratios - 16/9, 3/4, 1/1, golden ratio
/* Modern way - aspect-ratio */
.video {
aspect-ratio: 16 / 9;
width: 100%;
}
/* Common ratios */
.square { aspect-ratio: 1; }
.widescreen { aspect-ratio: 16 / 9; }
.portrait { aspect-ratio: 3 / 4; }
.golden { aspect-ratio: 1.618; }
/* Card with fixed ratio */
.card {
aspect-ratio: 3 / 4;
display: grid;
grid-template-rows: 2fr 1fr;
}
/* Object-fit for images */
img {
width: 100%;
height: 300px;
object-fit: cover; /* Crop to fill */
}
.avatar {
width: 100px;
height: 100px;
border-radius: 50%;
object-fit: cover;
object-position: center top; /* Focus on face */
}
Fluid Design
Create truly responsive designs using clamp(), calc(), and modern CSS functions for fluid typography and spacing.
- clamp(min, preferred, max) - Responsive sizing without media queries
- calc() - Complex calculations with mixed units
- min() / max() - Choose minimum or maximum value
- Fluid typography - Scale smoothly across viewports
/* Fluid typography with clamp() */
h1 {
font-size: clamp(2rem, 5vw, 4rem);
/* Never smaller than 2rem */
/* Scales with viewport at 5vw */
/* Never larger than 4rem */
}
/* Fluid spacing */
.section {
padding-block: clamp(2rem, 5vh, 8rem);
padding-inline: clamp(1rem, 5vw, 4rem);
}
/* Fluid container width */
.container {
width: clamp(320px, 90vw, 1200px);
margin-inline: auto;
}
/* calc() with CSS variables */
:root {
--header-height: 80px;
--sidebar-width: 250px;
}
.content {
height: calc(100dvh - var(--header-height));
width: calc(100% - var(--sidebar-width));
padding: calc(1rem + 2vw);
}
/* min() and max() */
.responsive-width {
width: min(100%, 1200px); /* Never wider than 1200px */
padding: max(1rem, 3vw); /* At least 1rem */
}
Pseudo-classes & Pseudo-elements
Powerful selectors for styling elements based on state, position, and creating decorative elements without extra HTML.
Key Differences
- Pseudo-classes (:) - Target element states (:hover, :focus, :nth-child)
- Pseudo-elements (::) - Create virtual elements (::before, ::after, ::first-letter)
- Combinators - Relationship selectors (>, +, ~, space)
- ::before / ::after - Insert content before/after elements
- :hover / :focus / :active - Interactive states
- :nth-child() / :nth-of-type() - Position-based selection
- > + ~ - Child, adjacent, and sibling combinators
/* Pseudo-elements - ::before and ::after */
.quote::before {
content: '"';
font-size: 3rem;
color: #667eea;
}
.quote::after {
content: '"';
font-size: 3rem;
color: #667eea;
}
/* Decorative elements */
.button::before {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(45deg, #667eea, #764ba2);
opacity: 0;
transition: opacity 0.3s;
}
.button:hover::before {
opacity: 1;
}
/* Pseudo-classes - states and positions */
button:hover {
background: #667eea;
}
input:focus {
outline: 2px solid #667eea;
outline-offset: 2px;
}
/* nth-child patterns */
li:nth-child(odd) { background: #f0f0f0; }
li:nth-child(even) { background: white; }
li:nth-child(3n) { color: #667eea; } /* Every 3rd */
li:first-child { font-weight: bold; }
li:last-child { border-bottom: none; }
/* Combinators - relationships */
/* > Direct child */
.parent > .child {
margin: 1rem;
}
/* + Adjacent sibling (immediately after) */
h2 + p {
font-size: 1.2rem;
color: #666;
}
/* ~ General sibling (any after) */
h2 ~ p {
line-height: 1.6;
}
/* Space - Descendant (any nested) */
.container p {
max-width: 65ch;
}
/* More pseudo-classes */
a:visited { color: purple; }
input:disabled { opacity: 0.5; }
input:checked + label { font-weight: bold; }
div:empty { display: none; }
p:first-of-type { font-size: 1.2rem; }
/* ::first-letter and ::first-line */
p::first-letter {
font-size: 3rem;
font-weight: bold;
float: left;
margin-right: 0.5rem;
}
p::first-line {
font-variant: small-caps;
}
Custom Properties & Theming
CSS Custom Properties (variables) enable dynamic theming and reusable values throughout your stylesheet.
- Define variables with
--variable-name - Use with
var(--variable-name, fallback) - Scope variables to specific elements or :root
- Change values with JavaScript for dynamic themes
:root {
--primary-color: #667eea;
--secondary-color: #764ba2;
--spacing-unit: 1rem;
--border-radius: 8px;
}
.button {
background: var(--primary-color);
padding: var(--spacing-unit);
border-radius: var(--border-radius);
}
/* Dark mode */
@media (prefers-color-scheme: dark) {
:root {
--primary-color: #8b9aff;
--bg-color: #1a1a1a;
}
}
/* Component-scoped variables */
.card {
--card-padding: 2rem;
--card-bg: white;
padding: var(--card-padding);
background: var(--card-bg);
}
.card.compact {
--card-padding: 1rem;
}
React Hooks Overview
React Hooks let you use state and other React features in function components, making code more reusable and easier to understand.
Key Concepts
- State Management: useState, useReducer for component state
- Side Effects: useEffect for data fetching, subscriptions, DOM manipulation
- Context: useContext for sharing data across components
- Performance: useMemo, useCallback for optimization
Component Patterns & Boilerplate
Common patterns for creating robust, typed functional components.
TypeScript Functional Component
The standard way to write components with typed props.
import { ReactNode } from 'react';
// 1. Define Props Interface
interface CardProps {
title: string;
description?: string; // Optional prop
children: ReactNode; // For nested content
onAction: (id: string) => void; // Event handler
variant?: 'primary' | 'secondary';
}
// 2. Component Definition
export const Card = ({
title,
description,
children,
onAction,
variant = 'primary' // Default value
}: CardProps) => {
return (
<div className={`card ${variant}`}>
<h3>{title}</h3>
{description && <p>{description}</p>}
<div className="card-content">{children}</div>
<button onClick={() => onAction('123')}>Action</button>
</div>
);
};
Forwarding Refs
For when you need to expose the DOM node to the parent.
import { forwardRef } from 'react';
export const Input = forwardRef<HTMLInputElement, InputProps>((props, ref) => {
return <input ref={ref} {...props} />;
});
Input.displayName = 'Input'; // Debugging name
Form Validation
Modern validation uses React Hook Form for performance and Zod for schema validation.
- Controlled Inputs: State updates on every keystroke (simpler, re-renders).
- Uncontrolled (React Hook Form): Refs read values on submit (faster, less re-renders).
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
// 1. Define Schema with Zod
const schema = z.object({
email: z.string().email('Invalid email address'),
password: z.string().min(8, 'Password must be 8 chars'),
age: z.number().min(18, 'Must be 18+'),
});
// Infer TS Type from Schema
type FormData = z.infer<typeof schema>;
export function SignupForm() {
// 2. Initialize Hook Form
const {
register,
handleSubmit,
formState: { errors, isSubmitting }
} = useForm<FormData>({
resolver: zodResolver(schema)
});
const onSubmit = async (data: FormData) => {
await api.signup(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<label>Email</label>
<input {...register('email')} />
{errors.email && <span>{errors.email.message}</span>}
</div>
<div>
<label>Password</label>
<input type="password" {...register('password')} />
{errors.password && <span>{errors.password.message}</span>}
</div>
<div>
<label>Age</label>
<input type="number" {...register('age', { valueAsNumber: true })} />
{errors.age && <span>{errors.age.message}</span>}
</div>
<button disabled={isSubmitting}>Sign Up</button>
</form>
);
}
useState
The most common hook for adding state to function components.
- Returns current state and updater function
- State updates trigger re-renders
- Can use functional updates for state based on previous state
- Initial state can be a value or function (lazy initialization)
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
<button onClick={() => setCount(c => c - 1)}>
Decrement (functional update)
</button>
</div>
);
}
// Lazy initialization
const [data, setData] = useState(() => {
const saved = localStorage.getItem('data');
return saved ? JSON.parse(saved) : [];
});
// Multiple state variables
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [isLoading, setIsLoading] = useState(false);
useEffect
Handle side effects like data fetching, subscriptions, and manual DOM manipulation.
- Runs after render by default
- Dependency array controls when effect runs
- Return cleanup function to prevent memory leaks
- Empty dependency array runs once on mount
import { useState, useEffect } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
let cancelled = false;
async function fetchUser() {
const response = await fetch(\`/api/users/\${userId}\`);
const data = await response.json();
if (!cancelled) setUser(data);
}
fetchUser();
return () => { cancelled = true; };
}, [userId]); // Re-run when userId changes
return <div>{user?.name}</div>;
}
Modern Alternatives to useEffect
In modern React (especially React 19), you often don't need useEffect for common tasks like
data
fetching or form submission.
Why replace useEffect?
- Race conditions:
useEffectfetching needs cleanup logic (see previous example) - Waterfalls: Effects run after render, causing slower data loading
- Complexity: Managing dependency arrays is error-prone
1. Data Fetching
Old Way (useEffect): Fetch on mount, handle loading state, handle race conditions.
New Way (use hook): Suspend execution until data is ready. No loading state variables needed!
// ❌ Old Way: useEffect
function Profile({ id }) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
let active = true;
fetchUser(id).then(d => {
if (active) {
setData(d);
setLoading(false);
}
});
return () => { active = false; };
}, [id]);
if (loading) return <Spinner />;
return <div>{data.name}</div>;
}
// ✅ New Way: use() hook (React 19)
function Profile({ userPromise }) {
// Automatically suspends! No loading state needed in component.
const data = use(userPromise);
return <div>{data.name}</div>;
}
function Parent() {
return (
<Suspense fallback={<Spinner />}>
<Profile userPromise={fetchUser(id)} />
</Suspense>
);
}
2. Form Submission
Old Way: Manual onSubmit handler.
New Way: Actions handle pending states automatically.
// ❌ Old Way: Manual Event Handling
function Form() {
const [isSubmitting, setIsSubmitting] = useState(false);
async function handleSubmit(e) {
e.preventDefault();
setIsSubmitting(true);
await submitData(new FormData(e.target));
setIsSubmitting(false);
}
return (
<form onSubmit={handleSubmit}>
<button disabled={isSubmitting}>Submit</button>
</form>
);
}
// ✅ New Way: Actions
function Form() {
const [state, action, isPending] = useActionState(submitData, null);
return (
<form action={action}>
<button disabled={isPending}>Submit</button>
</form>
);
}
3. Derived State
Don't use useEffect to update state based on props or other state. Just calculate it during
render!
// ❌ Bad: Redundant effect
function Form() {
const [firstName, setFirstName] = useState('John');
const [lastName, setLastName] = useState('Doe');
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(\`\${firstName} \${lastName}\`);
}, [firstName, lastName]);
return <div>{fullName}</div>;
}
// ✅ Good: Derived during render
function Form() {
const [firstName, setFirstName] = useState('John');
const [lastName, setLastName] = useState('Doe');
// Calculated immediately! No extra render.
const fullName = \`\${firstName} \${lastName}\`;
return <div>{fullName}</div>;
}
useContext
Access context values without prop drilling.
- Consume context created with React.createContext
- Component re-renders when context value changes
- Great for themes, auth, language preferences
import { createContext, useContext, useState } from 'react';
const ThemeContext = createContext();
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
function ThemedButton() {
const { theme, setTheme } = useContext(ThemeContext);
return (
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Current theme: {theme}
</button>
);
}
useReducer
Alternative to useState for complex state logic.
- Better for complex state objects
- Centralizes state update logic
- Similar to Redux pattern
import { useReducer } from 'react';
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
case 'reset':
return { count: 0 };
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0 });
return (
<>
Count: {state.count}
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
</>
);
}
useRef
Create mutable references that persist across renders without causing re-renders.
- Access DOM elements directly
- Store mutable values that don't trigger re-renders
- Persist values across renders
import { useRef, useEffect } from 'react';
function TextInput() {
const inputRef = useRef(null);
useEffect(() => {
inputRef.current.focus();
}, []);
return <input ref={inputRef} />;
}
// Store previous value
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
useMemo & useCallback
Optimize performance by memoizing values and functions.
- useMemo: Memoize expensive calculations
- useCallback: Memoize function references
- Both take dependency arrays
- Use sparingly - premature optimization can hurt readability
import { useMemo, useCallback } from 'react';
function ExpensiveComponent({ items, filter }) {
// Memoize expensive calculation
const filteredItems = useMemo(() => {
return items.filter(item => item.includes(filter));
}, [items, filter]);
// Memoize callback
const handleClick = useCallback((id) => {
console.log('Clicked:', id);
}, []);
return (
<ul>
{filteredItems.map(item => (
<li key={item} onClick={() => handleClick(item)}>
{item}
</li>
))}
</ul>
);
}
Custom Hooks
Create reusable logic by extracting hooks into custom functions.
- Must start with "use" prefix
- Can use other hooks inside
- Share stateful logic between components
- Keep components clean and focused
import { useState, useEffect } from 'react';
// Custom hook for fetching data
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
async function fetchData() {
try {
const response = await fetch(url);
const json = await response.json();
setData(json);
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
}
fetchData();
}, [url]);
return { data, loading, error };
}
// Usage
function UserList() {
const { data, loading, error } = useFetch('/api/users');
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return <ul>{data.map(user => <li>{user.name}</li>)}</ul>;
}
// Custom hook for local storage
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
const saved = localStorage.getItem(key);
return saved ? JSON.parse(saved) : initialValue;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}
// Usage
function Settings() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
return <button onClick={() => setTheme('dark')}>{theme}</button>;
}
React 19 Features
React 19 introduces powerful new features including Actions, the use() hook, and built-in optimistic updates.
What's New in React 19
- Actions: Built-in form handling with pending states
- use() hook: Async data fetching with Suspense
- useOptimistic: Optimistic UI updates
- ref as prop: No more forwardRef needed!
- useActionState - Form actions with pending states
- use() - Suspend until promise resolves
- useOptimistic - Instant UI feedback
- Document metadata - title, meta tags in components
// React 19: Form Actions
import { useActionState } from 'react';
function SignupForm() {
async function signup(prevState, formData) {
const email = formData.get('email');
// Server action or API call
const result = await fetch('/api/signup', {
method: 'POST',
body: JSON.stringify({ email })
});
return result.ok ? { success: true } : { error: 'Failed' };
}
const [state, formAction, isPending] = useActionState(signup, null);
return (
<form action={formAction}>
<input name="email" type="email" required />
<button disabled={isPending}>
{isPending ? 'Submitting...' : 'Sign Up'}
</button>
{state?.error && <p>{state.error}</p>}
</form>
);
}
// use() hook - Async data fetching
import { use, Suspense } from 'react';
function UserProfile({ userPromise }) {
const user = use(userPromise); // Suspends until resolved
return <div>{user.name}</div>;
}
function App() {
const userPromise = fetch('/api/user').then(r => r.json());
return (
<Suspense fallback={<Loading />}>
<UserProfile userPromise={userPromise} />
</Suspense>
);
}
// useOptimistic - Instant UI feedback
import { useOptimistic } from 'react';
function TodoList({ todos }) {
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
todos,
(state, newTodo) => [...state, { ...newTodo, pending: true }]
);
async function addTodo(formData) {
const title = formData.get('title');
addOptimisticTodo({ id: Date.now(), title });
await fetch('/api/todos', {
method: 'POST',
body: JSON.stringify({ title })
});
}
return (
<>
<form action={addTodo}>
<input name="title" />
<button>Add</button>
</form>
<ul>
{optimisticTodos.map(todo => (
<li style={{ opacity: todo.pending ? 0.5 : 1 }}>
{todo.title}
</li>
))}
</ul>
</>
);
}
Performance Tips
Essential performance optimization techniques every React developer should know.
- Avoid unnecessary re-renders - Use React.memo, useMemo, useCallback
- Use correct keys - Stable unique IDs, not array indices
- Lazy load components - Code splitting with React.lazy
- Virtualize long lists - Only render visible items
// ❌ BAD: Creates new object every render
function Parent() {
const config = { theme: 'dark' };
return <Child config={config} />;
}
// ✅ GOOD: Memoize or move outside
const CONFIG = { theme: 'dark' };
function Parent() {
return <Child config={CONFIG} />;
}
// Or use useMemo
function Parent() {
const config = useMemo(() => ({ theme: 'dark' }), []);
return <Child config={config} />;
}
// Lazy load components
import { lazy, Suspense } from 'react';
const HeavyChart = lazy(() => import('./HeavyChart'));
function Dashboard() {
return (
<Suspense fallback={<Spinner />}>
<HeavyChart data={data} />
</Suspense>
);
}
// Code splitting by route
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
// React.memo to prevent re-renders
const ExpensiveChild = React.memo(function ExpensiveChild({ data }) {
// Heavy computation
return <div>{data}</div>;
});
// Only re-renders if data changes
function Parent() {
const [count, setCount] = useState(0);
const data = useMemo(() => heavyComputation(), []);
return (
<>
<button onClick={() => setCount(count + 1)}>{count}</button>
<ExpensiveChild data={data} />
</>
);
}
Common Pitfalls
Avoid these common mistakes that junior developers often make.
- Inline functions in JSX - Creates new function every render
- Index as key - Causes bugs with dynamic lists
- Missing dependencies - useEffect dependency array issues
- Mutating state - Always create new objects/arrays
// ❌ BAD: Inline function creates new reference
<button onClick={() => handleClick(id)}>Click</button>
// ✅ GOOD: Use useCallback
const handleButtonClick = useCallback(() => handleClick(id), [id]);
<button onClick={handleButtonClick}>Click</button>
// ❌ BAD: Index as key
{items.map((item, index) => (
<Item key={index} data={item} />
))}
// ✅ GOOD: Stable unique key
{items.map(item => (
<Item key={item.id} data={item} />
))}
// ❌ BAD: Mutating state
const [items, setItems] = useState([]);
items.push(newItem); // DON'T DO THIS!
setItems(items);
// ✅ GOOD: Create new array
setItems([...items, newItem]);
// ❌ BAD: Mutating object
const [user, setUser] = useState({ name: 'John' });
user.name = 'Jane'; // DON'T DO THIS!
setUser(user);
// ✅ GOOD: Create new object
setUser({ ...user, name: 'Jane' });
// ❌ BAD: Missing dependencies
useEffect(() => {
fetchData(userId); // userId not in dependencies!
}, []);
// ✅ GOOD: Include all dependencies
useEffect(() => {
fetchData(userId);
}, [userId]);
// ❌ BAD: Stale closure
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setCount(count + 1); // Always uses initial count!
}, 1000);
return () => clearInterval(timer);
}, []);
// ✅ GOOD: Functional update
useEffect(() => {
const timer = setInterval(() => {
setCount(c => c + 1); // Uses current count
}, 1000);
return () => clearInterval(timer);
}, []);
API Integration Overview
Mastering data fetching is crucial for React interviews. You need to know when to fetch on the client, when to fetch on the server, and how to manage caching.
Key Strategies
- Client-side Fetching: Good for user-specific data, updates after page load.
- Server-side Fetching: Better for SEO, initial load performance, and secrets.
- Caching & Revalidation: Critical for performance (stale-while-revalidate).
REST APIs & Fetch
The fundamental way to fetch data. While useEffect works, modern apps typically use libraries.
// Basic Fetch with useEffect (Interview Basics)
import { useState, useEffect } from 'react';
function UserList() {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const controller = new AbortController(); // Cancel request on unmount
fetch('https://api.example.com/users', { signal: controller.signal })
.then(res => {
if (!res.ok) throw new Error('Network response was not ok');
return res.json();
})
.then(data => setData(data))
.catch(err => {
if (err.name !== 'AbortError') setError(err.message);
})
.finally(() => setLoading(false));
return () => controller.abort(); // Cleanup
}, []);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return <ul>{data.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}
TanStack Query (React Query)
The industry standard for client-side fetching. Handles caching, deduplication, and background updates automatically.
- useQuery: For fetching data (GET requests)
- useMutation: For modifying data (POST, PUT, DELETE)
- queryClient: For invalidating queries (refetching)
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
// Fetcher function
const fetchTodos = async () => {
const res = await fetch('/api/todos');
return res.json();
};
function TodoApp() {
const queryClient = useQueryClient();
// 1. Fetching Data
const { data: todos, isLoading, error } = useQuery({
queryKey: ['todos'], // Unique key for caching
queryFn: fetchTodos,
staleTime: 60000, // Data fresh for 1 min
});
// 2. Modifying Data
const mutation = useMutation({
mutationFn: (newTodo) => {
return fetch('/api/todos', {
method: 'POST',
body: JSON.stringify(newTodo),
});
},
// Invalidate and refetch todos on success
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});
if (isLoading) return 'Loading...';
return (
<div>
<button onClick={() => mutation.mutate({ title: 'New Task' })}>
Add Todo
</button>
<ul>
{todos.map(todo => <li>{todo.title}</li>)}
</ul>
</div>
);
}
Redux Saga & Boilerplate
Enterprise-grade side-effect management for Redux. Uses Generators (function*) to handle
complex async flows.
| Effect | Description |
|---|---|
call(fn, ...args) |
Calls a promise/function and pauses until it resolves. |
put(action) |
Dispatches an action to the Redux store. |
takeLatest(type, saga) |
Cancels previous running saga task if new action is fired. |
takeEvery(type, saga) |
Allows concurrent sagas for every action. |
select(selector) |
Access state from the Redux store. |
Full Boilerplate
// 1. slice.js (Redux Toolkit)
import { createSlice } from '@reduxjs/toolkit';
const userSlice = createSlice({
name: 'user',
initialState: { data: null, loading: false, error: null },
reducers: {
fetchUserRequest: (state) => { state.loading = true; },
fetchUserSuccess: (state, action) => {
state.loading = false;
state.data = action.payload;
},
fetchUserFailure: (state, action) => {
state.loading = false;
state.error = action.payload;
}
}
});
export const { fetchUserRequest, fetchUserSuccess, fetchUserFailure } = userSlice.actions;
export default userSlice.reducer;
// 2. sagas.js (Side Effects)
import { call, put, takeLatest } from 'redux-saga/effects';
import { fetchUserRequest, fetchUserSuccess, fetchUserFailure } from './slice';
import api from './api';
// Worker Saga
function* fetchUserSaga(action) {
try {
// call: Blocking call to promise
const user = yield call(api.fetchUser, action.payload);
// put: Dispatch success action
yield put(fetchUserSuccess(user));
} catch (e) {
yield put(fetchUserFailure(e.message));
}
}
// Watcher Saga
export function* rootSaga() {
// takeLatest: Cancels pending request if new one comes in
yield takeLatest(fetchUserRequest.type, fetchUserSaga);
}
// 3. store.js (Configuration)
import { configureStore } from '@reduxjs/toolkit';
import createSagaMiddleware from 'redux-saga';
import userReducer from './slice';
import { rootSaga } from './sagas';
const sagaMiddleware = createSagaMiddleware();
export const store = configureStore({
reducer: { user: userReducer },
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({ thunk: false }).concat(sagaMiddleware),
});
// Run the saga
sagaMiddleware.run(rootSaga);
// 4. Usage in Component
import { useDispatch, useSelector } from 'react-redux';
import { fetchUserRequest } from './slice';
function UserProfile({ id }) {
const dispatch = useDispatch();
const { data, loading } = useSelector(state => state.user);
useEffect(() => {
dispatch(fetchUserRequest(id));
}, [dispatch, id]);
if (loading) return <Spinner />;
return <div>{data?.name}</div>;
}
GraphQL Integration
GraphQL allows clients to request exactly the data they need. Apollo Client is the most popular library.
import { useQuery, useMutation, gql } from '@apollo/client';
// Define Queries
const GET_DOGS = gql`
query GetDogs {
dogs {
id
breed
displayImage
}
}
`;
const ADD_DOG = gql`
mutation AddDog($breed: String!) {
addDog(breed: $breed) {
id
breed
}
}
`;
function Dogs() {
// useQuery hook (auto-executes on render)
const { loading, error, data } = useQuery(GET_DOGS);
// useMutation hook (returns trigger function)
const [addDog, { loading: saving }] = useMutation(ADD_DOG, {
refetchQueries: [GET_DOGS] // Auto-refresh list
});
if (loading) return 'Loading...';
if (error) return \`Error! \${error.message}\`;
return (
<ul>
{data.dogs.map(dog => (
<li key={dog.id}>
{dog.breed}
<img src={dog.displayImage} />
</li>
))}
<button onClick={() => addDog({ variables: { breed: 'Pug' } })}>
Add Pug
</button>
</ul>
);
}
Next.js Data Fetching
Next.js offers different strategies based on the router version (App Router vs Pages Router).
App Router (Next.js 13+)
Fetch data directly in Server Components. It's safe, fast, and secure.
// app/page.tsx (Server Component by default)
async function getData() {
const res = await fetch('https://api.example.com/data', {
// Cache options (similar to getStaticProps/getServerSideProps)
next: { revalidate: 3600 } // Revalidate every hour (ISR)
});
if (!res.ok) throw new Error('Failed to fetch data');
return res.json();
}
export default async function Page() {
const data = await getData(); // Direct await!
return <main>{data.title}</main>;
}
Pages Router (Legacy/Interview)
You must know these for interviews!
// 1. getStaticProps (SSG)
// Runs at BUILD time. Good for blog posts, marketing pages.
export async function getStaticProps() {
const res = await fetch('https://.../posts');
const posts = await res.json();
return {
props: { posts },
revalidate: 10, // ISR: Regenerate page every 10s if requested
};
}
// 2. getServerSideProps (SSR)
// Runs on EVERY request. Good for personalized data/auth.
export async function getServerSideProps(context) {
const res = await fetch(\`https://.../data?id=\${context.query.id}\`);
const data = await res.json();
return { props: { data } };
}
Next.js Route Handlers
Create your own API endpoints within Next.js.
// app/api/route.ts (App Router)
import { NextResponse } from 'next/server';
// GET request handler
export async function GET() {
const data = { message: 'Hello from Next.js API' };
return NextResponse.json(data);
}
// POST request handler
export async function POST(request: Request) {
const body = await request.json();
// Database logic here...
return NextResponse.json({ success: true, data: body });
}
Next.js Server Actions
Execute server-side code directly from client components (Forms, Buttons).
// actions.ts ('use server' directive is key)
'use server'
export async function createTodo(formData: FormData) {
const title = formData.get('title');
await db.todo.create({ data: { title } });
revalidatePath('/todos'); // Refresh the page data
}
// Component.tsx
import { createTodo } from './actions';
export default function AddTodo() {
return (
<form action={createTodo}>
<input name="title" />
<button type="submit">Add</button>
</form>
);
}