In React, components are the foundation of your application. Think of them as the building blocks of your app’s UI (user interface). React components allow you to break down your app into smaller, more manageable pieces that you can reuse, combine, and maintain. In this article, we’ll walk you through the basics of creating components with props and JSX, explaining each concept thoroughly.
By the end, you’ll know how to build simple React components, pass data between them using props, and structure your components in a way that scales with your app.
This guide is part of the React Deep Dive series and is a longer read, split into three big sections:
- Core concepts – components, props, and JSX basics.
- Making components dynamic – conditional rendering, component composition, and reusability.
- Advanced props techniques – default values, prop validation, and flexible patterns.
You can read straight through or jump to what you need using the table of contents below.
📑 Table of Contents
-
Core Concepts
-
Making Components Dynamic
-
Advanced Props Techniques
What Is a React Component?
A component in React is essentially a function (or class) that:
- Accepts props (optional inputs).
- Returns JSX, which tells React what should be rendered on the screen.
Think of a React component like a Lego piece:
- You can reuse it in different places.
- You can combine it with other pieces to create something bigger.
- You can swap it out without breaking the whole structure.
Let’s look at an example of a simple function component:
function Greeting({ name }) {
return <h1>Hello, {name}!</h1>;
}
This Greeting
component is dynamic because it can accept any name and display a personalized message. The name is passed in as a prop, which we’ll get to in a moment.
Two Types of React Components: Function vs. Class
Before React introduced hooks (in version 16.8), there were two types of components:
- Class components (older style)
- Function components (modern style)
Class Components (The Old Way)
Class components were the only way to add state (dynamic data) and lifecycle methods (methods that run at certain points in a component’s life, like when it’s created or destroyed).
Here’s how a class component looks:
import React, { Component } from 'react';
class Greeting extends Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
-
class Greeting extends Component
: This defines a class-based component. -
render()
: This method returns JSX, which React uses to render the UI. -
this.props.name
:props
are accessed usingthis.props
in class components.
Function Components (The New Standard)
Function components, introduced later, are simpler. They are just JavaScript functions that return JSX and don’t require the boilerplate of classes.
Here’s the same Greeting component written as a function component:
function Greeting({ name }) {
return <h1>Hello, {name}!</h1>;
}
- This function component is much simpler, and we use props to pass data into the component.
Why Function Components Are Preferred in React 19
-
Less Boilerplate: No need for
constructor
,render
, orthis
. -
Hooks: With hooks like
useState
anduseEffect
, function components now have access to state and side effects — just like class components. (And no, not the kind from horror movies or pirate ships — we’ll meet React hooks later in the series, so for now just sail past this term.) - Easier to Read: Function components are more concise and readable.
- Performance: Function components can sometimes have smaller bundle sizes because there’s less code to load.
Props: Passing Data to Components
Now that we understand what components are, let’s dive into props. Props (short for "properties") are how data flows into components. You can think of props as the inputs to a component.
💻 See it in action: Here’s a short 3-minute clip from my React Crash Course where I demo props and dynamic rendering live in the browser.
It’s a quick look at the concepts we’re about to unpack in detail — the full deep dive (this series!) covers every detail in far more depth.
Props (short for "properties") are how you pass data into components from a parent component...
Props are passed from the parent component to the child component.
Here’s an example:
function Greeting({ name }) {
return <h1>Hello, {name}!</h1>;
}
function App() {
return <Greeting name="React Developer" />;
}
In the App
component, we are passing "React Developer"
as the name
prop to the Greeting
component.
What Happens Inside the Component?
In the Greeting
component:
function Greeting({ name }) {
return <h1>Hello, {name}!</h1>;
}
- The prop
name
is passed in. - We destructure
name
directly from the props object (i.e.,{ name }
). - The component renders the value of
name
inside the<h1>
tag.
Props Are Read-Only
Props are immutable — this means you cannot modify them inside the child component. If you need to change the data, you should store it in the component’s state (which will be covered in the next article).
Passing Multiple Props
You can pass multiple props to a component:
function UserCard({ name, age, isOnline }) {
return (
<div>
<h3>{name}</h3>
<p>Age: {age}</p>
<p>Status: {isOnline ? 'Online' : 'Offline'}</p>
</div>
);
}
function App() {
return <UserCard name="Alice" age={30} isOnline={true} />;
}
Here, we pass three props: name
, age
, and isOnline
.
Default Props
Sometimes, you may want to provide a default value for a prop if the parent component doesn’t pass one. You can do this directly in the component definition:
function Button({ label = "Click me" }) {
return <button>{label}</button>;
}
In this case, if no label
prop is provided, it will default to "Click me"
.
Dynamic Rendering: Making Components Flexible
Dynamic rendering means that components can adapt their output based on props, state, or conditions. This makes React incredibly flexible.
Conditional Rendering with Props
You can conditionally render different elements based on props. This means you can change how a component looks or behaves depending on the props it receives. In the example below, the type prop controls which background color the alert will use:
function Alert({ type, message }) {
const styles = {
success: { backgroundColor: 'lightgreen' },
error: { backgroundColor: 'salmon' },
info: { backgroundColor: 'lightblue' },
};
return <div style={styles[type]}>{message}</div>;
}
function App() {
return (
<>
<Alert type="success" message="Operation completed!" />
<Alert type="error" message="Something went wrong." />
</>
);
}
How it works:
The type prop ("success", "error", "info") is used as a key to pick the matching style from the styles object.
For example, if type="success", styles["success"] returns { backgroundColor: 'lightgreen' }.
That style object is applied to the
via the style attribute, changing the background color dynamically.This small pattern is common in React — props act as a “switch” that controls how a component looks or what it renders.
Rendering Lists Dynamically
You can also use props to render lists dynamically — for example, to display a list of to-do items that comes from a parent component.
function TodoList({ items }) {
return (
<ul>
{items.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
);
}
function App() {
const todos = [
{ id: 1, text: "Learn React" },
{ id: 2, text: "Build an app" },
];
return <TodoList items={todos} />;
}
In App, we define an array of todos and pass it as the items prop to TodoList.
Inside TodoList, we use the JavaScript .map() method to loop through the array and return one
The key prop is a special React attribute that helps React identify which list items have changed, been added, or removed. It should be unique for each item in the list — here we use the id.
This approach is preferred in React because .map() creates a new array of elements, which is exactly what JSX needs to render the list.
Recap: Components and Props Fundamentals
- Components are the reusable building blocks of your React app.
- Props let you pass data into components from their parent.
- Function components are simpler and the go-to in React 19.
- Conditional rendering and dynamic lists allow components to adapt to the data they receive.
You’ve now got the core building blocks of React: components, props, JSX, and some basic patterns for making them dynamic. Let’s build on that foundation by exploring how to make components even more flexible, reusable, and clean through composition and better rendering techniques.
Making Components Dynamic: Conditional Rendering and Composition
Previously, we looked at how to create React components, work with JSX, and pass props to make components dynamic and reusable. Now, we’re going to dive deeper into more advanced rendering techniques...
- Conditional rendering with props and state.
- Component composition and how components can be nested or combined.
- Reusable components and best practices to keep your code clean and maintainable.
By the end of this part, you'll be able to create components that can adapt to dynamic data and build complex UIs with ease!
Conditional Rendering in React
One of the most powerful features of React is conditional rendering. In React, you can change what is displayed on the screen based on certain conditions, like the value of props, state, or variables.
Conditional Rendering with JSX
React provides a few simple ways to conditionally render elements in JSX, which we’ll explore now:
-
Using
if
statements (within the function body). - Using ternary operators directly in JSX.
-
Using the logical AND (
&&
) operator for more complex conditions.
Let’s walk through each of these approaches:
1. Using if
Statements
You can use regular if
statements to handle conditional logic inside your component’s function.
function Greeting({ name }) {
if (!name) {
return <h1>Hello, guest!</h1>;
}
return <h1>Hello, {name}!</h1>;
}
- Here, if
name
is not passed, the component will render"Hello, guest!"
. Otherwise, it will render the personalized greeting.
2. Using Ternary Operator
The ternary operator is a shorter way to perform conditional rendering directly inside JSX.
function Greeting({ name }) {
return <h1>{name ? `Hello, ${name}!` : 'Hello, guest!'}</h1>;
}
- If
name
exists, it renders"Hello, {name}!"
, otherwise it renders"Hello, guest!"
.
3. Using the Logical AND (&&
) Operator
This is great for cases where you only want to display something if a condition is true.
function Notification({ message }) {
return <div>{message && <p>{message}</p>}</div>;
}
- If
message
is truthy, React will render the<p>
tag. Ifmessage
is empty ornull
, it won’t render anything.
Combining Multiple Conditions
You can combine conditions for more complex logic:
function UserStatus({ isLoggedIn, name }) {
return (
<div>
{isLoggedIn ? (
<h1>Welcome back, {name}!</h1>
) : (
<h1>Please log in to continue.</h1>
)}
</div>
);
}
Here, based on the value of isLoggedIn
, we either show the personalized greeting or a login prompt.
Component Composition: Nesting Components
As your app grows, you'll often need to combine smaller components to build larger, more complex components. This is called component composition, and it’s one of the most powerful patterns in React.
Example: Nested Components
Let's imagine we have a Card component that contains a Header, Body, and Footer. Instead of creating one large component, we break it into smaller ones and compose them together.
function CardHeader({ title }) {
return <header><h2>{title}</h2></header>;
}
function CardBody({ content }) {
return <section>{content}</section>;
}
function CardFooter({ footerText }) {
return <footer>{footerText}</footer>;
}
function Card({ title, content, footerText }) {
return (
<div className="card">
<CardHeader title={title} />
<CardBody content={content} />
<CardFooter footerText={footerText} />
</div>
);
}
function App() {
return (
<Card
title="React Overview"
content="React is a JavaScript library for building user interfaces."
footerText="Created with love by the React team"
/>
);
}
Explanation:
- Each part of the card is its own component:
CardHeader
,CardBody
, andCardFooter
. - The main
Card
component composes these smaller components together. - This allows us to reuse the smaller components wherever we need them, keeping our code modular and maintainable.
Reusable Components: Keeping Your Code DRY
In React, DRY stands for Don’t Repeat Yourself. One of the goals of React is to create reusable components that you can use throughout your app. This makes your code more maintainable and less error-prone.
Let’s look at how we can create a reusable button component.
Example: Reusable Button Component
function Button({ label, onClick, styleType }) {
const buttonStyles = {
primary: { backgroundColor: 'blue', color: 'white' },
secondary: { backgroundColor: 'gray', color: 'black' },
};
return (
<button
onClick={onClick}
style={buttonStyles[styleType] || buttonStyles.primary}
>
{label}
</button>
);
}
function App() {
return (
<div>
<Button label="Primary Button" onClick={() => alert("Primary clicked")} styleType="primary" />
<Button label="Secondary Button" onClick={() => alert("Secondary clicked")} styleType="secondary" />
</div>
);
}
Explanation:
- We created a Button component that accepts
label
,onClick
, andstyleType
as props. -
styleType
determines the button’s style (either "primary" or "secondary"). - The component is reusable, so you can create buttons with different labels and styles without repeating code.
When to Use Reusable Components?
Reusable components are ideal when you have elements that appear in multiple places in your application. By building these elements as separate components, you can manage their logic and styling in one place.
- Forms: A form can contain reusable form input components.
- Buttons: If multiple buttons share similar behavior, they can be made reusable.
- Cards: A card can be a component that you reuse throughout your app with different data.
Best Practices for Component Composition
- Keep Components Small: Each component should have a single responsibility. Try to break your app into small, reusable components that can be composed together.
- Don’t Repeat Yourself (DRY): Look for opportunities to reuse components rather than writing the same code multiple times.
- Pass Props for Customization: Use props to customize the behavior and content of reusable components.
- PropTypes for Validation: Use PropTypes to validate the props being passed to your components (more on this in the next article).
Recap: Making Components Dynamic
- Conditional rendering lets your UI change based on props, state, or other variables.
- Component composition helps you build larger UIs from smaller, reusable parts.
- Reusable components save time, reduce bugs, and keep code maintainable.
- Passing props is the key to customizing and reusing these components in different contexts.
We’ve seen how to make components adapt to different scenarios and work together in larger structures. Now it’s time to go deeper into props — adding default values, validating data, and using patterns that make components more powerful and versatile.
Advanced Props Techniques
So far, we’ve explored the fundamentals of React components, props, conditional rendering, and component composition. Now, let’s move into some advanced props techniques that make components more powerful, predictable, and reusable.
By the end of this section, you’ll understand how to handle default props, perform prop validation, and create highly reusable components that can adapt to a variety of use cases.
Default Props: Providing Default Values
Sometimes, you want to provide default values for props in case they aren’t passed by the parent component. React allows you to define default props for your components.
Example: Default Props
Let’s say you have a Button component, but you want to ensure it always has some default text unless explicitly passed in as a prop.
function Button({ label, onClick }) {
return <button onClick={onClick}>{label}</button>;
}
// Set default props
Button.defaultProps = {
label: "Click me",
};
function App() {
return <Button onClick={() => alert("Button clicked")} />;
}
Explanation:
-
Button.defaultProps
: This sets default values for props if they aren’t provided by the parent component. In this case, if nolabel
is passed, the button will display"Click me"
. - The
App
component uses theButton
component but doesn’t pass alabel
prop, so the button will render with the default"Click me"
text.
Why Use Default Props?
Default props are useful when you want your component to fallback to a sensible default value if certain props aren’t passed. This can be especially handy in larger apps where certain props may not always be provided.
Prop Validation: Ensuring Correct Data Types with PropTypes
When working with React, it’s important to ensure that the right types of props are passed to your components. This helps catch bugs early and makes your components easier to debug.
React provides a tool called PropTypes, which allows you to validate the types of props your components receive.
Example: Prop Validation with PropTypes
import PropTypes from 'prop-types';
function UserCard({ name, age }) {
return (
<div>
<h2>{name}</h2>
<p>Age: {age}</p>
</div>
);
}
// Set prop types for validation
UserCard.propTypes = {
name: PropTypes.string.isRequired,
age: PropTypes.number.isRequired,
};
function App() {
return <UserCard name="Alice" age={30} />;
}
Explanation:
-
PropTypes
: This package allows you to define the types of props that a component should expect. -
UserCard.propTypes
: Here we define thatname
should be a string andage
should be a number. The.isRequired
modifier ensures that the prop must be passed; if not, React will warn you.
Why Use PropTypes?
- Type safety: PropTypes can help ensure that your components receive the right type of data, preventing errors in your app.
- Self-documenting code: When you define prop types, it becomes easier for other developers (or even future you) to understand what kind of data the component expects.
Tip: For more complex types (like arrays or objects), PropTypes offers a variety of validators. For example,
PropTypes.array
orPropTypes.object
.
Handling Complex Props: Arrays and Objects
Props don’t always have to be simple values like strings or numbers. You can pass more complex data structures such as arrays, objects, or even functions as props.
Let’s explore how to handle these in React.
1. Passing Arrays as Props
If you want to pass an array of items to a component, you can do so like this:
function List({ items }) {
return (
<ul>
{items.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
);
}
function App() {
const fruits = ["Apple", "Banana", "Cherry"];
return <List items={fruits} />;
}
Explanation:
- We pass the array
fruits
from theApp
component to theList
component via theitems
prop. - In the
List
component, we map over the array and render each item in a list (<li>
tag).
2. Passing Objects as Props
You can also pass objects as props. Let’s say you want to pass a complex object to a component to display some user data.
function UserProfile({ user }) {
return (
<div>
<h2>{user.name}</h2>
<p>Email: {user.email}</p>
<p>Age: {user.age}</p>
</div>
);
}
function App() {
const user = {
name: "John Doe",
email: "john@example.com",
age: 28,
};
return <UserProfile user={user} />;
}
Explanation:
- The
user
object is passed as a single prop to theUserProfile
component. - Inside
UserProfile
, we access the values of theuser
object and render them in the UI.
3. Passing Functions as Props
Sometimes you need to pass a function as a prop. This is common in cases like handling events (e.g., button clicks).
function Button({ onClick, label }) {
return <button onClick={onClick}>{label}</button>;
}
function App() {
const handleClick = () => {
alert("Button clicked!");
};
return <Button onClick={handleClick} label="Click Me" />;
}
Explanation:
- The
handleClick
function is passed as theonClick
prop to theButton
component. - The
Button
component then calls theonClick
function when the button is clicked.
Reusable Component Patterns: Making Components Flexible
One of the most powerful aspects of React is the ability to create highly reusable components. Let’s explore a few patterns that can make your components more flexible.
1. Passing Children as Props
Sometimes, a component might need to render nested elements or other components. Instead of hardcoding that content, you can use the children
prop to make your component flexible.
function Card({ children }) {
return <div className="card">{children}</div>;
}
function App() {
return (
<Card>
<h2>Title</h2>
<p>This is some content inside the card.</p>
</Card>
);
}
Explanation:
- The
Card
component acceptschildren
as a prop and renders whatever is passed inside it. - In
App
, we pass HTML elements (<h2>
and<p>
) as children to theCard
.
2. Render Props
A more advanced pattern is the render prop. This pattern involves passing a function as a prop that returns JSX. This allows components to provide custom rendering logic.
function DataProvider({ render }) {
const data = "Some dynamic data";
return <div>{render(data)}</div>;
}
function App() {
return (
<DataProvider render={(data) => <h1>{data}</h1>} />
);
}
Explanation:
- The
DataProvider
component accepts arender
prop, which is a function that receives data and returns JSX. - In
App
, we pass a custom render function that renders the data inside an<h1>
tag.
Organizing Components in Larger Projects
As your app grows, group components by feature or domain rather than dumping every component in one big folder. This makes it easier to find related code, reason about boundaries, and scale the project.
Group-by-feature (recommended for medium+ apps):
src/
├── features/
│ ├── auth/
│ │ ├── LoginForm.jsx
│ │ ├── LoginForm.module.css
│ │ └── authApi.js
│ ├── dashboard/
│ ├── Dashboard.jsx
│ └── widgets/
Component-centric (also valid for UI libraries or small projects):
src/
├── components/
│ ├── Button/
│ │ ├── Button.jsx
│ │ └── Button.module.css
│ ├── Card/
│ ├── Card.jsx
│ └── Card.module.css
Practical tips
- Prefer feature folders when components, hooks, tests, and styles belong to a single user-facing feature.
- Co-locate tests and styles with the component (e.g.
Button/Button.test.jsx
,Button/Button.module.css
). - Avoid >3 levels of nesting unless you have a clear reason — shallow, predictable paths are easier to navigate.
Naming, exporting & importing components
Naming
- Use
UpperCamelCase
(aka PascalCase) for components:UserProfile
,TodoList
. - Component files are typically named to match the default export:
UserProfile.jsx
.
Export patterns
- Default export — convenient for one-component files:
export default function Greeting() { /* ... */ }
- Named exports — useful for multiple exports or libraries:
export function Button() { /* ... */ }
export function IconButton() { /* ... */ }
- Pro tip: named exports make refactoring and tree-shaking friendlier in some build setups.
Import examples
// default import
import Greeting from './Greeting';
// named import
import { Button } from './Button';
Common mistakes to avoid
- Over-large components — split responsibilities into smaller components.
- Excessive prop drilling — if many levels rely on the same prop, consider Context.
- Storing derived data in state — derive from props when possible.
-
Ignoring accessibility — use semantic HTML,
aria-*
attributes and keyboard support. - Copy/paste UI — extract repetitive UI into a reusable component.
React 19 tips (practical callouts)
- Use custom hooks to encapsulate logic and keep UI components thin.
- Lazy-load heavy components with
React.lazy
+Suspense
to improve initial load time. - Consider incremental adoption of new React 19 primitives where they fit your app (e.g., optimistic patterns or server actions) — but treat them as advanced tools and test thoroughly.
Putting it all together
// Button.jsx
export default function Button({ onClick, label }) {
return <button onClick={onClick}>{label}</button>;
}
// Card.jsx
export default function Card({ children }) {
return <div className="card">{children}</div>;
}
// App.jsx
import Button from './Button';
import Card from './Card';
export default function App() {
return (
<Card>
<h3>Welcome</h3>
<Button label="Click me" onClick={() => alert('Hi')} />
</Card>
);
}
This shows how small building blocks (Button, Card) compose into a page (App).
Recap: Advanced Props Techniques
- Default props provide fallback values to keep components predictable.
- PropTypes validate incoming props to catch errors early.
- You can pass arrays, objects, and functions as props for richer interactions.
- Patterns like children props and render props give components more flexibility and reuse potential.
- Organize by feature or component depending on app size.
- Choose clear naming and consistent export/import patterns.
- Keep components small, accessible, and reusable.
By mastering these props techniques, you can write components that are resilient, adaptable, and easy to maintain — ready to be used in everything from small experiments to production-grade applications.
- Next up in the series:
Mastering useState — React State Deep Dive: Basics, Gotchas & Patterns
Follow me on DEV for future posts in this deep-dive series.
https://dev.to/a1guy
If it helped, leave a reaction (heart / bookmark) — it keeps me motivated to create more content
Want video demos? Subscribe on YouTube: @LearnAwesome