React组件通讯方式详解

lxf2023-03-11 14:03:01

React组件通讯方式详解

最近在做代码重构,发现老代码在数据/信息传递上有很多方式使用不当,很影响维护和观感的,修复或者阅读代码的人会消耗很多心智去读懂他们。

所以乘机梳理一下,这样的话我们可以在开发的时候就选择合适的通讯方式。

罗列下通常情况下有以下场景:

  • 父组件向子组件通讯
  • 子组件向父组件通讯
  • 跨级组件通讯
  • 非嵌套关系组件通讯(含siblings)

示意图:

React组件通讯方式详解

父组件向子组件传递消息

1. 通过Props传递:

这个是最常见的场景,我们可以在父组件通过props传递信息:

React组件通讯方式详解

⚠️ 我们都知道这种方式,不过,其中有3点值得注意的是:

1. props 支持默认值

当父组件没有传递某个信息过来的时候,我们可以使用一个默认的值占位。比方说一个用户信息没有设置头像的话,我们可以展示一个默认的头像:

export const Avatar = ({ src = 'SomeHardCodedRemoteCDNUrl', size }) => {
    return <img src={src} alt={...} />
}

2. 可以使用对象展开运算符

这通常对于透传递信息很有帮助(不需要罗列):

export const User = ({ info, size }) => {
    return (
        <Card size={size}>
            <Avatar src={info.avatar} />
            <UserInfo {...props} /> {/** 这里接受全部info对象的信息 */}
        </Card>
    )
} 

3. 可以直接传递一个组件

children(或者是声明为React节点的属性)都可以通过 props 传递:

// 比方说我们有个会话弹窗组件,它可以展示接受任何我们指定的内容:

export const Modal = ({ open, children, ctrlBtns = CtrlBtns }) => {
    return (
        {open && <div className="FixedOnTop">
            <h2>提示</h2>
            <div className="Content">{children}</div>
            {ctrlBtns}
        </div>}
    )	
} 

让父组件控制子组件显示的内容。

2. 通过 ref 获得实例,触发实例方法

React组件通讯方式详解

在没有 Hooks 的时候,这种方式也比较容易通过 React Class Component 实现。

那么现在我们比较常用 Hooks 的情况下,如何获得通过ref获得子组件的setCount方法呢?

因为使用 React Hooks的组件都是函数,函数是没有实例的,所以也就没有实例方法。

但是 React API useImperativeHandler 可以让组件返回一个自定义的对象。

例子

想象,我们需要调用子组件 <Count />setCount 函数,并且传入参数:

export default function App() {
  const ref = useRef();

  return (
    <div className="App">
      <button
        onClick={() => {
          ref.current.setCount(1);  // 调用<Count />的setCount,并且传入参数1
        }}
      >
        setCount(1)
      </button>

      <Child ref={ref} />
    </div>
  );
}

在子组件中,我们透过 useImperativeHandler 暴露 setCount

const Child = forwardRef((props, ref) => {
  const [count, setCount] = useState(0);

  useImperativeHandle(ref, () => ({
    setCount
  }));

  return <p>Child count: {count}</p>;
});

DEMO 与代码可以查看: codesandbox.io/s/busy-joji…

然而,这种方法虽然看起来精巧,但是在实际开发场景中,是不应该被优先考虑的。一般来说,在React组件库中比较常见。

子组件向父组件通讯

1. 通过回调函数

React组件通讯方式详解

常见的模式,通常能够满足大部分的通讯需求,不展开说明。

2. 通过 Render Props

Render Props 其实也算是回调,只不过这种回调比较特殊,它是挂载在 children 属性上的。

逻辑上:

children 是 props 的一部分 → props 支持函数 → children 可以是函数

React组件通讯方式详解

某种程度上,Render Props 有点像:父组件在子组件上安排了一个奸细,每次子组件渲染的时候,父组件都能获得子组件内部的部分信息。

例子:


const Parent = () => {
    return (
        <div>
            <p>Parent UI</p>
            <Mouse>
              {mouse => (
                <p>鼠标位置: {mouse.x}, {mouse.y}</p>
              )}
            </Mouse>
        </div>
    )
}

const Mouse = ({ children }) => {
    const [pos, setPos] = useState({ x: 0, y: 0 })

    return (
        <div onMouseMove={(e) => {
            setPos({ x: e.clientX, y: e.clientY })
        }}>
            {/** 注意这里,我们把Mouse内部的信息作为children的参数传递给了Parent组件*/}
            {children(pos)}
        </div>
    )
}

跨组件通讯,非嵌套关系组件之前通讯

指的是需要通讯的组件之间隔了一层以上的结构的情况。

粗暴的方法是通过 Props Drilling ,也就是逐层传递。如果属性很多,这种情况会变得很啰嗦,也不好维护。

通常这种情况可以考虑 React Context:

1. 通过Context 实现跨级组件通讯

React组件通讯方式详解

一般来说,优先考虑只传递数据;在复杂情景下,可以通过结合 Context 和useReducer 来构建一个简便的状态管理器;出于性能上的考虑也可以结合使用 useMemo。

不过,这种方案只合适小型的状态管理,并不推荐大规模使用。

2. 通过观察者模式(Rxjs等)

React组件通讯方式详解

这种方法,与在 Vue 中我们常用的 EventBus 类似。

需要注意的是,在unmount的时候取消订阅避免内存泄漏。

同样,也是不推荐大规模使用。在大型应用中,这类消息传递很快就失控。

3. 使用 Redux 全局状态管理库(或Mobx等)

如果上方的通讯方式都不能很好地满足需要的话,可以开始考虑使用全局状态管理库。

可参考:Admin.net/post/703478… 在项目中使用 Redux Toolkit。

小结

根据场景选择合适的组件数据传递方式,能够让项目更具维护性。