Episode 2 — React Frontend Architecture NextJS / 2.2 — React Components and Props
2.2.a — Functional Components
In one sentence: Functional components are JavaScript functions that accept props and return JSX — they are the only way you need to write React components in 2026.
Navigation: ← Overview · Next → 2.2.b — Understanding Props
Table of Contents
- What Is a Functional Component?
- The Simplest Component
- Function Declarations vs Arrow Functions
- What a Component Returns
- Components That Return Nothing
- Class Components vs Functional Components — History
- Component Naming Rules
- Exporting and Importing Components
- One Component per File vs Multiple Components
- Components Are Just Functions — Implications
- Pure Components — The Ideal
- Component Composition — Using Components Inside Components
- Common Beginner Mistakes
- Building Your First Real Components
- Key Takeaways
1. What Is a Functional Component?
A functional component is a plain JavaScript function that:
- Accepts a single argument (props — an object of input data)
- Returns JSX (a description of what the UI should look like)
That's it. No classes, no inheritance, no this keyword. Just a function.
┌──────────────────────────────────────────────┐
│ Functional Component │
│ │
│ Input (props) ───→ Function ───→ Output (JSX)
│ │
│ { name: 'Alice' } Greeting <h1>Hello, Alice!</h1>
│ { name: 'Bob' } Greeting <h1>Hello, Bob!</h1>
│ │
│ Same function, different inputs, │
│ different outputs. │
└──────────────────────────────────────────────┘
Why "Functional"?
The word "functional" means two things here:
- It's literally a function — defined with
functionkeyword or arrow syntax. - It follows functional programming principles — given the same inputs (props), it should return the same output (JSX). No hidden side effects in the render phase.
The Mental Model
Think of a functional component like a template factory:
- You give it configuration data (props)
- It produces a piece of UI (JSX)
- It does this every time React calls it (on every render)
// This is a complete React component
function Greeting(props) {
return <h1>Hello, {props.name}!</h1>;
}
// React calls your function like this internally:
// const result = Greeting({ name: 'Alice' });
// result is: <h1>Hello, Alice!</h1>
2. The Simplest Component
Zero Props, Static Output
function Logo() {
return <img src="/logo.png" alt="Company Logo" />;
}
This component always returns the same thing. It takes no props. It's as simple as a component can be.
Using It
function App() {
return (
<header>
<Logo /> {/* Self-closing syntax */}
<Logo></Logo> {/* Also valid, but self-closing is preferred */}
<h1>Welcome</h1>
</header>
);
}
What Happens Under the Hood
When React encounters <Logo /> in JSX:
- React sees
Logostarts with uppercase → it's a component, not an HTML tag - React calls
Logo({})(empty props object) Logoreturns<img src="/logo.png" alt="Company Logo" />- React adds this to the Virtual DOM tree
- Eventually, React creates a real
<img>element in the browser
JSX: <Logo />
React calls: Logo({})
Returns: React.createElement('img', { src: '/logo.png', alt: 'Company Logo' })
Real DOM: <img src="/logo.png" alt="Company Logo">
3. Function Declarations vs Arrow Functions
Both work. The community is split. Pick one and be consistent.
Function Declaration
function ProductCard({ title, price }) {
return (
<div className="product-card">
<h3>{title}</h3>
<p>${price}</p>
</div>
);
}
Arrow Function (const)
const ProductCard = ({ title, price }) => {
return (
<div className="product-card">
<h3>{title}</h3>
<p>${price}</p>
</div>
);
};
Arrow Function with Implicit Return
// When the entire body is a single JSX expression
const ProductCard = ({ title, price }) => (
<div className="product-card">
<h3>{title}</h3>
<p>${price}</p>
</div>
);
Comparison
| Aspect | Function Declaration | Arrow Function |
|---|---|---|
| Hoisting | Yes — can use before definition | No — must define before use |
this binding | Has own this (not relevant in modern React) | Inherits this from parent scope |
| Syntax | function Name() {} | const Name = () => {} |
| Stack traces | Shows function name | May show anonymous (depends on tooling) |
| Community usage | Slightly more common | Very common, especially in teams with strict style guides |
| React DevTools | Always shows name | Shows name (via const assignment) |
My Recommendation
Use function declarations for top-level components (they're hoisted, named in stack traces):
// Top-level component — function declaration
function ProductPage() {
// Inline helper — arrow function is fine
const formatPrice = (price) => `$${price.toFixed(2)}`;
return <h1>{formatPrice(29.99)}</h1>;
}
But honestly, either is fine. Consistency matters more than the specific choice.
4. What a Component Returns
Valid Return Values
// 1. JSX element
function Example1() {
return <div>Hello</div>;
}
// 2. JSX fragment (multiple elements without wrapper)
function Example2() {
return (
<>
<h1>Title</h1>
<p>Paragraph</p>
</>
);
}
// 3. String
function Example3() {
return 'Just a text string';
}
// 4. Number
function Example4() {
return 42;
}
// 5. Boolean (renders nothing)
function Example5() {
return true; // renders nothing
}
// 6. null (renders nothing)
function Example6() {
return null; // intentionally renders nothing
}
// 7. undefined (renders nothing, but avoid — can indicate a bug)
function Example7() {
return undefined; // works but not recommended
}
// 8. Array of elements (each needs a key)
function Example8() {
return [
<li key="a">First</li>,
<li key="b">Second</li>,
<li key="c">Third</li>,
];
}
The Parentheses Convention
When return value spans multiple lines, wrap in parentheses:
// Without parens — BROKEN (returns undefined due to ASI)
function Broken() {
return
<div>
<h1>This will never render</h1>
</div>;
}
// JavaScript sees: return; (automatic semicolon insertion)
// Then <div>... is dead code
// With parens — CORRECT
function Working() {
return (
<div>
<h1>This renders correctly</h1>
</div>
);
}
Rule: Always use parentheses for multi-line JSX returns. Single-line returns don't need them:
function SingleLine() {
return <h1>Hello</h1>; // parens optional for single line
}
Single Root Element Rule
A component must return a single root element:
// WRONG: Two sibling elements with no wrapper
function Wrong() {
return (
<h1>Title</h1>
<p>Paragraph</p>
);
}
// Error: Adjacent JSX elements must be wrapped in an enclosing tag
// CORRECT: Wrapped in a div
function CorrectDiv() {
return (
<div>
<h1>Title</h1>
<p>Paragraph</p>
</div>
);
}
// CORRECT: Wrapped in a Fragment (no extra DOM node)
function CorrectFragment() {
return (
<>
<h1>Title</h1>
<p>Paragraph</p>
</>
);
}
// CORRECT: Named Fragment (can accept key prop)
function CorrectNamedFragment() {
return (
<React.Fragment>
<h1>Title</h1>
<p>Paragraph</p>
</React.Fragment>
);
}
5. Components That Return Nothing
Sometimes a component should render nothing based on a condition. Use null:
function ErrorBanner({ error }) {
// If there's no error, render nothing
if (!error) return null;
return (
<div className="error-banner" role="alert">
<p>{error.message}</p>
</div>
);
}
// Usage
<ErrorBanner error={null} /> {/* Renders nothing */}
<ErrorBanner error={{ message: 'Network error' }} /> {/* Renders the banner */}
When to Return Null vs Hide with CSS
| Approach | When to Use | Example |
|---|---|---|
return null | Component shouldn't exist in the DOM at all | Error banners, empty states, conditional features |
CSS display: none | Element should stay in DOM but be invisible | Tabs that preserve state, off-screen content for SEO |
CSS visibility: hidden | Element should take up space but be invisible | Placeholder layout, fade-in animations |
CSS opacity: 0 | Element should be invisible but interactive | Custom tooltips, accessible hidden text |
// return null: component removed from DOM entirely
function Tooltip({ show, text }) {
if (!show) return null;
return <div className="tooltip">{text}</div>;
}
// CSS approach: component stays in DOM
function Tooltip({ show, text }) {
return (
<div className="tooltip" style={{ display: show ? 'block' : 'none' }}>
{text}
</div>
);
}
6. Class Components vs Functional Components — History
The Class Component Era (2013–2018)
Before hooks, state and lifecycle methods were only available in class components:
// Class component (legacy pattern)
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.setState({ count: this.state.count + 1 });
}
componentDidMount() {
document.title = `Count: ${this.state.count}`;
}
componentDidUpdate() {
document.title = `Count: ${this.state.count}`;
}
render() {
return (
<div>
<p>Count: {this.state.count}</p>
<button onClick={this.handleClick}>Increment</button>
</div>
);
}
}
The Modern Equivalent (2019+)
// Functional component with hooks — same behavior, simpler code
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `Count: ${count}`;
}, [count]);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
Side-by-Side Comparison
| Aspect | Class Component | Functional Component |
|---|---|---|
| State | this.state + this.setState | useState hook |
| Lifecycle | componentDidMount, etc. | useEffect hook |
this keyword | Required everywhere | Not needed |
| Boilerplate | constructor, super, bind | Minimal |
| Lines of code | More | Fewer |
| Logic reuse | Higher-order components, render props | Custom hooks |
| Mental model | OOP — classes, instances, this | FP — functions, closures |
| React team recommendation | "Use for legacy code" | "Default choice for new code" |
Why Functional Won
- No
thisconfusion — the #1 source of bugs in class components - Hooks enable logic reuse — custom hooks are simpler than HOCs/render props
- Less code — same functionality in fewer lines
- Easier to test — pure functions are easier to test than class instances
- Better tree shaking — functions minify better than classes
- React's future — Server Components, React Compiler, Suspense — all designed for functions
Should You Ever Use Class Components?
Only if:
- You're working in a legacy codebase that already uses them
- You need
componentDidCatch/ Error Boundaries (the only feature still class-only as of 2026, though a hook equivalent is coming)
For new code: always use functional components.
7. Component Naming Rules
The PascalCase Requirement
// CORRECT — PascalCase
function ProductCard() { return <div>Card</div>; }
function UserAvatar() { return <img alt="avatar" />; }
// WRONG — lowercase (React treats as HTML element)
function productCard() { return <div>Card</div>; }
// <productCard /> renders as <productcard> HTML tag, not your component!
Why PascalCase?
React uses the case of the first letter to distinguish between:
- Lowercase → HTML elements:
<div>,<span>,<h1> - Uppercase → React components:
<ProductCard>,<App>
This is a fundamental rule of JSX compilation:
// JSX // Compiled output
<div /> React.createElement('div') // string → HTML
<ProductCard /> React.createElement(ProductCard) // reference → component
Naming Best Practices
// GOOD: Descriptive, tells you what it renders
function ProductCard() { ... }
function UserProfileHeader() { ... }
function ShoppingCartItem() { ... }
function NavigationSidebar() { ... }
function PaymentForm() { ... }
// BAD: Vague, could be anything
function Card() { ... } // card of what?
function Header() { ... } // header of what section?
function Item() { ... } // what kind of item?
function Form() { ... } // what kind of form?
function Wrapper() { ... } // wrapper of what?
// EXCEPTION: Truly generic, reusable components CAN have short names
// These are part of a component library, not app-specific
function Button() { ... } // generic button, used everywhere
function Modal() { ... } // generic modal container
function Input() { ... } // generic input wrapper
function Badge() { ... } // generic badge component
8. Exporting and Importing Components
Default Exports
One default export per file. Importing component can use any name:
// Button.jsx
function Button({ children, onClick }) {
return <button onClick={onClick}>{children}</button>;
}
export default Button;
// App.jsx — can use any name when importing
import Button from './Button'; // standard
import MyButton from './Button'; // also works (but confusing)
import Btn from './Button'; // also works (but don't)
Named Exports
Multiple exports per file. Import must use exact name (or alias):
// buttons.jsx
export function PrimaryButton({ children, onClick }) {
return <button className="btn-primary" onClick={onClick}>{children}</button>;
}
export function SecondaryButton({ children, onClick }) {
return <button className="btn-secondary" onClick={onClick}>{children}</button>;
}
export function DangerButton({ children, onClick }) {
return <button className="btn-danger" onClick={onClick}>{children}</button>;
}
// App.jsx — must use exact names
import { PrimaryButton, SecondaryButton } from './buttons';
// Or use alias
import { PrimaryButton as MainButton } from './buttons';
Which to Use?
| Pattern | Use When | Example |
|---|---|---|
| Default export | One main component per file (most common) | export default ProductCard |
| Named export | Multiple related components in one file | export function Tab, export function TabPanel |
| Both | Main component + utility types/helpers | export default Card + export type CardProps |
Barrel Files (Re-exports)
For feature-based organization, a barrel file (index.js) re-exports the public API:
// features/auth/index.js — barrel file
export { default as LoginForm } from './components/LoginForm';
export { default as SignupForm } from './components/SignupForm';
export { useAuth } from './hooks/useAuth';
// Usage — clean imports
import { LoginForm, SignupForm, useAuth } from './features/auth';
9. One Component per File vs Multiple Components
The Standard: One Component per File
components/
├── ProductCard.jsx ← one component
├── ProductGrid.jsx ← one component
├── ProductFilter.jsx ← one component
└── ProductSort.jsx ← one component
This is the default convention. Benefits:
- Easy to find components (file name = component name)
- Clear ownership (one file to edit for one component)
- Better tree shaking (unused components aren't bundled)
- Simpler git history (changes are scoped to one component)
When Multiple Components in One File Is OK
// Small, tightly coupled helper components that are ONLY used by the main component
function StarIcon({ filled }) {
return (
<svg className={filled ? 'star-filled' : 'star-empty'} viewBox="0 0 24 24">
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87..." />
</svg>
);
}
function HalfStarIcon() {
return (
<svg className="star-half" viewBox="0 0 24 24">
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87..." />
</svg>
);
}
// Main exported component
export default function StarRating({ rating, maxStars = 5 }) {
return (
<div className="star-rating" aria-label={`${rating} out of ${maxStars}`}>
{Array.from({ length: maxStars }, (_, i) => {
if (i < Math.floor(rating)) return <StarIcon key={i} filled />;
if (i === Math.floor(rating) && rating % 1 >= 0.5) return <HalfStarIcon key={i} />;
return <StarIcon key={i} filled={false} />;
})}
</div>
);
}
Rule of thumb: Helper components that are < 20 lines and used ONLY by the parent component can live in the same file. Everything else gets its own file.
10. Components Are Just Functions — Implications
This seemingly obvious statement has profound consequences.
Implication 1: Components Can Accept Any JavaScript Value as Props
// Strings
<Greeting name="Alice" />
// Numbers
<ProgressBar value={75} max={100} />
// Booleans
<Modal isOpen={true} />
<Modal isOpen /> // shorthand for isOpen={true}
// Arrays
<TagList tags={['React', 'JavaScript', 'CSS']} />
// Objects
<UserCard user={{ name: 'Alice', age: 30 }} />
// Functions
<Button onClick={() => console.log('clicked')} />
// Other components (as render props)
<List renderItem={(item) => <Card data={item} />} />
// JSX elements
<Layout sidebar={<Sidebar />} main={<Content />} />
Implication 2: Components Can Be Stored in Variables
const componentMap = {
text: TextInput,
number: NumberInput,
date: DatePicker,
select: SelectDropdown,
};
function DynamicField({ type, ...props }) {
const Component = componentMap[type]; // must be capitalized variable
if (!Component) return <p>Unknown field type: {type}</p>;
return <Component {...props} />;
}
// Usage
<DynamicField type="text" label="Name" /> // renders TextInput
<DynamicField type="date" label="Birthday" /> // renders DatePicker
Implication 3: Components Can Be Passed as Props
function PageLayout({ HeaderComponent, FooterComponent, children }) {
return (
<div className="page">
<HeaderComponent />
<main>{children}</main>
<FooterComponent />
</div>
);
}
// Usage
<PageLayout
HeaderComponent={AdminHeader}
FooterComponent={MinimalFooter}
>
<Dashboard />
</PageLayout>
Implication 4: Components Run on Every Render
Every time a component's parent re-renders or its state changes, React calls the component function again. This is a complete re-execution:
function Counter() {
console.log('Counter function called!'); // prints on EVERY render
const [count, setCount] = useState(0);
// This variable is created fresh on every render
const doubleCount = count * 2;
return (
<div>
<p>Count: {count} (Double: {doubleCount})</p>
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
);
}
Key insight: Local variables (doubleCount) are recalculated on every render. This is usually fine — JavaScript is fast. Only optimize with useMemo when the calculation is genuinely expensive.
11. Pure Components — The Ideal
What Is a Pure Function?
A pure function:
- Same inputs → same output (deterministic)
- No side effects (doesn't modify external state, no API calls, no DOM manipulation)
// PURE: same input always gives same output
function add(a, b) {
return a + b;
}
// IMPURE: different output for same input (depends on external state)
let counter = 0;
function incrementAndReturn() {
counter++;
return counter; // returns different value each time
}
// IMPURE: side effect (modifies external state)
function addToArray(arr, item) {
arr.push(item); // mutates the input array
return arr;
}
Pure Components in React
React components should be pure during rendering — given the same props and state, they should return the same JSX:
// PURE: Same props → same output
function UserBadge({ name, role }) {
return (
<div className="badge">
<span>{name}</span>
<span className={`role-${role}`}>{role}</span>
</div>
);
}
// IMPURE (during render): Different output for same props
function Clock() {
// new Date() gives different value on each render
const time = new Date().toLocaleTimeString();
return <p>Current time: {time}</p>;
}
// Fix: use state + useEffect for time updates
// IMPURE (during render): Side effect in render phase
function BadComponent({ data }) {
localStorage.setItem('lastData', JSON.stringify(data)); // WRONG — side effect in render!
return <div>{data.name}</div>;
}
// Fix: move side effect to useEffect
Why Purity Matters
| Benefit | Explanation |
|---|---|
| Predictable | Same props → same UI, always |
| Testable | Pass props, check output — no setup needed |
| Cacheable | React can skip re-rendering if props haven't changed (React.memo) |
| Concurrent safe | React Concurrent Mode can pause/resume rendering of pure components |
| Debugging | If UI looks wrong, check props — the problem is deterministic |
Where Side Effects Belong
Side effects are NOT banned — they just don't belong in the render phase:
┌────────────────────────────────────────────┐
│ Component Function │
│ │
│ ┌──────────────────────────────────────┐ │
│ │ RENDER PHASE (must be pure) │ │
│ │ - Read props and state │ │
│ │ - Compute derived values │ │
│ │ - Return JSX │ │
│ └──────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────┐ │
│ │ EFFECT PHASE (side effects OK) │ │
│ │ - useEffect: API calls, DOM access │ │
│ │ - Event handlers: user interactions │ │
│ │ - Refs: imperative DOM operations │ │
│ └──────────────────────────────────────┘ │
└────────────────────────────────────────────┘
12. Component Composition — Using Components Inside Components
Nesting Components
The core of React: components render other components:
function App() {
return (
<div className="app">
<Header />
<Main />
<Footer />
</div>
);
}
function Header() {
return (
<header>
<Logo />
<Navigation />
<UserMenu />
</header>
);
}
function Navigation() {
return (
<nav>
<NavLink to="/">Home</NavLink>
<NavLink to="/products">Products</NavLink>
<NavLink to="/about">About</NavLink>
</nav>
);
}
Composition Depth
App
├── Header
│ ├── Logo
│ ├── Navigation
│ │ └── NavLink × 3
│ └── UserMenu
│ └── Avatar
├── Main
│ └── ProductGrid
│ └── ProductCard × N
│ ├── ProductImage
│ ├── ProductTitle
│ ├── Price
│ └── AddToCartButton
└── Footer
└── FooterLinks
Never Define Components Inside Components
// WRONG: Component defined inside another component
function Parent() {
// This is re-created on every render of Parent!
function Child() {
return <p>I'm a child</p>;
}
return <Child />;
}
// Problems:
// 1. Child is a NEW function on every render → React unmounts and remounts it
// 2. All Child's state is lost on every Parent re-render
// 3. Performance is terrible
// CORRECT: Define components at the module level
function Child() {
return <p>I'm a child</p>;
}
function Parent() {
return <Child />;
}
This is a critical rule: never define a component inside another component's body. Always define them at the top level of the file or in separate files.
13. Common Beginner Mistakes
Mistake 1: Forgetting to Return JSX
// WRONG: No return statement
function Greeting({ name }) {
<h1>Hello, {name}!</h1>; // This creates JSX but doesn't return it!
}
// Renders: nothing (undefined)
// FIX: Add return
function Greeting({ name }) {
return <h1>Hello, {name}!</h1>;
}
Mistake 2: Returning Multiple Root Elements
// WRONG: Two root elements
function Profile() {
return (
<h1>Name</h1>
<p>Bio</p>
);
}
// Error: Adjacent JSX elements must be wrapped
// FIX: Use Fragment
function Profile() {
return (
<>
<h1>Name</h1>
<p>Bio</p>
</>
);
}
Mistake 3: Lowercase Component Name
// WRONG: lowercase
function myButton() {
return <button>Click me</button>;
}
<myButton /> // React renders <mybutton> HTML element!
// FIX: PascalCase
function MyButton() {
return <button>Click me</button>;
}
<MyButton /> // React renders your component
Mistake 4: Calling a Component as a Function
// WRONG: Calling as a function
function Parent() {
return (
<div>
{Child()} {/* This "works" but breaks React's tree reconciliation */}
</div>
);
}
// CORRECT: Using as JSX element
function Parent() {
return (
<div>
<Child /> {/* React manages the component lifecycle properly */}
</div>
);
}
Calling Child() directly means React doesn't track it as a separate component — it can't manage state, effects, or re-rendering correctly.
Mistake 5: Modifying Props
// WRONG: Mutating props
function UserCard(props) {
props.name = props.name.toUpperCase(); // NEVER mutate props!
return <h1>{props.name}</h1>;
}
// CORRECT: Create a new value
function UserCard({ name }) {
const displayName = name.toUpperCase(); // new variable, props untouched
return <h1>{displayName}</h1>;
}
Props are read-only. A component must never modify its own props. This is a core React rule.
14. Building Your First Real Components
Example 1: Alert Component
function Alert({ type = 'info', title, message, onDismiss }) {
const styles = {
info: { background: '#e3f2fd', borderColor: '#2196f3', color: '#0d47a1' },
success: { background: '#e8f5e9', borderColor: '#4caf50', color: '#1b5e20' },
warning: { background: '#fff3e0', borderColor: '#ff9800', color: '#e65100' },
error: { background: '#ffebee', borderColor: '#f44336', color: '#b71c1c' },
};
const style = styles[type];
return (
<div
role="alert"
style={{
padding: '12px 16px',
borderLeft: `4px solid ${style.borderColor}`,
backgroundColor: style.background,
color: style.color,
borderRadius: '4px',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'flex-start',
}}
>
<div>
{title && <strong style={{ display: 'block', marginBottom: 4 }}>{title}</strong>}
<span>{message}</span>
</div>
{onDismiss && (
<button
onClick={onDismiss}
style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: '18px' }}
aria-label="Dismiss"
>
×
</button>
)}
</div>
);
}
// Usage
<Alert type="success" title="Saved!" message="Your changes have been saved." />
<Alert type="error" message="Failed to connect to server." onDismiss={() => setError(null)} />
<Alert type="warning" title="Warning" message="Your session will expire in 5 minutes." />
Example 2: Avatar Component
function Avatar({ src, name, size = 'medium', status }) {
const sizeMap = { small: 32, medium: 48, large: 64, xlarge: 96 };
const pixels = sizeMap[size] || sizeMap.medium;
const statusColors = {
online: '#4caf50',
offline: '#9e9e9e',
busy: '#f44336',
away: '#ff9800',
};
return (
<div style={{ position: 'relative', display: 'inline-block' }}>
{src ? (
<img
src={src}
alt={`${name}'s avatar`}
width={pixels}
height={pixels}
style={{ borderRadius: '50%', objectFit: 'cover' }}
/>
) : (
<div
style={{
width: pixels,
height: pixels,
borderRadius: '50%',
backgroundColor: '#e0e0e0',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: pixels * 0.4,
fontWeight: 'bold',
color: '#616161',
}}
aria-label={`${name}'s avatar`}
>
{name.charAt(0).toUpperCase()}
</div>
)}
{status && (
<span
style={{
position: 'absolute',
bottom: 0,
right: 0,
width: pixels * 0.25,
height: pixels * 0.25,
borderRadius: '50%',
backgroundColor: statusColors[status] || '#9e9e9e',
border: '2px solid white',
}}
aria-label={`Status: ${status}`}
/>
)}
</div>
);
}
// Usage
<Avatar src="/alice.jpg" name="Alice" size="large" status="online" />
<Avatar name="Bob" size="medium" status="offline" /> {/* Shows "B" fallback */}
<Avatar src="/charlie.jpg" name="Charlie" /> {/* No status indicator */}
Example 3: Composing Components Together
function UserListItem({ user, onMessage }) {
return (
<div style={{
display: 'flex',
alignItems: 'center',
gap: '12px',
padding: '8px 16px',
borderBottom: '1px solid #eee',
}}>
<Avatar
src={user.avatar}
name={user.name}
size="medium"
status={user.isOnline ? 'online' : 'offline'}
/>
<div style={{ flex: 1 }}>
<strong>{user.name}</strong>
<p style={{ margin: 0, color: '#666', fontSize: '14px' }}>{user.email}</p>
</div>
<button onClick={() => onMessage(user.id)}>Message</button>
</div>
);
}
function UserList({ users, onMessage }) {
if (users.length === 0) {
return (
<Alert type="info" message="No users found." />
);
}
return (
<div>
{users.map(user => (
<UserListItem key={user.id} user={user} onMessage={onMessage} />
))}
</div>
);
}
This is the power of composition — small, focused components (Avatar, Alert) combine to build larger features (UserListItem, UserList).
15. Key Takeaways
-
A functional component is a JavaScript function that accepts props and returns JSX.
-
Function declarations and arrow functions both work. Pick one convention and be consistent.
-
Components must return a single root element — use Fragments (
<>...</>) to avoid unnecessary wrapper divs. -
Return
nullwhen a component should render nothing. Avoid returningundefined. -
Class components are legacy — use functional components for all new code.
-
Component names must be PascalCase — React uses casing to distinguish components from HTML elements.
-
Use default exports for one component per file, named exports for multiple related components.
-
Never define components inside other components — they get recreated on every render, losing all state.
-
Components should be pure during rendering — same props and state must produce the same JSX.
-
Props are read-only — never mutate props. Create new values instead.
-
Components are just functions — they can be stored in variables, passed as props, and composed freely.
Explain-It Challenge
-
The Recipe Card Analogy: Explain a functional component to someone who has never programmed. Use the analogy of a recipe card: the recipe (component) takes ingredients (props) and always produces the same dish (UI) if given the same ingredients. What happens if you modify the recipe card every time you use it?
-
Class vs Function Debate: A teammate insists on using class components because "they're more powerful." Construct a counterargument explaining why functional components with hooks are now the standard, using at least three specific advantages.
-
Composition Exercise: You're building a messaging app. Describe how you'd decompose the message list UI into components. What would each component be named, what props would it accept, and how would they compose together?
Navigation: ← Overview · Next → 2.2.b — Understanding Props