This section explores advanced patterns and techniques for the React essentials you’ve already learned.
JSX Fragments
Components can only return one root element. Fragments let you group elements without adding extra DOM nodes.
The Problem
This won’t work:function App() {
return (
<header>...</header>
<main>...</main> // Error: Adjacent JSX elements must be wrapped
);
}
The Solutions
Fragment Syntax
React.Fragment
function App() {
return (
<>
<header>...</header>
<main>...</main>
</>
);
}
import { Fragment } from 'react';
function App() {
return (
<Fragment>
<header>...</header>
<main>...</main>
</Fragment>
);
}
Use the short syntax <>...</> unless you need to pass a key prop to the fragment.
Splitting Components
As your app grows, split large components into smaller, focused ones.
Before: Monolithic Component
function App() {
return (
<>
<Header />
<main>
<section id="core-concepts">
<h2>Core Concepts</h2>
<ul>
{CORE_CONCEPTS.map((concept) => (
<CoreConcept key={concept.title} {...concept} />
))}
</ul>
</section>
<section id="examples">
<h2>Examples</h2>
<menu>
<TabButton onSelect={() => handleSelect('components')}>
Components
</TabButton>
</menu>
<div>{/* Example content */}</div>
</section>
</main>
</>
);
}
After: Split Components
import CoreConcept from './CoreConcept';
import { CORE_CONCEPTS } from '../data';
export default function CoreConcepts() {
return (
<section id="core-concepts">
<h2>Core Concepts</h2>
<ul>
{CORE_CONCEPTS.map((concept) => (
<CoreConcept key={concept.title} {...concept} />
))}
</ul>
</section>
);
}
Split components when they become too large or handle multiple responsibilities.
Forwarding Props
Forward all props to a component without listing them individually.
Using the Rest Operator
export default function Section({ title, children, ...props }) {
return (
<section {...props}>
<h2>{title}</h2>
{children}
</section>
);
}
<Section title="Core Concepts" id="core-concepts" className="highlight">
<p>Content here</p>
</Section>
// Renders as:
// <section id="core-concepts" className="highlight">
// <h2>Core Concepts</h2>
// <p>Content here</p>
// </section>
This pattern is useful for wrapper components that need to forward HTML attributes to underlying elements.
Multiple JSX Slots
Pass multiple pieces of JSX to a component using named props.
The Tabs Pattern
export default function Tabs({ children, buttons }) {
return (
<>
<menu>{buttons}</menu>
{children}
</>>
);
}
<Tabs
buttons={
<>
<TabButton onSelect={() => handleSelect('components')}>
Components
</TabButton>
<TabButton onSelect={() => handleSelect('jsx')}>JSX</TabButton>
</>
}
>
<div>{/* Tab content */}</div>
</Tabs>
This pattern gives you more control over component structure than using only children.
Dynamic Component Types
Render different components based on props or state.
Component Type as Prop
export default function Tabs({ children, buttons, ButtonsContainer = 'menu' }) {
return (
<>
<ButtonsContainer>{buttons}</ButtonsContainer>
{children}
</>
);
}
// Renders <menu>
<Tabs buttons={...}>...</Tabs>
// Renders <div>
<Tabs buttons={...} ButtonsContainer="div">...</Tabs>
// Renders custom component
<Tabs buttons={...} ButtonsContainer={CustomMenu}>...</Tabs>
Component identifier props must start with an uppercase letter: ButtonsContainer, not buttonsContainer.
State Management Patterns
Updating State Based on Old State
When new state depends on previous state, use the function form:
function App() {
const [userInput, setUserInput] = useState({
initialInvestment: 10000,
annualInvestment: 1200,
expectedReturn: 6,
duration: 10,
});
function handleChange(inputIdentifier, newValue) {
setUserInput((prevUserInput) => {
return {
...prevUserInput,
[inputIdentifier]: +newValue,
};
});
}
}
Always use the function form when new state depends on old state:// Right
setState(prevState => prevState + 1);
// Wrong (can lead to bugs)
setState(state + 1);
Two-Way Binding
Bind input values to state for controlled components:
export default function UserInput({ onChange, userInput }) {
return (
<section id="user-input">
<div className="input-group">
<p>
<label>Initial Investment</label>
<input
type="number"
required
value={userInput.initialInvestment}
onChange={(event) =>
onChange('initialInvestment', event.target.value)
}
/>
</p>
<p>
<label>Annual Investment</label>
<input
type="number"
required
value={userInput.annualInvestment}
onChange={(event) =>
onChange('annualInvestment', event.target.value)
}
/>
</p>
</div>
</section>
);
}
Controlled components receive their current value from props and notify changes via callbacks.
Updating State Immutably
DON’T mutate state directly:// Wrong - mutates existing array
const newBoard = board;
newBoard[rowIndex][colIndex] = 'X';
setBoard(newBoard);
DO create a new copy:// Right - creates new array
const newBoard = [...board.map(row => [...row])];
newBoard[rowIndex][colIndex] = 'X';
setBoard(newBoard);
Lifting State Up
Move state to the closest common ancestor when multiple components need access:
function App() {
const [userInput, setUserInput] = useState({
initialInvestment: 10000,
annualInvestment: 1200,
expectedReturn: 6,
duration: 10,
});
function handleChange(inputIdentifier, newValue) {
setUserInput((prevUserInput) => {
return {
...prevUserInput,
[inputIdentifier]: +newValue,
};
});
}
return (
<>
<Header />
<UserInput userInput={userInput} onChange={handleChange} />
<Results input={userInput} />
</>
);
}
Lift state to the lowest common ancestor that needs to share the data.
Avoid Intersecting State
DON’T store derived values in state:const [cart, setCart] = useState([]);
const [totalPrice, setTotalPrice] = useState(0); // Redundant!
DO compute them:const [cart, setCart] = useState([]);
const totalPrice = cart.reduce((sum, item) => sum + item.price, 0);
Prefer Computed Values
Derive values from state instead of storing them:
function GameBoard({ onSelectSquare, turns }) {
// Derive game board from turns instead of storing it in state
let gameBoard = initialGameBoard.map(array => [...array]);
for (const turn of turns) {
const { square, player } = turn;
const { row, col } = square;
gameBoard[row][col] = player;
}
return (
<ol id="game-board">
{gameBoard.map((row, rowIndex) => (
<li key={rowIndex}>
<ol>
{row.map((playerSymbol, colIndex) => (
<li key={colIndex}>
<button onClick={() => onSelectSquare(rowIndex, colIndex)}>
{playerSymbol}
</button>
</li>
))}
</ol>
</li>
))}
</ol>
);
}
Multi-Dimensional Lists
Render nested arrays with nested map() calls:
const gameBoard = [
[null, 'X', null],
['O', 'X', null],
[null, null, 'O'],
];
<ol id="game-board">
{gameBoard.map((row, rowIndex) => (
<li key={rowIndex}>
<ol>
{row.map((playerSymbol, colIndex) => (
<li key={colIndex}>
<button>{playerSymbol}</button>
</li>
))}
</ol>
</li>
))}
</ol>
Each level of nesting needs its own key prop.
Best Practices
Component Size
Keep components focused on a single responsibility
Immutable Updates
Always create new objects/arrays when updating state
Derived State
Compute values from state instead of storing duplicates
Prop Forwarding
Use rest/spread operators for flexible components
Common Patterns
function Card({ children, className, ...props }) {
return (
<div className={`card ${className}`} {...props}>
{children}
</div>
);
}
function Tabs({ children, buttons }) {
return (
<>
<menu>{buttons}</menu>
{children}
</>
);
}
function Input({ value, onChange }) {
return (
<input
value={value}
onChange={(e) => onChange(e.target.value)}
/>
);
}
Next Steps
Debugging React
Learn debugging techniques and tools for React applications