React Hook在项目

距*React Hooks* 推出已有很长一段时间,大家或多或少已开始接触起 Hooks。或有疑惑 ,或有思考,亦或在项目中用的风生水起,亦或止步于Hooks界的*"Hello World"*。

那么*Hooks* 究竟带来了什么?接下来让我们谈谈Hooks给我们的React世界带来了怎样的改变。

入门

思考下面这个组件?

KeyPress

首先我们用过去常用的class版本来实现下:

class KeyPress extends Component {
  constructor(props) {
    super(props);

    this.state = {
      top: false,
      bottom: false,
      left: false,
      right: false,
    };
  }
  componentDidMount() {
    document.addEventListener("keydown", this.onKeyDown.bind(this));
    document.addEventListener("keyup", this.onKeyUp.bind(this));
  }

  componentWillUnmount() {
    document.removeEventListener("keydown", this.onKeyDown.bind(this));
    document.removeEventListener("keyup", this.onKeyUp.bind(this));
  }

  onKeyDown(e) {
    switch (e.keyCode) {
      case 37:
        this.setState({
          left: true,
        });
        break;
      case 38:
        this.setState({
          top: true,
        });
        break;
      case 39:
        this.setState({
          right: true,
        });
        break;
      case 40:
        this.setState({
          bottom: true,
        });
        break;
      default:
        break;
    }
  }

  onKeyUp() {
    switch (e.keyCode) {
      case 37:
        this.setState({
          left: false,
        });
        break;
      case 38:
        this.setState({
          top: false,
        });
        break;
      case 39:
        this.setState({
          right: false,
        });
        break;
      case 40:
        this.setState({
          bottom: false,
        });
        break;
      default:
        break;
    }
  }

  render() {
    const { top, bottom, left, right } = this.state;
    return (
      <div>
        <p>
          <span role="img" aria-label="top">
            按↑显示'👆'
          </span>
          <span role="img" aria-label="bottom">
            按↓显示👇
          </span>
          <span role="img" aria-label="left">
            按←显示👈
          </span>
          <span role="img" aria-label="right">
            按→显示👉
          </span>
        </p>
        <p>
          {top && "👆"}
          {bottom && "👇"}
          {left && "👈"}
          {right && "👉"}
        </p>
      </div>
    );
  }
}

Hook版本又是怎么样的呢?

function KeyPress() {
  const [top, setTop] = useState(false),
    [bottom, setBottom] = useState(false),
    [left, setLeft] = useState(false),
    [right, setRight] = useState(false);

  useEffect(() => {
    window.addEventListener("keydown", onKeyDown);
    window.addEventListener("keyup", onKeyUp);
    return () => {
      window.removeEventListener("keydown", onKeyDown);
      window.removeEventListener("keyup", onKeyUp);
    };
  });

  function onKeyDown(e) {
    switch (e.keyCode) {
      case 37:
        setLeft(true);
        break;
      case 38:
        setTop(true);
        break;
      case 39:
        setRight(true);
        break;
      case 40:
        setBottom(true);
        break;
      default:
        break;
    }
  }

  function onKeyUp() {
    switch (e.keyCode) {
      case 37:
        setLeft(false);
        break;
      case 38:
        setTop(false);
        break;
      case 39:
        setRight(false);
        break;
      case 40:
        setBottom(false);
        break;
      default:
        break;
    }
  }
  return (
    // ...
  );
}

是的,仅此而已。

相比较于class版本,在Hook例子中,keyPress组件的变化好像不大,但你是否发现了几个问题

  • class版本需要在componentDidMount中建立监听事件,在componentWillUnmount中移除监听事件。
  • 存在很多重复的业务逻辑,比如topbotom等的操作逻辑其实都是类似的,按下去都是将值(top/botoom等)设为true,弹起时设为false

对于问题一:

使用*useEffect在同一个方法中既创建了监视器,又以返回清除函数的方式清除了监视器。而非class版本中在componentDidMount*,componentWillUnmount两个生命周期中单独创建和销毁。

而对于问题二:

这就需要Hook中最重要的一部分: 自定义Hook

自定义Hook

官方解释: 通过自定义 Hook,可以将组件逻辑提取到可重用的函数中。

直译看来,仅仅是一个函数,怎么去做呢?

接着看上面的例子。

//  hook/keypress.js
export function useKeyPress(targetKey) {
  const [keyPress, setKeyPress] = useState(false);

  function onKeyDown({ keyCode }) {
    if (keyCode === targetKey) {
      setKeyPress(true);
    }
  }

  function onKeyUp({ keyCode }) {
    if (keyCode === targetKey) {
      setKeyPress(false);
    }
  }
  useEffect(() => {
    window.addEventListener("keydown", onKeyDown);
    window.addEventListener("keyup", onKeyUp);
    return () => {
      window.removeEventListener("keydown", onKeyDown);
      window.removeEventListener("keyup", onKeyUp);
    };
  }, []);

  return keyPress;
}

// KeyPress.js
function KeyPress() {
  const top = useKeyPress(38);
  const bottom = useKeyPress(40);
  const left = useKeyPress(37);
  const right = useKeyPress(39);
  return (
   // ...略
  );
}

这样子就实现了个useKeyPress Hook

是否有种豁然一新的感觉,重复逻辑问题被巧妙的修复了。

没错,这正是自定义Hook真正的作用,也正如官方所说的*"可以将组件逻辑提取到可重用的函数中"*,即抽离业务代码中的公有逻辑。

但其实这段代码还是有问题的。

question

这里表示useEffect的缺少了onKeyDownonKeyUp依赖,主要是在于这2个函数引用了外界传入的targetKey, 这是一个*不安全的情况*

如何修复呢?

