All posts

React — Introduction

Components, JSX, props, state, hooks, the virtual DOM, and the mental model that makes React click — a complete introduction with interview coverage.


JavaScript is wild. You can build anything with it. But as your app grows, raw JS becomes a nightmare — juggling DOM manipulation, keeping track of what changed, wiring up event listeners. It gets messy fast, and it does not scale.

React was Facebook’s answer to that problem. And they had a big problem — Facebook is one of the most complex UIs on the planet. So they built a tool that makes building UIs predictable, scalable, and fast.

Let’s understand what React actually is before we write a single line of it.


What Is React?

React is a view library. That’s it. It does one thing: manages how your UI looks and updates.

Why does that matter? Because the “view” is the hard part. Keeping the DOM in sync with your data, making sure things update efficiently, handling complex nested UIs — React solves all of that.

And because it’s just the view layer, React isn’t locked to browsers. The same React you use to build websites can power mobile apps (React Native), VR apps, and desktop apps. What changes is the canvas, not React itself.

Companies like Facebook, Instagram, and Netflix run on React. Now let’s understand the four ideas that make it powerful.


Four Core Ideas

1. Thinking in Components

Before React, a webpage was one big pile — one HTML file, one CSS file, one JavaScript file. React introduced a better mental model: build your UI out of small, reusable components.

Think of it like Lego blocks. Every piece is its own self-contained unit.

Atoms       → smallest indivisible elements (image, button, label)
Molecules   → atoms combined with related functionality (nav item)
Organisms   → molecules combined into a feature (navbar, card)
Templates   → organisms combined into a layout
Pages       → the complete view

You write a Button component once. You use it everywhere. Need to change the button? Change it in one place and it updates everywhere. This is why React scales.

2. One-Way Data Flow

In React, data flows in one direction — from parent to child. Always. No exceptions.

         App (owns state)
        /              \
  SearchBox         CardList
                   /    |    \
               Card   Card   Card

If something changes in App, the change trickles down to its children. But Card can never reach up and change App’s state directly. It can only tell the parent “hey, something happened” by calling a function the parent passed down.

This makes bugs predictable. When something breaks, you know where to look.

3. Virtual DOM

Here’s the old way: you wrote code that told the browser what to do. “Remove this element. Add this one. Change this text.” Every instruction triggered expensive browser repaints.

React flips this completely. Instead of manipulating the DOM yourself, you describe what the UI should look like — as a JavaScript object. React calls this the Virtual DOM.

Your code → Virtual DOM (JS object) → React diffs it → Real DOM update

When state changes, React creates a new Virtual DOM, compares it with the previous one (diffing), finds exactly what changed, and updates only those parts of the real DOM. Minimum work, maximum speed.

This is also why React can work on non-browser platforms — the Virtual DOM is just a JS object. Swap out the rendering layer and you have React Native.

4. React Ecosystem

React has one of the largest ecosystems in JavaScript. There are packages for routing, state management, testing, animation — almost anything you can think of. If you hit a problem, someone has already solved it and published it to NPM.

Underneath the hood, React projects use Webpack (bundling) and Babel (transpiling JSX and modern JS for older browsers). You don’t configure these yourself. Create React App handles it all.


Setting Up: Create React App

You could wire up React manually with Webpack and Babel. But there’s a better way.

npx create-react-app my-app
cd my-app
npm start

That’s it. You have a working React app.

What’s npx? It’s a package runner that comes with npm (version 5.2+). Instead of installing create-react-app globally with npm install -g, npx downloads and runs it on the fly. Cleaner, no global installs needed.

What does Create React App give you out of the box?

  • A development server with live reload
  • Linting (errors highlighted as you type)
  • Testing setup
  • Production build optimization

The package.json has four key scripts:

"scripts": {
  "start": "react-scripts start",
  "build": "react-scripts build",
  "test": "react-scripts test",
  "eject": "react-scripts eject"
}

npm start — run the dev server
npm run build — create an optimized production bundle
npm test — run tests
npm run eject — expose the Webpack/Babel config (one-way, can’t un-eject)


Project Folder Structure

After create-react-app runs, you get this:

my-app/
├── public/
│   ├── index.html       ← the one HTML file React uses
│   └── favicon.ico
├── src/
│   ├── index.js         ← entry point of your app
│   ├── index.css        ← global styles
│   ├── App.js           ← root component
│   └── App.css
├── package.json
├── package-lock.json    ← locks exact dependency versions
├── .gitignore           ← excludes node_modules, build, etc.
└── node_modules/        ← all installed packages (never commit this)

The most important file is public/index.html. Open it and you’ll find one thing inside <body>:

<div id="root"></div>

That’s where your entire React app gets injected. One div. React takes over from there.


The Entry Point: index.js

src/index.js is the first file that runs. Here’s what it does:

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);

React — the core library. Handles component logic, state, the Virtual DOM.
ReactDOM — the connector between React and the browser’s DOM. They’re separate because React can render to other targets (React Native uses react-native instead of react-dom).

createRoot grabs that <div id="root"> and tells React: render everything inside here. Then .render(<App />) mounts the root component.

React 18 note: Older code used ReactDOM.render() directly. That’s gone in React 18. The new API is createRoot:

// Old (React 17 and below)
ReactDOM.render(<App />, document.getElementById('root'));

// New (React 18+)
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);

React.StrictMode: You’ll often see the app wrapped in <React.StrictMode> in index.js:

root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

StrictMode is a development-only tool — it has zero impact on production. What it does: intentionally double-invokes functions like render, useState, and lifecycle methods to help you catch side effects and bugs early. If your component behaves differently on the second call, something is wrong. You’ll see it in most CRA-generated projects. You can remove it without breaking anything.


JSX: HTML in Your JavaScript

Look at this:

const element = <h1>Hello, world</h1>;

That’s not a string. That’s not HTML. That’s JSX — a syntax extension that lets you write HTML-like markup inside JavaScript.

Browsers don’t understand JSX. Babel transforms it into regular JavaScript before it hits the browser:

// What JSX becomes after Babel transforms it
const element = React.createElement('h1', null, 'Hello, world');

React.createElement builds the Virtual DOM object. JSX is just a nicer way to write it.

One rule: JSX must return a single parent element. This fails:

return (
  <h1>Hello</h1>
  <p>World</p>   // Error — two siblings at the top level
);

Fix it by wrapping in a <div> — or better yet, a Fragment:

import { Fragment } from 'react';

return (
  <Fragment>
    <h1>Hello</h1>
    <p>World</p>
  </Fragment>
);

// Shorthand syntax
return (
  <>
    <h1>Hello</h1>
    <p>World</p>
  </>
);

Fragments let you group elements without adding extra DOM nodes. Semantic HTML stays intact.

className not class: In JSX, class is a reserved JavaScript keyword. Use className instead:

<div className="container">...</div>

JavaScript expressions in JSX: Wrap them in curly braces:

const name = 'Dhaivick';
return <h1>Hello, {name}</h1>;

// Inline styles use double curly brackets — outer {} for JS expression, inner {} for the object
return <div style={{ backgroundColor: 'blue', height: '500px' }}>...</div>;

Your First Component

A React component is a function (or class) that returns JSX. Here’s a functional component:

const Hello = () => {
  return <h1>Hello, world</h1>;
};

export default Hello;

Use it anywhere:

import Hello from './Hello';

// In your render/return:
<Hello />

The name must be capitalized. React uses this to distinguish your components from regular HTML tags. <div> is HTML. <Hello /> is your component.

export default means this file exports one main thing. Import it by name:

import Hello from './Hello'; // no curly braces needed

If a file exports multiple things (no default), use destructuring:

import { Hello, Goodbye } from './Greetings';

Class Components

Before hooks, state required a class component. You’ll encounter these in real codebases, so understand them.

import React, { Component } from 'react';

class Hello extends Component {
  render() {
    return <h1>Hello, world</h1>;
  }
}

export default Hello;

extends Component gives your class React’s powers — the render method, lifecycle hooks, state management. The render() method is mandatory and must return JSX.


Props: Passing Data to Components

Props (short for properties) are how you pass data from a parent component to a child. Think of them like HTML attributes, but for your custom components.

