Skip to main content
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

function App() {
  return (
    <>
      <header>...</header>
      <main>...</main>
    </>
  );
}
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

App.jsx
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

Section.jsx
export default function Section({ title, children, ...props }) {
  return (
    <section {...props}>
      <h2>{title}</h2>
      {children}
    </section>
  );
}
Usage
<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

Tabs.jsx
export default function Tabs({ children, buttons }) {
  return (
    <>
      <menu>{buttons}</menu>
      {children}
    </>>
  );
}
Usage
<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

Tabs.jsx
export default function Tabs({ children, buttons, ButtonsContainer = 'menu' }) {
  return (
    <>
      <ButtonsContainer>{buttons}</ButtonsContainer>
      {children}
    </>
  );
}
Usage
// 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:
UserInput.jsx
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

Build docs developers (and LLMs) love