Lesson 4 of 20

Components

A Component Is Just a Function

A React component is a JavaScript function that returns UI. There is no special base class to extend, no configuration object, no registration step. If a function returns JSX and its name starts with a capital letter, React can use it as a component.

The capital letter is not a style convention you can ignore — it changes what your code compiles to. When JSX sees a lowercase tag such as <button>, it produces the string 'button', which React understands as a built-in HTML element. When it sees a capitalised tag such as <Button>, it produces a reference to the variable Button in scope. Name a component userCard and write <userCard />, and React will hunt for an HTML element called usercard, find nothing meaningful, and render an empty unknown tag. No error, just a blank space where your component should be.

Once defined, you use a component exactly as you would use an HTML tag. <UserCard /> sits in your JSX next to <div> and <p>, and it can be used as many times as you like. Each use creates an independent instance with its own state, which is what makes it safe to render forty product cards from one definition.

Example
// A component: a function, a capital letter, some JSX.
function Badge({ text }) {
  return <span className="badge">{text}</span>;
}

// Components use other components, exactly like HTML tags.
function StudentCard({ name, course, isNew }) {
  return (
    <div className="card">
      <h3>{name}</h3>
      <p>{course}</p>
      {isNew && <Badge text="New" />}
    </div>
  );
}

function Batch() {
  return (
    <div className="grid">
      <StudentCard name="Aarav Mehta" course="Full Stack" isNew />
      <StudentCard name="Divya Nair" course="Data Science" />
      <StudentCard name="Kabir Singh" course="Full Stack" />
    </div>
  );
}

// <studentCard />  — lowercase: React looks for an HTML tag and renders nothing useful.
Notes
  • You will occasionally meet React components written as class declarations with methods like render() and componentDidMount(). That was the only way to hold state before hooks arrived. It still works and you should be able to read it, but no new code is written that way, and this course uses function components throughout.

Splitting a Screen into Components

Knowing the syntax is easy; knowing where to cut is the actual skill. A useful way to start is to print the design, or sketch it, and draw boxes around every region that has a name in your head. A box you can name — filter bar, product card, price block, pagination — is a candidate component. A box you cannot name is usually not one.

Take a product listing page. The whole page is one component. Inside it there is a search and filter area, a grid of products, and pagination controls at the bottom. Inside the grid, each product is a card; inside a card there is an image, a title, a price and an Add to Cart button. That is four or five components, and it is already far easier to work with than one function containing all of it.

The second question to ask is: does this piece appear more than once, or does it change independently of everything around it? A price block that is used in the grid, the cart and the order summary should certainly be a component. A component that re-renders on every keystroke — the search box — is worth separating from a component that re-renders rarely, because the boundary limits how much of the screen React has to reconsider.

Resist the urge to design the perfect tree in advance. Write the page as one component, get it working, and then pull pieces out when a piece grows large, repeats, or needs its own state. Refactoring JSX into a new component is a copy, a paste and a props list — cheap enough that guessing wrong costs almost nothing.

Example
// One page, cut along the lines you can name.

function ProductPage() {
  return (
    <div className="page">
      <FilterBar />
      <ProductGrid />
      <Pagination />
    </div>
  );
}

function ProductGrid() {
  const products = getProducts();
  return (
    <div className="grid">
      {products.map(p => <ProductCard key={p.id} product={p} />)}
    </div>
  );
}

function ProductCard({ product }) {
  return (
    <article className="card">
      <img src={product.image} alt={product.name} />
      <h3>{product.name}</h3>
      <PriceTag price={product.price} mrp={product.mrp} />
      <button>Add to cart</button>
    </article>
  );
}

// Reused in the grid, the cart and the order summary — clearly its own component.
function PriceTag({ price, mrp }) {
  const off = Math.round(((mrp - price) / mrp) * 100);
  return (
    <p className="price">
      ₹{price} <s>₹{mrp}</s> <span>{off}% off</span>
    </p>
  );
}
  • If you can give the region a name, it can probably be a component
  • If the markup appears in two places, make it a component
  • If a piece has its own state or changes on its own schedule, separate it
  • If a function is getting long enough that you scroll to read it, split it
  • Do not split just to make files smaller — a component with eleven props is usually a bad cut

One Component per File, and How to Export It

The convention almost every React project follows is one component per file, with the file named after the component: ProductCard.jsx holds ProductCard. Small helper components used only by that file can live alongside it, but anything used elsewhere gets its own file. This is not a rule React enforces — it is a rule that makes a project navigable six months later.

You then export the component so other files can import it. A default export allows one export per file and lets the importer choose any name. A named export requires the importer to use the exact name in braces. Both are common; teams usually pick one and stick to it.

The trap in default exports is precisely the freedom to rename. Import ProductCard as Card by mistake, or import from the wrong file, and nothing complains — you get a working import of the wrong thing, or of undefined. If a component renders as blank and the console says something about an element type being invalid, check the import line before you touch the component.

Example
// src/components/ProductCard.jsx
export default function ProductCard({ product }) {
  return <article className="card">{product.name}</article>;
}

// src/components/PriceTag.jsx  — named export instead
export function PriceTag({ price }) {
  return <p className="price">₹{price}</p>;
}

// Using them
import ProductCard from './components/ProductCard';       // default: no braces
import { PriceTag } from './components/PriceTag';         // named: braces required