// Passing props
<Hello greeting="Hello, React Ninja" />

// Receiving props in a class component
class Hello extends Component {
  render() {
    return <h1>{this.props.greeting}</h1>;
  }
}

// Receiving props in a functional component
const Hello = (props) => {
  return <h1>{props.greeting}</h1>;
};

// Destructuring in the parameter (cleanest way)
const Hello = ({ greeting }) => {
  return <h1>{greeting}</h1>;
};

Props are read-only. A child component never modifies its props. The parent controls the data; the child just displays it.


Rendering Lists with .map()

Want to render a list of items? Use JavaScript’s .map(). It’s the React way.

const robots = [
  { id: 1, name: 'Robot One', email: '[email protected]' },
  { id: 2, name: 'Robot Two', email: '[email protected]' },
];

const CardList = ({ robots }) => {
  return (
    <div>
      {robots.map((robot) => (
        <Card key={robot.id} id={robot.id} name={robot.name} email={robot.email} />
      ))}
    </div>
  );
};

The key prop is mandatory when rendering lists. React uses it to track which items changed, added, or removed in the Virtual DOM. Without a unique key, React has to re-render the entire list every time anything changes. With keys, it surgically updates only what changed.

Use a unique ID from your data as the key. Avoid using the array index — if items reorder or get removed, the index-based keys cause subtle bugs.


Conditional Rendering

React doesn’t have special template syntax for conditionals. It’s just JavaScript inside JSX.

Ternary — when you need an either/or:

return isLoggedIn ? <Dashboard /> : <Login />;

&& operator — when you only need to render something if a condition is true:

return (
  <div>
    {isLoading && <Spinner />}
    {error && <p className="error">{error}</p>}
    {data && <DataTable rows={data} />}
  </div>
);

Read it as: “if isLoading is truthy, render <Spinner />.” If isLoading is false, React renders nothing.

Watch out for falsy zero. This is a classic bug:

// Bug — renders "0" on screen when count is 0
{count && <p>Count: {count}</p>}

// Fix — force a boolean
{count > 0 && <p>Count: {count}</p>}
{!!count && <p>Count: {count}</p>}

false, null, and undefined render nothing. But 0 is falsy and a valid React render value — it shows up as text. Always convert to a real boolean when the condition could be zero.

Early return — cleanest for loading/error states:

const UserProfile = ({ user }) => {
  if (!user) return <p>Loading...</p>;
  if (user.error) return <p>Error loading user.</p>;

  return <div>{user.name}</div>;
};

State: The Memory of Your App

Props are external — they come from the parent. State is internal — it lives inside a component and can change over time.

Think of state as the description of your app at any given moment. In a search app, state might be:

  • the list of robots
  • whatever is typed in the search box

State changes trigger re-renders. When state updates, React re-runs the component function and updates the UI.

State in a Class Component

class App extends Component {
  constructor(props) {
    super(props);  // required — calls React.Component's constructor
    this.state = {
      robots: [],
      searchField: '',
    };
  }

  render() {
    return <div>{/* ... */}</div>;
  }
}

To change state, never mutate this.state directly. Always use setState:

// Wrong
this.state.searchField = 'react';

// Right
this.setState({ searchField: event.target.value });

setState tells React: “something changed, re-render.” Direct mutation bypasses React entirely — the UI won’t update.

setState is asynchronous. This trips up almost every beginner:

this.setState({ count: this.state.count + 1 });
console.log(this.state.count); // still the OLD value

React batches state updates and applies them together for performance. The new state isn’t available on the next line.

When the new state depends on the old state, use the functional update form:

// Wrong — stale state in rapid updates
this.setState({ count: this.state.count + 1 });
this.setState({ count: this.state.count + 1 }); // both see the same old count

// Right — each update receives the latest state
this.setState(prevState => ({ count: prevState.count + 1 }));
this.setState(prevState => ({ count: prevState.count + 1 })); // works correctly

useState’s setter has the same behaviour — always use the functional form when the next state depends on the current one:

setCount(prev => prev + 1);

Handling Events

Event handling in JSX looks familiar but has one gotcha.

