React Sharing Data Between Components

React applications are rarely made from a single component. A real application may contain navigation bars, forms, cards, lists, sidebars, dashboards, modals, and many other components that need to work together.

As components become more independent, an important question appears:

How can components share data with each other?

React provides several patterns for handling this. The right approach depends on the relationship between the components and how widely the data needs to be shared.

For closely related components, props and callback functions are usually enough. When sibling components need the same data, the state can often be moved to their common parent. When many components at different levels need access to the same data, React Context can be useful.

The key is to keep data flow clear and avoid creating unnecessary complexity.

What Does Sharing Data Between Components Mean?

Sharing data between components means allowing one or more components to access information needed to display UI or perform an action.

For example, an application might have:

App
├── Header
├── ProductList
└── Cart

The ProductList needs product information.

The Cart needs information about selected products.

The Header might need to display the number of items in the cart.

All three components may need access to related data.

A well-designed React application determines where that data should live and how it should flow to the components that need it.

Why Is Data Sharing Important?

Component-based development works because individual components can be separated into smaller responsibilities.

For example:

ProductCard
SearchBox
Cart
Checkout

Each component can have its own responsibility.

However, these components may still need to coordinate.

A product card may need to tell the application that a product was selected.

The cart may need to display that product.

The checkout page may need the current cart.

This requires a clear data-sharing strategy.

Without one, applications can become difficult to maintain because components may have duplicated state or complicated dependencies.

Understanding React’s Data Flow

React generally follows a one-way data flow.

Data commonly moves from a parent to a child:

Parent
  ↓
Props
  ↓
Child

When a child needs to communicate something back to its parent, the parent can provide a callback:

Parent
  ↓
Callback prop
  ↓
Child
  ↓
Calls callback
  ↓
Parent

This creates a predictable communication pattern.

When multiple components need the same information, the state can often be moved to a component that is high enough in the tree to coordinate them.

Sharing Data Between Parent and Child

The simplest form of data sharing is passing props from a parent to a child.

function App() {
  const userName = "Karim";

  return <UserProfile name={userName} />;
}

function UserProfile({ name }) {
  return <h2>Welcome, {name}</h2>;
}

The parent owns the value:

const userName = "Karim";

The parent passes it:

<UserProfile name={userName} />

The child receives it:

