Learn Frontend Part 10: React Fundamentals - JSX, Components, Props, State and Reconciliation

Learn Frontend Part 10: React Fundamentals - CodingHumans

React is the most used library for building user interfaces, and almost every frontend interview now has a React round. Most of those questions are not about obscure APIs. They test whether you understand the model: components are functions of props and state, React re-renders them when state changes, and it then works out the smallest set of DOM changes.

This is Part 10 of the Learn Frontend series. It builds on the DOM and events and JavaScript fundamentals parts. We cover JSX, components and props, state, how rendering and reconciliation work, lists and keys, events and forms, and composition. Hooks get their own deep dive in Part 11.

1. Why React exists

Before React, UIs were usually built imperatively: find a DOM node, change its text, add a class, remove a child. As apps grew, it became hard to keep the DOM in sync with the data, and bugs crept in whenever one update path forgot to change something.

React flips this around. You describe what the UI should look like for a given state, and React takes care of how to update the DOM to match. Three ideas make this work:

  • Declarative UI: a component returns a description of the UI. You never call appendChild yourself.
  • Components: the UI is split into small, reusable functions that each own a piece of the screen.
  • One-way data flow: data moves down from parent to child through props, and changes move up through callbacks. This makes it easy to trace where a value came from.

A useful mental model is UI = f(state). Given the same props and state, a component should always return the same output.

2. JSX: HTML-like syntax in JavaScript

JSX looks like HTML, but it is just syntax that a compiler (Babel, SWC, esbuild) turns into function calls. With the modern JSX transform, <h1 className="title">Hi</h1> becomes jsx("h1", { className: "title", children: "Hi" }), which returns a plain JavaScript object called a React element.

const name = "Asha";
const isAdmin = true;

function Greeting() {
  return (
    <>
      <h1 className="title">Hello, {name.toUpperCase()}!</h1>
      {isAdmin ? <p>You have admin access.</p> : <p>Welcome back.</p>}
      <label htmlFor="email">Email</label>
      <input id="email" type="email" />
    </>
  );
}

Rules worth remembering:

  • Use {} to embed any JavaScript expression. Statements like if or for cannot go directly inside JSX.
  • Attributes use camelCase: className instead of class, htmlFor instead of for, onClick instead of onclick.
  • A component must return a single root. Use a Fragment (<>...</>) to group siblings without adding an extra DOM node.
  • JSX escapes values by default, so {userInput} is safe from XSS. Only dangerouslySetInnerHTML bypasses this.
  • Component names must start with a capital letter. Lowercase tags are treated as built-in HTML elements.

3. Components and props

A component is a JavaScript function that takes a props object and returns JSX. Props are how a parent passes data and callbacks to a child.

function Card({ title, children }) {
  return (
    <section className="card">
      <h2>{title}</h2>
      {children}
    </section>
  );
}

function UserProfile({ user, size = 48 }) {
  return (
    <Card title={user.name}>
      <img src={user.avatar} width={size} height={size} alt="" />
      <p>{user.bio}</p>
    </Card>
  );
}
  • Props are read-only. A component must never change its own props. If a value needs to change, it should be state, owned by some component.
  • children is a special prop holding whatever you put between the opening and closing tags. It lets you build wrappers like Card, Modal or Layout.
  • Destructure props in the parameter list and give defaults there: function Avatar({ size = 40 }).
  • Class components still exist in older codebases, but new code uses function components with hooks.

4. State: data that changes over time

Props come from outside. State is data a component owns and can change. When you call the state setter, React schedules a re-render of that component (and its children) with the new value.

import { useState } from "react";

function Counter() {
  const [count, setCount] = useState(0);

  function addThree() {
    // Wrong: all three read the same snapshot, so count goes up by 1
    // setCount(count + 1); setCount(count + 1); setCount(count + 1);

    // Right: each updater receives the latest value
    setCount(c => c + 1);
    setCount(c => c + 1);
    setCount(c => c + 1);
  }

  return <button onClick={addThree}>Count: {count}</button>;
}

