在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,代码在 , 完全的api还请参考官方文档
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 ();}复制代码You clicked {count} times
在这里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 ();}// controlled input componentconst Item = ({ value, setValue }) => ( );// 可以将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 ()}复制代码{index}
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 ();}复制代码
function DeepChild() { const someValue = useContext(Context); return ({someValue})}复制代码
16.7之前的Consumer写法是render props
function DeepChild() { return ({ (someValue) => )}复制代码{someValue}}
似乎还能忍受,但是但是,为了避免不必要的刷新一般推荐用多个Context来传递刷新周期不同的数据,因此按原来的render-props写法很容易陷入多重嵌套地狱(wrapper-hell),很有可能你真正的渲染代码在十几个缩进后面才开始出现。继代码上下滚问题之后我们又出现了代码左右滚问题。
{ (value1) => ( // 我怎么还没有被同事打死?复制代码{ (value2) => ( ... ) } ) }
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} );}复制代码
我还用useReducer实现了一个todo的demo,代码分了好几个文件就不放上来了
为什么要用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正式版发布之后我还会来更新一次这个文档,在工程里正式使用一段时间之后会再更新一次,先奶一口。
参考
- 官方介绍hook的视频
- 官方文档
- 一些常见问题的官方解答