function UserProfile({ name }) {

This is the simplest and most common form of component communication.

Sharing Data from Child to Parent

A child cannot directly modify its parent’s state.

Instead, the parent can pass a function:

function App() {
  function handleMessage(message) {
    console.log(message);
  }

  return <MessageButton onSend={handleMessage} />;
}

The child receives the callback:

function MessageButton({ onSend }) {
  return (
    <button onClick={() => onSend("Hello from the child")}>
      Send
    </button>
  );
}

The child calls the function, allowing the parent to receive the information.

This pattern is particularly useful for forms, buttons, selections, and other interactive components.

Sharing Data Between Sibling Components

Sibling components share the same parent.

For example:

       App
      /   \
     ↓     ↓
 Search  Results

Suppose Search changes a search term and Results needs that search term.

The common parent can own the state:

import { useState } from "react";

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

  return (
    <div>
      <Search
        value={searchTerm}
        onChange={setSearchTerm}
      />

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

The Search component changes the state:

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

The Results component receives the current value:

function Results({ searchTerm }) {
  return (
    <p>
      Searching for: {searchTerm || "all products"}
    </p>
  );
}

The two siblings are connected through the parent.

Search
  ↓
Parent State
  ↓
Results

This is one of the most important patterns for sharing data between components.

Lifting State Up

When multiple components need access to the same state, the state can often be moved to their closest common parent.

This is called lifting state up.

Consider:

          App
        /     \
       ↓       ↓
   Component A  Component B

If both components need the same value, the parent can own it:

function App() {
  const [value, setValue] = useState("");

  return (
    <>
      <ComponentA
        value={value}
        onChange={setValue}
      />

      <ComponentB value={value} />
    </>
  );
}

Now there is one source of truth.

ComponentA can request changes.

ComponentB can display the current value.

The parent coordinates both.

Why One Source of Truth Matters

Suppose two components maintain separate copies of the same information:

Component A
   ↓
name = "Karim"

Component B
   ↓
name = "Karim"

If one component changes its value but the other does not, the application can become inconsistent.

Instead, use one shared state:

          Parent
            ↓
       name = "Karim"
        /         \
       ↓           ↓
 Component A   Component B

Both components receive information from the same source.

This makes synchronization much easier.

Sharing Arrays Between Components

Arrays are frequently shared in React applications.

Consider a todo application:

function App() {
  const [todos, setTodos] = useState([
    {
      id: 1,
      title: "Learn React"
    },
    {
      id: 2,
      title: "Build a project"
    }
  ]);

  return (
    <>
      <TodoList todos={todos} />
      <TodoSummary todos={todos} />
    </>
  );
}

Both children receive the same array.

The list can display individual tasks:

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

The summary can calculate information:

function TodoSummary({ todos }) {
  return <p>Total tasks: {todos.length}</p>;
}

Both components are working from the same state.

Updating Shared Arrays

If a child needs to add an item, the parent can provide a callback.

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

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

  return (
    <>
      <TodoForm onAddTodo={addTodo} />
      <TodoList todos={todos} />
    </>
  );
}

The form can call:

onAddTodo("Learn React");

The parent creates a new array:

setTodos((currentTodos) => [
  ...currentTodos,
  newTodo
]);

The updated array is then passed to TodoList.

Avoid directly mutating the existing state array with methods such as push().

Sharing Objects Between Components

Objects are useful for sharing related information.

For example:

const user = {
  name: "Karim",
  role: "Frontend Developer",
  location: "Hyderabad"
};

The parent can pass it to multiple components:

<UserProfile user={user} />
<UserSummary user={user} />

The components can read the values:

function UserProfile({ user }) {
  return <h2>{user.name}</h2>;
}
function UserSummary({ user }) {
  return <p>{user.role}</p>;
}

If the object is stored in state and needs to be updated, create a new object rather than mutating the existing one.

setUser((currentUser) => ({
  ...currentUser,
  role: "React Developer"
}));

Sharing Data Through Multiple Levels

Sometimes data needs to travel through several components.

For example:

App
 ↓
Dashboard
 ↓
Sidebar
 ↓
UserMenu

If UserMenu needs a user object owned by App, you could pass it through every level:

<App>
  <Dashboard user={user} />
</App>

Then:

<Sidebar user={user} />

Then:

<UserMenu user={user} />

This works, but when many intermediate components only forward the same prop without using it, the pattern can become prop drilling.

What Is Prop Drilling?

Prop drilling occurs when data is passed through components that do not actually need to use the data themselves.

For example:

App
 ↓ user
Dashboard
 ↓ user
Sidebar
 ↓ user
UserMenu

Dashboard and Sidebar may simply pass user along.

This is not automatically bad. Passing props through a few levels is often perfectly reasonable.

The problem can arise when the component tree becomes deep and many components have to forward unrelated data.

Using Context for Shared Data

React provides the Context API for cases where many components need access to the same information without passing props through every intermediate component.

For example:

import { createContext, useContext } from "react";

const UserContext = createContext(null);

The parent can provide the value:

function App() {
  const user = {
    name: "Karim",
    role: "Developer"
  };

  return (
    <UserContext value={user}>
      <Dashboard />
    </UserContext>
  );
}

A deeply nested component can read the context:

function UserMenu() {
  const user = useContext(UserContext);

  return <h2>{user.name}</h2>;
}

Context can be useful for data such as:

  • Theme settings
  • Authentication information
  • Language preferences
  • Application-level settings
  • Other values needed by many components

Context should not automatically replace props. Props are often clearer when data is needed by only a small number of closely related components.

Sharing Data with Context and State

Context becomes particularly useful when combined with state.

For example:

import {
  createContext,
  useContext,
  useState
} from "react";

const ThemeContext = createContext(null);

function App() {
  const [theme, setTheme] = useState("light");

  return (
    <ThemeContext value={{ theme, setTheme }}>
      <Dashboard />
    </ThemeContext>
  );
}

A deeply nested component can access the shared state:

function ThemeButton() {
  const { theme, setTheme } = useContext(ThemeContext);

  return (
    <button
      onClick={() =>
        setTheme(theme === "light" ? "dark" : "light")
      }
    >
      Current theme: {theme}
    </button>
  );
}

The exact state management architecture should depend on the size and needs of the application.

Sharing Data with children

Another useful pattern involves the special children prop.

For example:

function Card({ children }) {
  return (
    <section className="card">
      {children}
    </section>
  );
}

A parent can provide different content:

<Card>
  <h2>React</h2>
  <p>Learn React with Web4Learners.</p>
</Card>

The Card component does not need to know exactly what content it will contain.

This is an example of component composition, which is useful for building reusable UI structures.

Sharing Data Through Callback Functions

Sometimes the data does not need to be stored in the child.

Instead, the child can report an event to the parent.

For example:

function ProductCard({ product, onAddToCart }) {
  return (
    <button onClick={() => onAddToCart(product)}>
      Add to Cart
    </button>
  );
}

The parent can handle the product:

function App() {
  const [cart, setCart] = useState([]);

  function addToCart(product) {
    setCart((currentCart) => [
      ...currentCart,
      product
    ]);
  }

  return (
    <ProductCard
      product={{ id: 1, name: "Keyboard" }}
      onAddToCart={addToCart}
    />
  );
}

This creates a clean separation:

  • The child handles the user interaction.
  • The parent owns the cart state.
  • The callback connects the two.

Sharing Data in a Shopping Cart

A shopping cart is a useful real-world example.

Imagine:

              App
            /     \
           ↓       ↓
    ProductList    Cart

The parent owns the cart:

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

The product list receives an onAddToCart callback:

<ProductList onAddToCart={addToCart} />

The cart receives the cart data:

<Cart items={cart} />

The complete relationship is:

ProductList
     ↓
addToCart(product)
     ↓
   App State
     ↓
    Cart

This pattern can be extended to quantity changes, item deletion, totals, and checkout.

Sharing Data in a Search Interface

Search interfaces are another common example.

          App
        /     \
       ↓       ↓
   SearchBox  Results

The parent can own:

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

The search box updates it:

<SearchBox
  value={searchTerm}
  onChange={setSearchTerm}
/>

The results component receives it:

<Results searchTerm={searchTerm} />

This allows both components to remain focused on their individual responsibilities.

Sharing Data in a Dashboard

A dashboard may contain several components:

                 Dashboard
             /      |       \
            ↓       ↓        ↓
        Revenue    Users    Orders

The parent might receive dashboard data and pass the relevant values to each component:

<RevenueCard value={revenue} />
<UserCard count={users} />
<OrderCard count={orders} />

Each child does not need to know where the data originated.

It only needs the values required to render its UI.

Choosing the Right Data-Sharing Pattern

There is no single method that should be used for every situation.

Parent and Child

Use props when a parent needs to provide information to a child.

Parent → Child

Child and Parent

Use callback props when a child needs to notify or request an action from its parent.

Child → callback → Parent

Sibling Components

Use a common parent to coordinate shared state.

Sibling A
   ↓
Parent
   ↓
Sibling B

Deeply Shared Data

Consider Context when many components at different levels need the same value.

Context Provider
      ↓
Many Components

Complex Application State

For larger applications, you may eventually use a dedicated state management library or another architecture when the application’s requirements justify it.

The important thing is not to introduce a more complex solution before you actually need one.

Common Mistakes

Creating Duplicate State

If two components need the same information, avoid maintaining independent copies unless there is a specific reason.

Instead, consider keeping one source of truth.

Passing State Through Too Many Components

If a value is being forwarded through many components that do not use it, evaluate whether the structure is creating unnecessary prop drilling.

Using Context for Everything

Context is useful, but it should not automatically be used for every shared value.

For a simple parent-child relationship, props are usually easier to understand.

Mutating Shared Data

Avoid modifying arrays or objects directly.

Incorrect:

cart.push(product);

Prefer:

setCart((currentCart) => [
  ...currentCart,
  product
]);

For objects:

setUser((currentUser) => ({
  ...currentUser,
  name: "Karim"
}));

Storing Derived Values as Separate State

If a value can be calculated from existing state, you often do not need another state variable.

For example:

const totalItems = cart.length;

rather than maintaining a separate totalItems state that must always be synchronized with cart.

Passing the Wrong Callback

Remember the difference between passing a function and calling it immediately.

Correct:

<button onClick={handleSave}>
  Save
</button>

Correct when arguments are needed:

<button onClick={() => handleDelete(id)}>
  Delete
</button>

Avoid:

<button onClick={handleDelete(id)}>
  Delete
</button>

because that invokes the function while rendering.

Best Practices

Start with Props

For simple relationships, props are usually the clearest solution.

Lift State When Components Need the Same Data

If siblings need shared state, consider moving that state to their closest common parent.

Keep State as Local as Practical

Do not move state higher in the component tree unless another component needs access to it.

Maintain a Single Source of Truth

Avoid unnecessary duplicate copies of the same state.

Use Context for Appropriate Cases

Context is useful when many components need the same information, particularly across different levels of the component tree.

Keep Component APIs Simple

A component should generally receive only the props it needs.

For example:

<ProductCard
  product={product}
  onAddToCart={addToCart}
/>

is easier to understand than passing a large collection of unrelated values.

Prefer Immutable Updates

When updating arrays and objects in state, create new values rather than modifying the existing state directly.

A Realistic Web4Learners Example

Imagine Web4Learners has an interactive React course interface.

The page contains:

                  CoursePage
               /      |       \
              ↓       ↓        ↓
          Sidebar   Lesson    Progress

The parent can own the current lesson and completed lessons:

import { useState } from "react";

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

  const [completedLessons, setCompletedLessons] = useState([]);

  function handleLessonSelect(lesson) {
    setSelectedLesson(lesson);
  }

  function handleLessonComplete(lesson) {
    setCompletedLessons((currentLessons) => {
      if (currentLessons.includes(lesson)) {
        return currentLessons;
      }

      return [...currentLessons, lesson];
    });
  }

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

      <LessonContent
        lesson={selectedLesson}
        onComplete={handleLessonComplete}
      />

      <Progress
        completedLessons={completedLessons}
      />
    </div>
  );
}

