Tutorial: How to communicate between two React components

Tutorial: How to communicate between two React components

In React, the communication between components comes under three categories.

  • Parent component to child component communication.
  • Child component to parent component communication.
  • Communication between to components that do not have a parent child relationship between them.

The three scenarios employ different React primitives to attain this kind of communication. In this article we will see the common techniques that are used for these three cases. While the first two are simple and generally well supported by react's standard features for the third, you might have to think differently and rely on external libraries like redux.

Table of contents

  1. Understanding communication primitives in React

  2. Prop drilling : Parent can pass data to child component using props.

  3. Lifting state up: Child passing data to parent through custom events

  4. Two sibling components passing data to each other

  5. Understanding tradeoffs in inter component communication

  6. Alternatives of inter component communication

    1. Redux
    2. Provider pattern
  7. Conclusion

Understanding communication primitives in React

This section describes what kind of primitives React provides to pass messages between different components.

Prop drilling : Parent can pass data to child component using props.

A child component is inside parent. Hence parent can easily send data to the child component through props. If you have written any React code you must have done this already.

Let us see a simple example of parent to child communication in react.


import React from 'react';

function ChildComponent(props){
return <div className="thickborder">
<p>I am child 1</p>
{props.title}
</div>

}

export function App(props) {
return (
<div className='App thickborder'>
<p> I am parent </p>
<ChildComponent title="This comes from parent"/>
</div>
);
}

Here App is a parent component and child component is name ChildComponent. The property title is passed by parent to child.

Lifting state up: Child passing data to parent through custom events

While sending data from parent to child is very simple, sending data from child to parent is slightly complicated.

Child does not know which parent it is embedded into. It knows nothing about its parent. The only way child can notify its parent is if parent gives child an "event handler" method which child calls whenver it wants to inform its parent about something.

See this example. You can play with this event handler in the playcode.


import React from 'react';
import { useState } from 'react';

function ChildComponent(props) {
return (
<div className='thickborder'>
<p>I am child 1</p>

<p>{props.title}</p>

<div>
/** The child component calls the onChangesInChild method everytime
child wants to inform parent of something **/
<input
type='text'
onChange={e => props.onChangesInChild(e.target.value)}
/>
</div>
</div>
);
}

export function App(props) {
const [childValue, setChildValue] = useState('');
// Define the callback the parent passes to the child.
const handleChangesInChild = value => setChildValue(value);
return (
<div className='App thickborder'>
<p> I am parent </p>
<p> Value sent by child: {childValue} </p>

<ChildComponent
title='This comes from parent'
onChangesInChild={handleChangesInChild}
/>
</div>
);
}


In this example, the child component has a form element and it wants to inform the parent component about changes to this form. This happens via an callback named "onChangesInChild" which parent passes to the child and child is responsible to call it whenever it wants to pass data to parent.

What we learn here is:

  • Parent can pass data to child via props.
  • Child can pass data to parent via callback methods.

Intuitively, think of each component as a water tank. Props are like pipes that go into the tank, you can push water into the tank through these pipes. Callbacks are also pipes that go into tank but they have a Tap outside which can be turned on to bring water out of a tank.

React component imagined as water tank

For those who are completely new to React, this might still be confusing but remember that once you write lot of code this becomes second nature to you.

But this sort of communication get annoying when you have to pass data between child components which are deeply nested. For example something like the following



import React from 'react';
import { useState } from 'react';


function GrandChildComponent(props) {

return <div className="thickborder">
<input
type='text'
onChange={e => props.onChangesInChild(e.target.value)}
/>
</div>
}

function ChildComponent(props) {
return (
<div className='thickborder'>
<p>I am child 1</p>
<p>{props.title}</p>
<div>
<GrandChildComponent onChangesInChild={value=>props.onChangesInChild(value)}/>
</div>
</div>
);
}

