关于hook:面试官求你别再问我hook了

一 前言先问大家几个问题,这几个问题都是我在面试中实在被问到的,属实给我整不会了.... 写hooks跟写类组件比,hooks有啥劣势?咱们如何封装一个hook?hooks原理是什么?面试尽管凉了,然而学习还得持续 二 深刻理解hooksuseState应用useState的应用很简略,一句话带过,返回一个数组,数组的值为以后state和更新state的函数;useState的参数是变量、对象或者是函数,变量或者对象会作为state的初始值,如果是函数,函数的返回值会作为初始值。 批量更新看上面这段代码 function Count(){ let [count,setCount] = useState(0)const handleAdd = function(){ setCount(count+1) setCount(count+1)}return( <div> <p>{count}</p> /*每次点击加1*/ <button onClick={handleAdd}>加一</button> </div>)}复制代码在同一个事件中并不会因为调用了两次setCount而让count减少两次,试想如果在同一个事件中每次调用setCount都失效,那么每调用一次setCount组件就会从新渲染一次,这无疑使十分影响性能的;实际上如果批改的state是同一个,最初一个setCount函数中的新state会笼罩后面的 useEffect && useLayoutEffect应用这两个hook用法统一,第一个参数是回调函数,第二个参数是数组,数组的内容是依赖项deps,依赖项扭转后执行回调函数;留神组件每次渲染会默认执行一次,如果不传第二个参数只有该组件有state扭转就会触发回调函数,如果传一个空数组,只会在初始化执行一次。另外,如果用return返回了一个函数,组件每次从新渲染的时候都会先执行该函数再调用回调函数。 区别外表上看,这两个hook的区别是执行机会不同,useEffect的回调函数会在页面渲染后执行;useLayoutEffect会在页面渲染前执行。实际上是React对这两个hook的解决不同,useEffect是异步调用,而useLayoutEffect是同步调用。那什么时候用useEffect,什么时候用useLayoutEffect呢?我的了解是视状况而定 如果回调函数会批改state导致组件从新渲染,能够useLayoutEffect,因为这时候用useEffect可能会造成页面闪动; 如果回调函数中去申请数据或者js执行工夫过长,倡议应用useEffect;因为这时候用useLayoutEffect梗塞浏览器渲染。 useMemo && useCallback这两个hook可用于性能优化,缩小组件的反复渲染;当初就来看看这两个神奇的hook怎么用。 uesMemofunction MemoDemo() { let [count, setCount] = useState(0);let [render,setRender] = useState(false)const handleAdd = () => { setCount(count + 1);};const Childone = () => { console.log("子组件一被从新渲染"); return <p>子组件一</p>;};const Childtwo = (props) => { return ( <div> <p>子组件二</p> <p>count的值为:{props.count}</p> </div> );};const handleRender = ()=>{ setRender(true)}return ( <div style={{display:"flex",justifyContent:'center',alignItems:'center',height:'100vh',flexDirection:'column'}}> { useMemo(() => { return <Childone /> }, [render]) } <Childtwo count={count} /> <button onClick={handleAdd}>减少</button> <br/> <button onClick={handleRender} >子组件一渲染</button> </div>);}复制代码Childone组件只有render扭转才会从新渲染 ...

November 12, 2021 · 3 min · jiezi

关于hook:React-hook-中connect和forwardRef连用会导致传入子组件的ref失效

