hook!

1,460 阅读8分钟

在react conf 2018上,react发布了一个新的提案hook。稳定的正式版可能要等一两个月之后才能出来,目前可以在v16.7.0-alpha上试用到rfc上各种提问。

那么这个hook到底是个什么呢,官方的定义是这样的

Hooks are a new feature proposal that lets you use state and other React features without writing a class.

这是一个比class更直观的新写法,在这个写法中react组件都是纯函数,没有生命周期函数,但可以像class一样拥有state,可以由effect触发生命周期更新,提供一种新的思路来写react。(虽然官方再三声明我们绝对没有要拿掉class的意思,但hook未来的目标是覆盖所有class的应用场景)

其实在看demo演示的时候我是十分抗拒的,没有生命周期函数的react是个什么黑魔法,虽然代码变得干净了不少,但写法实在是发生了很大的转变,有种脱离掌控的不安全感,我甚至有点怀疑我能不能好好debug。

演示的最后dan的结束语是这样的

hook代表了我们对react未来的愿景,也是我们用来推动react前进的方法。因此我们不会做大幅的重写,我们会让旧的class模式和新的hook模式共存,所以我们可以一起慢慢的接纳这个新的react。

我接触react已经四年了,第一次接触它的时候,我第一个想问的是,为什么要用jsx。第二个想问的是,为什么要用这个logo,毕竟我们又不是叫atom,也不是什么物理引擎。现在我想到了了一个解释,原子的类型和属性决定了事物的外观和表现,react也是一样的,你可以把界面划分为一个个独立的组件,这些组件(component)的类型(type)和属性(props)决定了最终界面的外观和表现。讽刺的是,原子一直被认为是不可分的,所以当科学家第一次发现原子的时候认为这就是最小的单元,直到后来在原子中发现了电子,实际上电子的运动更能决定原子能做什么。hook也是一样的,我不认为hook是一个新的react特性,相反的,我认为hook能让我更直观的了解react的基本特性像是state、context、生命周期。hook能更直观的代表react,它解释了组件内部是如何工作的,我认为它被遗落了四年,当你看到react的logo,可以看到电子一直环绕在那里,hook也是,它一直在这里。

于是我决定干了这杯安利。

试了几个比较基本的api写了几个demo,代码在 github.com/lllbahol/re…, 完全的api还请参考官方文档 reactjs.org/docs/hooks-…

api

基本的hook有三个

  • useState(相当于state)
  • useEffect(相当于componentDidUpdate, componentDidMount, componentWillUnmount)
  • useContext(相当于Context api)

useState

const [state, setState] = useState(initialState);

import { useState } from 'react';

function Example() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

在这里react组件就是一个简单的function

  • useState(initialState)也是一个函数,定义了一个state,初始值为initialState,返回值是一个数组,0为state的值,1为setState的方法。

  • 当state发生变化时,函数组件刷新。

  • 可以useState多次来定义多个state,react会根据调用顺序来判断。

你一定也写过一个庞大的class, 有一堆handler函数,因为要setState所以不能挪到组件外面去,然后render函数就被挤出了页面,每次想看render都要把页面滚到底下。

现在因为useState是函数,所以它可以被挪到组件外面,连带handler一起,下面是一个具体一点的表单例子。

import React, { useState } from 'react';

// 表单组件,有name, phone两个输入框。
export default () => {
  const name = useSetValue('hello');
  const phone = useSetValue('120');
  return (
    <React.Fragment>
      <Item {...name} />
      <br />
      <Item {...phone} />
    </React.Fragment>
  );
}

// controlled input component
const Item = ({ value, setValue }) => (
  <React.Fragment>
    <label>{value}</label>
    <br />
    <input value={value} onChange={setValue} />
  </React.Fragment>
);

// 可以将state连同handler function一起挪到组件外面。
// 甚至可以export出去,让其他组件也能使用这个state逻辑
const useSetValue = (initvalue) => {
  const [value, setValue] = useState(initvalue);
  const handleChange = (e) => {
    setValue(e.target.value);
  }
  return {
    value,
    setValue: handleChange,
  };
}

useEffect

这个api可以让你在函数组件中使用副作用(use side effects),常见的会产生副作用的方式有获取数据,更新dom,绑定事件监听等,render只负责渲染,一般会等到dom加载好之后再去调用这些副作用方法。

useEffect(didUpdate/didMount);

useEffect(
  () => {
    const subscription = props.source.subscribe();
    return () => {
      subscription.unsubscribe();
    };
  },
  [props.source],
);

useEffect可以接受两个参数

  • 第一个参数为一个effect函数,effect函数在每次组件render之后被调用,相当于componentDidUpdate和componentDidMount两个生命周期之和。effect函数可以返回一个clear effect函数,会在下一次的effect函数执行之前执行,原来componentWillUnmount里执行的东西都可以交给它。调用顺序是:render(dom加载完成) => prevClearUseEffect => useEffect

  • 第二个参数是一个数组,只有当数组传入的值发生变化时,effect才会执行。

上面的写法如果用class实现的话应该是下面这样的。我们按时间先后将一个会产生副作用的函数的第1次调用、第2-n次调用、卸载分成3截,实际上它们总是一一对应出现的,应该是一个整体。

componentDidMount() {
  this.subscription = props.source.subscribe();
}