The sidebar can select a lesson:

function LessonSidebar({
  selectedLesson,
  onSelectLesson
}) {
  const lessons = [
    "React Introduction",
    "Components",
    "Props",
    "State",
    "Parent to Child",
    "Child to Parent",
    "Sibling Components"
  ];

  return (
    <aside>
      {lessons.map((lesson) => (
        <button
          key={lesson}
          onClick={() => onSelectLesson(lesson)}
        >
          {lesson}
        </button>
      ))}
    </aside>
  );
}

The lesson component can notify the parent when the lesson is completed:

function LessonContent({ lesson, onComplete }) {
  return (
    <main>
      <h2>{lesson}</h2>

      <p>
        Learn this React concept step by step.
      </p>

      <button onClick={() => onComplete(lesson)}>
        Mark as Complete
      </button>
    </main>
  );
}

The progress component receives the shared data:

function Progress({ completedLessons }) {
  return (
    <aside>
      <h2>Progress</h2>
      <p>
        Completed: {completedLessons.length}
      </p>
    </aside>
  );
}

This demonstrates several communication patterns working together.

LessonSidebar
      ↓
  callback
      ↓
 CoursePage
      ↓
 selectedLesson
      ↓
 LessonContent


LessonContent
      ↓
  callback
      ↓
 CoursePage
      ↓
 completedLessons
      ↓
 Progress

