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 padding
  • margin-inline / margin-block - Horizontal and vertical margins
  • inset - Shorthand for top, right, bottom, left positioning
  • inline-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.

<header>
<nav>
<main>
.container
<article>
<section>
<aside>
<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 */
}
1
2
3

Grid

CSS Grid provides a two-dimensional layout system for creating complex, responsive layouts with ease.

Property Values
display
grid inline-grid
grid-template-columns
grid-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; }
1
2
3
4
5
6

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; }
Aligned Title
This description aligns with the neighbor card despite different lengths.
Short Title
Short desc.
/* 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;
}
Gradient Text
/* 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 */
}
1:1
16:9
3:4

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: useEffect fetching 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>
  );
}