// Common failure:
// import { ProductCard } from './components/ProductCard';
//   -> ProductCard is undefined, and React reports an invalid element type.
Notes
  • React file paths are relative to the file doing the importing. './Button' means a sibling file, '../Button' means one folder up. Match the capitalisation of the file name exactly — it works either way on Windows and macOS, and breaks on the Linux server you deploy to.

Rendering Must Be Pure

React reserves the right to call your component function whenever it likes, more than once for the same data, and to throw the result away. That is only safe if your component behaves like a calculation: given the same props and state, it returns the same UI and changes nothing outside itself. React calls this being pure, and breaking it produces bugs that appear and disappear depending on timing.

In practice this means three prohibitions while rendering. Do not modify props or any variable declared outside the component. Do not touch the DOM directly. Do not start a network request or a timer. All of those belong either in an event handler, which runs in response to a user action, or in an effect, which runs after rendering — and both get their own lessons.

The example below is a real bug that catches people. Array.prototype.sort sorts in place, so calling products.sort() during render mutates the array the parent owns. The parent's data is now silently reordered, other components using the same array see a different order, and in development you may see the list flip around for no visible reason. The fix is one character: copy the array first with the spread operator, then sort the copy.

Example
// BROKEN: sort() mutates the array the parent passed in.
function ProductList({ products }) {
  products.sort((a, b) => a.price - b.price);   // mutating a prop
  return <ul>{products.map(p => <li key={p.id}>{p.name}</li>)}</ul>;
}

// FIXED: copy, then sort the copy.
function ProductList({ products }) {
  const sorted = [...products].sort((a, b) => a.price - b.price);
  return <ul>{sorted.map(p => <li key={p.id}>{p.name}</li>)}</ul>;
}

// Also impure — a value outside the component being changed on every render:
let renderCount = 0;
function Header() {
  renderCount++;                        // don't
  document.title = 'Products';          // don't (this belongs in an effect)
  return <h1>Products ({renderCount})</h1>;
}
Notes
  • This is exactly why StrictMode runs your components twice in development. Impure code behaves differently when called twice, so the extra call turns a bug that would have surfaced randomly in production into one you notice on day one.

Composition Instead of Inheritance

If you have studied object-oriented programming, your instinct for reuse is probably inheritance: make a base Card, then extend it into ProductCard and UserCard. React does not work that way, and the official guidance is explicit — there is no recommended use case for inheriting one component from another.

The React answer is composition: a general component accepts the specific parts as props, including whole chunks of UI. The special children prop holds whatever you put between a component's opening and closing tags, which lets you write a generic shell — a card, a modal, a page layout, a two-column split — and pass the contents in from outside.

You can go further and accept JSX through ordinary named props when a component has several slots. A Modal that takes title, children and footer can be reused for a delete confirmation, a login form and a filter panel without knowing anything about any of them. That is the entire pattern, and it covers essentially every case inheritance would have.

Example
// A generic shell that knows nothing about its contents
function Panel({ title, footer, children }) {
  return (
    <section className="panel">
      <header><h3>{title}</h3></header>
      <div className="panel-body">{children}</div>
      {footer && <footer className="panel-footer">{footer}</footer>}
    </section>
  );
}

// Two very different screens, one component
function DeleteConfirm({ onCancel, onDelete }) {
  return (
    <Panel
      title="Delete this order?"
      footer={
        <>
          <button onClick={onCancel}>Cancel</button>
          <button onClick={onDelete}>Delete</button>
        </>
      }
    >
      <p>This cannot be undone.</p>
    </Panel>
  );
}

function ProfilePanel({ user }) {
  return (
    <Panel title="Your profile">
      <p>{user.name}</p>
      <p>{user.email}</p>
    </Panel>
  );
}
  • children — everything between the opening and closing tags
  • A prop can hold JSX, so a component can have several named slots
  • Wrap a general component in a specific one to lock in defaults
  • Never extend one component from another; pass props instead

The Nested-Definition Trap

One mistake deserves its own section because the symptom looks like a React bug rather than a coding error. Never define a component inside another component's body.

Every time the outer component renders, the function declaration inside it runs again and creates a brand-new function. React compares component types between renders to decide what to keep. A new function is a different type, so React concludes the old component is gone, destroys it along with all of its state, and mounts a fresh one. The visible result is an input that loses its text as you type, a checkbox that resets, or a component whose state clears whenever anything on the page changes.

The fix is always the same: move the definition out to the top level of the file. If the inner component needs data from the outer one, pass it as a prop.

Example
// BROKEN: SearchBox is a new function on every render of Toolbar.
function Toolbar() {
  const [count, setCount] = useState(0);

  function SearchBox() {                 // defined inside — do not do this
    const [query, setQuery] = useState('');
    return <input value={query} onChange={e => setQuery(e.target.value)} />;
  }

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Clicked {count}</button>
      <SearchBox />   {/* loses its text every time the button is clicked */}
    </div>
  );
}

// FIXED: top-level definition, state survives.
function SearchBox() {
  const [query, setQuery] = useState('');
  return <input value={query} onChange={e => setQuery(e.target.value)} />;
}

function Toolbar() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Clicked {count}</button>
      <SearchBox />
    </div>
  );
}
Notes
  • The same reasoning explains why components should be declared at the top level of a module and imported where needed. If a component is only ever used by one other component, keeping it in the same file is fine — just keep it outside the other function.
Ask AI