The parent acts as the central coordinator while each child remains focused on its own responsibility.

Image Placement Instruction

Image placement: Add a visual diagram immediately after the section “Understanding React’s Data Flow”.

The image should show the major data-sharing directions in React:

                 Parent
                /      \
               ↓        ↓
            Child A   Child B
               ↑        ↓
          Callback    Props
               │        │
               └── Parent

The main message should be that React uses explicit data flow through props and callback functions.

Use a modern Web4Learners visual style with clean cards, rounded corners, subtle gradients, modern typography, and clear arrows. Avoid putting large paragraphs inside the image.

Second image placement: Add a diagram immediately after the section “Choosing the Right Data-Sharing Pattern”.

Show three patterns side by side:

Parent → Child
Props

Child → Parent
Callback

Sibling ↔ Sibling
Common Parent

The visual should make it easy for a beginner to decide which communication pattern applies to a particular component relationship.

Practice Exercise

Build a small React application for a course dashboard.

Create this component structure:

App
├── CourseSelector
├── CourseDetails
└── CourseProgress

The App component should maintain the selected course:

const [course, setCourse] = useState("React");

CourseSelector should contain buttons for:

  • React
  • JavaScript
  • Python
  • SQL

When the user selects a course, the child should notify the parent using a callback.

CourseDetails should receive the selected course and display it.

CourseProgress should also receive information from the parent and display the user’s progress.

Challenge

Add a list of completed lessons:

const [completedLessons, setCompletedLessons] = useState([]);

When the user clicks Complete Lesson, update the parent state.

Then pass the completed lessons to CourseProgress.

Calculate the completion count from the array instead of creating unnecessary duplicate state.

Cheat Sheet

SituationRecommended Pattern
Parent sends data to childProps
Child sends information to parentCallback prop
Two siblings need shared dataCommon parent
Multiple components need the same stateLift state up
Data travels through many levelsConsider Context
Render nested contentchildren prop
Share an arrayPass array through props
Share an objectPass object through props
Update array stateSpread, map(), filter()
Update object stateObject spread
Derived valueCalculate from existing state
Dynamic listUse stable keys

Key Takeaways

  • React applications often require multiple components to work with the same data.
  • Props are the primary way to share data from parent to child.
  • Callback functions allow children to notify parents or request actions.
  • Sibling components can share data through their common parent.
  • When several components need the same state, the state can often be lifted to their closest common parent.
  • Maintaining a single source of truth helps prevent inconsistent data.
  • Arrays and objects in React state should be updated immutably rather than mutated directly.
  • Prop drilling occurs when data is passed through components that do not use it themselves.
  • Context can help when many components at different levels need the same information.
  • Context should be used when it solves a real component communication problem, not simply because it is available.
  • Keep state as local as practical and lift it only when necessary.
  • Derived values can often be calculated from existing state instead of being stored separately.
  • Effective data sharing keeps React applications predictable, reusable, and easier to maintain.

Sharing Data Between Components in React

React applications are rarely made from a single component. A real application may contain navigation bars, forms, cards, lists, sidebars, dashboards, modals, and many other components that need to work together.

As components become more independent, an important question appears:

How can components share data with each other?

React provides several patterns for handling this. The right approach depends on the relationship between the components and how widely the data needs to be shared.

For closely related components, props and callback functions are usually enough. When sibling components need the same data, the state can often be moved to their common parent. When many components at different levels need access to the same data, React Context can be useful.

The key is to keep data flow clear and avoid creating unnecessary complexity.

What Does Sharing Data Between Components Mean?

Sharing data between components means allowing one or more components to access information needed to display UI or perform an action.

For example, an application might have:

App
├── Header
├── ProductList
└── Cart

The ProductList needs product information.

The Cart needs information about selected products.

The Header might need to display the number of items in the cart.

All three components may need access to related data.

A well-designed React application determines where that data should live and how it should flow to the components that need it.

Why Is Data Sharing Important?

Component-based development works because individual components can be separated into smaller responsibilities.

For example:

ProductCard
SearchBox
Cart
Checkout

Each component can have its own responsibility.

However, these components may still need to coordinate.

A product card may need to tell the application that a product was selected.

The cart may need to display that product.

The checkout page may need the current cart.

This requires a clear data-sharing strategy.

Without one, applications can become difficult to maintain because components may have duplicated state or complicated dependencies.

Understanding React’s Data Flow

React generally follows a one-way data flow.

Data commonly moves from a parent to a child:

Parent
  ↓
Props
  ↓
Child

When a child needs to communicate something back to its parent, the parent can provide a callback:

Parent
  ↓
Callback prop
  ↓
Child
  ↓
Calls callback
  ↓
Parent

This creates a predictable communication pattern.

When multiple components need the same information, the state can often be moved to a component that is high enough in the tree to coordinate them.

Sharing Data Between Parent and Child

The simplest form of data sharing is passing props from a parent to a child.

function App() {
  const userName = "Karim";

  return <UserProfile name={userName} />;
}

function UserProfile({ name }) {
  return <h2>Welcome, {name}</h2>;
}

The parent owns the value:

const userName = "Karim";

The parent passes it:

<UserProfile name={userName} />

The child receives it:

