React Component Communication

I have realized that how components talk to each other can make or break a project.

If you don’t get the communication pattern right early on, you often end up with a tangled “spaghetti” of code that is impossible to debug.

In this tutorial, I will show you the exact methods I use to share data between React components, ranging from simple parent-child setups to global state management.

Pass Data from Parent to Child Using Props

The most common way to move data in React is from the top down. We call this “Props Drilling” when it goes through many layers.

I use this method for almost every component I build. It is predictable and makes it very easy to track where your data is coming from.

Imagine you are building a simple dashboard for a California-based real estate agency to display property listings.

import React from 'react';

// The Child Component
const PropertyCard = (props) => {
  return (
    <div style={{ border: '1px solid #ccc', padding: '15px', margin: '10px' }}>
      <h2>{props.address}</h2>
      <p>Price: ${props.price}</p>
      <p>Location: {props.city}, CA</p>
    </div>
  );
};

// The Parent Component
const RealEstateDashboard = () => {
  const propertyData = {
    address: '123 Sunset Blvd',
    price: '1,250,000',
    city: 'Los Angeles'
  };

  return (
    <div>
      <h1>Available Listings in California</h1>
      <PropertyCard 
        address={propertyData.address} 
        price={propertyData.price} 
        city={propertyData.city} 
      />
    </div>
  );
};

export default RealEstateDashboard;

I executed the above example code and added the screenshot below.

React Component Communication

In this example, the RealEstateDashboard passes specific details to the PropertyCard using props. It is clean and easy.

Send Data from Child to Parent via Callbacks

Sometimes, a child component needs to tell the parent that something happened. Since data only flows down in React, we pass a function down instead.

I frequently use this for forms or search bars. When the user types something, the child “calls back” to the parent to update the main state.

Let’s look at a New York Stock Exchange (NYSE) ticker search where the search bar needs to update the main display.

import React, { useState } from 'react';

// Child Component
const StockSearch = ({ onSearchSubmit }) => {
  const [ticker, setTicker] = useState('');

  const handleInputChange = (e) => {
    setTicker(e.target.value.toUpperCase());
  };

  const sendToParent = () => {
    onSearchSubmit(ticker);
  };

  return (
    <div style={{ marginBottom: '20px' }}>
      <input 
        type="text" 
        placeholder="Enter Ticker (e.g. AAPL)" 
        value={ticker} 
        onChange={handleInputChange} 
      />
      <button onClick={sendToParent}>Search NYSE</button>
    </div>
  );
};

// Parent Component
const StockMarketApp = () => {
  const [activeTicker, setActiveTicker] = useState('NONE');

  const updateMarketView = (symbol) => {
    setActiveTicker(symbol);
  };

  return (
    <div style={{ padding: '20px' }}>
      <h1>Wall Street Data Portal</h1>
      <StockSearch onSearchSubmit={updateMarketView} />
      <div style={{ marginTop: '20px', fontWeight: 'bold' }}>
        Currently Viewing: {activeTicker}
      </div>
    </div>
  );
};

export default StockMarketApp;

I executed the above example code and added the screenshot below.

Component Communication React

Here, the StockMarketApp provides a function called onSearchSubmit. The child uses this to push the ticker symbol back up.

Use the Context API for Deeply Nested Components

When you have a piece of data that many components need, like a user’s subscription tier or a theme, passing props through every level is a nightmare.

I prefer the Context API for “global” data within a specific section of an app. It stops “prop drilling” and keeps your component signatures clean.

Think of a US-wide retail app where the user’s “Shipping State” needs to be known by every checkout component to calculate sales tax.

import React, { createContext, useContext, useState } from 'react';

// 1. Create the Context
const LocationContext = createContext();

// 2. Provider Component
export const LocationProvider = ({ children }) => {
  const [shippingState, setShippingState] = useState('New York');

  return (
    <LocationContext.Provider value={{ shippingState, setShippingState }}>
      {children}
    </LocationContext.Provider>
  );
};

// 3. Deeply Nested Child
const TaxCalculator = () => {
  const { shippingState } = useContext(LocationContext);
  
  const taxRate = shippingState === 'New York' ? '8.875%' : '0%';

  return (
    <div style={{ marginTop: '10px', color: 'blue' }}>
      Estimated Sales Tax for {shippingState}: {taxRate}
    </div>
  );
};