export function App(props) {
const [childValue, setChildValue] = useState('');
// Define the event handler the parent passes to the child.
const handleChangesInChild = value => setChildValue(value);
return (
<div className='App thickborder'>
<p> I am parent </p>
<p> Value sent by child: {childValue} </p>

<ChildComponent
title='This comes from parent'
onChangesInChild={handleChangesInChild}
/>
</div>
);
}



Here the grand child wants to pass a message to its grandparent. However the only way this message can be sent is if the grandparent send a callback to its immediate child, and that immediate child sends the same callback to its child. That middle child does nothing with the data other than passing it upwards.

When you have dozens of nested components with the need to pass data around, this code gets complicated.

Two sibling components passing data to each other

What if a component wants to pass data to another component which is neither its parent not child ? In React this can be done only if they share a common parent at some level. Since there is one root component in react app, it is always possible to find a common parent for any two components. Worse case it would be the root component itself.

In the following component tree, the child 1 and child 2's first common parent is the root node itself so the only way they can communicate with each other is via this root node.

Sibling React Components talking to each other

The child that wants to pass data to another sibling, sends it to its parent first using callback mechanism and hopes that the parent does the right thing.

The parent receives the data from one child and then passes it to the second child using props of that child.

You can play with the sample code here.

Sibling communication is merely the combination of methods 1 and 2 we discussed earlier.


import "./styles.css";
import { useState } from "react";

const ChildOne = ({ onChange }) => {
return (
<div>
Child 1
<input type="text" onChange={(e) => onChange(e.target.value)} />
</div>
);
};

const ChildTwo = ({ displayTitle }) => {
return (
<div>
Child 2<p>{displayTitle}</p>
</div>
);
};

export default function App() {
const [myValue, setMyValue] = useState("");
return (
<div className="App">
<h1> I am parent </h1>
<ChildOne onChange={(value) => setMyValue(value)} />
<ChildTwo displayTitle={myValue} />
</div>
);
}

Understanding tradeoffs in inter component communication

While this sort of communication mechanism is very powerful, remember that you must avoid any inter component communication as much as possible for the following reasons.

  • Any prop you define becomes part of the component API and must be supported, maintained as long as the component is in use. Else you end up refactoring the code a lot.
  • Everytime the state of a component changes, the component has to render itself again. If you pass around too much data, a component can trigger re-rendering of other unrelated components. This can take a peformance hit.
  • Once you end up having hundreds of components nested deeply, passing these sort of messages makes APIs of all the involved components complex. This can lead to bugs and tech debt.
  • A bug in one of the intermediate components can break down the whole message passing to the ancestor components.

Alternatives of inter component communication

If you think this sort of communication is ineffecient you are right. Passing callbacks down the tree is super inefficient. So people have created better solutions.

Redux

There are other ways which are separate from React's core principles which allow components to communicate with each other. One such way is using a library like Redux. I have written a guide to understand Redux.

Libraries like Redux act like a database store on the client. Whoever wishes to share the data with rest of the app just writes to it via what is called a "dispatch action" operation and all other components who are interested in that data listen to the data via subscribers.

This reduces the need to change props, pass callbacks, maintain intermediate state etc.

Provider pattern

Provider pattern is very common pattern in react like frameworks. I have seen exact same pattern in Flutter as well.

Provider patten leverages a core React feature called "React Context". You can think of context as an object that can be shared across different components no matter how they are related.

A Provider is a special component that you use to wrap one of your parent nodes and pass a context value to the provider. All the children in that hierarchy can then invoke the provider get fetch the context value. The context can also have setter values and allow children to modify the values. Any changes to context then automatically become available to whoever is using it.

A detailed discussion of provider and context will require a separate post but the following image from the official react documentaion gives a very good idea of how a provider provides a value to all the components under it.

context providing data to distant children

Conclusion

The standard way to communicate between components in React is through prop drilling, lifting state up and combination of both. While this is the standard way, complex applications often need better solutions which are provided by third party libraries like Redux or through React Providers. It is upto the engineer to decide which way they want to go and often it is possible to end up using all of these methods together to compliment each other.