function UserProfile({ name }) {

This is the simplest and most common form of component communication.

Sharing Data from Child to Parent

A child cannot directly modify its parent’s state.

Instead, the parent can pass a function:

function App() {
  function handleMessage(message) {
    console.log(message);
  }

  return <MessageButton onSend={handleMessage} />;
}

The child receives the callback:

function MessageButton({ onSend }) {
  return (
    <button onClick={() => onSend("Hello from the child")}>
      Send
    </button>
  );
}

The child calls the function, allowing the parent to receive the information.

This pattern is particularly useful for forms, buttons, selections, and other interactive components.

Sharing Data Between Sibling Components

Sibling components share the same parent.

For example:

       App
      /   \
     ↓     ↓
 Search  Results

Suppose Search changes a search term and Results needs that search term.

The common parent can own the state:

import { useState } from "react";

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

  return (
    <div>
      <Search
        value={searchTerm}
        onChange={setSearchTerm}
      />

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

The Search component changes the state:

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

The Results component receives the current value:

function Results({ searchTerm }) {
  return (
    <p>
      Searching for: {searchTerm || "all products"}
    </p>
  );
}

The two siblings are connected through the parent.

Search
  ↓
Parent State
  ↓
Results

This is one of the most important patterns for sharing data between components.

Lifting State Up

When multiple components need access to the same state, the state can often be moved to their closest common parent.

This is called lifting state up.

Consider:

          App
        /     \
       ↓       ↓
   Component A  Component B

If both components need the same value, the parent can own it:

function App() {
  const [value, setValue] = useState("");

  return (
    <>
      <ComponentA
        value={value}
        onChange={setValue}
      />

      <ComponentB value={value} />
    </>
  );
}

Now there is one source of truth.

ComponentA can request changes.

ComponentB can display the current value.

The parent coordinates both.

Why One Source of Truth Matters

Suppose two components maintain separate copies of the same information:

Component A
   ↓
name = "Karim"

Component B
   ↓
name = "Karim"

If one component changes its value but the other does not, the application can become inconsistent.

Instead, use one shared state:

          Parent
            ↓
       name = "Karim"
        /         \
       ↓           ↓
 Component A   Component B

Both components receive information from the same source.

This makes synchronization much easier.

Sharing Arrays Between Components

Arrays are frequently shared in React applications.

Consider a todo application:

function App() {
  const [todos, setTodos] = useState([
    {
      id: 1,
      title: "Learn React"
    },
    {
      id: 2,
      title: "Build a project"
    }
  ]);

  return (
    <>
      <TodoList todos={todos} />
      <TodoSummary todos={todos} />
    </>
  );
}

Both children receive the same array.

The list can display individual tasks:

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

The summary can calculate information:

function TodoSummary({ todos }) {
  return <p>Total tasks: {todos.length}</p>;
}

Both components are working from the same state.

Updating Shared Arrays

If a child needs to add an item, the parent can provide a callback.

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

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

  return (
    <>
      <TodoForm onAddTodo={addTodo} />
      <TodoList todos={todos} />
    </>
  );
}

The form can call:

onAddTodo("Learn React");

The parent creates a new array:

setTodos((currentTodos) => [
  ...currentTodos,
  newTodo
]);

The updated array is then passed to TodoList.

Avoid directly mutating the existing state array with methods such as push().

Sharing Objects Between Components

Objects are useful for sharing related information.

For example:

const user = {
  name: "Karim",
  role: "Frontend Developer",
  location: "Hyderabad"
};

The parent can pass it to multiple components:

<UserProfile user={user} />
<UserSummary user={user} />

The components can read the values:

function UserProfile({ user }) {
  return <h2>{user.name}</h2>;
}
function UserSummary({ user }) {
  return <p>{user.role}</p>;
}

If the object is stored in state and needs to be updated, create a new object rather than mutating the existing one.

setUser((currentUser) => ({
  ...currentUser,
  role: "React Developer"
}));

Sharing Data Through Multiple Levels

Sometimes data needs to travel through several components.

For example:

App
 ↓
Dashboard
 ↓
Sidebar
 ↓
UserMenu

If UserMenu needs a user object owned by App, you could pass it through every level:

<App>
  <Dashboard user={user} />
</App>

Then:

<Sidebar user={user} />

Then:

<UserMenu user={user} />

This works, but when many intermediate components only forward the same prop without using it, the pattern can become prop drilling.

What Is Prop Drilling?

Prop drilling occurs when data is passed through components that do not actually need to use the data themselves.

For example:

App
 ↓ user
Dashboard
 ↓ user
Sidebar
 ↓ user
UserMenu

Dashboard and Sidebar may simply pass user along.

This is not automatically bad. Passing props through a few levels is often perfectly reasonable.

The problem can arise when the component tree becomes deep and many components have to forward unrelated data.

Using Context for Shared Data

React provides the Context API for cases where many components need access to the same information without passing props through every intermediate component.

For example:

import { createContext, useContext } from "react";

const UserContext = createContext(null);

The parent can provide the value:

function App() {
  const user = {
    name: "Karim",
    role: "Developer"
  };

  return (
    <UserContext value={user}>
      <Dashboard />
    </UserContext>
  );
}

A deeply nested component can read the context:

function UserMenu() {
  const user = useContext(UserContext);

  return <h2>{user.name}</h2>;
}

Context can be useful for data such as:

  • Theme settings
  • Authentication information
  • Language preferences
  • Application-level settings
  • Other values needed by many components

Context should not automatically replace props. Props are often clearer when data is needed by only a small number of closely related components.

Sharing Data with Context and State

Context becomes particularly useful when combined with state.

For example:

import {
  createContext,
  useContext,
  useState
} from "react";

const ThemeContext = createContext(null);

function App() {
  const [theme, setTheme] = useState("light");

  return (
    <ThemeContext value={{ theme, setTheme }}>
      <Dashboard />
    </ThemeContext>
  );
}

A deeply nested component can access the shared state:

function ThemeButton() {
  const { theme, setTheme } = useContext(ThemeContext);

  return (
    <button
      onClick={() =>
        setTheme(theme === "light" ? "dark" : "light")
      }
    >
      Current theme: {theme}
    </button>
  );
}

The exact state management architecture should depend on the size and needs of the application.

Sharing Data with children

Another useful pattern involves the special children prop.

For example:

function Card({ children }) {
  return (
    <section className="card">
      {children}
    </section>
  );
}

A parent can provide different content:

<Card>
  <h2>React</h2>
  <p>Learn React with Web4Learners.</p>
</Card>

The Card component does not need to know exactly what content it will contain.

This is an example of component composition, which is useful for building reusable UI structures.

Sharing Data Through Callback Functions

Sometimes the data does not need to be stored in the child.

Instead, the child can report an event to the parent.

For example:

function ProductCard({ product, onAddToCart }) {
  return (
    <button onClick={() => onAddToCart(product)}>
      Add to Cart
    </button>
  );
}

The parent can handle the product:

function App() {
  const [cart, setCart] = useState([]);

  function addToCart(product) {
    setCart((currentCart) => [
      ...currentCart,
      product
    ]);
  }

  return (
    <ProductCard
      product={{ id: 1, name: "Keyboard" }}
      onAddToCart={addToCart}
    />
  );
}

This creates a clean separation:

  • The child handles the user interaction.
  • The parent owns the cart state.
  • The callback connects the two.

Sharing Data in a Shopping Cart

A shopping cart is a useful real-world example.

Imagine:

              App
            /     \
           ↓       ↓
    ProductList    Cart

The parent owns the cart:

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

The product list receives an onAddToCart callback:

<ProductList onAddToCart={addToCart} />

The cart receives the cart data:

<Cart items={cart} />

The complete relationship is:

ProductList
     ↓
addToCart(product)
     ↓
   App State
     ↓
    Cart

This pattern can be extended to quantity changes, item deletion, totals, and checkout.

Sharing Data in a Search Interface

Search interfaces are another common example.

          App
        /     \
       ↓       ↓
   SearchBox  Results

The parent can own:

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

The search box updates it:

<SearchBox
  value={searchTerm}
  onChange={setSearchTerm}
/>

The results component receives it:

<Results searchTerm={searchTerm} />

This allows both components to remain focused on their individual responsibilities.

Sharing Data in a Dashboard

A dashboard may contain several components:

                 Dashboard
             /      |       \
            ↓       ↓        ↓
        Revenue    Users    Orders

The parent might receive dashboard data and pass the relevant values to each component:

<RevenueCard value={revenue} />
<UserCard count={users} />
<OrderCard count={orders} />

Each child does not need to know where the data originated.

It only needs the values required to render its UI.

Choosing the Right Data-Sharing Pattern

There is no single method that should be used for every situation.

Parent and Child

Use props when a parent needs to provide information to a child.

Parent → Child

Child and Parent

Use callback props when a child needs to notify or request an action from its parent.

Child → callback → Parent

Sibling Components

Use a common parent to coordinate shared state.

Sibling A
   ↓
Parent
   ↓
Sibling B

Deeply Shared Data

Consider Context when many components at different levels need the same value.

Context Provider
      ↓
Many Components

Complex Application State

For larger applications, you may eventually use a dedicated state management library or another architecture when the application’s requirements justify it.

The important thing is not to introduce a more complex solution before you actually need one.

Common Mistakes

Creating Duplicate State

If two components need the same information, avoid maintaining independent copies unless there is a specific reason.

Instead, consider keeping one source of truth.

Passing State Through Too Many Components

If a value is being forwarded through many components that do not use it, evaluate whether the structure is creating unnecessary prop drilling.

Using Context for Everything

Context is useful, but it should not automatically be used for every shared value.

For a simple parent-child relationship, props are usually easier to understand.

Mutating Shared Data

Avoid modifying arrays or objects directly.

Incorrect:

cart.push(product);

Prefer:

setCart((currentCart) => [
  ...currentCart,
  product
]);

For objects:

setUser((currentUser) => ({
  ...currentUser,
  name: "Karim"
}));

Storing Derived Values as Separate State

If a value can be calculated from existing state, you often do not need another state variable.

For example:

const totalItems = cart.length;

rather than maintaining a separate totalItems state that must always be synchronized with cart.

Passing the Wrong Callback

Remember the difference between passing a function and calling it immediately.

Correct:

<button onClick={handleSave}>
  Save
</button>

Correct when arguments are needed:

<button onClick={() => handleDelete(id)}>
  Delete
</button>

Avoid:

<button onClick={handleDelete(id)}>
  Delete
</button>

because that invokes the function while rendering.

Best Practices

Start with Props

For simple relationships, props are usually the clearest solution.

Lift State When Components Need the Same Data

If siblings need shared state, consider moving that state to their closest common parent.

Keep State as Local as Practical

Do not move state higher in the component tree unless another component needs access to it.

Maintain a Single Source of Truth

Avoid unnecessary duplicate copies of the same state.

Use Context for Appropriate Cases

Context is useful when many components need the same information, particularly across different levels of the component tree.

Keep Component APIs Simple

A component should generally receive only the props it needs.

For example:

<ProductCard
  product={product}
  onAddToCart={addToCart}
/>

is easier to understand than passing a large collection of unrelated values.

Prefer Immutable Updates

When updating arrays and objects in state, create new values rather than modifying the existing state directly.

A Realistic Web4Learners Example

Imagine Web4Learners has an interactive React course interface.

The page contains:

                  CoursePage
               /      |       \
              ↓       ↓        ↓
          Sidebar   Lesson    Progress

The parent can own the current lesson and completed lessons:

import { useState } from "react";

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

  const [completedLessons, setCompletedLessons] = useState([]);

  function handleLessonSelect(lesson) {
    setSelectedLesson(lesson);
  }

  function handleLessonComplete(lesson) {
    setCompletedLessons((currentLessons) => {
      if (currentLessons.includes(lesson)) {
        return currentLessons;
      }

      return [...currentLessons, lesson];
    });
  }

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

      <LessonContent
        lesson={selectedLesson}
        onComplete={handleLessonComplete}
      />

      <Progress
        completedLessons={completedLessons}
      />
    </div>
  );
}

