关于 React 函数组件实现父子组件/组件间通信的三种方法, props, context, 函数组件函数化
前言
使用 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 编写"真 ` 易于维护和扩展"的组件(一)
转载自:https://juejin.cn/post/6924949740144558087