Skip to main content

Components

Advanced component patterns, lifecycle, composition techniques, and best practices for building scalable React applications.

Component Types

Functional Components

// Basic functional component
function Welcome({ name }) {
return <h1>Hello, {name}!</h1>;
}

// With destructuring and default props
function UserCard({ name, email, avatar = '/default-avatar.png' }) {
return (
<div className="user-card">
<img src={avatar} alt={name} />
<h2>{name}</h2>
<p>{email}</p>
</div>
);
}

// Arrow function component
const Button = ({ onClick, children, variant = 'primary' }) => (
<button className={`btn btn-${variant}`} onClick={onClick}>
{children}
</button>
);

Class Components (Legacy)

import React, { Component } from 'react';

class Timer extends Component {
constructor(props) {
super(props);
this.state = { seconds: 0 };
}

componentDidMount() {
this.interval = setInterval(() => {
this.setState({ seconds: this.state.seconds + 1 });
}, 1000);
}

componentWillUnmount() {
clearInterval(this.interval);
}

render() {
return <div>Timer: {this.state.seconds}s</div>;
}
}

Component Patterns

Container vs Presentation Components

// Container Component (logic/data)
function UserListContainer() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);

useEffect(() => {
fetchUsers().then(data => {
setUsers(data);
setLoading(false);
});
}, []);

if (loading) return <div>Loading...</div>;

return <UserList users={users} />;
}

// Presentation Component (UI only)
function UserList({ users }) {
return (
<div className="user-list">
{users.map(user => (
<UserCard key={user.id} {...user} />
))}
</div>
);
}

Compound Components

// Tabs component with compound pattern
function Tabs({ children, activeTab, onChange }) {
return (
<div className="tabs">
{React.Children.map(children, (child, index) =>
React.cloneElement(child, {
isActive: index === activeTab,
onClick: () => onChange(index),
})
)}
</div>
);
}

function Tab({ children, isActive, onClick }) {
return (
<button className={`tab ${isActive ? 'active' : ''}`} onClick={onClick}>
{children}
</button>
);
}

// Usage
<Tabs activeTab={0} onChange={setActiveTab}>
<Tab>Tab 1</Tab>
<Tab>Tab 2</Tab>
<Tab>Tab 3</Tab>
</Tabs>;

Render Props Pattern

// Data provider with render prop
function DataProvider({ url, children }) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);

useEffect(() => {
fetch(url)
.then(response => response.json())
.then(data => {
setData(data);
setLoading(false);
})
.catch(error => {
setError(error);
setLoading(false);
});
}, [url]);

return children({ data, loading, error });
}

// Usage
<DataProvider url="/api/users">
{({ data, loading, error }) => {
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return <UserList users={data} />;
}}
</DataProvider>;

Higher-Order Components (HOCs)

// HOC for loading state
function withLoading(WrappedComponent) {
return function WithLoadingComponent({ isLoading, ...props }) {
if (isLoading) {
return <div className="loading">Loading...</div>;
}
return <WrappedComponent {...props} />;
};
}

// HOC for error handling
function withErrorBoundary(WrappedComponent) {
return class extends Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}

static getDerivedStateFromError(error) {
return { hasError: true };
}

componentDidCatch(error, errorInfo) {
console.error('Error caught by boundary:', error, errorInfo);
}

render() {
if (this.state.hasError) {
return <div>Something went wrong.</div>;
}
return <WrappedComponent {...this.props} />;
}
};
}

// Usage
const UserListWithLoading = withLoading(UserList);
const SafeUserList = withErrorBoundary(UserListWithLoading);

Component Composition

Children Patterns

// Basic children
function Card({ title, children }) {
return (
<div className="card">
<h2>{title}</h2>
<div className="card-body">{children}</div>
</div>
);
}

// Multiple children with specific roles
function Modal({ children }) {
const header = React.Children.toArray(children).find(
child => child.type === ModalHeader
);
const body = React.Children.toArray(children).find(
child => child.type === ModalBody
);
const footer = React.Children.toArray(children).find(
child => child.type === ModalFooter
);

return (
<div className="modal">
{header}
{body}
{footer}
</div>
);
}

const ModalHeader = ({ children }) => (
<div className="modal-header">{children}</div>
);
const ModalBody = ({ children }) => (
<div className="modal-body">{children}</div>
);
const ModalFooter = ({ children }) => (
<div className="modal-footer">{children}</div>
);