componentDidUpdate() {
  this.subscription = props.source.subscribe();
}

componentWillUnmount () {
  subscription.unsubscribe();
}

具体案例可以看一个轮播组件的demo

import React, { useState, useEffect } from 'react';
import './index.css';

const IMG_NUM = 3;

export default () => {
  const [index, setIndex] = useState(0);
  const [isPlaying, setIsPlaying] = useState(false);
  useEffect(() => {
    // 每次组件刷新时触发effect, 相当cDM cDU
    if (isPlaying) {
      const timeout = setTimeout(() => {
        // 改变state, 刷新组件
        handleNext();
      }, 2000);
      // 返回清除effect的回调函数, 在每次effect调用完之后,如果有则执行
      return () => clearTimeout(timeout);
    }
    // 如果不想每次render之后都调一次effect, 可以使用第二个参数作为筛选条件
  }, [index, isPlaying]);

  const handleNext = () => {
    setIndex((index + 1) % IMG_NUM);
  }
  const handlePrev = () => {
    setIndex((index - 1 + IMG_NUM) % IMG_NUM);
  }
  const handlePause = () => {
    setIsPlaying(!isPlaying);
  };
  return (
    <div>
      <div className="img">{index}</div>
      <button onClick={handlePrev}>prev</button>
      <button onClick={handlePause}>pause</button>
      <button onClick={handleNext}>next</button>
    </div>
  )
}

useContext

const context = useContext(Context);

如果对react比较熟悉的话,应该用过Context这个api,用于在组件之间传递数据。useContext接受一个context对象(React.createContext生成),返回context.Consumer中获得的值。

export const Context = React.createContext(null);

function Parent() {
  const someValue = 'haha';
  return (
    <Context.Provider value={someValue}>
      <DeepTree>
       	<DeepChild />
      </DeepTree>
    </Context.Provider>
  );
}
function DeepChild() {
  const someValue = useContext(Context);
  return (<div>{someValue}</div>)
}

16.7之前的Consumer写法是render props

function DeepChild() {
  return (
    <Context.Consumer>
      {
        (someValue) => <div>{someValue}</div>
      }
    </Context.Consumer>
  )
}

似乎还能忍受,但是但是,为了避免不必要的刷新一般推荐用多个Context来传递刷新周期不同的数据,因此按原来的render-props写法很容易陷入多重嵌套地狱(wrapper-hell),很有可能你真正的渲染代码在十几个缩进后面才开始出现。继代码上下滚问题之后我们又出现了代码左右滚问题。

<Consumer1>
  {
    (value1) => (
      <Consumer2>
        {
          (value2) => (
            ...
          )
        }
      </Consumer2>
    )
  }
</Consumer1>

// 我怎么还没有被同事打死🤦

useReducer

还有一堆高级hook

其中有一个useReducer

就是大家熟悉的那个redux里的reducer,来段模板代码让大家回忆一下。

const mapStateToProps = createStructuredSelector({
	...
});

const mapDispatchToProps = (dispatch) => ({
  ...
});

const withReducer = injectReducer({ ... });

const withConnect = connect(mapStateToProps, mapDispatchToProps);

export default compose(withReducer, withConnect)(Component);

以上的这些,使用了useReducer之后都没有了。

function Counter({initialCount}) {
  const [state, dispatch] = useReducer(reducer, {count: initialCount});
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({type: 'reset'})}>
        Reset
      </button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
    </>
  );
}

我还用useReducer实现了一个todo的demo,代码分了好几个文件就不放上来了 github.com/lllbahol/re…

为什么要用hook

除了上面提到的,还有官方罗列出来的一些时常会在写class时遇到的麻烦

  • class组件间不能复用与state关联的代码,hook可以做到这一点。
  • 复杂而庞大的class组件很难被理解,hook能够让你把组件拆成更小的独立单元
  • 理解class是一件困难的事,无论是对人还是对开发工具而言都是这样。比如class里面的this指向的是组件,在箭头函数写法出来之前,我们不得不手动绑定this到调用函数的对象上。

总的来说

用react也好久了,工程越写越复杂,组件间的数据传递是一个很大的问题,从传统的传回调函数,到跨多层多组件共享数据的时候使用redux,后来嫌模板代码太多又自己封了一层render-props结果掉进wrapper嵌套地狱的坑里,Context出来的时候开心了一会儿然后发现依然在坑里。写是能写的,就是恐惧,每写一层,我的代码就又缩进了三个tab,离被同事打死又前进三步。

useContext,useReducer的用法让我想到了高阶,不同的是可以直接用变量接住而不是挂在props上,因此不用考虑props名冲突问题,但能达到高阶一层层包裹数据的效果。

从现有的文档来看,新的api非常的多,一些是我们熟悉的用法一些则是完全新的东西,且暂时还没能覆盖所有生命周期场景(比如getDeriveStateFromProps),但不着急,可以一步一步来。

hook正式版发布之后我还会来更新一次这个文档,在工程里正式使用一段时间之后会再更新一次,先奶一口。

参考

  1. www.youtube.com/watch?v=dpw… 官方介绍hook的视频
  2. reactjs.org/docs/hooks-… 官方文档
  3. reactjs.org/docs/hooks-… 一些常见问题的官方解答