React Hook丨真正的逻辑复用
说起逻辑复用,熟悉 react 小伙伴们一口道出了 HOC [高阶组件] 。没错,高阶组件可以实现逻辑复用,在 hook 之前 react 还有挺多不错的方案。那么,让我们来浅谈 HOC 与 自定义 hook。
HOC逻辑复用
说起HOC,我想到了两个标签:1.【嵌套】 2.【一直嵌套】
让我们来深入场景,举个例子:
以封装一个 input 双向绑定为例
我们经常会这样去做一个双向绑定
// ...
state = {
value: 1
};
onChange = (e: any) => {
this.setState({
value: e.target.value
});
};
// ...
<input value={this.state.value} onChange={this.onChange} />;
假设在一个组件内有多个 input 我们希望可以更好的去复用「双向绑定」的逻辑,于是我们对这块逻辑用 HOC 进行抽象:
HOCInput.tsx
const HOCInput = (WrappedComponent: any) => {
return class extends React.Component<
{},
{
fields: {
[key: string]: {
value: string;
onChange: (e: any) => void;
};
};
}
> {
constructor(props: any) {
super(props);
this.state = {
fields: {}
};
}
setField = (name: string) => {
if (!this.state.fields[name]) {
this.state.fields[name] = {
value: "",
onChange: (event: any) => {
this.state.fields[name].value = event.target.value;
this.forceUpdate();
}
};
}
return {
value: this.state.fields[name].value,
onChange: this.state.fields[name].onChange
};
};
getFieldValueTrim = (name: string) => {
return this.state.fields[name]
? this.state.fields[name].value.trim()
: "";
};
render() {
const { setField, getFieldValueTrim } = this;
const newProps = { setField, getFieldValueTrim };
return <WrappedComponent {...this.props} {...newProps} />;
}
};
};
在 Vue 中有不错的 v-model.trim 语法糖【自动去掉字符串头尾空格】,避免我们提交的时候有多余的空格。所以,这里我们实现这样的功能并将 getFieldValueTrim
方法挂到 props 上。
调用
Demo1Component.tsx
class Demo1Component extends React.Component<{
setField?: (name: string) => { value: string; onChange: (e: any) => {} };
getFieldValueTrim?: (name: string) => string;
}> {
render() {
const { setField, getFieldValueTrim } = this.props;
console.log("name :>> ", getFieldValueTrim!("name"));
return (
<div>
<input {...setField!("name")} />
<br />
<input {...setField!("email")} />
</div>
);
}
}
// 嵌套
const Demo1 = HOCInput(Demo1Component);
//...
<Demo1 />
// ...
这样,我们就用 HOC 完成了一个逻辑复用。假设,我们还有一个或多个「逻辑」需要抽象成一个高阶组件呢?
如:我想要点击按钮随机切换 input 框的背景颜色。
那就让我们继续封装 HOC
HOCInputBgColor.tsx
const HOCInputBgColor = (initialColor: string) => (WrappedComponent: any) => {
return class extends React.Component<{}, { color: string }> {
state = {
color: initialColor
};
getRandomColor = () => {
const randomNum = () => Math.floor(Math.random() * 100);
return `rgb(${randomNum()},${randomNum()},${randomNum()})`;
};
handleChangeColor = () => this.setState({ color: this.getRandomColor() });
render() {
const newProps = {
color: this.state.color,
handleChangeColor: this.handleChangeColor
};
return <WrappedComponent {...this.props} {...newProps} />;
}
};
};
在原来的组件上进行调用
Demo1Component.tsx
class Demo1Component extends React.Component<{
setField?: (name: string) => { value: string; onChange: (e: any) => {} };
getFieldValueTrim?: (name: string) => string;
color?: string;
handleChangeColor?: () => void;
}> {
render() {
const {
setField,
getFieldValueTrim,
color,
handleChangeColor
} = this.props;
return (
<div>
<input style={{ background: color! }} {...setField!("name")} />
<br />
<button onClick={handleChangeColor!}>change bg-color</button>
</div>
);
}
}
/
const Demo1 = HOCInput(HOCInputBgColor("rgb(158,158,158)")(Demo1Component));
当我们有更多的 HOC 时,那么就会一直嵌套下去,好在有ts装饰器的支持,让我们看这个「嵌套」看着更加舒适,如:
@HOCInput
@HOCInputBgColor("rgb(158,158,158)")
class Demo1Component extends React.Component { }
我们也不再需要重新把组件赋值给一个变量,在调用组件的时候,直接 <Demo1Component />
。
HOC缺点
当组件在调用多个HOC时,会调用 props 上 HOC 传递下来的 值/方法,如上面的例子:
const {
setField,
getFieldValueTrim,
color,
handleChangeColor
} = this.props;
要是 HOC 一多命名就要形成规范,否则将有可能导致重命名发生覆盖。这算是 HOC 的一个缺点吧。
自定义 hook 逻辑复用
官网:自定义 hook 解决了以前在 React 组件无法灵活共享逻辑的问题。
我们直接把上面的例子改成 hook 版看看。
useInput.ts【自定义 Hook 名称需要以 “use” 开头】
const useInput = (
initialValue = ""
): [{ value: string; onChange: (e: any) => void }, string] => {
const [value, setValue] = useState(initialValue);
const onChange = (e: any) => {
setValue(e.target.value);
};
return [
{
value,
onChange
},
`${value}`.trim()
];
};
使用
Demo2.tsx
const Demo2: React.FC = () => {
const [nameIpt, name] = useInput();
const [emailIpt, email] = useInput();
console.log("Hook-name :>> ", name);
console.log("Hook-email :>> ", email);
return (
<div>
<input {...nameIpt} />
<br />
<input {...emailIpt} />
</div>
);
};
可以明显的看到几个优点:
-
代码更简洁
-
不存在重命名覆盖
解释:现在的可复用状态没有像 HOC 挂到被包装组件的 this.props 上了,我们都知道 hook 的写法可以暴露出一个数组:[ 值 , 方法 ]。在使用的时候,可以用解构的手法来实现对数组内变量名的自定义,保证命名不重复。 -
没有嵌套
如果还有更多的逻辑需要被抽象,我们只管继续封装 useXxx
,然后在组件中进行使用。
如上面讲的 HOCInputBgColor
高阶组件,我们也可以用 hook版进行封装,如 useInputBgColor
,小伙伴们,动手试试看吧~
总结
在数据的处理中,我们知道在处理“平级”的数据,往往比嵌套的、树形的数据来得简单。
就如:
const arr = [1, [2, 3, [4, 5]]];
arr.flat('Infinity');
// [1, 2, 3, 4, 5]
个人觉得 自定义hook 就类似这样一个“拉平”,让我们对于数据的处理更直观,更不容易犯错。