PresentationInteractive, narrated presentation of the section content.

Code ExampleInteractive coding exercise.

Compile: idle | Execute: idle | Target: web
index.htmlclean
Loading...

No problems found.

ContentDetailed description of the section content.

Customizing React Components with Props

Alright, everyone, let’s dive deeper into React components today and explore how we can customise them to make them more dynamic and reusable. So far, we’ve covered the basics of components, but now we’re going to level up by focusing on something called props—short for "properties." Props are the key to passing data into components, and they’re what make React components so flexible. Let’s get started by looking at how we can create reusable components using props, and I’ll walk you through it step-by-step with some examples.

Imagine we’re building a simple Greeting component that displays a personalised hello message for a logged-in user. We want this component to be reusable, so we’ll pass in the user’s name as a prop. Here’s what that looks like in code:

import { render } from 'react';

type GreetingProps = {
  name: string;
};

const Greeting = (props: GreetingProps) => (
  <div>Hello: {props.name}</div>
);

const App = () => (
  <div>
    <Greeting name="Tomas" />
    <Greeting name="Valeria" />
  </div>
);

render(<App />);

Let’s break this down. First, we define a TypeScript type called GreetingProps—it’s a convention to name it after the component with a Props suffix. Here, it has just one property, name, which is a string. The Greeting component takes props as its parameter, and inside the JSX, we use {props.name} to interpolate the value of the name prop. Then, in the App component, we use Greeting twice, passing different names—"Tomas" and "Valeria"—as the name prop. When this renders, we’ll see "Hello: Tomas" and "Hello: Valeria" on the screen. Pretty easy, right? You can already see how props let us reuse the same component with different data.

Now, let’s talk about how we control and check the types of props we pass in. Since we’re using TypeScript, we get static, compile-time checking, which is great because it catches errors early—unlike vanilla JavaScript, where you’d rely on runtime checks. Let’s stick with our Greeting example for a moment. The name prop is typed as a string. But what happens if we try to pass something else, like a number? Say we write <Greeting name={42} />. If you’re using a TypeScript-enabled editor like Visual Studio Code, you’ll see an error pop up right away: “Type ‘number’ is not assignable to type ‘string’.” That’s TypeScript doing its job.

But here’s the interesting part: if you run this code, it’ll still work! Why? Because TypeScript’s type checking disappears when your code gets compiled to JavaScript—it’s purely a development-time tool. That said, if you’re using a framework like Next.js, it’ll catch these type errors during the production build process and flag them for you. To fix this in our example, we could change the type of name to number:

type GreetingProps = {
  name: number;
};

And the error goes away—or we could use a union type, like string | number, if we want to allow both. This flexibility is one of TypeScript’s strengths.

Next, let’s explore a scenario where we want a prop to accept arbitrary JSX or other renderable content. For this, React gives us a special type called ReactNode. It’s super powerful because it covers everything you can render in React—strings, numbers, JSX elements, you name it. Let’s extend our Greeting component to accept a content prop of type ReactNode:

import React, { render } from 'react';

type GreetingProps = {
  name: string;
  content: React.ReactNode;
};

const Greeting = (props: GreetingProps) => (
  <div>
    Hello: {props.name}
    <div>{props.content}</div>
  </div>
);

const App = () => (
  <div>
    <Greeting name="Tomas" content={<b>Welcome back!</b>} />
    <Greeting name="Valeria" content={42} />
  </div>
);

render(<App />);

Here, we pass JSX (<b>Welcome back!</b>) to the first Greeting and a number (42) to the second. Both render perfectly because ReactNode handles it all. This is really useful when you want a component to be flexible about what it displays.

So far, we’ve been passing props as attributes, but what if we want to create components with nested, hierarchical structures—like HTML elements inside other elements? That’s where the <strong>children</strong> prop comes in. Let’s build a Group component that renders a fieldset with a title and some nested content, like a to-do list. Here’s the code:

import React, { render } from 'react';

type GroupProps = {
  name: string;
  children: React.ReactNode;
};

const Group = (props: GroupProps) => (
  <fieldset>
    <legend>{props.name}</legend>
    {props.children}
  </fieldset>
);

