React Essentials — Components, JSX, Props, State & MoreEntry 8 of 11

Lifting State Up

Moving state to the closest ancestor component that contains every component needing that state.

Notes

When two sibling components need the same state, neither can own it, because props only flow down. Lift the state up to the closest ancestor component that has access to all components that need to work with that state. The ancestor then:

  • Manages the state with useState.
  • Passes the state value down via props to the children that need to read it.
  • Passes a function down via props that eventually changes the state. This lets a child initiate the state change without owning the state.

This builds directly on Entry 3 (passing functions as props). Lift only as high as necessary; state placed too high causes unnecessary re-renders and prop drilling.

What / Why / When

What

Relocating state to a shared parent.

Why

Keeps a single source of truth so siblings never fall out of sync.

When

Two or more components need to read or change the same data.

Use case

In a tic-tac-toe game, the game board changes whose turn it is, and the player list highlights the active player. Both need activePlayer, so it lives in their parent, App.

Code compare

React
import { useState } from "react";​type PlayerSymbol = "X" | "O";​// Ancestor: owns the stateexport function App() {  const [activePlayer, setActivePlayer] = useState<PlayerSymbol>("X");​  function handleSelectSquare() {    setActivePlayer((current) => (current === "X" ? "O" : "X"));  }​  return (    <main>      <ol>        <Player symbol="X" isActive={activePlayer === "X"} />        <Player symbol="O" isActive={activePlayer === "O"} />      </ol>      <GameBoard activePlayerSymbol={activePlayer} onSelectSquare={handleSelectSquare} />    </main>  );}​// Child 1: reads the statefunction Player({ symbol, isActive }: { symbol: PlayerSymbol; isActive: boolean }) {  return <li className={isActive ? "active" : undefined}>{symbol}</li>;}​// Child 2: reads the state and initiates changestype GameBoardProps = {  activePlayerSymbol: PlayerSymbol;  onSelectSquare: () => void;};​function GameBoard({ activePlayerSymbol, onSelectSquare }: GameBoardProps) {  return <button onClick={onSelectSquare}>Place {activePlayerSymbol}</button>;}

The same pattern: the parent owns a signal, passes the value down with input(), and listens for changes with output().

Angular
import { Component, input, output, signal } from "@angular/core";​type PlayerSymbol = "X" | "O";​// Child 1: reads the state@Component({  selector: "app-player",  template: `<li [class.active]="isActive()">{{ symbol() }}</li>`,})export class PlayerComponent {  symbol = input.required<PlayerSymbol>();  isActive = input(false);}​// Child 2: reads the state and initiates changes@Component({  selector: "app-game-board",  template: `<button (click)="squareSelected.emit()">Place {{ activePlayerSymbol() }}</button>`,})export class GameBoardComponent {  activePlayerSymbol = input.required<PlayerSymbol>();  squareSelected = output<void>();}​// Ancestor: owns the state@Component({  selector: "app-root",  imports: [PlayerComponent, GameBoardComponent],  template: `    <main>      <ol>        <app-player symbol="X" [isActive]="activePlayer() === 'X'" />        <app-player symbol="O" [isActive]="activePlayer() === 'O'" />      </ol>      <app-game-board        [activePlayerSymbol]="activePlayer()"        (squareSelected)="handleSelectSquare()"      />    </main>  `,})export class AppComponent {  activePlayer = signal<PlayerSymbol>("X");​  handleSelectSquare() {    this.activePlayer.update((current) => (current === "X" ? "O" : "X"));  }}

Differences and gotchas

  • The pattern is identical in both frameworks. Only the child-to-parent mechanism differs: React passes a callback prop down, Angular emits an output() up.
  • When state must be shared across many levels, Angular developers usually move it into an injectable service holding a signal. React's equivalent is Context (or a store such as Redux Toolkit), covered in later chapters.
  • In React, lifting state causes the ancestor and all its children to re-render. In Angular, only bindings that read the signal update.

Quick reference

StepReactAngular
Own the stateuseState in the ancestorsignal() in the ancestor
Pass the value downPropsinput()
Let a child change itCallback prop (onSelectSquare)output() + (squareSelected)
Share widelyContext / Redux ToolkitInjectable service with a signal