React组件通讯方式详解
React组件通讯方式详解
最近在做代码重构,发现老代码在数据/信息传递上有很多方式使用不当,很影响维护和观感的,修复或者阅读代码的人会消耗很多心智去读懂他们。
所以乘机梳理一下,这样的话我们可以在开发的时候就选择合适的通讯方式。
罗列下通常情况下有以下场景:
- 父组件向子组件通讯
- 子组件向父组件通讯
- 跨级组件通讯
- 非嵌套关系组件通讯(含siblings)
示意图:
父组件向子组件传递消息
1. 通过Props传递:
这个是最常见的场景,我们可以在父组件通过props传递信息:
⚠️ 我们都知道这种方式,不过,其中有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 获得实例,触发实例方法:
在没有 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. 通过回调函数
常见的模式,通常能够满足大部分的通讯需求,不展开说明。
2. 通过 Render Props
Render Props 其实也算是回调,只不过这种回调比较特殊,它是挂载在 children 属性上的。
逻辑上:
children 是 props 的一部分 → props 支持函数 → children 可以是函数
某种程度上,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 实现跨级组件通讯
一般来说,优先考虑只传递数据;在复杂情景下,可以通过结合 Context 和useReducer 来构建一个简便的状态管理器;出于性能上的考虑也可以结合使用 useMemo。
不过,这种方案只合适小型的状态管理,并不推荐大规模使用。
2. 通过观察者模式(Rxjs等)
这种方法,与在 Vue 中我们常用的 EventBus 类似。
需要注意的是,在unmount的时候取消订阅避免内存泄漏。
同样,也是不推荐大规模使用。在大型应用中,这类消息传递很快就失控。
3. 使用 Redux 全局状态管理库(或Mobx等)
如果上方的通讯方式都不能很好地满足需要的话,可以开始考虑使用全局状态管理库。
可参考:juejin.cn/post/703478… 在项目中使用 Redux Toolkit。
小结
根据场景选择合适的组件数据传递方式,能够让项目更具维护性。
转载自:https://juejin.cn/post/7156889133204537357