Slots Pattern

// Layout component with slots
function Layout({ header, sidebar, main, footer }) {
return (
<div className="layout">
<header className="header">{header}</header>
<div className="content">
<aside className="sidebar">{sidebar}</aside>
<main className="main">{main}</main>
</div>
<footer className="footer">{footer}</footer>
</div>
);
}

// Usage
<Layout
header={<Navigation />}
sidebar={<Sidebar />}
main={<MainContent />}
footer={<Footer />}
/>;

Composition with Context

// Theme context and provider
const ThemeContext = React.createContext();

function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');

return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}

// Component that uses theme
function ThemedButton({ children, ...props }) {
const { theme } = useContext(ThemeContext);

return (
<button className={`btn btn-${theme}`} {...props}>
{children}
</button>
);
}

Advanced Patterns

Controlled vs Uncontrolled Components

// Controlled component
function ControlledInput({ value, onChange }) {
return <input type="text" value={value} onChange={onChange} />;
}

// Uncontrolled component
function UncontrolledInput({ defaultValue, onSubmit }) {
const inputRef = useRef();

const handleSubmit = () => {
onSubmit(inputRef.current.value);
};

return (
<div>
<input ref={inputRef} defaultValue={defaultValue} />
<button onClick={handleSubmit}>Submit</button>
</div>
);
}

Flexible Component APIs

// Component with multiple ways to pass content
function Button({
children,
icon,
iconPosition = 'left',
loading = false,
...props
}) {
return (
<button {...props} disabled={loading}>
{loading && <Spinner />}
{!loading && icon && iconPosition === 'left' && icon}
{children}
{!loading && icon && iconPosition === 'right' && icon}
</button>
);
}

// Usage examples
<Button>Click me</Button>
<Button icon={<IconUser />}>Profile</Button>
<Button icon={<IconSave />} iconPosition="right">Save</Button>
<Button loading>Saving...</Button>

Polymorphic Components

// Component that can render as different elements
function Text({ as: Component = 'p', children, ...props }) {
return <Component {...props}>{children}</Component>;
}

// Usage
<Text>Default paragraph</Text>
<Text as="h1">As heading</Text>
<Text as="span">As span</Text>
<Text as={Link} to="/about">As Link component</Text>

Component Lifecycle (Hooks)

Component Mounting

function MyComponent() {
useEffect(() => {
// Component did mount
console.log('Component mounted');

// Cleanup function (component will unmount)
return () => {
console.log('Component will unmount');
};
}, []); // Empty dependency array = run once on mount

return <div>My Component</div>;
}

Component Updates

function MyComponent({ userId }) {
const [user, setUser] = useState(null);

// Run when userId changes
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]);

// Run on every render
useEffect(() => {
console.log('Component rendered');
});

// Run when user changes
useEffect(() => {
if (user) {
console.log('User data updated:', user);
}
}, [user]);

return <div>{user?.name}</div>;
}

Cleanup and Memory Management

function Timer() {
const [time, setTime] = useState(0);

useEffect(() => {
const interval = setInterval(() => {
setTime(prev => prev + 1);
}, 1000);

// Cleanup interval on unmount
return () => clearInterval(interval);
}, []);

return <div>Time: {time}</div>;
}

function EventListener() {
useEffect(() => {
const handleResize = () => {
console.log('Window resized');
};

window.addEventListener('resize', handleResize);

// Cleanup event listener
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);

return <div>Component with event listener</div>;
}

Error Boundaries

Error Boundary Class Component

class ErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}

static getDerivedStateFromError(error) {
return { hasError: true, error };
}

componentDidCatch(error, errorInfo) {
console.error('Error caught by boundary:', error, errorInfo);
// You can also log to error reporting service here
}

render() {
if (this.state.hasError) {
return (
<div className="error-boundary">
<h2>Something went wrong</h2>
<p>{this.state.error?.message}</p>
<button
onClick={() => this.setState({ hasError: false, error: null })}
>
Try again
</button>
</div>
);
}

return this.props.children;
}
}

Error Boundary with Custom UI

function App() {
return (
<ErrorBoundary
fallback={({ error, resetError }) => (
<div className="error-page">
<h1>Oops! Something went wrong</h1>
<pre>{error.message}</pre>
<button onClick={resetError}>Reset</button>
</div>
)}
>
<Router>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</Router>
</ErrorBoundary>
);
}

