During the daily development of the React project, we often encountered the following question: How to implement cross-component communication?

To better understand this problem, let’s go through a simple chestnut illustration.

Implement a video player

Suppose there is a requirement for us to implement a simple video player. Based on our understanding of the player, we can roughly divide the video player into the following parts:

  • Video window componentScreen
  • Bottom play controlBottomCtrl

For the video window component, it contains a play/pause button CenterPlayBtn; The bottom play control is composed of the following components:

  • Play/pause buttonBottomPlayBtn
  • Progress control barProgressCtrl
  • The volume buttonVolume

As a result, its composition should look like the picture below:

Similarly, our component organization should look like this :(simplifies code implementation here)

class MyVideo {
  render() {
    return (
      <div>
        <Screen />
        <BottomCtrl />
      </div>)}}// Bottom video control
class BottomCtrl {
  render() {
    return (
      <div>
        <BottomPlayBtn />
        <ProgressCtrl />
        <Volume />
      </div>)}}// Video window component
class Screen {
  render() {
    return (
      <div>
        <video />
        <ScreenPlayBtn />
      </div>)}}Copy the code

For video players, a very common interaction is that when we click the play button CenterPlayBtn in the center of the screen, we not only need to change its state (hidden), but also update the style of the play button BottomPlayBtn at the bottom

Since the center play button and the bottom control button belong to the Screen and BottomCtrl components respectively, this is a common cross-component communication problem: How to synchronize the state of CenterPlayBtn to BottomPlayBtn?

Scenario 1: Ancestor component state management

A very common approach is to have an ancestor component synchronize information to other child components via state management:

class MyVideo {
    constructor(props) {
        super(props);
        this.state = {
            isPlay: false,
        }
    }
    
    updatePlayState = isPlay= > {
        this.setState({ isPlay });
    }
    
    render() {
        const { isPlay } = this.state;
        return (
            <div>
                <Screen updatePlayState={this.updatePlayState} isPlay={isPlay} />
                <BottomCtrl updatePlayState={this.updatePlayState} isPlay={isPlay} />
            </div>
        )
    }
}
Copy the code

We define the corresponding state in the ancestor component’s state and pass the method of modifying the state to the child component. Then, when a child component calls updatePlayState, its new state can also be passed to other child components through the state update mechanism of React itself, realizing cross-component communication.

This solution is simple, but not very friendly in some complex scenarios:

  1. The state and methods need to be transmitted to the corresponding sub-components through layers of props. Once the components are nested too deeply, it is difficult to write and maintain, and unnecessary logic is added to the components passed in the middle.
  2. The ancestor component that manages state becomes more bloated. Imagine that in order to communicate between two deeply nested child components, we need to add additional states and methods to the ancestor component, which increases the maintenance cost of the ancestor component.

Scenario 2: Cross-component communication capabilities provided by Redux

Redux provides a subscription and publishing mechanism that allows any two components to communicate with each other. First, we need to add a key to state, and then we can subscribe to the change of key value on the two components that need to communicate by encapsulating connect.

// CenterPlayBtn
class CenterPlayBtn {
    play() {
        this.props.updatePlayStatus(); }}const mapDispatchToProps = dispatch= > {
  return {
    updatePlayStatus: isPlay= > {
      dispatch(updatePlayStatus(isPlay))
    }
  }
}

export default connect(null, mapDispatchToProps)(BottomPlayBtn)

Copy the code
class BottomPlayBtn {
    componentWillReceiveProps(nextProps) {
        if (this.props.isPlay ! == nextProps.isPlay) {// do something}}}const mapStateToProps = state= > ({
    isPlay: state.isPlay
})

export default connect(mapStateToProps, null)(BottomPlayBtn)
Copy the code

The redux approach to cross-component communication is a common approach and is often used in project development. The question arises again, since the premise of using this solution is to add ReDUx to the project, if my project is relatively simple and does not need to use ReDUx, do I need to do a series of REDUx configuration work to achieve simple communication between the two components? This obviously complicates a simple problem.

Scenario 3: EventEmitter

EventEmitter can also communicate across components. Of course, the event-based subscription-based design pattern itself is not relevant to React, but when our project was small, It was a simple and efficient way to use EventEmitter:

class CenterPlayBtn {

    constructor(props) {
        super(props);
        event.on('pause', () = > {// do something
        })
    }

    play() {
        event.emit('play'); }}class BottomPlayBtn {

    constructor(props) {
        super(props);
        event.on('play', () = > {// do something
        })
    }

    pause() {
        event.emit('pause'); }}Copy the code

Of course, this scheme has its drawbacks:

  • The organization is too discrete. The senderemitWith the receiveronScattered in each component, if we do not look at the code of each component, it is difficult to observe, track and manage these events as a whole;
  • It is possible to miss an event. If a component subscribs to the event too late, it will not receive any of the events previously published by the publisher. The advantage of schemes 1 and 2 is that the component gets the final state value of the key anyway.
  • There is a risk of memory leaks. If a component is destroyed without timely unsubscription, there is a risk of a memory leak;

Solution 4: Use the React native context to implement cross-component communication

React provides a context. React provides a context.

Context provides a way to pass data through the component tree without having to pass props down manually at every level.

React simply provides a way for you to access data across multiple layers of nested components without having to manually pass props one by one. This way we can also implement cross-component communication, which is similar to scenario 1, but the difference is that we don’t need to manually pass props to every middle-tier component that goes through it. For more specific usage, please refer to the example on the official website. Here is a simple example:

First we define a player-context.js file

import { createContext } from 'react';
const PlayerContext = createContext();
export default PlayerContext;
Copy the code

Then use playerContext.provider in the MyVideo component:

import PlayerContext from './player-context';

class MyVideo {
    constructor(props) {
        super(props);
        this.state = {
            isPlay: false.updatePlayState: this.updatePlayState,
        }
    }
    
    updatePlayState = isPlay= > {
        this.setState({ isPlay });
    }
    
    render() {
        return (
            <PlayerContext.Provider value={this.state}>
                <Screen />
                <BottomCtrl />
            </PlayerContext.Provider>)}}Copy the code

It is then used in CenterPlayBtn and BottomPlayBtn, where the data needs to be consumed. The CenterPlayBtn example is only shown here:

import PlayerContext from './player-context';

class CenterPlayBtn {

    constructor(props) {
        super(props);
    }

    play() {
        this.props.updatePlayStatus(!this.props.isPlay);
    }
    
    componentWillReceiveProps(nextProps) {
        if (this.props.isPlay ! == nextProps.isPlay) {// do something...}}}export defaultprops => (<PlayerContext.Consumer> { ({isPlay, updatePlayStatus}) => <CenterPlayBtn {... props} isPlay={isPlay} updatePlayStatus={updatePlayStatus} /> } </PlayerContext.Consumer>)Copy the code

In fact, I personally think this scheme is an “enhanced version” of Scheme 1:

  • First of all, it makes centralized control and management of data, that is, the ability to provide data content and modify data is concentrated on the upper components, so that the upper components become uniqueProviderFor consumers everywhere belowConsumerUse;
  • Second, it does not need to be as tedious as plan 1propsManual down pass;

In general, using context is a good choice if your project does not use Redux.

conclusion

The options listed above have their pros and cons, and it’s hard to decide which is the best, but it’s really important to learn how to analyze which is best for which scenario.

BTW, in fact, there are many ways of cross-component communication, far more than these, I have little knowledge, here can only list some of my common solutions, I hope this article can throw a brick to introduce better solutions and insights 🙂