const App = () => (
  <div>
    <Group name="To Do">
      <ul>
        <li>Item 1</li>
        <li>Item 2</li>
      </ul>
    </Group>
    <Group name="Done">
      <ul>
        <li><s>Item 1</s></li>
        <li><s>Item 2</s></li>
      </ul>
    </Group>
  </div>
);

render(<App />);

In this example, the Group component has two props: name for the title and children for the nested content. We use <legend>{props.name}</legend> to display the title, and {props.children} to render whatever’s nested inside the component tags. When we use it in App, we pass a list of to-do items for one group and a list of completed items (with strikethroughs) for the other. This setup lets us reuse the Group component with different titles and content—pretty cool, huh?

Now, let’s switch gears a bit and talk about destructuring props, which is a handy JavaScript feature we can use in React. In JavaScript, destructuring lets you pull individual values out of objects or arrays. For example:

const [a, b] = [1, 2]; // a = 1, b = 2
const { a, b, ...rest } = { a: 1, b: 2, c: 3 }; // a = 1, b = 2, rest = { c: 3 }

In React, we can destructure props to make our code cleaner. Compare these two versions of a Greeting component:

type Props = { name: string; age: number };

// Without destructuring
const Greeting = (props) => (
  <div>Hello {props.name}, age {props.age}</div>
);

// With destructuring
const Greeting = ({ name, ...rest }) => (
  <div>Hello {name}, age {rest.age}</div>
);

In the second version, we destructure name directly and use the spread operator (...rest) to capture the remaining props. It’s more concise and easier to read.

Destructuring also helps us handle optional props and default values. Let’s make age optional by adding a question mark in the type definition:

type Props = { 
  name: string;
  age?: number;
};

const Greeting = ({ name, age = 44 }) => (
  <div>Hello {name}, your age {age}</div>
);

const App = () => (
  <>
    <Greeting name="Tomas" />
    <Greeting name="Valeria" age={30} />
  </>
);

render(<App />);

Here, age defaults to 44 if it’s not provided. When we render <Greeting name="Tomas" />, it shows "Hello Tomas, your age 44," but <Greeting name="Valeria" age={30} /> shows "Hello Valeria, your age 30." This is standard JavaScript destructuring at work, and it’s super useful in React.

What about boolean props? There’s a shorthand for those. Check this out:

type Props = {
  name: string;
  age: number;
  showAge: boolean;
};

const Greeting = ({ name, age, showAge }) => (
  <div>
    Hello {name}, {showAge ? `age ${age}` : 'age hidden'}
  </div>
);

const App = () => (
  <>
    <Greeting name="Tomas" age={60} showAge />
    <Greeting name="Valeria" age={30} showAge={false} />
  </>
);

render(<App />);

If showAge is true, you just write showAge without a value—it’s implied. If it’s false, you explicitly write showAge={false}. This shorthand keeps your JSX clean.

Sometimes, components share props with their children. Instead of passing each prop individually, you can use the spread operator:

const Greeting = (props) => (
  <div>
    <h1>Greeting</h1>
    <InnerGreeting showAge {...props} />
  </div>
);

Here, ...props passes all the props from Greeting to InnerGreeting in one go. It’s a quick way to share data down the component tree.

One last thing: props are read-only. You should never try to modify them directly inside a component. For example:

import { render } from 'react';

const Greeting1 = (props) => {
  props.name = "Tomas"; // Don’t do this!
  return <div>{props.name}</div>;
};

const Greeting2 = ({ name }) => {
  let newName = name; // Do this instead
  newName = "Tomas";
  return <div>{newName}</div>;
};

render(<Greeting1 />);
render(<Greeting2 />);

In Greeting1, trying to change props.name will throw an error in development mode because props are immutable. In Greeting2, we destructure name and assign it to a new variable, which we can modify safely. That said, modifying props-like data isn’t usually the best approach—we’ll talk more about state management later.

That’s it for today! We’ve covered how to use props to customize components, handle types with TypeScript, work with children and ReactNode, and use destructuring for cleaner code. Next time, we’ll build on this with component state. Any questions?

👾 Exercise

 

No hints available.

Discuss with OthersAsk questions, share your thoughts, and discuss with other learners.

Loading discussion ...