Performance Optimization

React.memo

// Prevent re-renders with React.memo
const ExpensiveComponent = React.memo(function ExpensiveComponent({ data }) {
console.log('ExpensiveComponent rendered');

return (
<div>
{/* Expensive rendering logic */}
{data.map(item => (
<ComplexItem key={item.id} item={item} />
))}
</div>
);
});

// Custom comparison function
const OptimizedComponent = React.memo(
function OptimizedComponent({ user, settings }) {
return (
<div>
{user.name} - {settings.theme}
</div>
);
},
(prevProps, nextProps) => {
return (
prevProps.user.id === nextProps.user.id &&
prevProps.settings.theme === nextProps.settings.theme
);
}
);

useMemo and useCallback

function ExpensiveCalculation({ items, multiplier }) {
// Memoize expensive calculation
const expensiveValue = useMemo(() => {
console.log('Calculating expensive value');
return items.reduce((sum, item) => sum + item.value * multiplier, 0);
}, [items, multiplier]);

// Memoize callback to prevent child re-renders
const handleItemClick = useCallback(item => {
console.log('Item clicked:', item);
}, []);

return (
<div>
<h2>Total: {expensiveValue}</h2>
{items.map(item => (
<Item key={item.id} item={item} onClick={handleItemClick} />
))}
</div>
);
}

Component Testing Patterns

Testing Component Rendering

import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

// Component to test
function Counter({ initialCount = 0 }) {
const [count, setCount] = useState(initialCount);

return (
<div>
<span data-testid="count">{count}</span>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}

// Test
test('Counter increments when button is clicked', async () => {
const user = userEvent.setup();

render(<Counter initialCount={5} />);

expect(screen.getByTestId('count')).toHaveTextContent('5');

await user.click(screen.getByRole('button', { name: 'Increment' }));

expect(screen.getByTestId('count')).toHaveTextContent('6');
});

Testing Component Props

test('UserCard displays user information', () => {
const user = {
name: 'John Doe',
email: 'john@example.com',
avatar: '/avatar.jpg',
};

render(<UserCard {...user} />);

expect(screen.getByText('John Doe')).toBeInTheDocument();
expect(screen.getByText('john@example.com')).toBeInTheDocument();
expect(screen.getByRole('img')).toHaveAttribute('src', '/avatar.jpg');
});

Best Practices

Component Design Principles

  • Single Responsibility: Each component should have one clear purpose
  • Composition over Inheritance: Use composition to build complex UIs
  • Props Interface: Design clear, minimal props interfaces
  • Predictable Behavior: Components should behave consistently

Code Organization

// Good: Clear component structure
function UserProfile({ user, onEdit, onDelete }) {
if (!user) return <div>Loading...</div>;

return (
<div className="user-profile">
<UserAvatar src={user.avatar} />
<UserInfo user={user} />
<UserActions onEdit={onEdit} onDelete={onDelete} />
</div>
);
}

// Good: Separate concerns
const UserAvatar = ({ src }) => <img src={src} alt="User avatar" />;
const UserInfo = ({ user }) => (
<div>
{user.name} - {user.email}
</div>
);
const UserActions = ({ onEdit, onDelete }) => (
<div>
<button onClick={onEdit}>Edit</button>
<button onClick={onDelete}>Delete</button>
</div>
);

Common Anti-patterns to Avoid

// ❌ Bad: Inline object creation
<Component style={{ color: 'red' }} />

// ✅ Good: Extract to variable or useMemo
const redStyle = { color: 'red' };
<Component style={redStyle} />

// ❌ Bad: Inline function creation
<Button onClick={() => handleClick(id)} />

// ✅ Good: Use useCallback
const handleButtonClick = useCallback(() => handleClick(id), [id]);
<Button onClick={handleButtonClick} />

Quick Reference

Component Patterns

// Functional component with hooks
const MyComponent = ({ prop1, prop2 }) => {
const [state, setState] = useState(initialValue);

useEffect(() => {
// Side effect
}, [dependencies]);

return <div>{/* JSX */}</div>;
};

// Error boundary
<ErrorBoundary>
<ComponentThatMightError />
</ErrorBoundary>;

// Memoized component
const MemoizedComponent = React.memo(MyComponent);

// Higher-order component
const EnhancedComponent = withHOC(MyComponent);