关于 React 函数组件实现父子组件/组件间通信的三种方法, props, context, 函数组件函数化

6,637 阅读3分钟

前言

使用 React 开发应用, 最常见的问题或者说大多数奇怪的代码都来自组件通信需求, 本文主要讨论常见的两种方式, props 和 context, 以及一种特殊方式, 组件函数化

本文内代码均已函数组件为讨论基准, 使用 React Hook 来实现.

正文

利用 Props 实现组件通信

利用 Props 是最常见的方式, 也是最容易出问题的方式, why? 先来看看以下两种情况下的代码

Props 实现父子组件通信

function Child(props) {
  const { state, setState } = props;
  const onButtonClick = ()=>{
  	setState('后代知道了')
  }
  return (<><div>{state}</div><button onClick={onButtonClick}>后代知道了</button></>);
}
function Father() {
  const [state, setState] = useState("state");
  return (
    <div>
      <Child state={state} setState={setState}/>
    </div>
  );
}

如果你的组件结构都是标准的父子结构, 即父组件包裹一层子组件, 那么 Props 是一个轻量的合适的解决方案, 但实际开发中, 我看到过大量的家族组件也使用 Props 来传递, 尤其是深层嵌套结构下的传递, 这时候父组件直接晋升为祖先组件

function Child3(){
  return(
    <div>
      <Child2 />
    </div>
  )
}
function Child2(){
  return(
    <div>
      <Child1 />
    </div>
  )
}
function Child1(){
  return(
    <div></div>
  )
}
fucntion Father(){
  return(
    <div>
      <Child3 />
    </div
  )
}

为了将 Father 中的 state 传递到 Child1 中, 你需要利用 Props 层层透传 2 层组件. 你会发现这和一些写的很烂的函数有类似的效果

function clac1(father1){
  // clac1 需要 来自 father1 的参数
}
function clac2(father1){
  clac1(father1)
}
fucntion clac3(fatcher1){
  clac2(father1)
}
function father(){
  clac3(fatcher1)
}

clac2 clac3 都可能实现了一部分自身的逻辑, 但是在整个大的逻辑链中, 他们却承担了将 father1 传递到 clac1 中的责任, 这是一种典型的反模式. 但我们却大量的应用在我们的 React 组件编写中.

Props 实现组件间通信

看完父子传递, 我们再来看看组件间通信, Props 依赖组件传递, 所以组件间通信也需要一个桥梁, 本质上和父子通信是一样的, 区别在于目的不同.

function Channel() {
  const [stateA, setStateA] = useState("A");
  const [stateB, setStateB] = useState("B");
  return (
    <>
      <ComA stateA={stateA} setStateB={setStateB} setStateA={setStateA} />
      <h6>分割线</h6>
      <ComB stateB={stateB} setStateA={setStateA} setStateB={setStateB} />
    </>
  );
}
function ComA(props) {
  const onButtonClick = () => {
    props.setStateB(props.stateA);
  };
  const onResetButtonClick = () => {
    props.setStateA("A");
  };
  return (
    <>
      <div>{props.stateA}</div>
      <button onClick={onButtonClick}>把 B 变成 A</button>
      <button onClick={onResetButtonClick}>还原 A</button>
    </>
  );
}
function ComB(props) {
  const onButtonClick = () => {
    props.setStateA(props.stateB);
  };
  const onResetButtonClick = () => {
    props.setStateB("B");
  };
  return (
    <>
      <div>{props.stateB}</div>
      <button onClick={onButtonClick}>把 A 变成 B</button>
      <button onClick={onResetButtonClick}>还原 B</button>
    </>
  );
}

这个示例会有点复杂, 从代码上可能看起来不是很直观, 不如看一小段录屏

仅仅是 2 个状态, 利用 Props 来实现跨组件通信就已经很复杂了, 如果考虑到你要复用其中一个 ComB, 因为 Props 的特性你需要带上 Channel 才行, 带上 Channel 就等于戴上了 ComA, 于是就从复用一个小组件变成了依赖一大坨东西, 这是不是在你的工作中经常遇到呢?

看完 Props 再来看看 Context

利用 Context 实现组件通信

对于简单的父子结构, Context 比 Props 还复杂了一点, 让我们来看下对于家族型态组件会有什么效果

家族型态组件是指组件嵌套 3 层以上, 形成了非常庞大的嵌套结构, 就像一个繁盛的家族一样, 几代人都生活在一起彼此紧密联系.

来看看代码


const Context = React.createContext();
function Child(props) {
  const { state, setState } = useContext(Context);
  const onButtonClick = () => {
    setState("后代知道了");
  };
  return (
    <>
      <div>{state}</div>
      <button onClick={onButtonClick}>后代知道了</button>
    </>
  );
}
function Child1() {
  return (
    <div>
      <Child />
    </div>
  );
}
function Ancestor() {
  const [state, setState] = useState("state");
  return (
    <Context.Provider value={{ state, setState }}>
      <div>
        <Child1 />
      </div>
    </Context.Provider>
  );
}

可以看到利用 React.createContext 创建一个特殊的上下文组件, 可以避免 Props 嵌套传递的问题. 类比到函数的话...大概是这样


const context = {}

function father(){
  context.father1 = 0
}

function child1(){
  const father1 = context.father1
}


而对于组件间通信来说, Context 和 Props 区别不大, 唯一的不同你不需要再为通信的组件建立一条手动传递 Props 的 Channel

Context 的问题在于 Context 依赖一个特殊的组件, 因为其便利性可能导致滥用, 最后代码就成了


Context1.Provider
  Context2.Provider
    Context3.Provider
      ...

所以有没有更好的方案既能解决 Props 透传的繁琐, 又能避免 Context 对组件结构的修改呢?

我的想法是, 将函数组件函数化, 再 React Context 之外创建一个和组件结构无关的 Context 来看下代码

import React from "react";
import createStore from "structured-react-hook";

const useStore = createStore({
  initState: {
    text: "state"
  },
  controller: {
    onButtonClick() {
      this.rc.setText("后代知道了");
    }
  },
  view: {
    renderChild() {
      return (
        <>
          <div>{this.state.text}</div>
          <button onClick={this.controller.onButtonClick}>后代知道了</button>
        </>
      );
    },
    renderFather() {
      return <div>{this.view.renderChild()}</div>;
    }
  }
});

function AncestorStructured() {
  const store = useStore();
  return <div>{store.view.renderFather()}</div>;
}

通过将原有的 Child 组件和 Father 组件函数化成 renderChild 和 renderFather, 然后为这些函数构建一个共享的 this 上下文代替 React 的 Context, 就能实现一样的效果了.

在这种机制下, 函数组件只作为最顶层的父组件, 内部的子组件无论是组件间还是上下结构的通信, 从纵向到横向都不需要依赖 props 和 context 传递, 除非你构建了多个 store(一个 store 就是一个上下文)

后话

项目引用

structured-react-hook

往期和 structured-react-hook 相关文章 使用 Structured-React-Hook 编写"真 ` 易于维护和扩展"的组件(一)