React Sibling Components

React applications are built by combining small, reusable components. As an application grows, you will often have multiple components that share the same parent.

These components are called sibling components.

For example:

        App
       /   \
      ↓     ↓
   Header  Content

Header and Content are sibling components because they are both children of App.

A common challenge occurs when one sibling needs to share information with another sibling.

React does not provide a direct sibling-to-sibling communication mechanism. Instead, the common solution is to move the shared state to their closest common parent and then pass data and callback functions between the parent and its children.

The basic pattern is:

Understanding this pattern is important because it teaches you how React components can work together without directly accessing each other’s internal state.

What Are Sibling Components?

Sibling components are components that share the same immediate parent.

For example:

function App() {
  return (
    <div>
      <Header />
      <MainContent />
      <Footer />
    </div>
  );
}

Here:

  • Header is a child of App
  • MainContent is a child of App
  • Footer is a child of App

Because they have the same parent, they are siblings.

The component hierarchy looks like this:

App
├── Header
├── MainContent
└── Footer

Why Do Sibling Components Need to Communicate?

Sibling components often need to work with related information.

For example, imagine an online shopping application:

          App
         /   \
        ↓     ↓
 ProductList  Cart

When the user adds a product in ProductList, the Cart component needs to know about it.

The ProductList and Cart components are siblings.

A beginner might try to make ProductList directly change the state inside Cart.

That is not how React’s component model is normally designed.

Instead, the shared state can live in their common parent:

             App
          /       \
         ↓         ↓
 ProductList      Cart
      │             ↑
      └── callback ─┘

The parent owns the cart data, and both siblings receive what they need through props.

The Main Pattern for Sibling Communication

The most common pattern has four steps.

Find the Common Parent

First, identify the component that renders both siblings.

App
├── ProductList
└── Cart

Here, App is the common parent.

Move Shared State to the Parent

If both components need the same data, the parent can own that state.

const [cart, setCart] = useState([]);

Pass Data to the Sibling That Needs It

The parent can pass the cart to Cart:

<Cart items={cart} />

Pass a Callback to the Other Sibling

The parent can provide a callback to ProductList:

<ProductList onAddToCart={handleAddToCart} />

The child calls the callback:

onAddToCart(product);

The parent updates the state.

The updated state then flows to Cart.

A Simple Example

Let’s build a small example where one sibling selects a color and another sibling displays the selected color.

The component structure is:

        App
       /   \
      ↓     ↓
 ColorPicker Preview

The parent owns the selected color.

import { useState } from "react";

function App() {
  const [color, setColor] = useState("blue");

  return (
    <div>
      <ColorPicker onColorChange={setColor} />
      <Preview color={color} />
    </div>
  );
}

The first sibling changes the value:

function ColorPicker({ onColorChange }) {
  return (
    <div>
      <button onClick={() => onColorChange("red")}>
        Red
      </button>

      <button onClick={() => onColorChange("green")}>
        Green
      </button>

      <button onClick={() => onColorChange("blue")}>
        Blue
      </button>
    </div>
  );
}

The second sibling receives the updated value:

function Preview({ color }) {
  return (
    <div>
      <h2>Selected Color</h2>
      <p>{color}</p>
    </div>
  );
}

The communication flow is:

ColorPicker
     │
     │ calls onColorChange()
     ↓
    App
     │
     │ updates state
     ↓
  color
     │
     ↓
  Preview

The siblings never directly communicate with each other.

Understanding the Example Step by Step

The parent creates the shared state:

const [color, setColor] = useState("blue");

The parent passes the state updater to ColorPicker:

<ColorPicker onColorChange={setColor} />

The ColorPicker component receives it:

function ColorPicker({ onColorChange }) {

When the user clicks a button, the child calls the function:

onColorChange("red");

This updates the parent’s state:

color = "red"

The parent then passes the updated value to Preview:

<Preview color={color} />

Preview displays the value:

function Preview({ color }) {
  return <p>{color}</p>;
}

This is the fundamental pattern for coordinating sibling components.

Why Not Communicate Directly Between Siblings?

React components are designed around explicit data flow.

Suppose ComponentA directly accessed the internal state of ComponentB.

That would create a strong dependency between the two components.

Instead, React encourages this structure:

Component A
     ↓
  Parent
     ↓
Component B

The parent acts as the shared communication point.

This makes the data flow easier to understand:

Sibling A
   ↓
Parent state
   ↓
Sibling B

Sharing State Between Sibling Components

Suppose two sibling components need the same state.

For example:

        App
       /   \
      ↓     ↓
  Search   Results

The search component changes the search term.

The results component uses that search term.

The shared state can live in App.

import { useState } from "react";

function App() {
  const [searchTerm, setSearchTerm] = useState("");

  return (
    <div>
      <SearchBox
        searchTerm={searchTerm}
        onSearchChange={setSearchTerm}
      />

      <SearchResults searchTerm={searchTerm} />
    </div>
  );
}

The search component updates the parent:

function SearchBox({ searchTerm, onSearchChange }) {
  return (
    <input
      value={searchTerm}
      onChange={(event) =>
        onSearchChange(event.target.value)
      }
      placeholder="Search..."
    />
  );
}

The results component receives the current value:

function SearchResults({ searchTerm }) {
  return (
    <p>
      Searching for: {searchTerm || "nothing"}
    </p>
  );
}

Now both siblings work with the same state.

Sibling Communication with Objects

Shared state does not have to be a simple string or number.

A parent can store an object:

const [user, setUser] = useState({
  name: "Karim",
  role: "Developer"
});

One sibling could update the user:

<UserEditor onUpdate={setUser} />

Another sibling could display the user:

<UserProfile user={user} />

The structure becomes:

             App
           /     \
          ↓       ↓
    UserEditor  UserProfile
        │            ↑
        │            │
        └── App state ┘

The parent coordinates the shared data.

Sibling Communication with Arrays

Arrays are another common example.

Imagine a todo application:

             App
           /     \
          ↓       ↓
       TodoForm  TodoList

The parent owns the list:

const [todos, setTodos] = useState([]);

The form can request a new todo:

<TodoForm onAddTodo={addTodo} />

The list receives the todos:

<TodoList todos={todos} />

The parent controls the actual state update.

For example:

function addTodo(title) {
  setTodos((currentTodos) => [
    ...currentTodos,
    {
      id: crypto.randomUUID(),
      title
    }
  ]);
}

The existing array is not directly mutated. A new array is created using the spread syntax.

A Complete Todo Example

Here is a complete example:

import { useState } from "react";

function App() {
  const [todos, setTodos] = useState([]);

  function addTodo(title) {
    setTodos((currentTodos) => [
      ...currentTodos,
      {
        id: crypto.randomUUID(),
        title
      }
    ]);
  }

  function deleteTodo(id) {
    setTodos((currentTodos) =>
      currentTodos.filter((todo) => todo.id !== id)
    );
  }

  return (
    <div>
      <TodoForm onAddTodo={addTodo} />

      <TodoList
        todos={todos}
        onDeleteTodo={deleteTodo}
      />
    </div>
  );
}

function TodoForm({ onAddTodo }) {
  function handleSubmit(event) {
    event.preventDefault();

    const formData = new FormData(event.currentTarget);
    const title = formData.get("title").trim();

    if (!title) {
      return;
    }

    onAddTodo(title);
    event.currentTarget.reset();
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        name="title"
        placeholder="Enter a task"
      />

      <button type="submit">
        Add Todo
      </button>
    </form>
  );
}

function TodoList({ todos, onDeleteTodo }) {
  return (
    <ul>
      {todos.map((todo) => (
        <li key={todo.id}>
          {todo.title}

          <button onClick={() => onDeleteTodo(todo.id)}>
            Delete
          </button>
        </li>
      ))}
    </ul>
  );
}

export default App;

Here, TodoForm and TodoList are siblings.

TodoForm does not directly modify TodoList.

Instead:

TodoForm
    ↓
onAddTodo()
    ↓
App
    ↓
todos state changes
    ↓
TodoList receives new todos

The same approach works for many types of applications.

Sibling Components and Derived Data

Sometimes one sibling does not need its own copy of the state.

Instead, the parent can calculate information from shared state and pass the result down.

For example:

function App() {
  const [items, setItems] = useState([]);

  const itemCount = items.length;

  return (
    <div>
      <ItemList items={items} />
      <ItemSummary count={itemCount} />
    </div>
  );
}

Here, both siblings depend on the same source of truth.

The parent owns:

items

and derives:

itemCount

This avoids keeping duplicate state.

Avoiding Duplicate State

Suppose the parent already has:

const [items, setItems] = useState([]);

It would usually be unnecessary for another sibling to maintain a separate state containing the same list.

Avoid creating duplicate sources of truth when the value can be derived from existing state.

For example, instead of:

const [items, setItems] = useState([]);
const [itemCount, setItemCount] = useState(0);

you can often derive:

const itemCount = items.length;

This keeps the application easier to reason about.

Sibling Components with Forms

Forms frequently interact with sibling components.

Consider:

             App
           /     \
          ↓       ↓
       Form      Preview

The form collects information.

The preview displays the current information.

The parent can own the shared state:

function App() {
  const [name, setName] = useState("");

  return (
    <div>
      <ProfileForm
        name={name}
        onNameChange={setName}
      />

      <ProfilePreview name={name} />
    </div>
  );
}

The form changes the value:

function ProfileForm({ name, onNameChange }) {
  return (
    <input
      value={name}
      onChange={(event) =>
        onNameChange(event.target.value)
      }
    />
  );
}

The preview displays it:

function ProfilePreview({ name }) {
  return <h2>Hello, {name || "Guest"}</h2>;
}

This creates a live preview without requiring the two sibling components to know about each other’s implementation.

Sibling Communication and Events

User interactions often begin in one sibling and affect another.

For example:

             App
           /     \
          ↓       ↓
      Sidebar   Content

A sidebar button could change which content the Content component displays.

The parent might own:

const [page, setPage] = useState("home");

Then:

<Sidebar onNavigate={setPage} />
<Content page={page} />

The sidebar triggers:

onNavigate("settings");

The parent updates the state, and the content receives the new value.

Common Mistakes

Trying to Access a Sibling’s State Directly

A sibling should not try to access another sibling’s local state.

Avoid designing components around direct access to each other’s internals.

Instead, move the shared state to the closest common parent when appropriate.

Keeping Separate Copies of Shared Data

Suppose two siblings need the same list.

Creating separate versions can cause them to become inconsistent.

Instead, keep one source of truth in the parent and pass the data down.

Mutating Shared Arrays or Objects

Avoid:

items.push(newItem);

when items is state.

Instead:

setItems((currentItems) => [
  ...currentItems,
  newItem
]);

This creates a new array.

Using the Array Index as a Key for Dynamic Lists

Avoid:

items.map((item, index) => (
  <Item key={index} item={item} />
))

when items can be inserted, deleted, or reordered.

Prefer a stable identifier:

items.map((item) => (
  <Item key={item.id} item={item} />
))

Moving State Too High

Although lifting state up is useful, you should not automatically move every piece of state to the top-level component.

Keep state as close as practical to the components that need it.

If only one component needs some state, that component can usually own it.

If multiple siblings need it, their common parent may be a suitable owner.

Best Practices

Keep One Source of Truth

When multiple components need the same data, try to maintain one authoritative state value.

Lift Shared State to the Closest Common Parent

If two siblings need to coordinate through shared state, their closest common parent is often the right place to keep that state.

Keep Data Flow Explicit

Make relationships clear through props:

<Sidebar onSelect={setSelectedItem} />
<Content selectedItem={selectedItem} />

A developer reading the component can immediately understand how the data moves.

Pass Only What a Component Needs

If a component only needs an ID, pass the ID rather than an entire large object when practical.

Keep State Local When Possible

Do not lift state simply because you can.

If state belongs to one component, keeping it there can make the component simpler.

Use Functional State Updates

When a new state value depends on the previous state, use the updater form:

setItems((currentItems) => [
  ...currentItems,
  newItem
]);

This clearly expresses that the update is based on the previous state.

When Sibling Communication Becomes More Complex

For a small application, passing props through a common parent is usually enough.

As an application grows, you may have a deeper component tree:

App
├── Header
├── Dashboard
│   ├── Sidebar
│   │   └── Filter
│   └── Content
│       └── Results
└── Footer

If information needs to travel through many layers, you may encounter prop drilling.

React’s Context API can be useful when many components at different levels need access to the same information.

For larger applications, external state management solutions may also be appropriate depending on the application’s requirements.

The important principle remains the same: choose a state ownership structure that keeps data flow understandable.

A Realistic Web4Learners Example

Imagine Web4Learners has a React tutorial page with a sidebar containing lessons and a main content area displaying the selected lesson.

The structure could be:

                 CoursePage
                 /        \
                ↓          ↓
          LessonSidebar  LessonContent

The parent owns the selected lesson:

import { useState } from "react";

function CoursePage() {
  const [selectedLesson, setSelectedLesson] = useState(
    "React Introduction"
  );

  const lessons = [
    "React Introduction",
    "Components",
    "Props",
    "State",
    "Parent to Child",
    "Child to Parent",
    "Sibling Components"
  ];

  return (
    <div>
      <LessonSidebar
        lessons={lessons}
        selectedLesson={selectedLesson}
        onSelectLesson={setSelectedLesson}
      />

      <LessonContent lesson={selectedLesson} />
    </div>
  );
}

The sidebar receives the lesson list and callback:

function LessonSidebar({
  lessons,
  selectedLesson,
  onSelectLesson
}) {
  return (
    <aside>
      {lessons.map((lesson) => (
        <button
          key={lesson}
          className={
            lesson === selectedLesson
              ? "active"
              : ""
          }
          onClick={() => onSelectLesson(lesson)}
        >
          {lesson}
        </button>
      ))}
    </aside>
  );
}

The content component receives the selected lesson:

function LessonContent({ lesson }) {
  return (
    <main>
      <h2>{lesson}</h2>
      <p>
        You are currently reading the selected React lesson.
      </p>
    </main>
  );
}

The sidebar and content area are siblings.

The sidebar does not directly communicate with LessonContent.

Instead:

LessonSidebar
      ↓
onSelectLesson()
      ↓
CoursePage
      ↓
selectedLesson changes
      ↓
LessonContent

This is a clean and scalable approach for a tutorial website such as Web4Learners.

Image Placement Instruction

Image placement: Add an educational diagram immediately after the section “The Main Pattern for Sibling Communication”.

The image should clearly show that sibling components communicate through their common parent rather than directly.

Use this conceptual flow:

              Parent Component
             /               \
            ↓                 ↓
     Sibling A             Sibling B
     "Sends action"        "Receives data"
            │                 ↑
            └──── Parent ─────┘
                 State

Use a modern Web4Learners visual style with clean cards, subtle gradients, rounded corners, modern typography, and clear arrows. The image should be beginner-friendly and visually emphasize that the parent acts as the communication bridge.

Second image placement: Add an illustration after “A Simple Example”.

Show the color picker example visually:

ColorPicker
   │
   │ onColorChange("red")
   ↓
  App
   │
   │ color = "red"
   ↓
 Preview

Keep the visual focused on the data flow rather than adding large amounts of explanatory text.

Practice Exercise

Create a React application with this component structure:

App
├── ColorSelector
└── ColorPreview

The App component should own a state value:

const [color, setColor] = useState("blue");

The ColorSelector component should contain buttons for:

  • Red
  • Green
  • Blue
  • Purple

When the user selects a color, the ColorSelector should send the selected color to the parent.

The ColorPreview component should receive the selected color from the parent and display it.

Challenge

Create a small shopping cart application:

App
├── ProductList
└── Cart

The ProductList should display several products.

Each product should have an Add to Cart button.

When a product is added:

  1. ProductList sends the product information to App.
  2. App updates the cart state.
  3. App passes the updated cart to Cart.
  4. Cart displays the products.

Use a stable product ID as the key when rendering the product list.

This exercise will help you practice:

  • useState
  • Props
  • Callback functions
  • Parent-to-child communication
  • Child-to-parent communication
  • Sibling component coordination
  • Immutable array updates

Cheat Sheet

ConceptPattern
Sibling componentsComponents with the same parent
Shared stateKeep it in the common parent
Send data downPass data as props
Send action upwardPass a callback as a prop
Child triggers parentonSelect(value)
Parent updates statesetValue(value)
Send updated data down<Child value={value} />
Array updatesetItems([...items, newItem])
Remove itemsetItems(items.filter(...))
Stable list keykey={item.id}
Avoid direct sibling accessUse the common parent
Many levels of shared dataConsider Context or suitable state management

Key Takeaways

  • Sibling components are components that share the same parent.
  • React does not normally use direct sibling-to-sibling communication.
  • When siblings need to share information, the common parent can coordinate the data flow.
  • Shared state is often moved to the closest common parent.
  • One sibling can trigger a callback provided by the parent.
  • The parent can update its state and pass the updated value to another sibling.
  • This pattern combines parent-to-child and child-to-parent communication.
  • Keep a single source of truth for shared data when appropriate.
  • Avoid directly mutating arrays and objects stored in React state.
  • Use stable keys when rendering dynamic lists.
  • Do not lift state higher than necessary.
  • For complex applications with deeply shared data, Context or dedicated state management may be appropriate.
  • Understanding sibling communication prepares you for the next topics: Sharing Data Between Components, Lifting State Up, and Component Composition.

Leave a Comment