The sidebar can select a lesson:

function LessonSidebar({
  selectedLesson,
  onSelectLesson
}) {
  const lessons = [
    "React Introduction",
    "Components",
    "Props",
    "State",
    "Parent to Child",
    "Child to Parent",
    "Sibling Components"
  ];

  return (
    <aside>
      {lessons.map((lesson) => (
        <button
          key={lesson}
          onClick={() => onSelectLesson(lesson)}
        >
          {lesson}
        </button>
      ))}
    </aside>
  );
}

The lesson component can notify the parent when the lesson is completed:

function LessonContent({ lesson, onComplete }) {
  return (
    <main>
      <h2>{lesson}</h2>

      <p>
        Learn this React concept step by step.
      </p>

      <button onClick={() => onComplete(lesson)}>
        Mark as Complete
      </button>
    </main>
  );
}

The progress component receives the shared data:

function Progress({ completedLessons }) {
  return (
    <aside>
      <h2>Progress</h2>
      <p>
        Completed: {completedLessons.length}
      </p>
    </aside>
  );
}

This demonstrates several communication patterns working together.

LessonSidebar
      ↓
  callback
      ↓
 CoursePage
      ↓
 selectedLesson
      ↓
 LessonContent


LessonContent
      ↓
  callback
      ↓
 CoursePage
      ↓
 completedLessons
      ↓
 Progress

