Table of Contents
- What Are React Components?
- Types of React Components
- Anatomy of a Component
- Component Composition
- Component Reusability
- Styling React Components
- Performance Optimization
- Common Pitfalls and Best Practices
- Real-World Example: Building a Simple App
- Conclusion
- References
What Are React Components?
A React component is a reusable, independent piece of code that returns a React element (JSX) describing a portion of the UI. Components are designed to be modular, making them easy to test, debug, and maintain. Think of components as LEGO bricks: you can combine simple bricks (components) to build complex structures (applications).
Key characteristics of components:
- Reusable: Define once, use anywhere in your app.
- Independent: Isolate logic and UI, reducing side effects.
- Composable: Combine smaller components to build larger ones.
- Declarative: Describe what the UI should look like, not how to update it.
Types of React Components
React offers two primary types of components. While functional components are now the standard, class components are still relevant for legacy codebases.
Functional Components
Functional components are JavaScript functions that return React elements. With the introduction of React Hooks (in React 16.8), functional components can now handle state, lifecycle, and other features previously reserved for class components. They are concise, readable, and the preferred choice for new projects.
Basic Example:
// A simple functional component that displays a greeting
function Greeting({ name }) {
return <h1>Hello, {name || "Guest"}!</h1>;
}
// Usage: <Greeting name="Alice" />
Class Components (Legacy)
Class components are ES6 classes that extend React.Component and override the render() method to return UI. They were the primary way to manage state and lifecycle before hooks. While still supported, they are less common in modern React apps due to their verbosity.
Basic Example:
// A class component equivalent to the Greeting functional component
class Greeting extends React.Component {
render() {
const { name } = this.props;
return <h1>Hello, {name || "Guest"}!</h1>;
}
}
// Usage: <Greeting name="Bob" />
Anatomy of a Component
To fully understand components, let’s break down their core building blocks: props, state, and lifecycle.
Props: Passing Data to Components
Props (short for “properties”) are read-only data passed from a parent component to a child component. They enable components to be dynamic and reusable by allowing customization via external input.
- Props are immutable: A child component cannot modify the props it receives (they are “read-only”).
- Passing Props: Props are passed like HTML attributes.
Example: Using Props
// Parent component passing props to Child
function Parent() {
return <Child message="Hello from Parent" count={42} isActive={true} />;
}
// Child component receiving and using props
function Child(props) {
return (
<div>
<p>{props.message}</p>
<p>Count: {props.count}</p>
<p>Status: {props.isActive ? "Active" : "Inactive"}</p>
</div>
);
}
// Destructuring props for cleaner code
function Child({ message, count, isActive }) {
return (
<div>
<p>{message}</p>
<p>Count: {count}</p>
<p>Status: {isActive ? "Active" : "Inactive"}</p>
</div>
);
}
State: Managing Internal Data
State is mutable data that lives inside a component and determines its behavior and rendering. Unlike props (external), state is controlled by the component itself. When state changes, React re-renders the component to reflect the new data.
State in Functional Components (with useState)
The useState hook lets functional components manage state. It returns a state variable and a function to update it.
Example: Counter with State
function Counter() {
// Initialize state: [stateVariable, setStateFunction]
const [count, setCount] = React.useState(0); // 0 is the initial value
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
);
}
State in Class Components
Class components use this.state and this.setState() to manage state.
Example: Counter with Class State
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 }; // Initialize state in constructor
}
render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Click me
</button>
</div>
);
}
}
Lifecycle: Component Behavior Over Time
Components have a lifecycle: they are mounted (added to the DOM), updated (re-rendered with new data), and unmounted (removed from the DOM). Understanding lifecycle helps control behavior like data fetching, subscriptions, or cleanup.
Lifecycle in Functional Components (with useEffect)
The useEffect hook replaces class lifecycle methods. It runs side effects (e.g., data fetching, DOM updates) after render.
Common Use Cases:
componentDidMount: Run once after the component mounts.componentDidUpdate: Run after updates (e.g., when props/state change).componentWillUnmount: Cleanup before the component unmounts.
Example: Data Fetching with useEffect
function UserProfile({ userId }) {
const [user, setUser] = React.useState(null);
const [loading, setLoading] = React.useState(true);
// Runs after render (like componentDidMount and componentDidUpdate)
React.useEffect(() => {
const fetchUser = async () => {
setLoading(true);
const response = await fetch(`https://api.example.com/users/${userId}`);
const data = await response.json();
setUser(data);
setLoading(false);
};
fetchUser();
// Cleanup: Runs before the component unmounts or re-runs the effect
return () => {
// Cancel pending requests or subscriptions here
};
}, [userId]); // Re-run effect only if userId changes
if (loading) return <p>Loading...</p>;
if (!user) return <p>User not found</p>;
return (
<div>
<h1>{user.name}</h1>
<p>Email: {user.email}</p>
</div>
);
}
Lifecycle in Class Components
Class components use explicit lifecycle methods:
| Method | Purpose |
|---|---|
componentDidMount | Runs once after the component mounts. |
componentDidUpdate | Runs after props/state change. |
componentWillUnmount | Runs before the component unmounts. |
Component Composition
React encourages composition over inheritance to build complex UIs. Composition involves combining smaller components into larger ones, creating a hierarchy of parent and child components.
Parent-Child Relationships
In a React app, components form a tree structure, with parent components passing data to children via props. Children can communicate back to parents using callback props (functions passed as props).
Example: Parent-Child Communication
// Child component: Accepts a callback to notify parent of clicks
function Button({ label, onClick }) {
return <button onClick={onClick}>{label}</button>;
}
// Parent component: Manages state and passes data/callbacks to child
function Parent() {
const [clicks, setClicks] = React.useState(0);
const handleClick = () => {
setClicks(clicks + 1);
};
return (
<div>
<p>Button clicked {clicks} times</p>
<Button label="Click Me" onClick={handleClick} />
</div>
);
}
Children Prop
The children prop is a special prop that allows a parent component to pass content (elements, components, or text) to a child component, similar to how HTML elements wrap content (e.g., <div>Content</div>).
Example: Using children Prop
// A reusable Card component that wraps content
function Card({ children, title }) {
return (
<div style={{ border: "1px solid #ccc", padding: "1rem", borderRadius: "8px" }}>
<h2>{title}</h2>
<div>{children}</div> {/* Render the wrapped content */}
</div>
);
}
// Usage: Pass content as children
function App() {
return (
<Card title="Welcome">
<p>This is a card with custom content!</p>
<Button label="Learn More" />
</Card>
);
}
Component Reusability
Reusability is a core principle of React. Here are common patterns to reuse component logic:
Custom Hooks
Custom hooks are reusable functions that encapsulate stateful logic (e.g., data fetching, form handling) and can be shared across components. They follow the naming convention useXyz (e.g., useFetch, useForm).
Example: Custom useFetch Hook
// Reusable hook for data fetching
function useFetch(url) {
const [data, setData] = React.useState(null);
const [loading, setLoading] = React.useState(true);
const [error, setError] = React.useState(null);
React.useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
const response = await fetch(url);
if (!response.ok) throw new Error("Failed to fetch");
const result = await response.json();
setData(result);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchData();
}, [url]);
return { data, loading, error };
}
// Usage in a component
function UserList() {
const { data: users, loading, error } = useFetch("https://api.example.com/users");
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
Higher-Order Components (HOCs)
An HOC is a function that takes a component and returns a new component with enhanced functionality. HOCs were popular before hooks but are now less common.
Example: HOC for Logging
// HOC that logs component mounts/unmounts
function withLogging(WrappedComponent) {
return class extends React.Component {
componentDidMount() {
console.log(`${WrappedComponent.name} mounted`);
}
componentWillUnmount() {
console.log(`${WrappedComponent.name} unmounted`);
}
render() {
return <WrappedComponent {...this.props} />;
}
};
}
// Usage: Enhance a component with logging
const LoggedButton = withLogging(Button);
Render Props
A render prop is a function prop that a component uses to share logic with another component. Like HOCs, render props are less common with hooks but still relevant for legacy code.
Styling React Components
Styling is a critical part of component design. React offers multiple approaches to style components:
Inline Styles
Inline styles use JavaScript objects to define styles directly on elements. They are scoped to the component but lack advanced CSS features (e.g., media queries).
Example:
function StyledButton() {
const buttonStyle = {
padding: "8px 16px",
backgroundColor: "blue",
color: "white",
border: "none",
borderRadius: "4px",
};
return <button style={buttonStyle}>Click Me</button};
}
CSS Modules
CSS Modules scope styles to a component by generating unique class names, preventing style conflicts.
Example:
/* Button.module.css */
.button {
padding: 8px 16px;
background: blue;
color: white;
}
// Import and use the CSS module
import styles from "./Button.module.css";
function StyledButton() {
return <button className={styles.button}>Click Me</button>;
}
Styled Components
Styled Components is a library that lets you write CSS in JavaScript, creating reusable, styled elements.
Example:
import styled from "styled-components";
// Define a styled button component
const StyledButton = styled.button`
padding: 8px 16px;
background: blue;
color: white;
border: none;
border-radius: 4px;
&:hover {
background: darkblue;
}
`;
// Usage
function App() {
return <StyledButton>Click Me</StyledButton>;
}
Utility-First Frameworks (e.g., Tailwind CSS)
Utility-first frameworks like Tailwind provide pre-defined classes to style components directly in JSX, reducing the need for custom CSS.
Example:
function TailwindButton() {
return (
<button className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-700">
Click Me
</button>
);
}
Performance Optimization
Poorly optimized components can lead to slow apps. Here are key strategies to optimize component performance:
React.memo
React.memo is a higher-order component that memoizes functional components, preventing unnecessary re-renders if props haven’t changed (shallow comparison).
Example:
const MemoizedComponent = React.memo(function MyComponent(props) {
// Only re-renders if props change
});
useMemo and useCallback
useMemo: Memoizes the result of expensive calculations to avoid re-computing on every render.useCallback: Memoizes functions to prevent them from being recreated on every render (useful for passing callbacks as props to memoized components).
Example:
function ExpensiveComponent({ items }) {
// Memoize the sorted list to avoid re-sorting on every render
const sortedItems = React.useMemo(() => {
return items.sort((a, b) => a.value - b.value);
}, [items]); // Re-run only if items change
// Memoize the callback to avoid re-creating it
const handleClick = React.useCallback(() => {
console.log("Item clicked");
}, []); // Empty dependency array: callback never changes
return (
<ul>
{sortedItems.map((item) => (
<li key={item.id} onClick={handleClick}>
{item.name}
</li>
))}
</ul>
);
}
Avoiding Unnecessary Re-renders
- Shallow Compare Props/State: Ensure props and state are immutable (e.g., use
setStatewith new objects/arrays instead of mutating existing ones). - Split Large Components: Break monolithic components into smaller, focused ones to limit re-renders.
Common Pitfalls and Best Practices
Pitfalls to Avoid
- Mutating State Directly: Always use
setState(class) or state updater functions (hooks) to modify state.// Bad: Mutates state directly const [todos, setTodos] = useState([]); todos.push("New todo"); // ❌ // Good: Returns a new array setTodos([...todos, "New todo"]); // ✅ - Overusing State: Lift state to parent components if multiple children need access (avoid duplicating state).
- Prop Drilling: Passing props through multiple levels of components. Use React Context or state management libraries (e.g., Redux) for deep data sharing.
Best Practices
- Keep Components Small: Each component should handle one responsibility (single-responsibility principle).
- Use Functional Components with Hooks: Prefer functional components over class components for simplicity.
- Validate Props: Use
PropTypesor TypeScript to enforce prop types and catch errors early.import PropTypes from "prop-types"; function Greeting({ name }) { return <h1>Hello, {name}!</h1>; } Greeting.propTypes = { name: PropTypes.string.isRequired, // Enforce name is a required string };
Real-World Example: Building a Simple App
Let’s tie together everything we’ve learned by building a Todo List App with components, props, state, and hooks.
App Structure
App: Parent component managing the todo list state.TodoForm: Child component to add new todos (communicates withAppvia callbacks).TodoList: Child component to render the list of todos.TodoItem: Child component for individual todo items (with delete functionality).
Code Implementation:
import React, { useState } from "react";
// TodoItem: Displays a single todo and handles deletion
function TodoItem({ todo, onDelete }) {
return (
<li style={{ display: "flex", gap: "8px", margin: "4px 0" }}>
{todo.text}
<button onClick={() => onDelete(todo.id)}>×</button>
</li>
);
}
// TodoForm: Input field to add new todos
function TodoForm({ onAddTodo }) {
const [text, setText] = useState("");
const handleSubmit = (e) => {
e.preventDefault();
if (!text.trim()) return;
onAddTodo({ id: Date.now(), text }); // Pass new todo to parent
setText(""); // Clear input
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="Add a new todo..."
/>
<button type="submit">Add</button>
</form>
);
}
// TodoList: Renders all todos
function TodoList({ todos, onDeleteTodo }) {
return (
<ul style={{ listStyle: "none", padding: 0 }}>
{todos.map((todo) => (
<TodoItem key={todo.id} todo={todo} onDelete={onDeleteTodo} />
))}
</ul>
);
}
// App: Parent component managing state and coordinating children
function App() {
const [todos, setTodos] = useState([]);
const addTodo = (newTodo) => {
setTodos([...todos, newTodo]);
};
const deleteTodo = (id) => {
setTodos(todos.filter((todo) => todo.id !== id));
};
return (
<div style={{ maxWidth: "400px", margin: "2rem auto" }}>
<h1>Todo List</h1>
<TodoForm onAddTodo={addTodo} />
<TodoList todos={todos} onDeleteTodo={deleteTodo} />{" "}
</div>
);
}
export default App;
Conclusion
React components are the backbone of React applications, enabling developers to build modular, reusable, and maintainable UIs. By mastering components—their types, anatomy, composition, and optimization—you’ll be well-equipped to tackle complex React projects.
Key takeaways:
- Components are reusable, independent units of UI.
- Functional components with hooks are the modern standard.
- Props pass data down, state manages internal data, and hooks handle lifecycle and side effects.
- Composition and reusability (via custom hooks) are critical for scaling apps.