20 minlesson

Component Patterns and Best Practices

Component Patterns and Best Practices

Now that you understand props and components, let's explore patterns for organizing and structuring React components effectively.

Component File Organization

There are several ways to organize component files:

Single Component per File

1src/
2├── components/
3│ ├── Button.jsx
4│ ├── Card.jsx
5│ ├── Header.jsx
6│ └── Footer.jsx

Feature-Based Organization

1src/
2├── features/
3│ ├── auth/
4│ │ ├── LoginForm.jsx
5│ │ ├── SignupForm.jsx
6│ │ └── AuthContext.jsx
7│ ├── products/
8│ │ ├── ProductCard.jsx
9│ │ ├── ProductList.jsx
10│ │ └── ProductDetail.jsx

Component with Styles and Tests

1src/
2├── components/
3│ ├── Button/
4│ │ ├── Button.jsx
5│ │ ├── Button.module.css
6│ │ ├── Button.test.jsx
7│ │ └── index.js

Choose based on project size and team preferences. Consistency matters most.

Naming Conventions

Follow these conventions for clear, readable code:

jsx
1// Component names: PascalCase
2function UserProfile() { ... }
3function NavigationMenu() { ... }
4
5// Props: camelCase
6<UserProfile userName="alice" isActive={true} />
7
8// Event handlers: handle + Event
9function handleClick() { ... }
10function handleSubmit() { ... }
11
12// Event handler props: on + Event
13<Button onClick={handleClick} onHover={handleHover} />
14
15// Boolean props: is/has/can/should prefix
16<Modal isOpen={true} hasCloseButton={false} />

Container vs Presentational

A classic pattern for separating concerns:

Presentational Components - Focus on UI:

jsx
1// Only cares about how things look
2function UserCard({ name, avatar, bio, onFollow }) {
3 return (
4 <div className="user-card">
5 <img src={avatar} alt={name} />
6 <h3>{name}</h3>
7 <p>{bio}</p>
8 <button onClick={onFollow}>Follow</button>
9 </div>
10 );
11}

Container Components - Focus on logic:

jsx
1// Only cares about how things work
2function UserCardContainer({ userId }) {
3 const [user, setUser] = useState(null);
4
5 useEffect(() => {
6 fetchUser(userId).then(setUser);
7 }, [userId]);
8
9 const handleFollow = () => {
10 followUser(userId);
11 };
12
13 if (!user) return <Loading />;
14
15 return (
16 <UserCard
17 name={user.name}
18 avatar={user.avatar}
19 bio={user.bio}
20 onFollow={handleFollow}
21 />
22 );
23}

Note: With hooks, this separation is less strict. Logic can be extracted to custom hooks instead.

Lifting State Up

When siblings need to share state, lift it to their common parent:

jsx
1// BEFORE: Each input manages its own state
2function TemperatureInput() {
3 const [value, setValue] = useState('');
4 return <input value={value} onChange={e => setValue(e.target.value)} />;
5}
6
7// AFTER: Parent manages shared state
8function TemperatureCalculator() {
9 const [celsius, setCelsius] = useState('');
10 const [fahrenheit, setFahrenheit] = useState('');
11
12 const handleCelsiusChange = (value) => {
13 setCelsius(value);
14 setFahrenheit(value ? ((parseFloat(value) * 9/5) + 32).toFixed(1) : '');
15 };
16
17 const handleFahrenheitChange = (value) => {
18 setFahrenheit(value);
19 setCelsius(value ? ((parseFloat(value) - 32) * 5/9).toFixed(1) : '');
20 };
21
22 return (
23 <div>
24 <TemperatureInput
25 scale="Celsius"
26 value={celsius}
27 onChange={handleCelsiusChange}
28 />
29 <TemperatureInput
30 scale="Fahrenheit"
31 value={fahrenheit}
32 onChange={handleFahrenheitChange}
33 />
34 </div>
35 );
36}
37
38function TemperatureInput({ scale, value, onChange }) {
39 return (
40 <label>
41 {scale}:
42 <input
43 value={value}
44 onChange={e => onChange(e.target.value)}
45 />
46 </label>
47 );
48}

Prop Validation with PropTypes

Runtime type checking for development:

jsx
1import PropTypes from 'prop-types';
2
3function UserCard({ name, age, email, role, onSelect }) {
4 return (
5 <div onClick={() => onSelect(email)}>
6 <h3>{name}</h3>
7 <p>Age: {age}</p>
8 <p>Role: {role}</p>
9 </div>
10 );
11}
12
13UserCard.propTypes = {
14 name: PropTypes.string.isRequired,
15 age: PropTypes.number,
16 email: PropTypes.string.isRequired,
17 role: PropTypes.oneOf(['admin', 'user', 'guest']),
18 onSelect: PropTypes.func.isRequired,
19};
20
21UserCard.defaultProps = {
22 age: 0,
23 role: 'user',
24};

Common PropTypes:

  • PropTypes.string
  • PropTypes.number
  • PropTypes.bool
  • PropTypes.func
  • PropTypes.array
  • PropTypes.object
  • PropTypes.node (anything renderable)
  • PropTypes.element (React element)
  • PropTypes.oneOf(['a', 'b'])
  • PropTypes.arrayOf(PropTypes.string)
  • PropTypes.shape({ name: PropTypes.string })