The parent acts as the central coordinator while each child remains focused on its own responsibility.

Image Placement Instruction

Image placement: Add a visual diagram immediately after the section “Understanding React’s Data Flow”.

The image should show the major data-sharing directions in React:

                 Parent
                /      \
               ↓        ↓
            Child A   Child B
               ↑        ↓
          Callback    Props
               │        │
               └── Parent

The main message should be that React uses explicit data flow through props and callback functions.

Use a modern Web4Learners visual style with clean cards, rounded corners, subtle gradients, modern typography, and clear arrows. Avoid putting large paragraphs inside the image.

Second image placement: Add a diagram immediately after the section “Choosing the Right Data-Sharing Pattern”.

Show three patterns side by side:

Parent → Child
Props

Child → Parent
Callback

Sibling ↔ Sibling
Common Parent

The visual should make it easy for a beginner to decide which communication pattern applies to a particular component relationship.

Practice Exercise

Build a small React application for a course dashboard.

Create this component structure:

App
├── CourseSelector
├── CourseDetails
└── CourseProgress

The App component should maintain the selected course:

const [course, setCourse] = useState("React");

CourseSelector should contain buttons for:

  • React
  • JavaScript
  • Python
  • SQL

When the user selects a course, the child should notify the parent using a callback.

CourseDetails should receive the selected course and display it.

CourseProgress should also receive information from the parent and display the user’s progress.

Challenge

Add a list of completed lessons:

const [completedLessons, setCompletedLessons] = useState([]);

When the user clicks Complete Lesson, update the parent state.

Then pass the completed lessons to CourseProgress.

Calculate the completion count from the array instead of creating unnecessary duplicate state.

Cheat Sheet

SituationRecommended Pattern
Parent sends data to childProps
Child sends information to parentCallback prop
Two siblings need shared dataCommon parent
Multiple components need the same stateLift state up
Data travels through many levelsConsider Context
Render nested contentchildren prop
Share an arrayPass array through props
Share an objectPass object through props
Update array stateSpread, map(), filter()
Update object stateObject spread
Derived valueCalculate from existing state
Dynamic listUse stable keys

Key Takeaways

  • React applications often require multiple components to work with the same data.
  • Props are the primary way to share data from parent to child.
  • Callback functions allow children to notify parents or request actions.
  • Sibling components can share data through their common parent.
  • When several components need the same state, the state can often be lifted to their closest common parent.
  • Maintaining a single source of truth helps prevent inconsistent data.
  • Arrays and objects in React state should be updated immutably rather than mutated directly.
  • Prop drilling occurs when data is passed through components that do not use it themselves.
  • Context can help when many components at different levels need the same information.
  • Context should be used when it solves a real component communication problem, not simply because it is available.
  • Keep state as local as practical and lift it only when necessary.
  • Derived values can often be calculated from existing state instead of being stored separately.
  • Effective data sharing keeps React applications predictable, reusable, and easier to maintain.

Leave a Comment