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:
Parent
/ \
↓ ↓
Sibling A Sibling B
│ ↑
│ │
└── data ──┘
through
parent
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:
Headeris a child ofAppMainContentis a child ofAppFooteris a child ofApp
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:
ProductListsends the product information toApp.Appupdates the cart state.Apppasses the updated cart toCart.Cartdisplays 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
| Concept | Pattern |
|---|---|
| Sibling components | Components with the same parent |
| Shared state | Keep it in the common parent |
| Send data down | Pass data as props |
| Send action upward | Pass a callback as a prop |
| Child triggers parent | onSelect(value) |
| Parent updates state | setValue(value) |
| Send updated data down | <Child value={value} /> |
| Array update | setItems([...items, newItem]) |
| Remove item | setItems(items.filter(...)) |
| Stable list key | key={item.id} |
| Avoid direct sibling access | Use the common parent |
| Many levels of shared data | Consider 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.