TypeScript Props (Brief Introduction)

TypeScript provides compile-time type checking:

tsx
1// Define props interface
2interface ButtonProps {
3 variant: 'primary' | 'secondary' | 'danger';
4 size?: 'small' | 'medium' | 'large';
5 disabled?: boolean;
6 onClick: () => void;
7 children: React.ReactNode;
8}
9
10// Use interface in component
11function Button({
12 variant,
13 size = 'medium',
14 disabled = false,
15 onClick,
16 children
17}: ButtonProps) {
18 return (
19 <button
20 className={`btn btn-${variant} btn-${size}`}
21 disabled={disabled}
22 onClick={onClick}
23 >
24 {children}
25 </button>
26 );
27}
28
29// TypeScript catches errors at compile time
30<Button variant="invalid">Click</Button> // Error!
31<Button variant="primary">Click</Button> // Missing onClick - Error!

Slot Pattern with Children

Create flexible layouts using children as slots:

jsx
1function Card({ header, children, footer }) {
2 return (
3 <div className="card">
4 {header && <div className="card-header">{header}</div>}
5 <div className="card-body">{children}</div>
6 {footer && <div className="card-footer">{footer}</div>}
7 </div>
8 );
9}
10
11// Usage with named slots
12<Card
13 header={<h2>Card Title</h2>}
14 footer={<Button>Action</Button>}
15>
16 <p>Card content goes here</p>
17</Card>

Compound Components Pattern (Preview)

Group related components that work together:

jsx
1// Parent provides context
2function Tabs({ children, defaultValue }) {
3 const [activeTab, setActiveTab] = useState(defaultValue);
4
5 return (
6 <TabsContext.Provider value={{ activeTab, setActiveTab }}>
7 <div className="tabs">{children}</div>
8 </TabsContext.Provider>
9 );
10}
11
12// Children consume context
13function TabList({ children }) {
14 return <div className="tab-list">{children}</div>;
15}
16
17function Tab({ value, children }) {
18 const { activeTab, setActiveTab } = useContext(TabsContext);
19 return (
20 <button
21 className={activeTab === value ? 'active' : ''}
22 onClick={() => setActiveTab(value)}
23 >
24 {children}
25 </button>
26 );
27}
28
29function TabPanel({ value, children }) {
30 const { activeTab } = useContext(TabsContext);
31 return activeTab === value ? <div>{children}</div> : null;
32}
33
34// Attach children to parent
35Tabs.List = TabList;
36Tabs.Tab = Tab;
37Tabs.Panel = TabPanel;
38
39// Clean, readable usage
40<Tabs defaultValue="tab1">
41 <Tabs.List>
42 <Tabs.Tab value="tab1">First</Tabs.Tab>
43 <Tabs.Tab value="tab2">Second</Tabs.Tab>
44 </Tabs.List>
45 <Tabs.Panel value="tab1">First panel content</Tabs.Panel>
46 <Tabs.Panel value="tab2">Second panel content</Tabs.Panel>
47</Tabs>

We'll explore this pattern in depth in the Advanced Patterns topic.

Component Best Practices

1. Keep Components Small and Focused

jsx
1// Too much in one component
2function UserDashboard({ user }) {
3 // 200 lines of mixed concerns...
4}
5
6// Better: Split into focused components
7function UserDashboard({ user }) {
8 return (
9 <div>
10 <UserHeader user={user} />
11 <UserStats userId={user.id} />
12 <UserActivityFeed userId={user.id} />
13 <UserSettings user={user} />
14 </div>
15 );
16}

2. Use Meaningful Names

jsx
1// Unclear
2function Item({ d }) {
3 return <div>{d.n}</div>;
4}
5
6// Clear
7function ProductCard({ product }) {
8 return <div>{product.name}</div>;
9}

3. Extract Complex Conditions

jsx
1// Hard to read
2{user && user.isActive && !user.isBlocked && user.role === 'admin' && <AdminPanel />}
3
4// Better: Extract to variable or function
5const canAccessAdmin = user?.isActive && !user?.isBlocked && user?.role === 'admin';
6{canAccessAdmin && <AdminPanel />}

4. Avoid Prop Drilling

When props pass through many layers, consider:

  • Context API (covered in Topic 6)
  • State management libraries
  • Component composition
jsx
1// Prop drilling (avoid)
2<App user={user}>
3 <Layout user={user}>
4 <Sidebar user={user}>
5 <UserMenu user={user} />
6 </Sidebar>
7 </Layout>
8</App>
9
10// Better: Use context or composition
11<UserProvider user={user}>
12 <App>
13 <Layout>
14 <Sidebar>
15 <UserMenu /> {/* Gets user from context */}
16 </Sidebar>
17 </Layout>
18 </App>
19</UserProvider>

Summary

Key patterns for clean React components:

  1. Organize files consistently (by feature or type)
  2. Name clearly - PascalCase components, camelCase props
  3. Separate concerns - presentational vs container (or use hooks)
  4. Lift state when siblings need to share
  5. Validate props with PropTypes or TypeScript
  6. Use slots for flexible composition
  7. Keep focused - small components with single responsibility

Next, let's put these patterns into practice by building a reusable card component system!