// Updating arrays and objects without mutation
const [todos, setTodos] = useState([]);
setTodos(prev => [...prev, { id: crypto.randomUUID(), text: "Learn React", done: false }]);
setTodos(prev => prev.map(t => t.id === id ? { ...t, done: !t.done } : t));
setTodos(prev => prev.filter(t => t.id !== id));

Key behaviours that interviewers love to test:

  • State is a snapshot. Inside one render, count is a constant. Calling setCount(count + 1) three times in a row only adds 1, because all three calls read the same snapshot.
  • Use the updater form setCount(c => c + 1) when the next value depends on the previous one.
  • Batching: since React 18, multiple updates in the same event, timeout or promise are batched into a single re-render.
  • Never mutate state. items.push(x); setItems(items) passes the same array reference, so React may skip the update. Create a new array or object instead: setItems([...items, x]).
  • Keep state minimal. If a value can be computed from props or other state, compute it during render instead of storing it.

5. How rendering and reconciliation work

A React update happens in two phases:

  1. Render phase: React calls your component functions to get a new tree of React elements. This must be pure: no DOM changes, no network calls, no subscriptions.
  2. Commit phase: React compares the new tree with the previous one and applies only the differences to the real DOM. Then it runs effects.

The comparison step is called reconciliation. A full tree diff would be O(n3), so React uses two heuristics to make it O(n):

  • If the element type changes at a position (for example <div> to <span>, or ProfileA to ProfileB), React throws away the old subtree, including its state, and builds a new one.
  • For lists of children, React uses keys to match old and new items.

Important: a re-render is not the same as a DOM update. A component can re-render many times while the DOM changes rarely or not at all. When a parent re-renders, all its children re-render by default, even if their props did not change. React.memo, covered in Part 11, can skip that. The internal engine that does this work is called Fiber; it splits rendering into small units so React can pause and prioritise urgent updates. Read more in how React rendering and reconciliation work.

6. Lists and keys

To render a list, map over an array and return an element for each item. Every item needs a key that is unique among its siblings and stable across renders.

function TodoList({ todos }) {
  if (todos.length === 0) return <p>Nothing to do.</p>;

  return (
    <ul>
      {todos.map(todo => (
        // Good: stable id from the data
        <li key={todo.id}>{todo.text}</li>
      ))}
    </ul>
  );
}
  • Keys tell React which item is which between renders, so it can move, insert or remove DOM nodes instead of rebuilding them.
  • Avoid array index as key when the list can be reordered, filtered or have items inserted. If item 0 is deleted, every later item gets a new index, so React matches the wrong state (for example, input text or checkbox state) to the wrong row.
  • Index keys are fine for static lists that never change order.
  • Never generate keys with Math.random() during render. A new key every render forces React to unmount and remount every item.
  • Changing a component's key on purpose is a handy trick to reset its state, for example <Form key={userId} />.

7. Conditional rendering, events and forms

Conditional rendering is plain JavaScript: use a ternary, &&, or an early return. Watch out for {count && <Badge />}: when count is 0, React renders the number 0. Use {count > 0 && ...} instead.

Events are passed as functions, not called: onClick={handleClick}, not onClick={handleClick()}. React wraps native events in a SyntheticEvent that behaves the same across browsers, and since React 17 it attaches listeners at the root container rather than on document.

Controlled inputs keep the input value in state, so React is the single source of truth:

function SignupForm() {
  const [email, setEmail] = useState("");
  const isValid = email.includes("@"); // derived, not stored in state

  function handleSubmit(e) {
    e.preventDefault(); // stop the browser from reloading the page
    console.log("Submitting", email);
  }

  return (
    <form onSubmit={handleSubmit}>
      <input value={email} onChange={e => setEmail(e.target.value)} />
      {!isValid && email.length > 0 && <p className="error">Enter a valid email</p>}
      <button disabled={!isValid}>Sign up</button>
    </form>
  );
}

