React 跨组件通信

4,829 阅读13分钟

承读

由于 React 是一个组件化框架,那么基于组件树的位置分布,组件与组件之间的关系,大致可分为 4 种。

  • 父与子:父组件包裹子组件,父组件向子组件传递数据。
  • 子与父:子组件存在于父组件之中,子组件需要向父组件传递数据
  • 兄弟:两个组件并列存在于父组件中,需要数据进行相互传递。
  • 无直接关系:两个组件并没有直接的关联关系,处在一棵树中相距甚远的位置,但需要共享、传递数据。

基于以上分类,就有了下面的知识导图:

入手

父与子

父与子通信是比较常见的场景,基本在工作中都经常使用到

Props
就像下面的场景:
- 父组件想要传title供子组件展示
const Son = ({ msg }) => {
   return (<span>{msg}</span>)
}

class Father extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            msg'父组件消息'
        }
    }

    render() {
        return (
            <div>
                <Son msg={this.state.msg}/>
            </div>
        )
    }
}

这段示例代码就能够说明父像子传值,运行发现子组件(Son)会展示“父组件消息”,达到了我们要的效果。

子传父

回调函数
就像下面的场景:
- 子组件想要“发送消息”给父组件
class Son extends React.Component {
    handleSend = () => {
        this.props.handleSend('i am you son')
    }
    render() {
        return (
            <div>
                <button onClick={this.handleSend}>send msg to my father</button>
            </div>
        )
    }
}

class Father extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            msg''
        }
    }
    handeReceive = (msg) => {
        this.setState({
            msg
        })
    }
    render() {
        return (
            <div>
                {this.state.msg}
                <Son handleSend={this.handeReceive}/>
            </div>
        )
    }
}

这段示例代码就能够说明子像父传值,运行发现父组件(Father)会展示“i am you son”,达到了我们要的效果。如果了解过Vue框架的同学会发现,其实前两种父子组件通信方式跟Vue是类似的,当你接触了一个框架之后再去看另外一个框架是不是发现很多类似点。

兄弟

状态提升
概念:通常,多个组件需要反映相同的变化数据,这时我们建议将共享状态提升到最近的共同父组件中去。让我们看看它是如何运作的。[官方解释](https://react.docschina.org/docs/lifting-state-up.html)

就像下面的场景:
- 兄弟组件A想要告诉兄弟B,我晚上要来找你玩(实现思路:A=>父组件(拿到消息), 父组件(消息)=>B)
const ChildB = ({ msg }) => {
   return (<span>{msg}</span>)
}

class ChildA extends React.Component {
    handleClick = () =>{
        this.props.handleSend('我晚上要来找你玩')
    }
    render() {
        return (
            <div>
                <button onClick={this.handleClick}>click Me</button>
            </div>
        )
    }
}

class Father extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            msg''
        }
    }
    
    handleSend = (msg) => {
        this.setState({
            msg
        })
    }
    
    render() {
        return (
            <div>
                <ChildB msg={this.state.msg}/>
                <ChildA handleSend={this.handleSend}/>
            </div>
        )
    }
}

这段示例代码就能够说明通过状态提升实现兄弟传值,运行发现子组件(ChildB)会展示“我晚上要来找你玩”,达到了我们要的效果。再思考一下,是不是发现可以通过状态提升实现类似Vue框架的双向数据绑定效果.

无直接关系

无直接关系就是两个组件的直接关联性并不大,它们身处于多层级的嵌套关系中,既不是父子关系,也不相邻,并且相对遥远,但仍然需要共享数据,完成通信。那具体怎样做呢?我们先从 React 提供的通信方案 Context 说起。

Context
Context 第一个最常见的用途就是做 i18n,也就是常说的国际化语言包(Ant Design 提供的国际化处理方式也是这么干的,感兴趣的朋友可以去看看)。我们一起来看下这个案例:
import { createContext } from 'react';

const I18nContext = createContext({
    translate() => '',
    getLocale() => { },
    setLocale() => { },
});

export default I18nContext;

首先通过React.createContext创建Context初始值,这里包含了三个函数:

  • translate: 用于翻译指定键值
  • getLocale: 得到当前语言类型
  • setLocale: 设置显示某种语言类型

有了Context,然后需要一个Provider(I18nProvider)包裹住Context:

import React, { useState } from 'react';
import I18nContext from './I18nContext';

class I18nProvider extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            locale'en-US',
        }
    }
    
    render() {
        const i18n = {
            translatekey => this.props.languages[this.state.locale][key],
            getLocale() => this.state.locale,
            setLocalelocale => this.setState({ locale, })
        }
        
        return (
            <I18nContext.Provider value={i18n}>
                {this.props.children}
            </I18nContext.Provider>
        )
    }
}

export default I18nProvider;