// 4. Intermediate Component (Doesn't need the prop!)
const CheckoutForm = () => {
  return (
    <div style={{ border: '1px solid black', padding: '10px' }}>
      <h3>Secure Checkout</h3>
      <TaxCalculator />
    </div>
  );
};

// Main App
const RetailApp = () => {
  return (
    <LocationProvider>
      <h1>National Retail Store</h1>
      <CheckoutForm />
    </LocationProvider>
  );
};

export default RetailApp;

I executed the above example code and added the screenshot below.

Component Communication in React

By using useContext, the TaxCalculator grabs the shipping state directly. The CheckoutForm doesn’t even have to know that data exists.

Sibling Component Communication

Siblings cannot talk to each other directly in React. To make them communicate, you must “lift the state up” to their closest common parent.

I see developers struggle with this often. The trick is to keep the state in the parent and pass a “setter” to one sibling and the “value” to the other.

Consider a simple US Airline booking system with a seat selector and a price summary.

import React, { useState } from 'react';

// Sibling 1: The Selector
const SeatPicker = ({ onSeatSelect }) => {
  const seats = ['1A (First Class)', '12B (Economy)', '22C (Economy)'];
  
  return (
    <div>
      <h3>Select Your Seat</h3>
      {seats.map(seat => (
        <button key={seat} onClick={() => onSeatSelect(seat)} style={{ margin: '5px' }}>
          Book {seat}
        </button>
      ))}
    </div>
  );
};

// Sibling 2: The Display
const BookingSummary = ({ selectedSeat }) => {
  return (
    <div style={{ backgroundColor: '#f0f0f0', padding: '10px', marginTop: '20px' }}>
      <h3>Reservation Details</h3>
      <p>Flight: JFK to LAX</p>
      <p>Confirmed Seat: {selectedSeat || 'None selected'}</p>
    </div>
  );
};

// Parent: The Coordinator
const AirlineBookingApp = () => {
  const [seat, setSeat] = useState('');

  return (
    <div style={{ padding: '30px' }}>
      <h2>Delta Airlines Reservation</h2>
      <SeatPicker onSeatSelect={setSeat} />
      <BookingSummary selectedSeat={seat} />
    </div>
  );
};

export default AirlineBookingApp;

The AirlineBookingApp holds the seat state. When you click a button in the picker, it updates the parent, which then updates the summary.

Use Ref for Imperative Communication

There are rare cases where you need to trigger an action inside a child component without changing props. I use useImperativeHandle for this.

This is useful for things like focusing an input, playing a video, or resetting a complex form manually from the parent.

Let’s look at a US Federal Tax Form where a “Clear All” button in the header needs to reset a specific field in the child.

import React, { useRef, useImperativeHandle, forwardRef, useState } from 'react';

// Child Component wrapped in forwardRef
const TaxField = forwardRef((props, ref) => {
  const [ssn, setSsn] = useState('');

  // Expose a function to the parent
  useImperativeHandle(ref, () => ({
    resetField() {
      setSsn('');
    }
  }));

  return (
    <div style={{ margin: '20px 0' }}>
      <label>Enter SSN (Internal Use): </label>
      <input 
        type="password" 
        value={ssn} 
        onChange={(e) => setSsn(e.target.value)} 
      />
    </div>
  );
});

// Parent Component
const IRSFormManager = () => {
  const taxFieldRef = useRef();

  const handleClearEverything = () => {
    // Calling the child's method directly
    taxFieldRef.current.resetField();
  };

  return (
    <div style={{ padding: '20px' }}>
      <h1>Official Tax Filing Portal</h1>
      <TaxField ref={taxFieldRef} />
      <button onClick={handleClearEverything}>Reset All Fields</button>
    </div>
  );
};

export default IRSFormManager;

In this scenario, the parent uses a ref to reach inside the child and trigger the resetField function directly.

Each of these methods has a specific place in a well-architected React application.

For most cases, props and callbacks will handle 90% of your needs. When things get complex, moving to Context or State Management is the right move.

I hope this helps you build cleaner, more maintainable React apps for your users.

Youmay also like to read:

51 Python Programs

51 PYTHON PROGRAMS PDF FREE

Download a FREE PDF (112 Pages) Containing 51 Useful Python Programs.

pyython developer roadmap

Aspiring to be a Python developer?

Download a FREE PDF on how to become a Python developer.

Let’s be friends

Be the first to know about sales and special discounts.