class App extends Component {
  onSearchChange = (event) => {
    this.setState({ searchField: event.target.value });
  };

  render() {
    return (
      <input
        type="search"
        placeholder="Search robots"
        onChange={this.onSearchChange}
      />
    );
  }
}

Why the arrow function? Regular methods lose this when called as event handlers. Arrow functions capture this from the surrounding scope (the class), so this.setState works correctly.

// This breaks — 'this' is undefined inside the event handler
onSearchChange(event) {
  this.setState({ searchField: event.target.value }); // Error!
}

// This works — arrow function preserves 'this'
onSearchChange = (event) => {
  this.setState({ searchField: event.target.value }); // ✓
};

Component Communication

Here’s the pattern. Search box and card list are siblings — they need to share data. How?

They can’t talk to each other directly. Instead, they both talk to their parent.

         App (owns state: robots, searchField)
        /                          \
  SearchBox                     CardList
  (calls onSearchChange)        (receives filtered robots)
  1. App creates an onSearchChange function and passes it as a prop to SearchBox.
  2. When the user types, SearchBox calls that function.
  3. App’s state updates.
  4. React re-renders CardList with filtered robots.
class App extends Component {
  constructor(props) {
    super(props);
    this.state = { robots: [], searchField: '' };
  }

  onSearchChange = (event) => {
    this.setState({ searchField: event.target.value });
  };

  render() {
    const { robots, searchField } = this.state;
    const filteredRobots = robots.filter((robot) =>
      robot.name.toLowerCase().includes(searchField.toLowerCase())
    );

    return (
      <div>
        <SearchBox searchChange={this.onSearchChange} />
        <CardList robots={filteredRobots} />
      </div>
    );
  }
}

This is lifting state up — the classic React pattern. When two sibling components need shared data, move the state to their common parent.


props.children

Every component automatically receives a special prop: children. It’s whatever you put between the opening and closing tags of your component.

const Scroll = (props) => {
  return (
    <div style={{ overflowY: 'scroll', height: '500px' }}>
      {props.children}
    </div>
  );
};

// Usage — CardList becomes the children of Scroll
<Scroll>
  <CardList robots={filteredRobots} />
</Scroll>

This is how you build wrapper components — layout containers, modals, theme providers, scroll areas. The child content is completely generic.


Smart vs. Dumb Components

Once you have state and logic, a useful pattern emerges:

Dumb (Presentational) Components — pure functions. Receive props, return JSX. No state, no side effects. They’re predictable: same input always gives the same output.

const Card = ({ name, email }) => (
  <div>
    <h2>{name}</h2>
    <p>{email}</p>
  </div>
);

Smart (Container) Components — manage state, make API calls, pass data down. In class-based React, these are the class components.

containers/
  App.js         ← smart, owns state
components/
  Card.js        ← dumb, just renders
  CardList.js    ← dumb, just renders
  SearchBox.js   ← dumb, just renders
  Scroll.js      ← dumb, just renders

This folder structure is intentional. Keep your logic in containers. Keep your rendering in components. It makes testing, debugging, and maintenance dramatically easier.


Lifecycle Methods

Class components have lifecycle methods — functions that React calls automatically at specific moments in a component’s life.

MOUNTING              UPDATING              UNMOUNTING
─────────────────     ─────────────────     ──────────────────
constructor()         getDerivedStateFrom   componentWillUnmount()
render()              Props()
componentDidMount()   shouldComponentUpdate()
                      render()
                      componentDidUpdate()

The three you’ll use most:

componentDidMount() — runs once after the component renders for the first time. Perfect for fetching data.

componentDidMount() {
  fetch('https://jsonplaceholder.typicode.com/users')
    .then(response => response.json())
    .then(users => this.setState({ robots: users }));
}

shouldComponentUpdate(nextProps, nextState) — runs before every re-render during the update phase. Return true to allow the re-render, false to block it. React re-renders by default on every setState call — shouldComponentUpdate is your escape hatch when you know a render would be wasteful.

shouldComponentUpdate(nextProps, nextState) {
  return nextProps.id !== this.props.id; // only re-render if id changed
}