既然有提供者那势必要有消费者,我们新建两个组件,一个是Content(展示内容),一个是Footer(用来切换语言),如下:

  import I18nContext from '../Context/I18nContext';

  class Footer extends React.Component {
    render() {
        return (
            <I18nContext.Consumer>
                {({setLocale}) => {
                    return (
                        <select onChange={(e) => setLocale(e.target.value)}>
                            <option value="en-US">英文</option>
                            <option value="zh-CN">中文</option>
                        </select>
                     )
                }}
            </I18nContext.Consumer>
        )
    }
}

  class Title extends React.Component {
      render() {
          return (
              <I18nContext.Consumer>
                  {({translate}) => {
                      return (<div>{translate('hello')}{translate('name')}</div>)
                  }}
              </I18nContext.Consumer>
          )
      }
  }

(在此之前还需要创建文件夹locals并且包含两个语言文件)

//路径:locals/en-US.js
const en_US = {
    hello: 'Hello, world!',
    name: 'my name is mary'
}    

export default en_US; 

//路径:locals/zh_CN.js
const zh_CN = {
    hello: '你好,世界!',
    name: '我的名字是 玛丽'
  }
  export default zh_CN; 
  

最后一步需要在顶层注入Provider,如下:

//路径:locals/en-US.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import I18nProvider from './Context/I18nProvider';
import enUS from './locales/en-US';
import zhCN from './locales/zh-CN';

const locales = [ 'en-US''zh-CN' ];
const languages = {
  'en-US': enUS,
  'zh-CN': zhCN,
}

ReactDOM.render(
  <I18nProvider locales={locales} languages={languages}>
    <App />
  </I18nProvider>,

  document.getElementById('root')

);

这样我们的效果就实现了,切换不容的语言显示显示不同的语言。效果如下:

咱们别关看效果,有的时候还要看看代码是不是能够优化一下,仔细一看发现Content,Footer两个组件看起来好像都差不多啊,是不是可以优化一下该代码呢,这个时候我们是不是想到了高阶组件,那我们就来试试,新建一个withI18n.js,下面写了两个版本的:

import React from 'react';
import I18nContext from './I18nContext';
//第一版本
function withI18n(WrapperComponent) {
  return class LogoProps extends React.Component {
    render() { 
      return (
        <I18nContext.Consumer>
          {i18n => <WrapperComponent {...i18n} {...this.props} />}
        </I18nContext.Consumer>
      )
    }
  }
}
//第一版本(推荐)
const withI18n = (WrappedComponent) => {
    return (props) => (
      <I18nContext.Consumer>
        {i18n => <WrappedComponent {...i18n} {...props} />}
      </I18nContext.Consumer>
    )
};
export default withI18n;

这时候我们去改造一下Content, Footer组件,如下:

import withI18n from '../Context/withI18n';

const Title = withI18n(
    ({ translate }) => { 
      return ( <div>{translate('hello')}</div> )
    }
  )
  
const Footer = withI18n(
    ({ setLocale }) => {
      return (<button onClick={() => {setLocale('zh-CN')}} >Click Me</button> )
    }
  )

改造之后是不是代码看起来是不是简洁多了。

有同学就会问了,除了这种方式还有啥方式能实现无直接关系组件传值呢?我们继续看:

全局变量与事件

全局变量:顾名思义就是放在 Window 上的变量。但值得注意的是修改 Window 上的变量并不会引起 React 组件重新渲染。

所以在使用场景上,全局变量更推荐用于暂存临时数据。比如在 CallPage 页面点击了按钮之后,需要收集一个 callId,然后在 ReportPage 上报这个 callId。如下代码所示:

class CallPage extends React.Component { 
    render() {
        return <Button onClick={() => {
              window.callId = this.props.callId         
        }} />
}

class ReportPage extends React.Component {
    render() {
        return <Button onClick={() => {
              fetch('/api/report', { id: window.callId })          
        }} />
    }
}

弊端:假如有个业务,我们要在callId变化的时候进行某组件显示与隐藏,显然就不能这么干了,因为修改 Window 上的变量并不会引起 React 组件重新渲染。

全局事件:

class CallPage extends React.Component {
    dispatchEvent = () => {
        document.dispatchEvent(new CustomEvent('callEvent', {
          detail: {
             callIdthis.props.callId
          }
        }))
    }

    render() {
        return <Button onClick={this.dispatchEvent} />
}

class ReportPage extends React.Component {
    state = {
      callIdnull,
    }

    changeCallId = (e) => {
      this.setState({
        callId: e.detail.callId
      })
    } 

    componentDidMount() {
        document.addEventListener('callEvent'this.changeCallId)
    }

    componentWillUnmount() {
        document.removeEventListener('callEvent'this.changeCallId)
    }

    render() {
        return <Button onClick={() => {
              fetch('/api/report', { id: this.state.callId })          
        }} />
    }
}

粗略的看我们通过自定义事件(callEvent),最终监听改变state,效果是页面重新渲染了,弥补了全局事件的弊端,但是我们都知道事件的注册时往往是在组件加载时放入的,所有也就意味着这两个组件ReportPage,CallPage必须放在同一页面。

END

最终我们可以总结一下,就有了下面的知识导图: