Components: The Heart of React
Components are the fundamental building blocks of React applications. They encapsulate a piece of the UI, along with its functionality and state, into reusable, self-contained units.
Analogy: Components as LEGO Blocks
Think of React components like LEGO blocks:
- Reusable: The same component can be used in different parts of your application, just like the same LEGO piece can be used in different models.
- Self-Contained: Each component manages its own rendering logic, just like each LEGO piece has its own shape and purpose.
- Composable: Complex UIs are built by combining simpler components, just like complex LEGO models are built from simpler blocks.
- Hierarchical: Components can be nested inside other components, forming a tree structure, just like LEGO blocks can be stacked and nested.
Two Types of Components
React provides two primary ways to define components: Functional Components and Class Components. Both serve the same purpose but with different syntax and capabilities.
Functional Components
Functional components are JavaScript functions that accept props as an argument and return React elements (JSX). They're also known as "stateless components" (though with Hooks, they can now have state).
Basic Structure
// Basic functional component
function Greeting(props) {
return <h1>Hello, {props.name}!</h1>;
}
// Using arrow function syntax
const Greeting = (props) => {
return <h1>Hello, {props.name}!</h1>;
};
// Shortened arrow function with implicit return
const Greeting = (props) => <h1>Hello, {props.name}!</h1>;
With Destructured Props
// Destructuring props in the parameter
function ProfileCard({ name, title, avatar }) {
return (
<div className="profile-card">
<img src={avatar} alt={name} />
<h2>{name}</h2>
<p>{title}</p>
</div>
);
}
With Default Props
// Providing default values for props
function Button({ text = "Click me", type = "primary", onClick }) {
return (
<button
className={`btn btn-${type}`}
onClick={onClick}
>
{text}
</button>
);
}
With State (Using Hooks)
import React, { useState } from 'react';
function Counter() {
// useState returns a state value and a function to update it
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
When to Use Functional Components
Functional components are now recommended as the default choice for most cases in React. They offer:
- Simplicity: Cleaner, more concise syntax
- Performance: Potentially better performance (though differences are minimal)
- Hooks: Full access to React's hooks system
- Future-Proof: Aligned with React team's recommended patterns
Class Components
Class components are ES6 classes that extend React.Component. They have more built-in features like lifecycle methods and were the standard before Hooks were introduced.
Basic Structure
import React, { Component } from 'react';
class Greeting extends Component {
render() {
return <h1>Hello, {this.props.name}!</h1>;
}
}
With State
import React, { Component } from 'react';
class Counter extends Component {
constructor(props) {
super(props);
// Initialize state in constructor
this.state = {
count: 0
};
// Bind the event handler
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
// Update state using setState
this.setState(prevState => ({
count: prevState.count + 1
}));
}
render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={this.handleClick}>
Click me
</button>
</div>
);
}
}
With Lifecycle Methods
import React, { Component } from 'react';
class UserProfile extends Component {
constructor(props) {
super(props);
this.state = {
user: null,
loading: true,
error: null
};
}
componentDidMount() {
// Fetch data when component mounts
fetch(`https://api.example.com/users/${this.props.userId}`)
.then(response => response.json())
.then(data => {
this.setState({
user: data,
loading: false
});
})
.catch(error => {
this.setState({
error: error.message,
loading: false
});
});
}
componentDidUpdate(prevProps) {
// Refetch if userId prop changes
if (prevProps.userId !== this.props.userId) {
this.setState({ loading: true });
fetch(`https://api.example.com/users/${this.props.userId}`)
.then(response => response.json())
.then(data => {
this.setState({
user: data,
loading: false
});
})
.catch(error => {
this.setState({
error: error.message,
loading: false
});
});
}
}
componentWillUnmount() {
// Clean up code here (e.g., cancel subscriptions)
console.log('Component is unmounting');
}
render() {
const { user, loading, error } = this.state;
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
if (!user) return <div>No user found</div>;
return (
<div className="user-profile">
<h2>{user.name}</h2>
<p>Email: {user.email}</p>
</div>
);
}
}
When to Use Class Components
While functional components are now preferred, class components are still relevant in certain scenarios:
- Legacy Code: Understanding class components is essential for maintaining older React codebases
- Error Boundaries: Currently, only class components can use
componentDidCatchfor error boundaries - Complex Lifecycle Logic: Some advanced patterns might be more explicit with lifecycle methods
- Familiar to OOP Developers: Might be more intuitive for developers with strong OOP backgrounds
Comparing Functional and Class Components
| Feature | Functional Components | Class Components |
|---|---|---|
| Syntax | Simple JavaScript function | ES6 class extending React.Component |
| Props | Received as function arguments | Accessed via this.props |
| State | useState Hook | this.state and this.setState() |
| Lifecycle | useEffect Hook | Dedicated lifecycle methods |
| Context | useContext Hook | static contextType or Context.Consumer |
| Performance | Slightly better in most cases | Slightly more overhead |
| Code Size | Usually more concise | Usually more verbose |
| Error Boundaries | Not supported | Supported with componentDidCatch |
Converting Between Component Types
Class Component
class Welcome extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
Equivalent Functional Component
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
Class Component with State
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
render() {
return (
<div>
<p>Count: {this.state.count}</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Increment
</button>
</div>
);
}
}
Equivalent Functional Component with useState
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
);
}
Component Lifecycle and Hooks Equivalence
Understanding how class component lifecycle methods map to functional component hooks can ease the transition between the two paradigms.
Lifecycle Methods and their Hook Equivalents
| Class Lifecycle Method | Hooks Equivalent |
|---|---|
| constructor(props) | useState(initialState) |
| componentDidMount() | useEffect(() => {}, []) |
| componentDidUpdate(prevProps, prevState) | useEffect(() => {}, [dependencies]) |
| componentWillUnmount() | useEffect(() => { return () => {} }, []) |
| shouldComponentUpdate(nextProps, nextState) | React.memo, useMemo, useCallback |
| static getDerivedStateFromProps(props, state) | Update state during render using updated props |
| getSnapshotBeforeUpdate(prevProps, prevState) | No direct equivalent |
| componentDidCatch(error, info) | Currently no hook equivalent |
Practical Example: Converting a Class Component to a Functional Component
Let's take a more complex class component and convert it to a functional component with hooks:
Class Component Version
import React, { Component } from 'react';
class WeatherWidget extends Component {
constructor(props) {
super(props);
this.state = {
temperature: null,
conditions: null,
loading: true,
error: null,
city: props.city || 'New York'
};
this.updateCity = this.updateCity.bind(this);
}
componentDidMount() {
this.fetchWeatherData();
}
componentDidUpdate(prevProps, prevState) {
if (prevState.city !== this.state.city) {
this.fetchWeatherData();
}
}
fetchWeatherData() {
this.setState({ loading: true });
fetch(`https://api.weather.example.com/current?city=${this.state.city}`)
.then(response => {
if (!response.ok) {
throw new Error('Weather data not available');
}
return response.json();
})
.then(data => {
this.setState({
temperature: data.temperature,
conditions: data.conditions,
loading: false,
error: null
});
})
.catch(error => {
this.setState({
loading: false,
error: error.message
});
});
}
updateCity(newCity) {
this.setState({ city: newCity });
}
render() {
const { temperature, conditions, loading, error, city } = this.state;
if (loading) {
return <div className="weather-widget loading">Loading weather data...</div>;
}
if (error) {
return <div className="weather-widget error">Error: {error}</div>;
}
return (
<div className="weather-widget">
<h2>Weather in {city}</h2>
<div className="temperature">{temperature}°F</div>
<div className="conditions">{conditions}</div>
<div className="city-selector">
<label>Change city:</label>
<select
value={city}
onChange={(e) => this.updateCity(e.target.value)}
>
<option value="New York">New York</option>
<option value="London">London</option>
<option value="Tokyo">Tokyo</option>
<option value="Sydney">Sydney</option>
</select>
</div>
</div>
);
}
}
Functional Component Version
import React, { useState, useEffect } from 'react';
function WeatherWidget({ city: initialCity = 'New York' }) {
// State with useState hooks
const [temperature, setTemperature] = useState(null);
const [conditions, setConditions] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [city, setCity] = useState(initialCity);
// useEffect for data fetching (combines componentDidMount & componentDidUpdate)
useEffect(() => {
async function fetchWeatherData() {
setLoading(true);
try {
const response = await fetch(`https://api.weather.example.com/current?city=${city}`);
if (!response.ok) {
throw new Error('Weather data not available');
}
const data = await response.json();
setTemperature(data.temperature);
setConditions(data.conditions);
setError(null);
} catch (error) {
setError(error.message);
} finally {
setLoading(false);
}
}
fetchWeatherData();
}, [city]); // Dependency array - effect runs when city changes
// Event handler function
const updateCity = (newCity) => {
setCity(newCity);
};
// Conditional rendering
if (loading) {
return <div className="weather-widget loading">Loading weather data...</div>;
}
if (error) {
return <div className="weather-widget error">Error: {error}</div>;
}
return (
<div className="weather-widget">
<h2>Weather in {city}</h2>
<div className="temperature">{temperature}°F</div>
<div className="conditions">{conditions}</div>
<div className="city-selector">
<label>Change city:</label>
<select
value={city}
onChange={(e) => updateCity(e.target.value)}
>
<option value="New York">New York</option>
<option value="London">London</option>
<option value="Tokyo">Tokyo</option>
<option value="Sydney">Sydney</option>
</select>
</div>
</div>
);
}
Key Points in the Conversion
- State Management: Replaced constructor and this.state with multiple useState hooks
- Lifecycle Methods: Replaced componentDidMount and componentDidUpdate with useEffect
- Props Access: Destructured props directly in function parameters
- Event Handling: No need to bind methods; arrow functions maintain context
- Async Logic: Used async/await for cleaner asynchronous code
Real-World Component Patterns
Controlled Components
// Functional Controlled Component
function SearchInput({ value, onChange }) {
return (
<input
type="text"
value={value}
onChange={onChange}
placeholder="Search..."
className="search-input"
/>
);
}
// Using the controlled component
function SearchBar() {
const [query, setQuery] = useState('');
const handleQueryChange = (e) => {
setQuery(e.target.value);
};
return (
<div className="search-bar">
<SearchInput
value={query}
onChange={handleQueryChange}
/>
<button>Search</button>
</div>
);
}
Data Fetching Component
// Reusable data fetching component
function DataFetcher({ url, children, loadingComponent, errorComponent }) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
async function fetchData() {
try {
setLoading(true);
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const result = await response.json();
setData(result);
setError(null);
} catch (err) {
setError(err.message);
setData(null);
} finally {
setLoading(false);
}
}
fetchData();
}, [url]);
if (loading) {
return loadingComponent || <div>Loading...</div>;
}
if (error) {
return errorComponent || <div>Error: {error}</div>;
}
return children(data);
}
// Using the data fetcher component
function UserList() {
return (
<DataFetcher
url="https://api.example.com/users"
loadingComponent={<div className="spinner">Loading users...</div>}
errorComponent={<div className="error-box">Failed to load users</div>}
>
{(users) => (
<ul className="user-list">
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)}
</DataFetcher>
);
}
Higher-Order Component (HOC) Pattern
// Higher-Order Component example
function withLogger(WrappedComponent) {
// Return a component
return function WithLogger(props) {
useEffect(() => {
console.log(`Component ${WrappedComponent.name} mounted with props:`, props);
return () => {
console.log(`Component ${WrappedComponent.name} will unmount`);
};
}, []);
console.log(`Rendering ${WrappedComponent.name} with props:`, props);
// Pass through all props to the wrapped component
return <WrappedComponent {...props} />;
};
}
// Using the HOC
const Button = ({ text, onClick }) => (
<button onClick={onClick}>{text}</button>
);
const LoggedButton = withLogger(Button);
// Usage
function App() {
return (
<LoggedButton
text="Click Me"
onClick={() => console.log('Button clicked')}
/>
);
}
Component Naming Conventions
Following consistent naming conventions makes your code more readable and maintainable:
- PascalCase for Components: Always name component functions with PascalCase (e.g.,
UserProfile, notuserProfile) - Descriptive and Specific Names: Use names that clearly describe the component's purpose (e.g.,
ProductCardinstead ofCard) - Matching Filenames: Match component names with their filenames (e.g.,
UserProfile.jsxforUserProfilecomponent) - Prefix for HOCs: Use "with" prefix for Higher-Order Components (e.g.,
withAuth) - Suffix for Context Providers: Use "Provider" suffix for context providers (e.g.,
ThemeProvider) - Suffix for Custom Hooks: Use "use" prefix for custom hooks (e.g.,
useFormValidation)
File Organization Strategies
There are several approaches to organizing React component files:
- By Feature/Module: Group related components by feature
- By Component Type: Separate containers, components, hooks, etc.
- Atomic Design: Organize by complexity (atoms, molecules, organisms, templates, pages)
Whichever approach you choose, consistency is key!
Error Boundaries with Class Components
Error boundaries are a React pattern that uses class components to catch JavaScript errors in their child component tree and display fallback UI.
import React, { Component } from 'react';
class ErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null, errorInfo: null };
}
static getDerivedStateFromError(error) {
// Update state so the next render will show the fallback UI
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// Log the error to an error reporting service
console.error("Error caught by boundary:", error, errorInfo);
this.setState({
error: error,
errorInfo: errorInfo
});
}
render() {
if (this.state.hasError) {
// You can render any custom fallback UI
return (
<div className="error-boundary">
<h2>Something went wrong.</h2>
{this.props.fallback || <button onClick={() => window.location.reload()}>Refresh Page</button>}
{process.env.NODE_ENV !== 'production' && (
<details style={{ whiteSpace: 'pre-wrap' }}>
{this.state.error && this.state.error.toString()}
<br />
{this.state.errorInfo && this.state.errorInfo.componentStack}
</details>
)}
</div>
);
}
return this.props.children;
}
}
// Using the error boundary
function App() {
return (
<div className="app">
<h1>My Application</h1>
<ErrorBoundary fallback={<p>User profile failed to load.</p>}>
<UserProfile userId="123" />
</ErrorBoundary>
<ErrorBoundary fallback={<p>Weather widget failed to load.</p>}>
<WeatherWidget city="New York" />
</ErrorBoundary>
</div>
);
}
Error boundaries are an important example of functionality that still requires class components in React.
Practice Activities
Activity 1: Component Conversion
Objective: Practice converting between functional and class components.
Instructions:
- Convert the following class component to a functional component using hooks:
class ToggleButton extends React.Component {
constructor(props) {
super(props);
this.state = {
isOn: false
};
this.toggle = this.toggle.bind(this);
}
toggle() {
this.setState(prevState => ({
isOn: !prevState.isOn
}));
}
render() {
return (
<button
className={this.state.isOn ? 'btn-on' : 'btn-off'}
onClick={this.toggle}
>
{this.state.isOn ? 'ON' : 'OFF'}
</button>
);
}
}
Activity 2: Building Components from Scratch
Objective: Practice creating functional components for different use cases.
Instructions:
- Create a
UserProfilefunctional component that displays a user's details (name, role, email, avatar) - Create a
TabPanelcomponent that takes an array of tabs and displays the selected tab's content - Create a
ThemeTogglecomponent that switches between light and dark themes
Implement each component in both functional and class styles, and compare the code.
Activity 3: Error Boundary in Action
Objective: Experience how error boundaries work in practice.
Instructions:
- Implement the
ErrorBoundaryclass component from the example - Create a
BuggyCountercomponent that throws an error after a certain count - Wrap the
BuggyCounterin yourErrorBoundary - Test how the error boundary catches errors and displays fallback UI
// Example BuggyCounter component
function BuggyCounter() {
const [count, setCount] = useState(0);
function handleClick() {
const newCount = count + 1;
setCount(newCount);
// Simulate an error at count 5
if (newCount === 5) {
throw new Error('I crashed on purpose!');
}
}
return (
<div>
<p>Count: {count}</p>
<button onClick={handleClick}>Increment</button>
</div>
);
}
Resources for Further Learning
Official Documentation:
Online Learning Resources:
Community Resources:
- A Complete Guide to useEffect by Dan Abramov
- React+TypeScript Cheatsheets
- Awesome React - A collection of resources
Summary
- React components are the fundamental building blocks of React applications.
- Functional components are JavaScript functions that return JSX and can use hooks for state and lifecycle features.
- Class components are ES6 classes that extend React.Component and have access to lifecycle methods and state.
- Modern React development favors functional components with hooks over class components.
- Both component types accept props and can manage internal state.
- Hooks (useState, useEffect, etc.) in functional components provide equivalent functionality to state and lifecycle methods in class components.
- Error boundaries are a special type of component that require class syntax.
- Following consistent naming conventions and organization patterns helps maintain clean, readable code.
In the next lecture, we'll dive deeper into props and explore data flow between React components.