推荐是把onKeyDownonKeyUp移到effect内部, 这样子依赖则变成了targetKey

export function useKeyPress(targetKey) {
  const [keyPress, setKeyPress] = useState(false);

  useEffect(() => {
    function onKeyDown({ keyCode }) {
      if (keyCode === targetKey) {
        setKeyPress(true);
      }
    }

    function onKeyUp({ keyCode }) {
      if (keyCode === targetKey) {
        setKeyPress(false);
      }
    }
    window.addEventListener("keydown", onKeyDown);
    window.addEventListener("keyup", onKeyUp);
    return () => {
      window.removeEventListener("keydown", onKeyDown);
      window.removeEventListener("keyup", onKeyUp);
    };
  }, [targetKey]);

  return keyPress;
}

另外还有种解决方案,这就需要用到了Hook中的useCallback API

// hook/keypress.js
export function useKeyPress(targetKey) {
  const [keyPress, setKeyPress] = useState(false);

  const onKeyDown = useCallback(
    ({ keyCode }) => {
      if (keyCode === targetKey) {
        setKeyPress(true);
      }
    },
    [targetKey]
  );
  const onKeyUp = useCallback(
    ({ keyCode }) => {
      if (keyCode === targetKey) {
        setKeyPress(false);
      }
    },
    [targetKey]
  );
  useEffect(() => {
    window.addEventListener("keydown", onKeyDown);
    window.addEventListener("keyup", onKeyUp);
    return () => {
      window.removeEventListener("keydown", onKeyDown);
      window.removeEventListener("keyup", onKeyUp);
    };
  }, [onKeyDown, onKeyUp]);

  return keyPress;
}

通过useCallback 缓存onKeyDown,onKeyUp函数,只有当targetKey改变时才会更新,也就确保了targetKey prop的变化会自动触发useEffect

这么一来,keyPress这个公共逻辑就被完美的从组件中抽离出来。

仔细想想,是否别的组件是否也有键盘事件,是不是也可以直接调用useKeyPress来抽离公共逻辑。

Perfect!🎉

是不是除了键盘事件,项目中还有很多的公共逻辑可以抽离出来。比如说鼠标操作事件、setTimeout/setInterval定时器事件、节流/防抖等等。

是不是如同哥伦布发现新大陆一般兴奋🥰

虽说Hooks很棒,但它并不可以毫无约束的去使用!

Hook约定

只在最顶层使用Hook

不要在循环,条件或嵌套函数中调用 Hook, 确保总是在你的 React 函数的最顶层调用他们。

这个主要是在于React Hook需要按一定的顺序调用, 如下面的官方例子:

function Form() {
  // 1. Use the name state variable
  const [name, setName] = useState('Mary');

  // 2. Use an effect for persisting the form
  useEffect(function persistForm() {
    localStorage.setItem('formData', name);
  });

  // 3. Use the surname state variable
  const [surname, setSurname] = useState('Poppins');

  // 4. Use an effect for updating the title
  useEffect(function updateTitle() {
    document.title = name + ' ' + surname;
  });

  // ...
}

本来Hook的调用顺序如下:

useState('Mary')           
useEffect(persistForm)   
useState('Poppins')        
useEffect(updateTitle)    

如果函数中存在 条件判断等会改变调用顺序的情况,如:

  // 🔴 在条件语句中使用 Hook 违反第一条规则
  if (name !== '') {
    useEffect(function persistForm() {
      localStorage.setItem('formData', name);
    });
  }

这样子,会导致调用顺序会在运行过程中发生改变

useState('Mary')
// useEffect(persistForm)
useState('Poppins')
useEffect(updateTitle)

Hooks的执行数据保存在同一个储存空间中。

useStateuseEffect都是Hook,他们用的都是同一个储存空间。如果调用顺序发生改变,则会发生下面 的情况

//  第一次渲染
useState('Mary')         // 1. 使用 'Mary' 初始化变量名为 name 的 state
useEffect(persistForm)   // 2. 添加 effect 以保存 form 操作
useState('Poppins')      // 3. 使用 'Poppins' 初始化变量名为 surname 的 state
useEffect(updateTitle)   // 4. 添加 effect 以更新标题

// 第二次渲染
useState('Mary')           // 1. 读取变量名为 name 的 state(参数被忽略)
// useEffect(persistForm)  // 🔴 此 Hook 被忽略!
useState('Poppins')        // 🔴 2 (之前为 3)。读取变量名为 surname 的 state 失败
useEffect(updateTitle)     // 🔴 3 (之前为 4)。替换更新标题的 effect 失败

useState默认会返回*[state, setState], 如果调用顺序发生了改变,相当于[state, setState]*得不到正确返回,也就会造成问题,并且后续的执行顺序都会提前,每个执行也会相应地产生意料之外的结果。

关于Hooks执行顺序可以参考:为什么顺序调用对 React Hooks 很重要?

只在React函数中调用Hook

不要在普通的 JavaScript 函数中调用 Hook。

  • ✅ 在 React 的函数组件中调用 Hook
  • ✅ 在自定义 Hook 中调用其他 Hook

这个O(∩_∩)O哈哈~,太好理解了,你能在vue中用React Hook吗?

另外Hook的本质就是在不编写 class 的情况下使用 state 以及其他的 React 特性

结语

Hook有效地抽离公共逻辑,分割开业务逻辑与公共逻辑,提高了代码的质量与可维护性。但缺点在于Hook的学习与认知成本,倘若知识不到位,那只能起到反作用力,导致各种问题。

HOC正值壮年,而Hook才刚刚起步,但Hook才是*React的未来。早日掌握Hook*,方能早日掌握市场力。

参考资料

上次更新: 2020/4/17 下午8:36:22