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>
  );
};

可以明显的看到几个优点:

  1. 代码更简洁

  2. 不存在重命名覆盖
    解释:现在的可复用状态没有像 HOC 挂到被包装组件的 this.props 上了,我们都知道 hook 的写法可以暴露出一个数组:[ 值 , 方法 ]。在使用的时候,可以用解构的手法来实现对数组内变量名的自定义,保证命名不重复。

  3. 没有嵌套

如果还有更多的逻辑需要被抽象,我们只管继续封装 useXxx,然后在组件中进行使用。
如上面讲的 HOCInputBgColor 高阶组件,我们也可以用 hook版进行封装,如 useInputBgColor,小伙伴们,动手试试看吧~

总结

在数据的处理中,我们知道在处理“平级”的数据,往往比嵌套的、树形的数据来得简单。

就如:

const arr = [1, [2, 3, [4, 5]]];
arr.flat('Infinity'); 
// [1, 2, 3, 4, 5]

个人觉得 自定义hook 就类似这样一个“拉平”,让我们对于数据的处理更直观,更不容易犯错。

推荐阅读更多精彩内容