componentWillReceiveProps(nextProps) — runs when a component is about to receive new props from its parent. Deprecated in React 16.3 (use getDerivedStateFromProps or componentDidUpdate instead), but you’ll encounter it in older codebases.

componentWillReceiveProps(nextProps) {
  if (nextProps.id !== this.props.id) {
    this.setState({ data: null }); // reset state when prop changes
  }
}

componentDidUpdate(prevProps, prevState) — runs after every re-render. Useful for reacting to state/prop changes.

componentWillUnmount() — runs just before the component is removed from the DOM. Use it to clean up timers, subscriptions, event listeners.

Execution order:

constructor → render → componentDidMount
                         ↓ (setState called)
                       render → componentDidUpdate

Fetching Data

React has no built-in HTTP library. Use the browser’s fetch API. Call it in componentDidMount so data loads after the first render:

componentDidMount() {
  fetch('https://jsonplaceholder.typicode.com/users')
    .then(response => response.json())
    .then(users => this.setState({ robots: users }));
}

During the initial render (before data arrives), show a loading state:

render() {
  const { robots, searchField } = this.state;

  if (!robots.length) {
    return <h1>Loading...</h1>;
  }

  const filteredRobots = robots.filter(robot =>
    robot.name.toLowerCase().includes(searchField.toLowerCase())
  );

  return (
    <div>
      <SearchBox searchChange={this.onSearchChange} />
      <CardList robots={filteredRobots} />
    </div>
  );
}

!robots.length evaluates to true when the array is empty. Clean ternary equivalent:

return !robots.length
  ? <h1>Loading...</h1>
  : <div>...</div>;

Error Boundaries

What happens when a component crashes mid-render? Before React 16, the entire app would break with a cryptic error. React 16 introduced Error Boundaries to handle this gracefully.

An Error Boundary is a class component that uses componentDidCatch:

class ErrorBoundary extends Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  componentDidCatch(error, info) {
    this.setState({ hasError: true });
  }

  render() {
    if (this.state.hasError) {
      return <h1>Something went wrong. Please try again.</h1>;
    }
    return this.props.children;
  }
}

Wrap any component that might throw:

<ErrorBoundary>
  <CardList robots={filteredRobots} />
</ErrorBoundary>

If CardList crashes, ErrorBoundary catches it. Users see a friendly message instead of a broken UI.


Production Build

When you’re ready to ship:

npm run build

This creates a build/ folder. Everything is:

  • Minified (whitespace and comments stripped)
  • Tree-shaken (unused code removed)
  • Transpiled via Babel (works in older browsers)
  • Optimized for performance

Take that build/ folder and host it anywhere — Netlify, Vercel, GitHub Pages, S3. It’s just static files.


Keeping Dependencies Updated

Libraries change. Security vulnerabilities get discovered. Here’s the update flow:

npm update            # update to latest minor versions within semver range
npm audit             # show known vulnerabilities
npm audit fix         # auto-fix what's fixable
npm audit fix --force # fix breaking changes too (review first)

package.json controls the allowed version range:

  • ^16.8.0 — accept minor and patch updates (16.x.x), not major (17.x.x)
  • ~16.8.0 — accept patch updates only (16.8.x)

package-lock.json locks exact versions. Commit it. It ensures everyone on your team runs the exact same dependency tree.


React Hooks

React 16.8 introduced Hooks — a way to use state and lifecycle features inside functional components, without writing a class.

Why? Three problems with class components:

  1. Stateful logic is hard to reuse across components
  2. Complex components get hard to read (lifecycle methods mix unrelated logic)
  3. Classes are confusing — this binding, super(props), constructor boilerplate

Hooks solve all three by letting you write everything as functions.

Two rules of hooks:

  1. Only call hooks inside React function components (not regular JS functions)
  2. Only call hooks at the top level (not inside loops, conditions, or nested functions)

useState

Replaces this.state and this.setState.

import { useState } from 'react';

const Counter = () => {
  const [count, setCount] = useState(0);  // initial state = 0

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
};

useState returns an array with two things: the current state value and the setter function. Array destructuring lets you name them whatever you want.

For multiple state values, call useState multiple times:

const [robots, setRobots] = useState([]);
const [searchField, setSearchField] = useState('');

No more giant this.state object. Each piece of state is independent.

Compare the same logic in class vs hooks:

// Class
this.setState({ searchField: event.target.value });

// Hooks
setSearchField(event.target.value);

useReducer

An alternative to useState for complex state logic. When state has multiple sub-values, or when the next state depends on the previous one in non-trivial ways, useReducer is cleaner.

import { useReducer } from 'react';

const initialState = { count: 0 };

const reducer = (state, action) => {
  switch (action.type) {
    case 'increment': return { count: state.count + 1 };
    case 'decrement': return { count: state.count - 1 };
    case 'reset':     return initialState;
    default:          return state;
  }
};

const Counter = () => {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
      <button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
    </div>
  );
};

useReducer takes a reducer function and an initial state. It returns the current state and a dispatch function. You dispatch actions — plain objects describing what happened — and the reducer decides how state changes.

useState vs useReducer — when to use which:

useState     → simple, independent values (a string, a boolean, a number)
useReducer   → multiple related values that change together, or complex update logic

If you find yourself writing several useState calls that always change at the same time, or using setState(prev => ...) in complicated ways, that’s a signal to reach for useReducer.


useRef

useRef returns a mutable object with a .current property. Two completely different use cases, same hook.

Use case 1 — access a DOM element directly:

import { useRef } from 'react';

const SearchBox = () => {
  const inputRef = useRef(null);

  const focusInput = () => {
    inputRef.current.focus(); // direct DOM access
  };

  return (
    <>
      <input ref={inputRef} type="text" />
      <button onClick={focusInput}>Focus</button>
    </>
  );
};

Use case 2 — persist a value between renders without triggering a re-render:

const Timer = () => {
  const intervalRef = useRef(null);

  const start = () => {
    intervalRef.current = setInterval(() => console.log('tick'), 1000);
  };

  const stop = () => {
    clearInterval(intervalRef.current);
  };

  return (
    <>
      <button onClick={start}>Start</button>
      <button onClick={stop}>Stop</button>
    </>
  );
};

The key distinction interviewers test: updating a ref does not cause a re-render. useState does. Use useRef when you need to remember a value across renders but don’t want that value change to update the UI.


useEffect

Replaces componentDidMount, componentDidUpdate, and componentWillUnmount — all in one hook.

import { useState, useEffect } from 'react';

const App = () => {
  const [robots, setRobots] = useState([]);

  useEffect(() => {
    fetch('https://jsonplaceholder.typicode.com/users')
      .then(response => response.json())
      .then(users => setRobots(users));
  }, []); // empty array = run once on mount

  return <CardList robots={robots} />;
};

The second argument controls when the effect runs:

useEffect(fn)          → runs after every render (dangerous — can infinite loop)
useEffect(fn, [])      → runs once on mount (componentDidMount equivalent)
useEffect(fn, [value]) → runs when 'value' changes (componentDidUpdate equivalent)

The infinite loop trap. If you call setRobots inside useEffect without a dependency array, this happens:

render → useEffect → setRobots → re-render → useEffect → setRobots → ...

Always pass the dependency array. For fetch-on-mount, pass an empty array [].

Cleanup with useEffect:

useEffect(() => {
  const timer = setInterval(() => console.log('tick'), 1000);

  return () => clearInterval(timer); // cleanup on unmount
}, []);

Return a function from useEffect to clean up — equivalent to componentWillUnmount.


Custom Hooks

You can build your own hooks to extract and reuse stateful logic across components.

const useFetch = (url) => {
  const [data, setData] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetch(url)
      .then(res => res.json())
      .then(result => {
        setData(result);
        setLoading(false);
      });
  }, [url]);

  return { data, loading };
};

// Use it anywhere
const App = () => {
  const { data: robots, loading } = useFetch('https://jsonplaceholder.typicode.com/users');

  if (loading) return <h1>Loading...</h1>;
  return <CardList robots={robots} />;
};

Custom hooks start with use (convention React enforces for the linting rules). They’re just functions that call other hooks.


Context API