let Component = (props)=>{ const {refInstance} = props; // 只有是实例都行useForm和useRef创立的都能够 const [form] = Form.useForm(); useImperativeHandle(refInstance,()=>({ submit:()=>{ form.submit(); } })); return ( <div>Hello Word</div> )};Component = connect(xxx, xxx)(Component);//留神:这里不要在Component上应用ref;换个属性名字比方refInstance;不然会导致笼罩export default React.forwardRef((props,ref) => (<Component {...props} refInstance={ref} />));查阅connect后发现应用connect的第四个参数也可实现 export default connect((state) => { return { list: state.list, }}, null, null, { forwardRef: true })(C2)

July 9, 2021 · 1 min · jiezi

4-个-useState-Hook-示例

为了保证的可读性,本文采用意译而非直译。 想阅读更多优质文章请猛戳GitHub博客,一年百来篇优质文章等着你! 到 React 16.8 目前为止,如果编写函数组件,然后遇到需要添加状态的情况,咱们就必须将组件转换为类组件。 编写 class Thing extends React.Component,将函数体复制到render()方法中,修复缩进,最后添加需要的状态。 今天,可以使用 Hook 获得相同的功能,并为自己节省了工作时间。在本文中,主要介绍useState hook。 useState 做啥子的useState hook 允许咱们向函数组件添加状态,我们通常称这些为“ hooks”,但它们实际上是函数,与 React 16.8 捆绑在一起。 通过在函数组件中调用useState,就会创建一个单独的状态。 在类组件中,state 总是一个对象,可以在该对象上添加保存属性。 对于 hooks,state 不必是对象,它可以是你想要的任何类型-数组、数字、布尔值、字符串等等。每次调用useState都会创建一个state块,其中包含一个值。 示例:使用 useState 显示/隐藏组件这个示例是一个组件,它显示一些文本,并在末尾显示一个read more链接,当单击链接时,它展开剩下的文本。 import React, { useState } from 'react';import ReactDOM from 'react-dom';// 两个 props:// text - 显示的内容// maxLength - 在点击“read more”之前显示多少个字符function LessText({ text, maxLength }) { // 创建一个状态,并将其初始化为“true” const [hidden, setHidden] = useState(true); if (text <= maxLength) { return <span>{text}</span>; } return ( <span> {hidden ? `${text.substr(0, maxLength).trim()} ...` : text} {hidden ? ( <a onClick={() => setHidden(false)}> read more</a> ) : ( <a onClick={() => setHidden(true)}> read less</a> )} </span> );}ReactDOM.render( <LessText text={`专注、努力是成功的真正关键。把你的眼睛盯在目标上,然后朝着目标迈出下一步`} maxLength={35} />, document.querySelector('#root'));仅用一行代码,我们就使这个函数组件有状态: ...

August 21, 2019 · 2 min · jiezi

快速了解-React-Hooks-原理

为了保证的可读性,本文采用意译而非直译。 想阅读更多优质文章请猛戳GitHub博客,一年百来篇优质文章等着你! 我们大部分 React 类组件可以保存状态,而函数组件不能? 并且类组件具有生命周期,而函数组件却不能? React 早期版本,类组件可以通过继承PureComponent来优化一些不必要的渲染,相对于函数组件,React 官网没有提供对应的方法来缓存函数组件以减少一些不必要的渲染,直接 16.6 出来的 React.memo函数。 React 16.8 新出来的Hook可以让React 函数组件具有状态,并提供类似 componentDidMount和componentDidUpdate等生命周期方法。 类被会替代吗?Hooks不会替换类,它们只是一个你可以使用的新工具。React 团队表示他们没有计划在React中弃用类,所以如果你想继续使用它们,可以继续用。 我能体会那种总有新东西要学的感觉有多痛苦,不会就感觉咱们总是落后一样。Hooks 可以当作一个很好的新特性来使用。当然没有必要用 Hook 来重构原来的代码, React团队也建议不要这样做。 Go Go来看看Hooks的例子,咱们先从最熟悉的开始:函数组件。 以下 OneTimeButton 是函数组件,所做的事情就是当我们点击的时候调用 sayHi 方法。 import React from 'react';import { render } from 'react-dom';function OneTimeButton(props) { return ( <button onClick={props.onClick}> 点我点我 </button> )}function sayHi() { console.log('yo')}render( <OneTimeButton onClick={sayHi}/>, document.querySelector('#root'))我们想让这个组件做的是,跟踪它是否被点击,如果被点击了,禁用按钮,就像一次性开关一样。 但它需要一个state,因为是一个函数,它不可能有状态(React 16.8之前),所以需要重构成类。 函数组件转换为类组件的过程中大概有5个阶段: *否认:也许它不需要是一个类,我们可以把 state 放到其它地方。 实现: 废话,必须把它变成一个class,不是吗?接受:好吧,我会改的。努力加班重写:首先 写 class Thing extends React.Component,然后 实现 render等等 。最后:添加state。class OneTimeButton extends React.Component { state = { clicked: false } handleClick = () => { this.props.onClick(); // Ok, no more clicking. this.setState({ clicked: true }); } render() { return ( <button onClick={this.handleClick} disabled={this.state.clicked} > You Can Only Click Me Once </button> ); }}这是相当多的代码,组件的结构也发生了很大的变化, 我们需要多个小的功能,就需要改写很多。 ...

August 20, 2019 · 2 min · jiezi

你不知道的-useCallback

欢迎关注我的公众号睿Talk,获取我最新的文章: 一、前言对于新手来说,没写过几次死循环的代码都不好意思说自己用过 React Hooks。本文将以useCallback为切入点,谈谈几个 hook 的使用场景,以及性能优化的一些思考。 这算是 Hooks 系列的第 3 篇,之前 2 篇的传送门:React Hooks 解析(上):基础React Hooks 解析(下):进阶 二、useCallback 使用场景先看一个最简单的例子: // 用于记录 getData 调用次数let count = 0;function App() { const [val, setVal] = useState(""); function getData() { setTimeout(()=>{ setVal('new data '+count); count++; }, 500) } useEffect(()=>{ getData(); }, []); return ( <div>{val}</div> );}getData模拟发起网络请求。在这种场景下,没有useCallback什么事,组件本身是高内聚的。 如果涉及到组件通讯,情况就不一样了: // 用于记录 getData 调用次数let count = 0;function App() { const [val, setVal] = useState(""); function getData() { setTimeout(() => { setVal("new data " + count); count++; }, 500); } return <Child val={val} getData={getData} />;}function Child({val, getData}) { useEffect(() => { getData(); }, [getData]); return <div>{val}</div>;}就这么轻轻松松,一个死循环就诞生了... ...

August 18, 2019 · 2 min · jiezi

react-hooks-使用

hooks 是react 16.8 引入的特性,他允许你在不写class的情况下操作state 和react的其他特性。hooks 只是多了一种写组件的方法,使编写一个组件更简单更方便,同时可以自定义hook把公共的逻辑提取出来,让逻辑在多个组件之间共享。 基本用法import React, { useState } from 'react'export default function () { const [count, setCount] = useState(0) return <div> <button onClick={() => { setCount(count + 1) }}>+</button> {count} <button onClick={() => { setCount(count - 1) }}>-</button> </div>}hook 使用起来非常的简单,上面我们就写使用 useState hook 的函数组件,useState函数返回了一对值一个是state的值另一个是更新state的函数,我们在点击按钮的时候调动更改state的函数,最终重新渲染ui hook 在class中不起作用Hook 是什么Hook 是什么? Hook 是一个特殊的函数,它可以让你“钩入” React 的特性。例如,useState 是允许你在 React 函数组件中添加 state 的 Hook。稍后我们将学习其他 Hook。 什么时候我会用 Hook? 如果你在编写函数组件并意识到需要向其添加一些 state,以前的做法是必须将其它转化为 class。现在你可以在现有的函数组件中使用 Hook。 ...

June 18, 2019 · 1 min · jiezi

React Hooks系列之useState

在React Hooks出现之前,组件添加state, 只能在class中完成。class方式React 16.7 alpha之后,可以在function组件中创建state了,不用再每次都需要创建一个class component,更加函数式了。useState方式不同场景下,应该如何使用useState场景1:隐藏/显示一个组件场景2:根据上一个state更新statesetSteps方法中第一个参数是prevState场景3:state是一个数组值场景4:state是一个对象下边是一个表单,表单中有username, password两个字段。展示了如何初始化表单数据,和更新对应的字段

April 14, 2019 · 1 min · jiezi

【React深入】从Mixin到HOC再到Hook

导读前端发展速度非常之快,页面和组件变得越来越复杂,如何更好的实现状态逻辑复用一直都是应用程序中重要的一部分,这直接关系着应用程序的质量以及维护的难易程度。本文介绍了React采用的三种实现状态逻辑复用的技术,并分析了他们的实现原理、使用方法、实际应用以及如何选择使用他们。本文略长,下面是本文的思维导图,您可以从头开始阅读,也可以选择感兴趣的部分阅读:Mixin设计模式Mixin(混入)是一种通过扩展收集功能的方式,它本质上是将一个对象的属性拷贝到另一个对象上面去,不过你可以拷贝任意多个对象的任意个方法到一个新对象上去,这是继承所不能实现的。它的出现主要就是为了解决代码复用问题。很多开源库提供了Mixin的实现,如Underscore的_.extend方法、JQuery的extend方法。使用_.extend方法实现代码复用:var LogMixin = { actionLog: function() { console.log(‘action…’); }, requestLog: function() { console.log(‘request…’); },};function User() { /../ }function Goods() { /../ }.extend(User.prototype, LogMixin);.extend(Goods.prototype, LogMixin);var user = new User();var good = new Goods();user.actionLog();good.requestLog();我们可以尝试手动写一个简单的Mixin方法:function setMixin(target, mixin) { if (arguments[2]) { for (var i = 2, len = arguments.length; i < len; i++) { target.prototype[arguments[i]] = mixin.prototype[arguments[i]]; } } else { for (var methodName in mixin.prototype) { if (!Object.hasOwnProperty(target.prototype, methodName)) { target.prototype[methodName] = mixin.prototype[methodName]; } } }}setMixin(User,LogMixin,‘actionLog’);setMixin(Goods,LogMixin,‘requestLog’);您可以使用setMixin方法将任意对象的任意方法扩展到目标对象上。React中应用MixinReact也提供了Mixin的实现,如果完全不同的组件有相似的功能,我们可以引入来实现代码复用,当然只有在使用createClass来创建React组件时才可以使用,因为在React组件的es6写法中它已经被废弃掉了。例如下面的例子,很多组件或页面都需要记录用户行为,性能指标等。如果我们在每个组件都引入写日志的逻辑,会产生大量重复代码,通过Mixin我们可以解决这一问题:var LogMixin = { log: function() { console.log(’log’); }, componentDidMount: function() { console.log(‘in’); }, componentWillUnmount: function() { console.log(‘out’); }};var User = React.createClass({ mixins: [LogMixin], render: function() { return (<div>…</div>) }});var Goods = React.createClass({ mixins: [LogMixin], render: function() { return (<div>…</div>) }});Mixin带来的危害React官方文档在Mixins Considered Harmful一文中提到了Mixin带来了危害:Mixin 可能会相互依赖,相互耦合,不利于代码维护不同的 Mixin 中的方法可能会相互冲突Mixin非常多时,组件是可以感知到的,甚至还要为其做相关处理,这样会给代码造成滚雪球式的复杂性React现在已经不再推荐使用Mixin来解决代码复用问题,因为Mixin带来的危害比他产生的价值还要巨大,并且React全面推荐使用高阶组件来替代它。另外,高阶组件还能实现更多其他更强大的功能,在学习高阶组件之前,我们先来看一个设计模式。装饰模式装饰者(decorator)模式能够在不改变对象自身的基础上,在程序运行期间给对像动态的添加职责。与继承相比,装饰者是一种更轻便灵活的做法。高阶组件(HOC)高阶组件可以看作React对装饰模式的一种实现,高阶组件就是一个函数,且该函数接受一个组件作为参数,并返回一个新的组件。高阶组件(HOC)是React中的高级技术,用来重用组件逻辑。但高阶组件本身并不是React API。它只是一种模式,这种模式是由React自身的组合性质必然产生的。function visible(WrappedComponent) { return class extends Component { render() { const { visible, …props } = this.props; if (visible === false) return null; return <WrappedComponent {…props} />; } }}上面的代码就是一个HOC的简单应用,函数接收一个组件作为参数,并返回一个新组件,新组建可以接收一个visible props,根据visible的值来判断是否渲染Visible。下面我们从以下几方面来具体探索HOC。HOC的实现方式属性代理函数返回一个我们自己定义的组件,然后在render中返回要包裹的组件,这样我们就可以代理所有传入的props,并且决定如何渲染,实际上 ,这种方式生成的高阶组件就是原组件的父组件,上面的函数visible就是一个HOC属性代理的实现方式。function proxyHOC(WrappedComponent) { return class extends Component { render() { return <WrappedComponent {…this.props} />; } }}对比原生组件增强的项:可操作所有传入的props可操作组件的生命周期可操作组件的static方法获取refs反向继承返回一个组件,继承原组件,在render中调用原组件的render。由于继承了原组件,能通过this访问到原组件的生命周期、props、state、render等,相比属性代理它能操作更多的属性。function inheritHOC(WrappedComponent) { return class extends WrappedComponent { render() { return super.render(); } }}对比原生组件增强的项:可操作所有传入的props可操作组件的生命周期可操作组件的static方法获取refs可操作state 可以渲染劫持HOC可以实现什么功能组合渲染可使用任何其他组件和原组件进行组合渲染,达到样式、布局复用等效果。通过属性代理实现function stylHOC(WrappedComponent) { return class extends Component { render() { return (<div> <div className=“title”>{this.props.title}</div> <WrappedComponent {…this.props} /> </div>); } }}通过反向继承实现function styleHOC(WrappedComponent) { return class extends WrappedComponent { render() { return <div> <div className=“title”>{this.props.title}</div> {super.render()} </div> } }}条件渲染根据特定的属性决定原组件是否渲染通过属性代理实现function visibleHOC(WrappedComponent) { return class extends Component { render() { if (this.props.visible === false) return null; return <WrappedComponent {…props} />; } }}通过反向继承实现function visibleHOC(WrappedComponent) { return class extends WrappedComponent { render() { if (this.props.visible === false) { return null } else { return super.render() } } }}操作props可以对传入组件的props进行增加、修改、删除或者根据特定的props进行特殊的操作。通过属性代理实现function proxyHOC(WrappedComponent) { return class extends Component { render() { const newProps = { …this.props, user: ‘ConardLi’ } return <WrappedComponent {…newProps} />; } }}获取refs高阶组件中可获取原组件的ref,通过ref获取组件实力,如下面的代码,当程序初始化完成后调用原组件的log方法。(不知道refs怎么用,请????Refs & DOM)通过属性代理实现function refHOC(WrappedComponent) { return class extends Component { componentDidMount() { this.wapperRef.log() } render() { return <WrappedComponent {…this.props} ref={ref => { this.wapperRef = ref }} />; } }}这里注意:调用高阶组件的时候并不能获取到原组件的真实ref,需要手动进行传递,具体请看传递refs状态管理将原组件的状态提取到HOC中进行管理,如下面的代码,我们将Input的value提取到HOC中进行管理,使它变成受控组件,同时不影响它使用onChange方法进行一些其他操作。基于这种方式,我们可以实现一个简单的双向绑定,具体请看双向绑定。通过属性代理实现function proxyHoc(WrappedComponent) { return class extends Component { constructor(props) { super(props); this.state = { value: ’’ }; } onChange = (event) => { const { onChange } = this.props; this.setState({ value: event.target.value, }, () => { if(typeof onChange ===‘function’){ onChange(event); } }) } render() { const newProps = { value: this.state.value, onChange: this.onChange, } return <WrappedComponent {…this.props} {…newProps} />; } }}class HOC extends Component { render() { return <input {…this.props}></input> }}export default proxyHoc(HOC);操作state上面的例子通过属性代理利用HOC的state对原组件进行了一定的增强,但并不能直接控制原组件的state,而通过反向继承,我们可以直接操作原组件的state。但是并不推荐直接修改或添加原组件的state,因为这样有可能和组件内部的操作构成冲突。通过反向继承实现function debugHOC(WrappedComponent) { return class extends WrappedComponent { render() { console.log(‘props’, this.props); console.log(‘state’, this.state); return ( <div className=“debuging”> {super.render()} </div> ) } }}上面的HOC在render中将props和state打印出来,可以用作调试阶段,当然你可以在里面写更多的调试代码。想象一下,只需要在我们想要调试的组件上加上@debug就可以对该组件进行调试,而不需要在每次调试的时候写很多冗余代码。(如果你还不知道怎么使用HOC,请????如何使用HOC)渲染劫持高阶组件可以在render函数中做非常多的操作,从而控制原组件的渲染输出。只要改变了原组件的渲染,我们都将它称之为一种渲染劫持。实际上,上面的组合渲染和条件渲染都是渲染劫持的一种,通过反向继承,不仅可以实现以上两点,还可直接增强由原组件render函数产生的React元素。通过反向继承实现function hijackHOC(WrappedComponent) { return class extends WrappedComponent { render() { const tree = super.render(); let newProps = {}; if (tree && tree.type === ‘input’) { newProps = { value: ‘渲染被劫持了’ }; } const props = Object.assign({}, tree.props, newProps); const newTree = React.cloneElement(tree, props, tree.props.children); return newTree; } }}注意上面的说明我用的是增强而不是更改。render函数内实际上是调用React.creatElement产生的React元素:虽然我们能拿到它,但是我们不能直接修改它里面的属性,我们通过getOwnPropertyDescriptors函数来打印下它的配置项:可以发现,所有的writable属性均被配置为了false,即所有属性是不可变的。(对这些配置项有疑问,请????defineProperty)不能直接修改,我们可以借助cloneElement方法来在原组件的基础上增强一个新组件: React.cloneElement()克隆并返回一个新的React元素,使用 element 作为起点。生成的元素将会拥有原始元素props与新props的浅合并。新的子级会替换现有的子级。来自原始元素的 key 和 ref 将会保留。React.cloneElement() 几乎相当于:<element.type {…element.props} {…props}>{children}</element.type>如何使用HOC上面的示例代码都写的是如何声明一个HOC,HOC实际上是一个函数,所以我们将要增强的组件作为参数调用HOC函数,得到增强后的组件。class myComponent extends Component { render() { return (<span>原组件</span>) }}export default inheritHOC(myComponent);compose在实际应用中,一个组件可能被多个HOC增强,我们使用的是被所有的HOC增强后的组件,借用一张装饰模式的图来说明,可能更容易理解:假设现在我们有logger,visible,style等多个HOC,现在要同时增强一个Input组件:logger(visible(style(Input)))这种代码非常的难以阅读,我们可以手动封装一个简单的函数组合工具,将写法改写如下:const compose = (…fns) => fns.reduce((f, g) => (…args) => g(f(…args)));compose(logger,visible,style)(Input);compose函数返回一个所有函数组合后的函数,compose(f, g, h) 和 (…args) => f(g(h(…args)))是一样的。很多第三方库都提供了类似compose的函数,例如lodash.flowRight,Redux提供的combineReducers函数等。Decorators我们还可以借助ES7为我们提供的Decorators来让我们的写法变的更加优雅:@logger@visible@styleclass Input extends Component { // …}Decorators是ES7的一个提案,还没有被标准化,但目前Babel转码器已经支持,我们需要提前配置babel-plugin-transform-decorators-legacy:“plugins”: [“transform-decorators-legacy”]还可以结合上面的compose函数使用:const hoc = compose(logger, visible, style);@hocclass Input extends Component { // …}HOC的实际应用下面是一些我在生产环境中实际对HOC的实际应用场景,由于文章篇幅原因,代码经过很多简化,如有问题欢迎在评论区指出:日志打点实际上这属于一类最常见的应用,多个组件拥有类似的逻辑,我们要对重复的逻辑进行复用,官方文档中CommentList的示例也是解决了代码复用问题,写的很详细,有兴趣可以????使用高阶组件(HOC)解决横切关注点。某些页面需要记录用户行为,性能指标等等,通过高阶组件做这些事情可以省去很多重复代码。function logHoc(WrappedComponent) { return class extends Component { componentWillMount() { this.start = Date.now(); } componentDidMount() { this.end = Date.now(); console.log(${WrappedComponent.dispalyName} 渲染时间:${this.end - this.start} ms); console.log(${user}进入${WrappedComponent.dispalyName}); } componentWillUnmount() { console.log(${user}退出${WrappedComponent.dispalyName}); } render() { return <WrappedComponent {…this.props} /> } }}可用、权限控制function auth(WrappedComponent) { return class extends Component { render() { const { visible, auth, display = null, …props } = this.props; if (visible === false || (auth && authList.indexOf(auth) === -1)) { return display } return <WrappedComponent {…props} />; } }}authList是我们在进入程序时向后端请求的所有权限列表,当组件所需要的权限不列表中,或者设置的visible是false,我们将其显示为传入的组件样式,或者null。我们可以将任何需要进行权限校验的组件应用HOC: @auth class Input extends Component { … } @auth class Button extends Component { … } <Button auth=“user/addUser”>添加用户</Button> <Input auth=“user/search” visible={false} >添加用户</Input>双向绑定在vue中,绑定一个变量后可实现双向数据绑定,即表单中的值改变后绑定的变量也会自动改变。而React中没有做这样的处理,在默认情况下,表单元素都是非受控组件。给表单元素绑定一个状态后,往往需要手动书写onChange方法来将其改写为受控组件,在表单元素非常多的情况下这些重复操作是非常痛苦的。我们可以借助高阶组件来实现一个简单的双向绑定,代码略长,可以结合下面的思维导图进行理解。首先我们自定义一个Form组件,该组件用于包裹所有需要包裹的表单组件,通过contex向子组件暴露两个属性:model:当前Form管控的所有数据,由表单name和value组成,如{name:‘ConardLi’,pwd:‘123’}。model可由外部传入,也可自行管控。changeModel:改变model中某个name的值。class Form extends Component { static childContextTypes = { model: PropTypes.object, changeModel: PropTypes.func } constructor(props, context) { super(props, context); this.state = { model: props.model || {} }; } componentWillReceiveProps(nextProps) { if (nextProps.model) { this.setState({ model: nextProps.model }) } } changeModel = (name, value) => { this.setState({ model: { …this.state.model, [name]: value } }) } getChildContext() { return { changeModel: this.changeModel, model: this.props.model || this.state.model }; } onSubmit = () => { console.log(this.state.model); } render() { return <div> {this.props.children} <button onClick={this.onSubmit}>提交</button> </div> }}下面定义用于双向绑定的HOC,其代理了表单的onChange属性和value属性:发生onChange事件时调用上层Form的changeModel方法来改变context中的model。在渲染时将value改为从context中取出的值。function proxyHoc(WrappedComponent) { return class extends Component { static contextTypes = { model: PropTypes.object, changeModel: PropTypes.func } onChange = (event) => { const { changeModel } = this.context; const { onChange } = this.props; const { v_model } = this.props; changeModel(v_model, event.target.value); if(typeof onChange === ‘function’){onChange(event);} } render() { const { model } = this.context; const { v_model } = this.props; return <WrappedComponent {…this.props} value={model[v_model]} onChange={this.onChange} />; } }}@proxyHocclass Input extends Component { render() { return <input {…this.props}></input> }}上面的代码只是简略的一部分,除了input,我们还可以将HOC应用在select等其他表单组件,甚至还可以将上面的HOC兼容到span、table等展示组件,这样做可以大大简化代码,让我们省去了很多状态管理的工作,使用如下:export default class extends Component { render() { return ( <Form > <Input v_model=“name”></Input> <Input v_model=“pwd”></Input> </Form> ) }}表单校验基于上面的双向绑定的例子,我们再来一个表单验证器,表单验证器可以包含验证函数以及提示信息,当验证不通过时,展示错误信息:function validateHoc(WrappedComponent) { return class extends Component { constructor(props) { super(props); this.state = { error: ’’ } } onChange = (event) => { const { validator } = this.props; if (validator && typeof validator.func === ‘function’) { if (!validator.func(event.target.value)) { this.setState({ error: validator.msg }) } else { this.setState({ error: ’’ }) } } } render() { return <div> <WrappedComponent onChange={this.onChange} {…this.props} /> <div>{this.state.error || ‘’}</div> </div> } }}const validatorName = { func: (val) => val && !isNaN(val), msg: ‘请输入数字’}const validatorPwd = { func: (val) => val && val.length > 6, msg: ‘密码必须大于6位’}<HOCInput validator={validatorName} v_model=“name”></HOCInput><HOCInput validator={validatorPwd} v_model=“pwd”></HOCInput>当然,还可以在Form提交的时候判断所有验证器是否通过,验证器也可以设置为数组等等,由于文章篇幅原因,代码被简化了很多,有兴趣的同学可以自己实现。Redux的connectredux中的connect,其实就是一个HOC,下面就是一个简化版的connect实现:export const connect = (mapStateToProps, mapDispatchToProps) => (WrappedComponent) => { class Connect extends Component { static contextTypes = { store: PropTypes.object } constructor () { super() this.state = { allProps: {} } } componentWillMount () { const { store } = this.context this._updateProps() store.subscribe(() => this._updateProps()) } _updateProps () { const { store } = this.context let stateProps = mapStateToProps ? mapStateToProps(store.getState(), this.props): {} let dispatchProps = mapDispatchToProps? mapDispatchToProps(store.dispatch, this.props) : {} this.setState({ allProps: { …stateProps, …dispatchProps, …this.props } }) } render () { return <WrappedComponent {…this.state.allProps} /> } } return Connect}代码非常清晰,connect函数其实就做了一件事,将mapStateToProps和mapDispatchToProps分别解构后传给原组件,这样我们在原组件内就可以直接用props获取state以及dispatch函数了。使用HOC的注意事项告诫—静态属性拷贝当我们应用HOC去增强另一个组件时,我们实际使用的组件已经不是原组件了,所以我们拿不到原组件的任何静态属性,我们可以在HOC的结尾手动拷贝他们:function proxyHOC(WrappedComponent) { class HOCComponent extends Component { render() { return <WrappedComponent {…this.props} />; } } HOCComponent.staticMethod = WrappedComponent.staticMethod; // … return HOCComponent;}如果原组件有非常多的静态属性,这个过程是非常痛苦的,而且你需要去了解需要增强的所有组件的静态属性是什么,我们可以使用hoist-non-react-statics来帮助我们解决这个问题,它可以自动帮我们拷贝所有非React的静态方法,使用方式如下:import hoistNonReactStatic from ‘hoist-non-react-statics’;function proxyHOC(WrappedComponent) { class HOCComponent extends Component { render() { return <WrappedComponent {…this.props} />; } } hoistNonReactStatic(HOCComponent,WrappedComponent); return HOCComponent;}告诫—传递refs使用高阶组件后,获取到的ref实际上是最外层的容器组件,而非原组件,但是很多情况下我们需要用到原组件的ref。高阶组件并不能像透传props那样将refs透传,我们可以用一个回调函数来完成ref的传递:function hoc(WrappedComponent) { return class extends Component { getWrappedRef = () => this.wrappedRef; render() { return <WrappedComponent ref={ref => { this.wrappedRef = ref }} {…this.props} />; } }}@hocclass Input extends Component { render() { return <input></input> }}class App extends Component { render() { return ( <Input ref={ref => { this.inpitRef = ref.getWrappedRef() }} ></Input> ); }}React 16.3版本提供了一个forwardRef API来帮助我们进行refs传递,这样我们在高阶组件上获取的ref就是原组件的ref了,而不需要再手动传递,如果你的React版本大于16.3,可以使用下面的方式:function hoc(WrappedComponent) { class HOC extends Component { render() { const { forwardedRef, …props } = this.props; return <WrappedComponent ref={forwardedRef} {…props} />; } } return React.forwardRef((props, ref) => { return <HOC forwardedRef={ref} {…props} />; });}告诫—不要在render方法内使用高阶组件React Diff算法的原则是:使用组件标识确定是卸载还是更新组件如果组件的和前一次渲染时标识是相同的,递归更新子组件如果标识不同卸载组件重新挂载新组件每次调用高阶组件生成的都是是一个全新的组件,组件的唯一标识响应的也会改变,如果在render方法调用了高阶组件,这会导致组件每次都会被卸载后重新挂载。约定-不要改变原始组件官方文档对高阶组件的说明:高阶组件就是一个没有副作用的纯函数。我们再来看看纯函数的定义:如果函数的调用参数相同,则永远返回相同的结果。它不依赖于程序执行期间函数外部任何状态或数据的变化,必须只依赖于其输入参数。该函数不会产生任何可观察的副作用,例如网络请求,输入和输出设备或数据突变。如果我们在高阶组件对原组件进行了修改,例如下面的代码:InputComponent.prototype.componentWillReceiveProps = function(nextProps) { … }这样就破坏了我们对高阶组件的约定,同时也改变了使用高阶组件的初衷:我们使用高阶组件是为了增强而非改变原组件。约定-透传不相关的props使用高阶组件,我们可以代理所有的props,但往往特定的HOC只会用到其中的一个或几个props。我们需要把其他不相关的props透传给原组件,如下面的代码:function visible(WrappedComponent) { return class extends Component { render() { const { visible, …props } = this.props; if (visible === false) return null; return <WrappedComponent {…props} />; } }}我们只使用visible属性来控制组件的显示可隐藏,把其他props透传下去。约定-displayName在使用React Developer Tools进行调试时,如果我们使用了HOC,调试界面可能变得非常难以阅读,如下面的代码:@visibleclass Show extends Component { render() { return <h1>我是一个标签</h1> }}@visibleclass Title extends Component { render() { return <h1>我是一个标题</h1> }}为了方便调试,我们可以手动为HOC指定一个displayName,官方推荐使用HOCName(WrappedComponentName):static displayName = Visible(${WrappedComponent.displayName})这个约定帮助确保高阶组件最大程度的灵活性和可重用性。使用HOC的动机回顾下上文提到的 Mixin 带来的风险:Mixin 可能会相互依赖,相互耦合,不利于代码维护不同的 Mixin 中的方法可能会相互冲突Mixin非常多时,组件是可以感知到的,甚至还要为其做相关处理,这样会给代码造成滚雪球式的复杂性而HOC的出现可以解决这些问题:高阶组件就是一个没有副作用的纯函数,各个高阶组件不会互相依赖耦合高阶组件也有可能造成冲突,但我们可以在遵守约定的情况下避免这些行为高阶组件并不关心数据使用的方式和原因,而被包裹的组件也不关心数据来自何处。高阶组件的增加不会为原组件增加负担HOC的缺陷HOC需要在原组件上进行包裹或者嵌套,如果大量使用HOC,将会产生非常多的嵌套,这让调试变得非常困难。HOC可以劫持props,在不遵守约定的情况下也可能造成冲突。HooksHooks是React v16.7.0-alpha中加入的新特性。它可以让你在class以外使用state和其他React特性。使用Hooks,你可以在将含有state的逻辑从组件中抽象出来,这将可以让这些逻辑容易被测试。同时,Hooks可以帮助你在不重写组件结构的情况下复用这些逻辑。所以,它也可以作为一种实现状态逻辑复用的方案。阅读下面的章节使用Hook的动机你可以发现,它可以同时解决Mixin和HOC带来的问题。官方提供的HooksState Hook我们要使用class组件实现一个计数器功能,我们可能会这样写:export default class Count extends Component { constructor(props) { super(props); this.state = { count: 0 } } render() { return ( <div> <p>You clicked {this.state.count} times</p> <button onClick={() => { this.setState({ count: this.state.count + 1 }) }}> Click me </button> </div> ) }}通过useState,我们使用函数式组件也能实现这样的功能:export default function HookTest() { const [count, setCount] = useState(0); return ( <div> <p>You clicked {count} times</p> <button onClick={() => { setCount(count + 1); setNumber(number + 1); }}> Click me </button> </div> );}useState是一个钩子,他可以为函数式组件增加一些状态,并且提供改变这些状态的函数,同时它接收一个参数,这个参数作为状态的默认值。Effect HookEffect Hook 可以让你在函数组件中执行一些具有 side effect(副作用)的操作参数useEffect方法接收传入两个参数:1.回调函数:在第组件一次render和之后的每次update后运行,React保证在DOM已经更新完成之后才会运行回调。2.状态依赖(数组):当配置了状态依赖项后,只有检测到配置的状态变化时,才会调用回调函数。 useEffect(() => { // 只要组件render后就会执行 }); useEffect(() => { // 只有count改变时才会执行 },[count]);回调返回值useEffect的第一个参数可以返回一个函数,当页面渲染了下一次更新的结果后,执行下一次useEffect之前,会调用这个函数。这个函数常常用来对上一次调用useEffect进行清理。export default function HookTest() { const [count, setCount] = useState(0); useEffect(() => { console.log(‘执行…’, count); return () => { console.log(‘清理…’, count); } }, [count]); return ( <div> <p>You clicked {count} times</p> <button onClick={() => { setCount(count + 1); setNumber(number + 1); }}> Click me </button> </div> );}执行上面的代码,并点击几次按钮,会得到下面的结果:注意,如果加上浏览器渲染的情况,结果应该是这样的: 页面渲染…1 执行… 1 页面渲染…2 清理… 1 执行… 2 页面渲染…3 清理… 2 执行… 3 页面渲染…4 清理… 3 执行… 4那么为什么在浏览器渲染完后,再执行清理的方法还能找到上次的state呢?原因很简单,我们在useEffect中返回的是一个函数,这形成了一个闭包,这能保证我们上一次执行函数存储的变量不被销毁和污染。你可以尝试下面的代码可能更好理解 var flag = 1; var clean; function effect(flag) { return function () { console.log(flag); } } clean = effect(flag); flag = 2; clean(); clean = effect(flag); flag = 3; clean(); clean = effect(flag); // 执行结果 effect… 1 clean… 1 effect… 2 clean… 2 effect… 3模拟componentDidMountcomponentDidMount等价于useEffect的回调仅在页面初始化完成后执行一次,当useEffect的第二个参数传入一个空数组时可以实现这个效果。function useDidMount(callback) { useEffect(callback, []);}官方不推荐上面这种写法,因为这有可能导致一些错误。模拟componentWillUnmountfunction useUnMount(callback) { useEffect(() => callback, []);}不像 componentDidMount 或者 componentDidUpdate,useEffect 中使用的 effect 并不会阻滞浏览器渲染页面。这让你的 app 看起来更加流畅。ref Hook使用useRef Hook,你可以轻松的获取到dom的ref。export default function Input() { const inputEl = useRef(null); const onButtonClick = () => { inputEl.current.focus(); }; return ( <div> <input ref={inputEl} type=“text” /> <button onClick={onButtonClick}>Focus the input</button> </div> );}注意useRef()并不仅仅可以用来当作获取ref使用,使用useRef产生的ref的current属性是可变的,这意味着你可以用它来保存一个任意值。模拟componentDidUpdatecomponentDidUpdate就相当于除去第一次调用的useEffect,我们可以借助useRef生成一个标识,来记录是否为第一次执行:function useDidUpdate(callback, prop) { const init = useRef(true); useEffect(() => { if (init.current) { init.current = false; } else { return callback(); } }, prop);}使用Hook的注意事项使用范围只能在React函数式组件或自定义Hook中使用Hook。Hook的提出主要就是为了解决class组件的一系列问题,所以我们能在class组件中使用它。声明约束不要在循环,条件或嵌套函数中调用Hook。Hook通过数组实现的,每次 useState 都会改变下标,React需要利用调用顺序来正确更新相应的状态,如果 useState 被包裹循环或条件语句中,那每就可能会引起调用顺序的错乱,从而造成意想不到的错误。我们可以安装一个eslint插件来帮助我们避免这些问题。// 安装npm install eslint-plugin-react-hooks –save-dev// 配置{ “plugins”: [ // … “react-hooks” ], “rules”: { // … “react-hooks/rules-of-hooks”: “error” }}自定义Hook像上面介绍的HOC和mixin一样,我们同样可以通过自定义的Hook将组件中类似的状态逻辑抽取出来。自定义Hook非常简单,我们只需要定义一个函数,并且把相应需要的状态和effect封装进去,同时,Hook之间也是可以相互引用的。使用use开头命名自定义Hook,这样可以方便eslint进行检查。下面我们看几个具体的Hook封装:日志打点我们可以使用上面封装的生命周期Hook。const useLogger = (componentName, …params) => { useDidMount(() => { console.log(${componentName}初始化, …params); }); useUnMount(() => { console.log(${componentName}卸载, …params); }) useDidUpdate(() => { console.log(${componentName}更新, …params); });};function Page1(props){ useLogger(‘Page1’,props); return (<div>…</div>)}修改title根据不同的页面名称修改页面title:function useTitle(title) { useEffect( () => { document.title = title; return () => (document.title = “主页”); }, [title] );}function Page1(props){ useTitle(‘Page1’); return (<div>…</div>)}双向绑定我们将表单onChange的逻辑抽取出来封装成一个Hook,这样所有需要进行双向绑定的表单组件都可以进行复用:function useBind(init) { let [value, setValue] = useState(init); let onChange = useCallback(function(event) { setValue(event.currentTarget.value); }, []); return { value, onChange };}function Page1(props){ let value = useBind(’’); return <input {…value} />;}当然,你可以向上面的HOC那样,结合context和form来封装一个更通用的双向绑定,有兴趣可以手动实现一下。使用Hook的动机减少状态逻辑复用的风险Hook和Mixin在用法上有一定的相似之处,但是Mixin引入的逻辑和状态是可以相互覆盖的,而多个Hook之间互不影响,这让我们不需要在把一部分精力放在防止避免逻辑复用的冲突上。在不遵守约定的情况下使用HOC也有可能带来一定冲突,比如props覆盖等等,使用Hook则可以避免这些问题。避免地狱式嵌套大量使用HOC的情况下让我们的代码变得嵌套层级非常深,使用HOC,我们可以实现扁平式的状态逻辑复用,而避免了大量的组件嵌套。让组件更容易理解在使用class组件构建我们的程序时,他们各自拥有自己的状态,业务逻辑的复杂使这些组件变得越来越庞大,各个生命周期中会调用越来越多的逻辑,越来越难以维护。使用Hook,可以让你更大限度的将公用逻辑抽离,将一个组件分割成更小的函数,而不是强制基于生命周期方法进行分割。使用函数代替class相比函数,编写一个class可能需要掌握更多的知识,需要注意的点也越多,比如this指向、绑定事件等等。另外,计算机理解一个class比理解一个函数更快。Hooks让你可以在classes之外使用更多React的新特性。理性的选择实际上,Hook在react 16.8.0才正式发布Hook稳定版本,笔者也还未在生产环境下使用,目前笔者在生产环境下使用的最多的是HOC。React官方完全没有把classes从React中移除的打算,class组件和Hook完全可以同时存在,官方也建议避免任何“大范围重构”,毕竟这是一个非常新的版本,如果你喜欢它,可以在新的非关键性的代码中使用Hook。小结mixin已被抛弃,HOC正当壮年,Hook初露锋芒,前端圈就是这样,技术迭代速度非常之快,但我们在学习这些知识之时一定要明白为什么要学,学了有没有用,要不要用。不忘初心,方得始终。文中如有错误,欢迎在评论区指正,谢谢阅读。推荐阅读【React深入】React事件机制【React深入】setState的执行机制 ...

April 10, 2019 · 8 min · jiezi

【译】如何在React Hooks中获取数据?

原文链接: https://www.robinwieruch.de/r…在本教程中,我想通过state和effect hook来像你展示如何用React Hooks来获取数据。我将会使用Hacker News的API来获取热门的技术文章。你将会实现一个属于你自己的自定义hook来在你程序的任何地方复用,或者是作为一个npm包发布出来。如果你还不知道这个React的新特性,那么点击React Hooks介绍,如果你想直接查看最后的实现效果,请点击这个github仓库。注意:在未来,React Hooks将不会用于React的数据获取,一个叫做Suspense的特性将会去负责它。但下面的教程仍会让你去更多的了解关于React中的state和effect hook。用React Hooks去获取数据如果你对在React中获取数据还不熟悉,可以查看我其他的React获取数据的文章。它将会引导你通过使用React的class组件来获取数据,并且还可以和render props或者高阶组件一起使用,以及结合错误处理和加载状态。在这篇文章中,我将会在function组件中使用React Hooks来展示这些功能。import React, { useState } from ‘react’;function App() { const [data, setData] = useState({ hits: [] }); return ( <ul> {data.hits.map(item => ( <li key={item.objectID}> <a href={item.url}>{item.title}</a> </li> ))} </ul> );}export default App;这个App组件展示了一个包含很多项的list(hits = Hacker News 文章)。state和state的更新函数来自于state hook中useState的调用,它负责管理我们用来渲染list数据的本地状态,初始状态是一个空数组,此时还没有为其设置任何的状态。我们将使用axios来获取数据,当然你也可以使用其他的库或者fetch API,如果你还没安装axios,你可以在命令行使用npm install axios来安装它。然后来实现用于数据获取的effect hook:import React, { useState, useEffect } from ‘react’;import axios from ‘axios’;function App() { const [data, setData] = useState({ hits: [] }); useEffect(async () => { const result = await axios( ‘http://hn.algolia.com/api/v1/search?query=redux', ); setData(result.data); }); return ( <ul> {data.hits.map(item => ( <li key={item.objectID}> <a href={item.url}>{item.title}</a> </li> ))} </ul> );}export default App;通过axios在useEffect中获取数据,然后通过setData将数据放到组件本地的state中,并通过async/await来处理Promise。然而当你运行程序的时候,你应该会遇到一个讨厌的循环。effect hook不仅在组件mount的时候也会在update的时候运行。因为我们在每一次的数据获取之后,会去通过setState设置状态,这时候组件update然后effect就会运行一遍,这就造成了数据一次又一次的获取。我们仅仅是想要在组件mount的时候来获取一次数据,这就是为什么我们需要在useEffect的第二个参数提供一个空数组,从而实现只在mount的时候触发数据获取而不是每一次update。import React, { useState, useEffect } from ‘react’;import axios from ‘axios’;function App() { const [data, setData] = useState({ hits: [] }); useEffect(async () => { const result = await axios( ‘http://hn.algolia.com/api/v1/search?query=redux', ); setData(result.data); }, []); return ( <ul> {data.hits.map(item => ( <li key={item.objectID}> <a href={item.url}>{item.title}</a> </li> ))} </ul> );}export default App;第二个参数可以定义hooks所依赖的变量(在一个数组中去分配),如果一个变量改变了,hooks将会执行一次,如果是一个空数组的话,hooks将不会在组件更新的时候执行,因为它没有监听到任何的变量。这里还有一个陷阱,在代码中,我们使用async/await从第三方的API中获取数据,根据文档,每一个async函数都将返回一个promise,async函数声明定义了一个异步函数,它返回一个asyncFunction对象,异步函数是通过事件循环异步操作的函数,使用隐式Promise返回其结果。但是,effect hook应该不返回任何内容或清除功能,这就是为什么你会在控制台看到以下警告:07:41:22.910 index.js:1452 Warning: useEffect function must return a cleanup function or nothing. Promises and useEffect(async () => …) are not supported, but you can call an async function inside an effect.. 这就是为什么不允许在useEffect函数中直接使用async的原因。让我们通过在effect内部使用异步函数来实现它的解决方案。import React, { useState, useEffect } from ‘react’;import axios from ‘axios’;function App() { const [data, setData] = useState({ hits: [] }); useEffect(() => { const fetchData = async () => { const result = await axios( ‘http://hn.algolia.com/api/v1/search?query=redux', ); setData(result.data); }; fetchData(); }, []); return ( <ul> {data.hits.map(item => ( <li key={item.objectID}> <a href={item.url}>{item.title}</a> </li> ))} </ul> );}export default App;简而言之,这就是用React Hooks获取数据。但是,如果你对错误处理、加载提示、如何从表单中触发数据获取以及如何实现可重用的数据获取hook感兴趣,请继续阅读。如何通过编程方式/手动方式触发hook?好的,我们在mount后获取了一次数据,但是,如果使用input的字段来告诉API哪一个话题是我们感兴趣的呢?“Redux”可以作为我们的默认查询,如果是关于“React”的呢?让我们实现一个input元素,使某人能够获取“Redux”以外的话题。因此,为input元素引入一个新的状态。import React, { Fragment, useState, useEffect } from ‘react’;import axios from ‘axios’;function App() { const [data, setData] = useState({ hits: [] }); const [query, setQuery] = useState(‘redux’); useEffect(() => { const fetchData = async () => { const result = await axios( ‘http://hn.algolia.com/api/v1/search?query=redux', ); setData(result.data); }; fetchData(); }, []); return ( <Fragment> <input type=“text” value={query} onChange={event => setQuery(event.target.value)} /> <ul> {data.hits.map(item => ( <li key={item.objectID}> <a href={item.url}>{item.title}</a> </li> ))} </ul> </Fragment> );}export default App;目前,这两个状态彼此独立,但现在希望将它们耦合起来,以获取由input中的输入来查询指定的项目。通过下面的更改,组件应该在挂载之后通过查询词获取所有数据。function App() { const [data, setData] = useState({ hits: [] }); const [query, setQuery] = useState(‘redux’); useEffect(() => { const fetchData = async () => { const result = await axios( http://hn.algolia.com/api/v1/search?query=${query}, ); setData(result.data); }; fetchData(); }, []); return ( … );}export default App;还差一部分:当你尝试在input中输入一些内容时,在mount之后就不会再获取任何数据了,这是因为我们提供了空数组作为第二个参数,effect没有依赖任何变量,因此只会在mount的时候触发,但是现在的effect应该依赖query,每当query改变的时候,就应该触发数据的获取。function App() { const [data, setData] = useState({ hits: [] }); const [query, setQuery] = useState(‘redux’); useEffect(() => { const fetchData = async () => { const result = await axios( http://hn.algolia.com/api/v1/search?query=${query}, ); setData(result.data); }; fetchData(); }, [query]); return ( … );}export default App;现在每当input的值更新的时候就可以重新获取数据了。但这又导致了另一个问题:对于input中键入的每个字符,都会触发该效果,并执行一个数据提取请求。如何提供一个按钮来触发请求,从而手动hook呢?function App() { const [data, setData] = useState({ hits: [] }); const [query, setQuery] = useState(‘redux’); const [search, setSearch] = useState(’’); useEffect(() => { const fetchData = async () => { const result = await axios( http://hn.algolia.com/api/v1/search?query=${query}, ); setData(result.data); }; fetchData(); }, [query]); return ( <Fragment> <input type=“text” value={query} onChange={event => setQuery(event.target.value)} /> <button type=“button” onClick={() => setSearch(query)}> Search </button> <ul> {data.hits.map(item => ( <li key={item.objectID}> <a href={item.url}>{item.title}</a> </li> ))} </ul> </Fragment> );}现在,effect依赖于于search,而不是随输入字段中变化的query。一旦用户点击按钮,新的search就会被设置,并且应该手动触发effect hook。function App() { const [data, setData] = useState({ hits: [] }); const [query, setQuery] = useState(‘redux’); const [search, setSearch] = useState(‘redux’); useEffect(() => { const fetchData = async () => { const result = await axios( http://hn.algolia.com/api/v1/search?query=${search}, ); setData(result.data); }; fetchData(); }, [search]); return ( … );}export default App;此外,search的初始值也设置为与query相同,因为组件也在mount时获取数据,因此结果应反映输入字段中的值。但是,具有类似的query和search状态有点令人困惑。为什么不将实际的URL设置为状态而来代替search?function App() { const [data, setData] = useState({ hits: [] }); const [query, setQuery] = useState(‘redux’); const [url, setUrl] = useState( ‘http://hn.algolia.com/api/v1/search?query=redux', ); useEffect(() => { const fetchData = async () => { const result = await axios(url); setData(result.data); }; fetchData(); }, [url]); return ( <Fragment> <input type=“text” value={query} onChange={event => setQuery(event.target.value)} /> <button type=“button” onClick={() => setUrl(http://hn.algolia.com/api/v1/search?query=${query}) } > Search </button> <ul> {data.hits.map(item => ( <li key={item.objectID}> <a href={item.url}>{item.title}</a> </li> ))} </ul> </Fragment> );}这就是使用effect hook获取隐式编程数据的情况。你可以决定effect依赖于哪个状态。一旦在点击或其他effect中设置此状态,此effect将再次运行。在这种情况下,如果URL状态发生变化,effect将再次运行以从API获取数据。React Hooks和loading让我们为数据获取引入一个加载提示。它只是另一个由state hook管理的状态。loading被用于在组件中渲染一个loading提示。import React, { Fragment, useState, useEffect } from ‘react’;import axios from ‘axios’;function App() { const [data, setData] = useState({ hits: [] }); const [query, setQuery] = useState(‘redux’); const [url, setUrl] = useState( ‘http://hn.algolia.com/api/v1/search?query=redux', ); const [isLoading, setIsLoading] = useState(false); useEffect(() => { const fetchData = async () => { setIsLoading(true); const result = await axios(url); setData(result.data); setIsLoading(false); }; fetchData(); }, [url]); return ( <Fragment> <input type=“text” value={query} onChange={event => setQuery(event.target.value)} /> <button type=“button” onClick={() => setUrl(http://hn.algolia.com/api/v1/search?query=${query}) } > Search </button> {isLoading ? ( <div>Loading …</div> ) : ( <ul> {data.hits.map(item => ( <li key={item.objectID}> <a href={item.url}>{item.title}</a> </li> ))} </ul> )} </Fragment> );}export default App;一旦调用该effect进行数据获取(当组件mount或URL状态更改时发生),加载状态将设置为true。一旦请求完成,加载状态将再次设置为false。React Hooks和错误处理如果在React Hooks中加上错误处理呢,错误只是用state hook初始化的另一个状态。一旦出现错误状态,应用程序组件就可以为用户提供反馈。使用async/await时,通常使用try/catch块进行错误处理。你可以在effect内做到:import React, { Fragment, useState, useEffect } from ‘react’;import axios from ‘axios’;function App() { const [data, setData] = useState({ hits: [] }); const [query, setQuery] = useState(‘redux’); const [url, setUrl] = useState( ‘http://hn.algolia.com/api/v1/search?query=redux', ); const [isLoading, setIsLoading] = useState(false); const [isError, setIsError] = useState(false); useEffect(() => { const fetchData = async () => { setIsError(false); setIsLoading(true); try { const result = await axios(url); setData(result.data); } catch (error) { setIsError(true); } setIsLoading(false); }; fetchData(); }, [url]); return ( <Fragment> <input type=“text” value={query} onChange={event => setQuery(event.target.value)} /> <button type=“button” onClick={() => setUrl(http://hn.algolia.com/api/v1/search?query=${query}) } > Search </button> {isError && <div>Something went wrong …</div>} {isLoading ? ( <div>Loading …</div> ) : ( <ul> {data.hits.map(item => ( <li key={item.objectID}> <a href={item.url}>{item.title}</a> </li> ))} </ul> )} </Fragment> );}export default App;React在表单中获取数据到目前为止,我们只有input和按钮的组合。一旦引入更多的输入元素,您可能需要用一个表单元素包装它们。此外,表单还可以通过键盘上的“enter”来触发。function App() { … return ( <Fragment> <form onSubmit={() => setUrl(http://hn.algolia.com/api/v1/search?query=${query}) } > <input type=“text” value={query} onChange={event => setQuery(event.target.value)} /> <button type=“submit”>Search</button> </form> {isError && <div>Something went wrong …</div>} … </Fragment> );}但是现在浏览器在单击提交按钮时页面会重新加载,因为这是浏览器在提交表单时的固有行为。为了防止默认行为,我们可以通过event.preventDefault()取消默认行为。这也是在React类组件中实现的方法。function App() { … const doFetch = () => { setUrl(http://hn.algolia.com/api/v1/search?query=${query}); }; return ( <Fragment> <form onSubmit={event => { doFetch(); event.preventDefault(); }}> <input type=“text” value={query} onChange={event => setQuery(event.target.value)} /> <button type=“submit”>Search</button> </form> {isError && <div>Something went wrong …</div>} … </Fragment> );}现在,当你单击提交按钮时,浏览器不会再重新加载。它和以前一样工作,但这次使用的是表单,而不是简单的input和按钮组合。你也可以按键盘上的“回车”键。自定义数据获取hook为了提取用于数据获取的自定义hook,请将属于数据获取的所有内容,移动到一个自己的函数中。还要确保能够返回App组件所需要的全部变量。const useHackerNewsApi = () => { const [data, setData] = useState({ hits: [] }); const [url, setUrl] = useState( ‘http://hn.algolia.com/api/v1/search?query=redux', ); const [isLoading, setIsLoading] = useState(false); const [isError, setIsError] = useState(false); useEffect(() => { const fetchData = async () => { setIsError(false); setIsLoading(true); try { const result = await axios(url); setData(result.data); } catch (error) { setIsError(true); } setIsLoading(false); }; fetchData(); }, [url]); const doFetch = () => { setUrl(http://hn.algolia.com/api/v1/search?query=${query}); }; return { data, isLoading, isError, doFetch };}现在,你可以在App组件中使用新的hook了。function App() { const [query, setQuery] = useState(‘redux’); const { data, isLoading, isError, doFetch } = useHackerNewsApi(); return ( <Fragment> … </Fragment> );}接下来,从dofetch函数外部传递URL状态:const useHackerNewsApi = () => { … useEffect( … ); const doFetch = url => { setUrl(url); }; return { data, isLoading, isError, doFetch };};function App() { const [query, setQuery] = useState(‘redux’); const { data, isLoading, isError, doFetch } = useHackerNewsApi(); return ( <Fragment> <form onSubmit={event => { doFetch( http://hn.algolia.com/api/v1/search?query=${query}, ); event.preventDefault(); }} > <input type=“text” value={query} onChange={event => setQuery(event.target.value)} /> <button type=“submit”>Search</button> </form> … </Fragment> );}初始状态也可以变为通用状态。把它简单地传递给新的自定义hook:import React, { Fragment, useState, useEffect } from ‘react’;import axios from ‘axios’;const useDataApi = (initialUrl, initialData) => { const [data, setData] = useState(initialData); const [url, setUrl] = useState(initialUrl); const [isLoading, setIsLoading] = useState(false); const [isError, setIsError] = useState(false); useEffect(() => { const fetchData = async () => { setIsError(false); setIsLoading(true); try { const result = await axios(url); setData(result.data); } catch (error) { setIsError(true); } setIsLoading(false); }; fetchData(); }, [url]); const doFetch = url => { setUrl(url); }; return { data, isLoading, isError, doFetch };};function App() { const [query, setQuery] = useState(‘redux’); const { data, isLoading, isError, doFetch } = useDataApi( ‘http://hn.algolia.com/api/v1/search?query=redux', { hits: [] }, ); return ( <Fragment> <form onSubmit={event => { doFetch( http://hn.algolia.com/api/v1/search?query=${query}, ); event.preventDefault(); }} > <input type=“text” value={query} onChange={event => setQuery(event.target.value)} /> <button type=“submit”>Search</button> </form> {isError && <div>Something went wrong …</div>} {isLoading ? ( <div>Loading …</div> ) : ( <ul> {data.hits.map(item => ( <li key={item.objectID}> <a href={item.url}>{item.title}</a> </li> ))} </ul> )} </Fragment> );}export default App;这就是使用自定义hook获取数据的方法。hook本身对API一无所知。它从外部接收所有参数,只管理必要的状态,如数据、加载和错误状态。它执行请求并将数据作为自定义数据获取hook返回给组件。Reducer的数据获取hookreducer hook返回一个状态对象和一个改变状态对象的函数。dispatch函数接收type和可选的payload。所有这些信息都在实际的reducer函数中使用,从以前的状态、包含可选payload和type的action中提取新的状态。让我们看看这在代码中是如何工作的:import React, { Fragment, useState, useEffect, useReducer,} from ‘react’;import axios from ‘axios’;const dataFetchReducer = (state, action) => { …};const useDataApi = (initialUrl, initialData) => { const [url, setUrl] = useState(initialUrl); const [state, dispatch] = useReducer(dataFetchReducer, { isLoading: false, isError: false, data: initialData, }); …};Reducer Hook接受reducer函数和一个初始化的状态对象作为参数,在我们的例子中,数据、加载和错误状态的初始状态的参数没有改变,但是它们被聚合到由一个reducer hook管理的一个状态对象,而不是单个state hook。const dataFetchReducer = (state, action) => { …};const useDataApi = (initialUrl, initialData) => { const [url, setUrl] = useState(initialUrl); const [state, dispatch] = useReducer(dataFetchReducer, { isLoading: false, isError: false, data: initialData, }); useEffect(() => { const fetchData = async () => { dispatch({ type: ‘FETCH_INIT’ }); try { const result = await axios(url); dispatch({ type: ‘FETCH_SUCCESS’, payload: result.data }); } catch (error) { dispatch({ type: ‘FETCH_FAILURE’ }); } }; fetchData(); }, [url]); …};现在,在获取数据时,可以使用dispatch向reducer函数发送信息。dispatch函数发送的对象包括一个必填的type属性和可选的payload。type告诉Reducer函数需要应用哪个状态转换,并且Reducer还可以使用payload来提取新状态。毕竟,我们只有三种状态转换:初始化获取过程,通知成功的数据获取结果,以及通知错误的数据获取结果。在自定义hook的最后,状态像以前一样返回,但是因为我们有一个状态对象,而不再是独立状态,所以需要用扩展运算符返回state。这样,调用useDataApi自定义hook的用户仍然可以访问data、isloading和isError:const useDataApi = (initialUrl, initialData) => { const [url, setUrl] = useState(initialUrl); const [state, dispatch] = useReducer(dataFetchReducer, { isLoading: false, isError: false, data: initialData, }); … const doFetch = url => { setUrl(url); }; return { …state, doFetch };};最后,还缺少了reducer函数的实现。它需要处理三种不同的状态转换,即FETCH_INIT、FETCH_SUCCESS和FETCH_FAILURE。每个状态转换都需要返回一个新的状态对象。让我们看看如何用switch case语句实现这一点:const dataFetchReducer = (state, action) => { switch (action.type) { case ‘FETCH_INIT’: return { …state }; case ‘FETCH_SUCCESS’: return { …state }; case ‘FETCH_FAILURE’: return { …state }; default: throw new Error(); }};reducer函数可以通过其参数访问当前状态和action。到目前为止,switch case语句中的每个状态转换只会返回原来的状态。…语句用于保持状态对象不变(意味着状态永远不会直接改变),现在,让我们重写一些当前状态返回的属性,以便在每次状态转换时更改状态:const dataFetchReducer = (state, action) => { switch (action.type) { case ‘FETCH_INIT’: return { …state, isLoading: true, isError: false }; case ‘FETCH_SUCCESS’: return { …state, isLoading: false, isError: false, data: action.payload, }; case ‘FETCH_FAILURE’: return { …state, isLoading: false, isError: true, }; default: throw new Error(); }};现在,每个状态转换(由操作的type决定)都将基于先前的状态和可选的payload返回一个新的状态。例如,在成功请求的情况下,payload用于设置新状态对象的数据。总之,reducer hook确保状态管理的这一部分是用自己的逻辑封装的。通过提供type和可选payload,你将始终已一个可预测的状态结束。此外,你将永远不会进入无效状态。例如,以前可能会意外地将isloading和isError状态设置为true。在这个案例的用户界面中应该显示什么?现在,reducer函数定义的每个状态转换都会导致一个有效的状态对象。在effect hook中禁止数据获取即使组件已经卸载(例如,由于使用react路由器导航而离开),设置组件状态也是react中的一个常见问题。我以前在这里写过这个问题,它描述了如何防止在各种场景中为unmount的组件设置状态。让我们看看如何防止在自定义hook中为数据获取设置状态:const useDataApi = (initialUrl, initialData) => { const [url, setUrl] = useState(initialUrl); const [state, dispatch] = useReducer(dataFetchReducer, { isLoading: false, isError: false, data: initialData, }); useEffect(() => { let didCancel = false; const fetchData = async () => { dispatch({ type: ‘FETCH_INIT’ }); try { const result = await axios(url); if (!didCancel) { dispatch({ type: ‘FETCH_SUCCESS’, payload: result.data }); } } catch (error) { if (!didCancel) { dispatch({ type: ‘FETCH_FAILURE’ }); } } }; fetchData(); return () => { didCancel = true; }; }, [url]); const doFetch = url => { setUrl(url); }; return { …state, doFetch };};每个effect hook都有一个clean功能,在组件卸载时运行。clean函数是从hook返回的一个函数。在我们的例子中,我们使用一个名为didCancel的布尔标志,让我们的数据获取逻辑知道组件的状态(已装载/未装载)。如果组件已卸载,则标志应设置为“tree”,这将导致在最终异步解决数据提取后无法设置组件状态。注意:事实上,数据获取不会中止——这可以通过axios的Cancellation实现——但是对于未安装的组件,状态转换会不再执行。因为在我看来,axios的Cancellation并不是最好的API,所以这个防止设置状态的布尔标志也能起到作用。你已经了解了在React中state和effect hook如何用于获取数据。如果您对使用render props和高阶组件在类组件(和函数组件)中获取数据很感兴趣,请从一开始就去我的另一篇文章。否则,我希望本文对您了解react hook以及如何在现实场景中使用它们非常有用。 ...

April 1, 2019 · 8 min · jiezi

浅谈React Hooks

由于工作的原因我已经很长时间没接触过React了。前段时间圈子里都在讨论React Hooks,出于好奇也学习了一番,特此整理以加深理解。缘由在web应用无所不能的9012年,组成应用的Components也越来越复杂,冗长而难以复用的代码给开发者们造成了很多麻烦。比如:难以复用stateful的代码,render props及HOC虽然解决了问题,但对组件的包裹改变了组件树的层级,存在冗余;在ComponentDidMount、ComponentDidUpdate、ComponentWillUnmount等生命周期中做获取数据,订阅/取消事件,操作ref等相互之间无关联的操作,而把订阅/取消这种相关联的操作分开,降低了代码的可读性;与其他语言中的class概念差异较大,需要对事件处理函数做bind操作,令人困扰。另外class也不利于组件的AOT compile,minify及hot loading。在这种背景下,React在16.8.0引入了React Hooks。特性主要介绍state hook,effect hook及custom hookState Hook最基本的应用如下:import React, { useState } from ‘react’function counter() { const [count, setCount] = useState(0) return ( <div> <p>You have clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click </button> </div> )}调用useState,传入初始值,通过数组的结构赋值得到独立的local state count,及setCount。count可以理解为class component中的state,可见这里的state不局限于对象,可以为number,string,当然也可以是一个对象。而setCount可以理解为class component中的setState,不同的是setState会merge新老state,而hook中的set函数会直接替换,这就意味着如果state是对象时,每次set应该传入所有属性,而不能像class component那样仅传入变化的值。所以在使用useState时,尽量将相关联的,会共同变化的值放入一个object。再看看有多个“local state”的情况:import React, { useState } from ‘react’function person() { const [name, setName] = useState(‘simon’) const [age, setAge] = useState(24) return ( <div> <p>name: {name}</p> <p>age: {age}</p> </div> )}我们知道当函数执行完毕,函数作用域内的变量都会销毁,hooks中的state在component首次render后被React保留下来了。那么在下一次render时,React如何将这些保留的state与component中的local state对应起来呢。这里给出一个简单版本的实现:const stateArr = []const setterArr = []let cursor = 0let isFirstRender = truefunction createStateSetter(cursor) { return state => { stateArr[cursor] = state }}function useState(initState) { if (isFirstRender) { stateArr.push(initState) setterArr.push(createStateSetter(cursor)) isFirstRender = false } const state = stateArr[cursor] const setter = setterArr[cursor] cursor++ return [state, setter]}可以看出React需要保证多个hooks在component每次render的时候的执行顺序都保持一致,否则就会出现错误。这也是React hooks rule中必须在top level使用hooks的由来——条件,遍历等语句都有可能会改变hooks执行的顺序。Effect Hookimport React, { useState, useEffect } from ‘react’function FriendStatus(props) { const [isOnline, setIsOnline] = useState(null) function handleStatusChange(status) { setIsOnline(status.isOnline) } // 基本写法 useEffect(() => { document.title = ‘Dom is ready’ }) // 需要取消操作的写法 useEffect(() => { ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange) return function cleanup() { ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange) } }) if (isOnline === null) { return ‘Loading…’ } return isOnline ? ‘Online’ : ‘Offline’}可以看到上面的代码在传入useEffect的函数(effect)中做了一些"side effect",在class component中我们通常会在componentDidMount,componentDidUpdate中去做这些事情。另外在class component中,需要在componentDidMount中订阅,在componentWillUnmount中取消订阅,这样将一件事拆成两件事做,不仅可读性低,还容易产生bug:class FriendStatus extends React.Component { constructor(props) { super(props); this.state = { isOnline: null }; this.handleStatusChange = this.handleStatusChange.bind(this); } componentDidMount() { ChatAPI.subscribeToFriendStatus( this.props.friend.id, this.handleStatusChange ); } componentWillUnmount() { ChatAPI.unsubscribeFromFriendStatus( this.props.friend.id, this.handleStatusChange ); } handleStatusChange(status) { this.setState({ isOnline: status.isOnline }); } render() { if (this.state.isOnline === null) { return ‘Loading…’; } return this.state.isOnline ? ‘Online’ : ‘Offline’; }}如上代码,如果props中的friend.id发生变化,则会导致订阅和取消的id不一致,如需解决需要在componentDidUpdate中先取消订阅旧的再订阅新的,代码非常冗余。而useEffect hook在这一点上是浑然天成的。另外effect函数在每次render时都是新创建的,这其实是有意而为之,因为这样才能取得最新的state值。有同学可能会想,每次render后都会执行effect,这样会不会对性能造成影响。其实effect是在页面渲染完成之后执行的,不会阻塞,而在effect中执行的操作往往不要求同步完成,除了少数如要获取宽度或高度,这种情况需要使用其他的hook(useLayoutEffect),此处不做详解。即使这样,React也提供了控制的方法,及useEffect的第二个参数————一个数组,如果数组中的值不发生变化的话就跳过effect的执行:useEffect(() => { ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange) return () => { ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange); }}, [props.friend.id])Custom HookA custom Hook is a JavaScript function whose name starts with ”use” and that may call other Hooks.Custom Hook的使命是解决stateful logic复用的问题,如上面例子中的FriendStatus,在一个聊天应用中可能多个组件都需要知道好友的在线状态,将FriendStatus抽象成这样的hook:function useFriendStatus(friendID) { const [isOnline, setIsOnline] = useState(null); function handleStatusChange(status) { setIsOnline(status.isOnline); } useEffect(() => { ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange); return () => { ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange); }; }); return isOnline;}function FriendStatus(props) { const isOnline = useFriendStatus(props.friend.id) if (isOnline === null) { return ‘Loading…’ } return isOnline ? ‘Online’ : ‘Offline’}function FriendListItem(props) { const isOnline = useFriendStatus(props.friend.id) return ( <li style={{ color: isOnline ? ‘green’ : ‘black’ }}> {props.friend.name} </li> )}FriendStatus和FriendListItem中的isOnline是独立的,因custom hook复用的是stateful logic,而不是state本身。另外custom hook必须以use开头来命名,这样linter工具才能正确检测其是否符合规范。除了以上三种hook,React还提供了useContext, useReducer, useCallback, useMemo, useRef, useImperativeHandle, useLayoutEffect, useDebugValue内置hook,它们的用途可以参考官方文档,这里我想单独讲讲useRef。顾名思义,这个hook应该跟ref相关的:function TextInputWithFocusButton() { const inputEl = useRef(null) const onButtonClick = () => { inputEl.current.focus() } return ( <> <input ref={inputEl} type=“text” /> <button onClick={onButtonClick}>Focus the input</button> </> )}来看看官方文档上的说明:useRef returns a mutable ref object whose .current property is initialized to the passed argument (initialValue). The returned object will persist for the full lifetime of the component.这句话告诉我们在组件的整个生命周期里,inputEl.current都是存在的,这扩展了useRef本身的用途,可以使用useRef维护类似于class component中实例属性的变量:function Timer() { const intervalRef = useRef() useEffect(() => { const id = setInterval(() => { // … }) intervalRef.current = id return () => { clearInterval(intervalRef.current) } }) // …}这在class component中是理所当然的,但不要忘记Timer仅仅是一个函数,函数执行完毕后函数作用域内的变量将会销毁,所以这里需要使用useRef来保持这个timerId。类似的useRef还可以用来获取preState:function Counter() { const [count, setCount] = useState(0) const prevCountRef = useRef() useEffect(() => { prevCountRef.current = count // 由于useEffect中的函数是在render完成之后异步执行的,所以在每次render时prevCountRef.current的值为上一次的count值 }) const prevCount = prevCountRef.current return <h1>Now: {count}, before: {prevCount}</h1>}参考文章&拓展阅读React Hooks官方文档React hooks: not magic, just arraysWhy Isn’t X a Hook?Making Sense of React Hooks ...

February 28, 2019 · 3 min · jiezi

[译] React v16.8: 含有Hooks的版本

原文出处:React v16.8: The One With HooksFebruary 06, 2019 by Dan Abramov伴随 React 16.8,React Hooks可以在稳定版本中使用!什么是 Hooks?Hooks 可以让我们不用写一个 class 就能使用 state 和其他的 React 特性。我们也可以构建我们自己的 Hooks 来在组件之间共享可重复使用的有状态逻辑。如果您之前从未听说 Hooks,您可能对下列这些资源感兴趣:Hooks 介绍解释了我们向 React 添加 Hooks 的原因。Hooks 概览 是一个对内置 Hookks 的快速概览。构建自己的 Hooks演示了使用自定义 Hooks 来复用代码。理解 React Hooks探索了通过解锁 Hooks 带来的新的可能。useHooks.com展示了社区维护的 Hooks 指导手册和演示示例。您现在没必要一定要学习 Hooks 。 Hooks 并没有突破性的改变,而且我们也没有打算 React 中移除 class。这篇Hooks FAQ描述了逐步采用的策略。_无需大规模重写我们不推荐重写您已经存在的应用,以便一夜之间就使用上 Hooks。相反,我们更建议您尝试在一些新的组件中使用 Hooks,并且请让我们了解到您的使用感受。使用 Hooks 的代码会与现存使用 class 的代码并肩在一起工作。现在可以使用 Hooks 了吗?当然!从 16.8.0 开始,React 包含了 React Hooks 的稳定实现来用于:React DOMReact DOM ServerReact Test RendererReact Shallow Renderer请注意,要启用 Hooks,所有的 React 包均需要升级到 16.8.0 或者更高版本。 如果忘记升级,不能使用 Hooks,比如 React DOM。React Native 将会在 0.59 发布版本中得到支持。_工具支持React DevTools 目前支持 React Hooks。在最新的 Flow 和 TypeScript 对 React 的定义中也同样支持。我们强烈建议使用一条名为 eslint-plugin-react-hooks 的新检查规则,来强制实现 Hooks 的最佳实践。很快它也会默认包含进 Create React App 中。_下一步呢?我们已经在最近发布的 React Roadmap 中,描述了我们接下来几个月的计划。请注意 React Hooks 还没有涵盖 class 的所有用例,但是差不多了快完成了。目前,只有 getSnapshotBeforeUpdate() 和 componentDidCatch() 这俩方法没有相对应的 Hooks API,而且它们的生命周期相对来说比较特殊少见。如果您需要,您应该可以在您写的大多数新代码中使用 Hooks。尽管 Hooks 还处于初步阶段,React 社区已经使用 Hooks 为动画、表单、订阅、与其他库集成等方面,创作了很多有意思的示例和指南手册。Hooks 使得代码复用更加简单,帮助我们用更加简洁的方式写出组件,以提供更好的用户体验,这让我们感到很兴奋。我们等不及要看到您下一次的创作!_测试 Hooks我们在发布版中添加了一个名为 ReactTestUtils.act() 的新 API。它可以确保测试中的行为与浏览器中的行为更紧密地匹配。我们建议将任何渲染和触发组件更新的代码包装进 act() 调用。测试库也可以用它包装它们的API(例如,react-testing-library 的 render 和 fireEvent 实用工具就是这样做的)。例如,此页面的计数器可以像这样进行测试:import React from ‘react’;import ReactDOM from ‘react-dom’;import { act } from ‘react-dom/test-utils’;import Counter from ‘./Counter’;let container;beforeEach(() => { container = document.createElement(‘div’); document.body.appendChild(container);});afterEach(() => { document.body.removeChild(container); container = null;});it(‘can render and update a counter’, () => { // Test first render and effect act(() => { ReactDOM.render(<Counter />, container); }); const button = container.querySelector(‘button’); const label = container.querySelector(‘p’); expect(label.textContent).toBe(‘You clicked 0 times’); expect(document.title).toBe(‘You clicked 0 times’); // Test second render and effect act(() => { button.dispatchEvent(new MouseEvent(‘click’, {bubbles: true})); }); expect(label.textContent).toBe(‘You clicked 1 times’); expect(document.title).toBe(‘You clicked 1 times’);});如果打算测试自定义的 Hooks,可以通过在测试中创建一个组件并使用它的 Hook 来实现,然后您就可以测试您写的 Hooks 了。为了减少样板,建议使用 react-testing-library 来进行测试,该库是为鼓励编写测试用例而设计的。_感谢感谢所有在 Hooks RFC 中进行评论来分享自己的反馈信息的同学。我们已经阅读了你们的所有评论意见,并在其基础上为最终的 API 做了些许调整。_安装ReactReact v16.8.0 在 npm 上已可以使用。使用 Yarn 来安装 React 16,运行命令:yarn add react@^16.8.0 react-dom@^16.8.0使用 npm 来安装 React 16,运行命令:npm install –save react@^16.8.0 react-dom@^16.8.0同时也通过 CDN 提供React 的 UMD 构建版本:<script crossorigin src=“https://unpkg.com/react@16/umd/react.production.min.js"></script><script crossorigin src=“https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>有关详细安装说明,请参阅文档。用于 React Hooks 的 ESLint 插件注意:如上所述,强烈建议您使用eslint-plugin-react-hooks 规则。如果正在使用 Create React App,而不是手动配置 ESLint,建议等待下一个版本的 react-scripts,即将发布并且会包含这条规则。假定您已经安装过了 ESLint,运行命令:# npmnpm install eslint-plugin-react-hooks@next –save-dev# yarnyarn add eslint-plugin-react-hooks@next –dev然后,添加下面的内容到 ESLint 配置中:{ “plugins”: [ // … “react-hooks” ], “rules”: { // … “react-hooks/rules-of-hooks”: “error” }} ...

February 15, 2019 · 2 min · jiezi