组件逻辑复用
React为什么设计成组件化的形式?其实最大的原因就是为了方便复用。
然而组件的复用虽然方便,逻辑的复用却很麻烦,因为state的存在,逻辑被锁死在组件内部,很难分离出去。
下面以一个可以改变背景色的步进器为例,展示react中常见的三种逻辑复用模式
高阶组件(HOC)实现逻辑复用
import React from "react";
const Count = ({ count, add, minus, theme, changeTheme }) => {
return (
<div
style={{
backgroundColor: theme,
flex: 1,
alignItems: "center",
justifyContent: "center",
}}
>
<h1>You clicked *{count}* times</h1>
<h1 onClick={add}>add 1</h1>
<h1 onClick={minus}>minus 1</h1>
<h1 onClick={changeTheme}>change background</h1>
</div>
);
};
export default Count;
------------------------------------------------------
import React from "react";
import getRandomColor from "../utils/ColorHelper";
const changeTheme = (initColor) => (WrappedComponent) =>
class ChangeTheme extends React.Component {
state = {
theme: initColor,
};
changeTheme = () => this.setState({ theme: getRandomColor() });
render() {
return (
<WrappedComponent
{...this.props}
theme={this.state.theme}
changeTheme={this.changeTheme.bind(this)}
/>
);
}
};
export default changeTheme;
------------------------------------------------------
import React from "react";
const countNumber = (initNumber) => (WrappedComponent) =>
class CountNumber extends React.Component {
state = { count: initNumber };
add = () => this.setState({ count: this.state.count + 1 });
minus = () => this.setState({ count: this.state.count - 1 });
render() {
return (
<WrappedComponent
{...this.props}
count={this.state.count}
add={this.add.bind(this)}
minus={this.minus.bind(this)}
/>
);
}
};
export default countNumber;
******************************************************
import countNumber from "./HOC/countNumber";
import changeTheme from "./HOC/changeTheme";
import Count from "./HOC/Count";
<!-- 链式调用 -->
const StepperComponent = changeTheme("white")(countNumber(0)(Count));
function App() {
return (
<div className="App">
<StepperComponent />
</div>
);
}
高阶组件的原理是这样的。我们定义了两个高阶组件changeTheme、countNumber(其实是一个函数),然后把一个不包含state和逻辑的组件Count(一般称作无状态组件)作为高阶组件的参数传进去,然后返回一个新的class组件。在这个class组件内部,我们实现了计数器的state和逻辑,然后把state和各种函数作为无状态组件的Props传进作为高阶组件参数的无状态组件中去,最后在render里面返回被处理过后的无状态组件
当高阶组件内部的state更新时,由于该state成为了无状态组件的Props,所以就能同时带动内部的无状态组件进行刷新。这样,我们就分离了UI和逻辑,不仅让代码更清晰,而且高阶组件也可以拿给其他的组件反复使用
高阶组件有一个问题是属性被写死了。如果子组件需求的属性名写得不一样,高阶组件就无能为力了。比如上面的Count组件,接受了count,add,minus这个三个属性,但如果另一个组件需要的是num,addNum,minusNum这三个属性呢?两个组件明明需要相同的功能,逻辑却没法在这里复用了
同时虽然可以链式调用,但是链式调用过多的话,会生成很长的异常栈,导致错误难以定位
而且这种方式的确与 render 距离太远了,以至于在 class 内部可能都得纠结一下各个 props 究竟是来自于哪里,如果有问题、问题又出自于哪里。
render props实现逻辑复用
import React from "react";
import getRandomColor from '../utils/ColorHelper'
class ChangeTheme extends React.Component {
state = { theme: this.props.initColor };
changeTheme = () => this.setState({ theme: getRandomColor() });
render() {
return this.props.render({
theme: this.state.theme,
changeTheme: this.changeTheme.bind(this),
});
}
}
export default ChangeTheme;
------------------------------------------------------
import React from "react";
class CountNumber extends React.Component {
state = { count: this.props.initNumber };
add = () => this.setState({ count: this.state.count + 1 });
minus = () => this.setState({ count: this.state.count - 1 });
render() {
return this.props.render({
count: this.state.count,
add: this.add.bind(this),
minus: this.minus.bind(this),
});
}
}
export default CountNumber;
------------------------------------------------------
import React from "react";
import CountNumber from "./CountNumber";
import ChangeTheme from "./ChangeTheme";
const Stepper = () => {
return (
<ChangeTheme
initColor={"white"}
render={({ theme, changeTheme }) => (
<div
style={{
backgroundColor: theme,
flex: 1,
alignItems: "center",
justifyContent: "center",
}}
>
<CountNumber
initNumber={0}
render={({ count, add, minus }) => {
// 属性名不同不影响逻辑复用
const num = count;
const addNum = add;
const minusNum = minus;
return (
<>
<h1>You clicked *{num}* times</h1>
<h1 onClick={addNum}>add 1</h1>
<h1 onClick={minusNum}>minus 1</h1>
<h1 onClick={changeTheme}>change background</h1>
</>
);
}}
></CountNumber>
</div>
)}
></ChangeTheme>
);
};
export default Stepper;
******************************************************
import "./App.css";
import Stepper from "./RenderProps/Stepper";
function App() {
return (
<div className="App">
<Stepper />
</div>
);
}
export default App;
render props模式会假设子组件是一个函数,并把当前组件内部的state和逻辑传入该函数中。而我们真正想要渲染的无状态组件,则可以通过这个函数的参数得到它想要的属性
在render props模式下,属性名不同导致的逻辑不能复用,就能在这里完美得到解决
但render props很容易造成组件之间的嵌套严重
Hooks实现逻辑复用
import useCount from './useCount'
import useTheme from './useTheme'
const HookCount = () => {
const [count, addCount, minusCount] = useCount(0);
const [theme,changeBackgroundColor] = useTheme('white');
return (
<div style={{ backgroundColor:theme, flex: 1, alignItems: "center", justifyContent: "center" }}>
<h1>You clicked *{count}* times</h1>
<h1 onClick={addCount}>add 1</h1>
<h1 onClick={minusCount}>minus 1</h1>
<h1 onClick={changeBackgroundColor}>change background</h1>
</div>
);
};
export default HookCount;
------------------------------------------------------
import { useState } from "react";
const useCount = (initNumber) => {
const [count, setCount] = useState(initNumber);
const addCount = () => setCount(count + 1);
const minusCount = () => setCount(count - 1);
return [count, addCount, minusCount];
};
export default useCount;
------------------------------------------------------
import { useState } from "react";
import getRandomColor from "../utils/ColorHelper";
const useTheme = (initColor) => {
const [theme, changeTheme] = useState(initColor);
const changeBackgroundColor = () => changeTheme(getRandomColor());
return [theme, changeBackgroundColor];
};
export default useTheme;
******************************************************
import "./App.css";
import Count from "./Hooks/Count";
function App() {
return (
<div className="App">
<Count />
</div>
);
}
export default App;
在hooks中,可以将一个组件中需要的逻辑,也就是state单独抽离,这样也显得语义更清晰、结构更优雅,代码量大大减少
github地址:
https://github.com/huziiijian/components_structure
参考资料:
https://zhuanlan.zhihu.com/p/62791765?utm_source=com.gzqwkj.cshu
https://blog.csdn.net/qq_40962320/article/details/87043581
https://react.docschina.org/