Props work great for parent-to-child communication. But what if you need to pass data through many layers of components that don’t actually use it — they’re just passing it down to reach a child deep in the tree? That’s called prop drilling, and it gets messy fast.

App (has user)
  └── Layout (doesn't need user, just passes it)
        └── Sidebar (doesn't need user, just passes it)
              └── UserAvatar (finally uses user)

Context solves this. It lets you broadcast a value to any component in the tree — no matter how deep — without threading it through props.

Step 1 — Create the context:

import { createContext } from 'react';

const UserContext = createContext(null); // null is the default value

Step 2 — Provide it high in the tree:

const App = () => {
  const [user, setUser] = useState({ name: 'Dhaivick' });

  return (
    <UserContext.Provider value={user}>
      <Layout />
    </UserContext.Provider>
  );
};

Step 3 — Consume it anywhere below, with useContext:

import { useContext } from 'react';

const UserAvatar = () => {
  const user = useContext(UserContext); // no props needed

  return <img src={user.avatarUrl} alt={user.name} />;
};

Layout and Sidebar don’t touch user at all. UserAvatar just reaches into context and grabs it directly.

When to use Context vs props:

Props    → a few levels deep, direct parent-child relationships
Context  → global data needed by many components (theme, auth, language)

Context isn’t a state management replacement — it doesn’t optimise re-renders the way Redux or Zustand do. Every component that consumes a context re-renders when that context value changes. For high-frequency updates (like mouse position), prefer other solutions. For stable values (user, theme, locale), Context is perfect.


Classes vs. Hooks: Which One?

The honest answer: you need to know both.

Real-world codebases still have class components everywhere. Facebook itself has tens of thousands of them. Hooks don’t replace classes — they’re an alternative.

When you’re starting fresh, hooks are the modern approach. When you’re working on an existing codebase, you’ll see both. Know how to read each.


Interview Essentials

These don’t always appear in beginner courses but come up constantly in interviews.

Reconciliation

When state changes, React creates a new Virtual DOM tree and diffs it against the previous one. This process is called reconciliation. React’s diffing algorithm is O(n) — it compares nodes level by level and only updates what changed.

This is why the key prop matters so much. Without it, React can’t efficiently track list items during reconciliation.

Controlled vs. Uncontrolled Components

A controlled component has its value managed by React state:

const [value, setValue] = useState('');

<input
  value={value}
  onChange={(e) => setValue(e.target.value)}
/>

React owns the source of truth. You always know what’s in the input.

An uncontrolled component manages its own state through the DOM (using a ref):

const inputRef = useRef(null);

<input ref={inputRef} />
// Read value with: inputRef.current.value

Prefer controlled components. They’re easier to validate, test, and reason about.

React.memo

Prevents functional components from re-rendering if their props haven’t changed:

const Card = React.memo(({ name, email }) => {
  return (
    <div>
      <h2>{name}</h2>
      <p>{email}</p>
    </div>
  );
});

Without React.memo, every time the parent re-renders, Card re-renders too — even if name and email are identical. React.memo adds a shallow prop comparison. If nothing changed, the render is skipped.

PureComponent does the same thing for class components.

useMemo and useCallback

useMemo memoizes the result of a computation — only recalculates when dependencies change:

const filteredRobots = useMemo(
  () => robots.filter(robot => robot.name.toLowerCase().includes(searchField.toLowerCase())),
  [robots, searchField]
);

Without useMemo, the filter runs on every render even when robots and searchField haven’t changed.

useCallback memoizes a function reference:

const onSearchChange = useCallback((event) => {
  setSearchField(event.target.value);
}, []); // recreate only when dependencies change

Why does this matter? If you pass a function as a prop to a memoized child component, a new function reference every render breaks the memoization. useCallback keeps the reference stable.

Use these for performance-sensitive components. Don’t reach for them by default — the overhead of memoization is real too.


The Mental Model

Here’s React in one paragraph:

Your app has state — data that can change. State lives in components (usually parent/container components). When state changes, React re-renders the component and all its children. It compares the new Virtual DOM against the old one, figures out the minimum number of real DOM changes, and makes them. You describe what the UI should look like. React handles how to get there.

That’s it. Everything else is just details.