An uncontrolled input keeps its own value in the DOM, and you read it with a ref when needed. Controlled inputs make validation and conditional UI easy. Uncontrolled inputs need less code and can be useful for large forms or file inputs.

8. Lifting state up and composition

When two siblings need the same data, move the state to their closest common parent and pass it down as props. This is called lifting state up. The parent owns the value, and children receive it along with a callback to change it.

function TemperatureApp() {
  const [celsius, setCelsius] = useState(25); // state lives in the parent

  return (
    <>
      <CelsiusInput value={celsius} onChange={setCelsius} />
      <FahrenheitView celsius={celsius} />
    </>
  );
}

function CelsiusInput({ value, onChange }) {
  return <input type="number" value={value} onChange={e => onChange(Number(e.target.value))} />;
}

function FahrenheitView({ celsius }) {
  return <p>{(celsius * 9) / 5 + 32} F</p>;
}

React prefers composition over inheritance. Instead of extending components, you combine them, pass components as children, or pass them as props (sometimes called slots). If you find yourself passing the same prop through many layers that do not use it (prop drilling), first try composition, then reach for Context, which Part 12 covers.

9. Common beginner mistakes

  • Mutating state or props directly instead of creating new objects.
  • Calling a setter during render, which causes an infinite render loop.
  • Storing derived data in state and then forgetting to keep it in sync.
  • Using array index as key in a list that changes.
  • Defining a component inside another component. A new function each render means a new type, so React remounts it and loses its state.
  • Expecting state to update immediately after calling the setter.
  • Writing onClick={doThing()}, which runs the function during render.
Key takeaways
  • React is declarative: describe the UI for the current state, and React updates the DOM to match.
  • JSX compiles to function calls that return plain objects called React elements.
  • Props flow down and are read-only. State is owned by a component and changed only through its setter.
  • State is a snapshot per render. Use the updater form when the next value depends on the previous one, and never mutate.
  • Rendering means calling components. Committing means changing the DOM. Reconciliation diffs element trees using type and keys.
  • Use stable, unique keys from your data, not array indexes, for lists that change.
  • Lift shared state to the closest common parent, and prefer composition over inheritance.

Practice problems

Build these small apps without a tutorial. Each one exercises a different fundamental.

  1. Build a counter with increment, decrement and reset buttons, and disable decrement at 0.
  2. Build a todo list with add, toggle done, delete and a "remaining items" count computed during render.
  3. Render a list of 5 inputs with index keys, delete the first item, and observe the bug. Fix it with stable ids.
  4. Build a tabs component where the parent owns the active tab and passes it to a TabList and a TabPanel.
  5. Build a controlled signup form with email and password validation and a disabled submit button.
  6. Create a reusable Modal component that renders its children and closes on a button click.

Interview questions and FAQ

What is the virtual DOM?

It is the in-memory tree of React elements that components return. React compares the new tree with the previous one and applies only the differences to the real DOM. The benefit is not that it is always faster than hand-written DOM code, but that you can write declarative code and still get reasonably efficient updates.

What is the difference between props and state?

Props are passed in by the parent and are read-only for the child. State is owned by the component itself and changes through its setter. Changing either one causes a re-render.

Why should you not use the index as a key?

If items are inserted, removed or reordered, indexes shift, so React matches old state and DOM nodes to the wrong items. This causes bugs with inputs and animations, and extra work. Use a stable id from the data.

Is setState synchronous or asynchronous?

The setter does not change the value right away. It schedules a re-render, and React batches updates. The current render keeps seeing the old value, which is why you read the new value in the next render or use the updater function.

What is the difference between a controlled and an uncontrolled component?

A controlled input takes its value from React state and reports changes through onChange. An uncontrolled input keeps its value in the DOM, and you read it with a ref. Controlled inputs are easier to validate. Uncontrolled ones need less code.

When does a component re-render?

When its own state changes, when its parent re-renders, or when a context it uses changes. Prop changes alone do not trigger a render; it is the parent rendering that passes new props.

Comments

Answered 0/0Correct 0