React详解二

合成事件

此参考指南记录了构成 React 事件系统一部分的 SyntheticEvent 包装器。请参考有关事件处理的指南来了解更多。

SyntheticEvent 实例将被传递给你的事件处理函数,它是浏览器的原生事件的跨浏览器包装器。除兼容所有浏览器外,它还拥有和浏览器原生事件相同的接口,包括 stopPropagation() 和 preventDefault()。

如果因为某些原因,当你需要使用浏览器的底层事件时,只需要使用 nativeEvent 属性来获取即可。每个 SyntheticEvent 对象都包含以下属性:

boolean bubbles
boolean cancelable
DOMEventTarget currentTarget
boolean defaultPrevented
number eventPhase
boolean isTrusted
DOMEvent nativeEvent
void preventDefault()
boolean isDefaultPrevented()
void stopPropagation()
boolean isPropagationStopped()
void persist()
DOMEventTarget target
number timeStamp
string type

截止 v0.14,当事件处理函数返回 false 时,不再阻止事件冒泡。你可以选择使用 e.stopPropagation() 或者 e.preventDefault() 替代。

事件池
SyntheticEvent 是合并而来。这意味着 SyntheticEvent 对象可能会被重用,而且在事件回调函数被调用后,所有的属性都会无效。出于性能考虑,你不能通过异步访问事件。

function onClick(event){
    console.log(event) //nullified object
    console.log(event.type) // => click
    const eventType = event.type //=> click
     
    setTimeout(function(){
        console.log(event.type); //=>null
        console.log(eventType); //=> click
    }, 0);
    
    //不起作用 this.state.clickEvent的值将会只包含null
    this.setState({clickEvent:event})

    //你仍然可以导出事件属性
    this.setState({eventType:event.type})
}

如果你想异步访问事件属性,你需在事件上调用 event.persist(),此方法会从池中移除合成事件,允许用户代码保留对事件的引用。

支持的事件

React 通过将事件 normalize 以让他们在不同浏览器中拥有一致的属性。

以下的事件处理函数在冒泡阶段被触发。如需注册捕获阶段的事件处理函数,则应为事件名添加 Capture。例如,处理捕获阶段的点击事件请使用 onClickCapture,而不是 onClick。
剪贴板事件
事件名:

onCopy onCut onPaste

属性:

DOMDataTransfer clipboardData

复合事件

事件名:

onCompositionEnd onCompositionStart onCompositionUpdate

属性:

string data

键盘事件
事件名:

onKeyDown onKeyPress onKeyUp

属性:

boolean altKey
number charCode
boolean ctrlKey
boolean getModifierState(key)
string key
number keyCode
string locale
number location
boolean metaKey
boolean repeat
boolean shiftKey
number which

key 属性可以是 DOM Level 3 Events spec 里记录的任意值。
焦点事件
事件名:

onFocus onBlur

这些焦点事件在 React DOM 上的所有元素都有效,不只是表单元素。

属性:

DOMEventTarget relatedTarget

表单事件
事件名:

onChange onInput onInvalid onReset onSubmit 

想了解 onChange 事件的更多信息,查看 Forms 。

Mouse Events
鼠标事件:

onClick onContextMenu onDoubleClick onDrag onDragEnd onDragEnter onDragExit
onDragLeave onDragOver onDragStart onDrop onMouseDown onMouseEnter onMouseLeave
onMouseMove onMouseOut onMouseOver onMouseUp
onMouseEnter 和 onMouseLeave 事件从离开的元素向进入的元素传播,不是正常的冒泡,也没有捕获阶段。

属性:

boolean altKey
number button
number buttons
number clientX
number clientY
boolean ctrlKey
boolean getModifierState(key)
boolean metaKey
number pageX
number pageY
DOMEventTarget relatedTarget
number screenX
number screenY
boolean shiftKey

指针事件
事件名:

onPointerDown onPointerMove onPointerUp onPointerCancel onGotPointerCapture
onLostPointerCapture onPointerEnter onPointerLeave onPointerOver onPointerOut
onPointerEnter 和 onPointerLeave 事件从离开的元素向进入的元素传播,不是正常的冒泡,也没有捕获阶段。

属性:

如 W3 spec 中定义的,指针事件通过以下属性扩展了鼠标事件:


number pointerId
number width
number height
number pressure
number tangentialPressure
number tiltX
number tiltY
number twist
string pointerType
boolean isPrimary

关于跨浏览器支持的说明:

并非每个浏览器都支持指针事件(在写这篇文章时,已支持的浏览器有:Chrome,Firefox,Edge 和 Internet Explorer)。React 故意不通过 polyfill 的方式适配其他浏览器,主要是符合标准的 polyfill 会显著增加 react-dom 的 bundle 大小。

如果你的应用要求指针事件,我们推荐添加第三方的指针事件 polyfil 。

选择事件
事件名:

onSelect

触摸事件
事件名:

onTouchCancel onTouchEnd onTouchMove onTouchStart

属性:

boolean altKey
DOMTouchList changedTouches
boolean ctrlKey
boolean getModifierState(key)
boolean metaKey
boolean shiftKey
DOMTouchList targetTouches
DOMTouchList touches

UI 事件
事件名:

onScroll

属性:

number detail
DOMAbstractView view

滚轮事件
事件名:

onWheel

属性:

number deltaMode
number deltaX
number deltaY
number deltaZ

媒体事件
事件名:

onAbort onCanPlay onCanPlayThrough onDurationChange onEmptied onEncrypted
onEnded onError onLoadedData onLoadedMetadata onLoadStart onPause onPlay
onPlaying onProgress onRateChange onSeeked onSeeking onStalled onSuspend
onTimeUpdate onVolumeChange onWaiting

图像事件
事件名:

onLoad onError

动画事件
事件名:

onAnimationStart onAnimationEnd onAnimationIteration

属性:

string animationName
string pseudoElement
float elapsedTime

过渡事件
事件名:

onTransitionEnd

属性:

string propertyName
string pseudoElement
float elapsedTime

其他事件
事件名:

onToggle

Test Utilities

import ReactTestUtils from 'react-dom/test-utils'; // ES6
var ReactTestUtils = require('react-dom/test-utils'); // ES5 使用 npm 的方式

ReactTestUtils 可搭配你所选的测试框架,轻松实现 React 组件测试。在 Facebook 内部,我们使用 Jest 来轻松实现 JavaScript 测试。你可以从 Jest 官网的 React 教程中了解如何开始使用它。

注意:

我们推荐使用 React Testing Library,它使得针对组件编写测试用例就像终端用户在使用它一样方便。

另外,Airbnb 发布了一款叫作 Enzyme 的测试工具,通过它能够轻松对 React 组件的输出进行断言、操控和遍历。

act()
为断言准备一个组件,包裹要渲染的代码并在调用 act() 时执行更新。这会使得测试更接近 React 在浏览器中的工作方式。

注意

如果你使用了 react-test-renderer,它也提供了与 act 行为相同的函数。

例如,假设我们有个 Counter 组件:

class Counter extends React.Component{
    constructor(props){
        super(props)
        this.state = {count:0}
        this.handleClick = this.handleClick.bind(this)
    }
    componentDidMount(){
        document.title = `You Clicked ${this.state.count} times`
    }
    componentDidUpdate(){
        document.title = `You Clicked ${this.state.count} times`
    }
    handleClick(){
        this.setState(state=>{
            count:state.count + 1
        })
    }

    render(){
        return(
            <div>
             <p>You Clicked {this.state.count} times</p>
             <button onClick={this.handleClick}>Click Me</button>
            </div>
        )
    }
}

以下是其测试代码:

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',()=>{
    //首先测试render和componentDidMount
    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 cliked 0 times')

    //再测试render和componentDidUpdate
    act(()=>{
        button.dispatchEvent(new MouseEvent('click',{bubbles:true}))
    })
    expect(label.textContent).toBe('You clicked 1 times')
    expect(docuemnt.title).toBe('You Cliked 1 times')
})

千万不要忘记,只有将 DOM 容器添加到 document 时,触发 DOM 事件才生效。你可以使用类似于 React Testing Library 等库来减少样板代码(boilerplate code)。

recipes 文档包含了关于 act() 函数的示例、用法及更多详细信息。

mockComponent()
mockComponent(
  componentClass,
  [mockTagName]
)

将模拟组件模块传入这个方法后,React 内部会使用有效的方法填充该模块,使其成为虚拟的 React 组件。与通常的渲染不同,组件将变成一个简单的

(如果提供了 mockTagName 则是其他标签),包含任何提供的子级。

注意:

mockComponent() 是一个过时的 API,我们推荐使用 jest.mock() 来代替。
isElement()

isElement(element)

当 element 是任何一种 React 元素时,返回 true。

isElementOfType()

isElementOfType(
  element,
  componentClass
)

当 element 是一种 React 元素,并且它的类型是参数 componentClass 的类型时,返回 true。

isDOMComponent()

isDOMComponent(instance)

当 instance 是一个 DOM 组件(比如

或 )时,返回 true。

isCompositeComponent()

isCompositeComponent(instance)

当 instance 是一个用户自定义的组件,比如一个类或者一个函数时,返回 true。

isCompositeComponentWithType()

isCompositeComponentWithType(
  instance,
  componentClass
)

当 instance 是一个组件,并且它的类型是参数 componentClass 的类型时,返回 true。

findAllInRenderedTree()

findAllInRenderedTree(
  tree,
  test
)

遍历所有在参数 tree 中的组件,记录所有 test(component) 为 true 的组件。单独调用此方法不是很有用,但是它常常被作为底层 API 被其他测试方法使用。

scryRenderedDOMComponentsWithClass()

scryRenderedDOMComponentsWithClass(
  tree,
  className
)

查找渲染树中组件的所有 DOM 元素,这些组件是 css 类名与参数 className 匹配的 DOM 组件。

findRenderedDOMComponentWithClass()

findRenderedDOMComponentWithClass(
  tree,
  className
)

用法与 scryRenderedDOMComponentsWithClass() 保持一致,但期望仅返回一个结果。不符合预期的情况下会抛出异常。

scryRenderedDOMComponentsWithTag()

scryRenderedDOMComponentsWithTag(
  tree,
  tagName
)

查找渲染树中组件的所有的 DOM 元素,这些组件是标记名与参数 tagName 匹配的 DOM 组件。

findRenderedDOMComponentWithTag()

findRenderedDOMComponentWithTag(
  tree,
  tagName
)

用法与 scryRenderedDOMComponentsWithTag() 保持一致,但期望仅返回一个结果。不符合预期的情况下会抛出异常。

scryRenderedComponentsWithType()

scryRenderedComponentsWithType(
  tree,
  componentClass
)

查找组件类型等于 componentClass 组件的所有实例。

findRenderedComponentWithType()

findRenderedComponentWithType(
  tree,
  componentClass
)

用法与 scryRenderedComponentsWithType() 保持一致,但期望仅返回一个结果。不符合预期的情况下会抛出异常。

renderIntoDocument()
renderIntoDocument(element)
渲染 React 元素到 document 中的某个单独的 DOM 节点上。这个函数需要一个 DOM 对象。 它实际相当于:

const domContainer = document.createElement('div');
ReactDOM.render(element, domContainer);

注意:

你需要在引入 React 之前确保 window 存在,window.document 和 window.document.createElement 能在全局环境中获取到。不然 React 会认为它没有权限去操作 DOM,以及像 setState 这样的方法将不可用。

其他工具方法
Simulate

Simulate.{eventName}(
  element,
  [eventData]
)

使用可选的 eventData 事件数据来模拟在 DOM 节点上触发事件。

React 所支持的所有事件 在 Simulate 中都有对应的方法。

点击元素

// <button ref={(node) => this.button = node}>...</button>
const node = this.button;
ReactTestUtils.Simulate.click(node);

修改一个 input 输入框的值,然后按回车键。

// <input ref={(node) => this.textInput = node} />
const node = this.textInput;
node.value = 'giraffe';
ReactTestUtils.Simulate.change(node);
ReactTestUtils.Simulate.keyDown(node, {key: "Enter", keyCode: 13, which: 13});

你必须提供一切需要在组件中用到的事件属性(比如:keyCode、which 等等),因为 React 没有为你创建这些属性。

Test Renderer

import TestRenderer from 'react-test-renderer'; // ES6
const TestRenderer = require('react-test-renderer'); // ES5 with npm

这个 package 提供了一个 React 渲染器,用于将 React 组件渲染成纯 JavaScript 对象,无需依赖 DOM 或原生移动环境。

这个 package 提供的主要功能是在不依赖浏览器或 jsdom 的情况下,返回某个时间点由 React DOM 或者 React Native 平台渲染出的视图结构(类似与 DOM 树)快照。

import TestRenderer from 'react-test-renderer'

function Link(props){
    return <a href="props.page">{props.children}</a>
}

const testRenderer = TestRenderer.create(
    <Link page="http://www.baidu.com">Facebook</Link>
)

console.log(testRenderer.toJSON())
// {
//     type:'a',
//     props:{href:'https://www.baidu.com'},
//     children:['Facebook']
// }

你可以使用 Jest 的快照测试功能来自动保存当前 JSON 树结构到一个文件中,并在测试中检查它是否被修改:了解更多。

你也可以通过遍历输出来查找特定节点,并对它们进行断言。

import TestRenderer from 'react-test-renderer'

function MyComponent(){
    return (
        <div>
          <SubComponent foo="bar">
          <p className="my">Hello</p>
        </div>
    )
}

function SubComponen(){
    return (
        <p className="sub">Sub</p>
    )
}

const TestRenderer = TestRenderer.create(<MyComponent />)
const testInstance = testRenderer.root

expect(testInstance.findByType(SubComponent).props.foo).toBe('bar')
expect(testInstance,findByType({className:'sub'}).children).toEqual(['Sub'])

TestRenderer
TestRenderer.create()
TestRenderer.act()
TestRenderer instance
testRenderer.toJSON()
testRenderer.toTree()
testRenderer.update()
testRenderer.unmount()
testRenderer.getInstance()
testRenderer.root
TestInstance
testInstance.find()
testInstance.findByType()
testInstance.findByProps()
testInstance.findAll()
testInstance.findAllByType()
testInstance.findAllByProps()
testInstance.instance
testInstance.type
testInstance.props
testInstance.parent
testInstance.children

参考
TestRenderer.create()

TestRenderer.create(element, options);

通过传来的 React 元素创建一个 TestRenderer 实例。它并不使用真实的 DOM,但是它依然将组件树完整地渲染到内存,以便于你对它进行断言。此时将返回一个 TestRenderer 实例。

TestRenderer.act()

TestRenderer.act(callback);

与 react-dom/test-utils 中的 act() 相似,TestRender.act 为断言准备一个组件。可以使用 act() 来包装 TestRenderer.create 和 testRenderer.update。

import {create,act} from 'react-test-renderer'
import App from './app.js'
//渲染组件
let root

act(()=>{
    root = create(<App value={1}/>)
})
//对根元素进行断言、
expect(root.toJSON()).toMatchSnapshot()
//更新props

act(()=>{
    root.update(<App value={2}/>)
})

//对根元素进行断言
expect(root.toJSON()).toMatchSnapshot

testRenderer.toJSON()
testRenderer.toJSON()
返回一个已渲染的的树对象。该树仅包含特定平台的节点,例如

或 和它们的 props,但并不包含任何用户编写的组件。这对于快照测试非常方便。

testRenderer.toTree()

testRenderer.toTree()

返回一个已渲染的的树对象。它所表示的内容比 toJSON() 提供的内容要更加详细,并且包含用户编写的组件。除非你要在测试渲染器(test renderer)之上编写自己的断言库,否则你可能并不需要这个方法。

testRenderer.unmount()
testRenderer.unmount()
卸载内存中的树,会触发相应的生命周期事件。

testRenderer.getInstance()
testRenderer.getInstance()
如果可用的话,返回与根元素相对应的实例。如果根元素是函数定义组件,该方法无效,因为函数定义组件没有实例。

testRenderer.root
testRenderer.root
返回根元素“测试实例”对象,它对于断言树中的特定节点十分有用。你可以利用它来查找其他更深层的“测试实例”。

testInstance.find()
testInstance.find(test)
找到一个 test(testInstance) 返回 true 的后代测试实例。如果不只有一个测试实例匹配,将会报错。

testInstance.findByType()
testInstance.findByType(type)
找到匹配指定 type 的后代测试实例,如果不是只有一个测试实例匹配指定的 type,将会报错。

testInstance.findByProps()
testInstance.findByProps(props)
找到匹配指定 props的后代测试实例,如果不是正好只有一个测试实例匹配指定的 props,将会报错。

testInstance.findAll()
testInstance.findAll(test)
找到所有 test(testInstance) 返回 true 的后代测试实例。

testInstance.findAllByType()
testInstance.findAllByType(type)
找到所有匹配指定 type 的后代测试实例。

testInstance.findAllByProps()
testInstance.findAllByProps(props)
找到所有匹配指定 props 的后代测试实例。

testInstance.instance
testInstance.instance
该测试实例相对应的组件实例。它只能用于类定义组件,因为函数定义组件没有实例。它匹配给定的组件内部的 this 的值。

testInstance.type
testInstance.type
该测试实例相对应的组件的类型。例如,一个 组件有一个 Button 类型。

testInstance.props
testInstance.props
该测试实例相对应的组件的 props。例如,一个 组件的 props 为 {size: ‘small’}。

testInstance.parent
testInstance.parent
该测试实例的父测试实例。

testInstance.children
testInstance.children
该测试实例的子测试实例。

想法
你可以把 createNodeMock 函数作为选项(option)传递给 TestRenderer.create,进行自定义 refs 模拟。createNodeMock 接受当前元素作为参数,并且返回一个模拟 ref 对象的。这十分有利于依赖 refs 组件的测试。

import TestRenderer from 'react-test-renderer'
class MyComponent extends React.Component{
    constructor(props){
        super(props)
        this.input = null
    }
    componentDidMount(){
        this,.input.focus()
    }
    render(){
        return <input type="text" ref={el=>this.input = el}/>
    }
}

let focused = false
TestRenderer.create(<MyComponent />,{
    createNodeMock:(element)=>{
        //模拟focus函数
        if(element.type=='input'){
            return {
                focus:()=>{
                    focused = true
                }
            }
        }
        return null
    }
})

expect (focused).toBe(true

JavaScript 环境要求

React 16 依赖集合类型 Map 和 Set 。如果你要支持无法原生提供这些能力(例如 IE < 11)或实现不规范(例如 IE 11)的旧浏览器与设备,考虑在你的应用库中包含一个全局的 polyfill ,例如 core-js 或 babel-polyfill 。

React 16 使用 core-js 支持老版本浏览器,其 polyfill 环境可能如下:

import 'core-js/es/map';
import 'core-js/es/set';

import React from 'react';
import ReactDOM from 'react-dom';

ReactDOM.render(
  <h1>Hello, world!</h1>,
  document.getElementById('root')
);

React 同时还依赖于 requestAnimationFrame(甚至包括测试环境)。 你可以使用 raf 的 package 增添 requestAnimationFrame 的 shim:

import 'raf/polyfill';

React 术语词汇表

单页面应用

单页面应用(single-page application),是一个应用程序,它可以加载单个 HTML 页面,以及运行应用程序所需的所有必要资源(例如 JavaScript 和 CSS)。与页面或后续页面的任何交互,都不再需要往返 server 加载资源,即页面不会重新加载。

你可以使用 React 来构建单页应用程序,但不是必须如此。React 还可用于增强现有网站的小部分,使其增加额外交互。用 React 编写的代码,可以与服务器端渲染的标记(例如 PHP)或其他客户端库和平共处。实际上,这也正是 Facebook 内部使用 React 的方式。

ES6, ES2015, ES2016 等

这些首字母缩写都是指 ECMAScript 语言规范标准的最新版本,JavaScript 语言是此标准的一个实现。其中 ES6 版本(也称为 ES2015)包括对前面版本的许多补充,例如:箭头函数、class、模板字面量、let 和 const 语句。可以在这里了解此规范特定版本的详细信息。

Compiler(编译器)

JavaScript compiler 接收 JavaScript 代码,然后对其进行转换,最终返回不同格式的 JavaScript 代码。最为常见的使用示例是,接收 ES6 语法,然后将其转换为旧版本浏览器能够解释执行的语法。Babel 是 React 最常用的 compiler。

Bundler(打包工具)

bundler 会接收写成单独模块(通常有数百个)的 JavaScript 和 CSS 代码,然后将它们组合在一起,最终生成出一些为浏览器优化的文件。常用的打包 React 应用的工具有 webpack 和 Browserify。

Package 管理工具

package 管理工具,是帮助你管理项目依赖的工具。npm 和 Yarn 是两个常用的管理 React 应用依赖的 package 管理工具。它们都是使用了相同 npm package registry 的客户端。

CDN

CDN 代表内容分发网络(Content Delivery Network)。CDN 会通过一个遍布全球的服务器网络来分发缓存的静态内容。

JSX

JSX 是一个 JavaScript 语法扩展。它类似于模板语言,但它具有 JavaScript 的全部能力。JSX 最终会被编译为 React.createElement() 函数调用,返回称为 “React 元素” 的普通 JavaScript 对象。通过查看这篇文档获取 JSX 语法的基本介绍,在[这篇文档]中可以找到 JSX 语法的更多深入教程。

React DOM 使用 camelCase(驼峰式命名)来定义属性的名称,而不使用 HTML 属性名称的命名约定。例如,HTML 的 tabindex 属性变成了 JSX 的 tabIndex。而 class 属性则变为 className,这是因为 class 是 JavaScript 中的保留字:

const name = 'Clementine';
ReactDOM.render(
  <h1 className="hello">My name is {name}!</h1>,
  document.getElementById('root')
);
元素

React 元素是构成 React 应用的基础砖块。人们可能会把元素与广为人知的“组件”概念相互混淆。元素描述了你在屏幕上想看到的内容。React 元素是不可变对象。

const element = <h1>Hello, world</h1>;

通常我们不会直接使用元素,而是从组件中返回元素。

组件

React 组件是可复用的小的代码片段,它们返回要在页面中渲染的 React 元素。React 组件的最简版本是,一个返回 React 元素的普通 JavaScript 函数:

function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

组件也可以使用 ES6 的 class 编写:

class Welcome extends React.Component {
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}

组件可被拆分为不同的功能片段,这些片段可以在其他组件中使用。组件可以返回其他组件、数组、字符串和数字。根据经验来看,如果 UI 中有一部分被多次使用(Button,Panel,Avatar),或者组件本身就足够复杂(App,FeedStory,Comment),那么它就是一个可复用组件的候选项。组件名称应该始终以大写字母开头( 而不是 )。有关渲染组件的更多信息,请参阅这篇文档。

props

props 是 React 组件的输入。它们是从父组件向下传递给子组件的数据。

记住,props 是只读的。不应以任何方式修改它们:

// 错误做法!
props.number = 42;
如果你想要修改某些值,以响应用户输入或网络响应,请使用 state 来作为替代。

props.children

每个组件都可以获取到 props.children。它包含组件的开始标签和结束标签之间的内容。例如:

<Welcome>Hello world</Welcome>

在 Welcome 组件中获取 props.children,就可以得到字符串 Hello world!:

function Welcome(props){
    return <p>{props.children}</p>
}

对于 class 组件,请使用 this.props.children 来获取:

class Welcome extends React.Component {
  render() {
    return <p>{this.props.children}</p>;
  }
}
state

当组件中的一些数据在某些时刻发生变化时,这时就需要使用 state 来跟踪状态。例如,Checkbox 组件可能需要 isChecked 状态,而 NewsFeed 组件可能需要跟踪 fetchedPosts 状态。

state 和 props 之间最重要的区别是:props 由父组件传入,而 state 由组件本身管理。组件不能修改 props,但它可以修改 state。

对于所有变化数据中的每个特定部分,只应该由一个组件在其 state 中“持有”它。不要试图同步来自于两个不同组件的 state。相反,应当将其提升到最近的共同祖先组件中,并将这个 state 作为 props 传递到两个子组件。

生命周期方法

生命周期方法,用于在组件不同阶段执行自定义功能。在组件被创建并插入到 DOM 时(即挂载中阶段(mounting)),组件更新时,组件取消挂载或从 DOM 中删除时,都有可以使用的生命周期方法。

受控组件 vs 非受控组件

React 有两种不同的方式来处理表单输入。

如果一个 input 表单元素的值是由 React 控制,就其称为受控组件。当用户将数据输入到受控组件时,会触发修改状态的事件处理器,这时由你的代码来决定此输入是否有效(如果有效就使用更新后的值重新渲染)。如果不重新渲染,则表单元素将保持不变。

一个非受控组件,就像是运行在 React 体系之外的表单元素。当用户将数据输入到表单字段(例如 input,dropdown 等)时,React 不需要做任何事情就可以映射更新后的信息。然而,这也意味着,你无法强制给这个表单字段设置一个特定值。

在大多数情况下,你应该使用受控组件

key

“key” 是在创建元素数组时,需要用到的一个特殊字符串属性。key 帮助 React 识别出被修改、添加或删除的 item。应当给数组内的每个元素都设定 key,以使元素具有固定身份标识。

只需要保证,在同一个数组中的兄弟元素之间的 key 是唯一的。而不需要在整个应用程序甚至单个组件中保持唯一。

不要将 Math.random() 之类的值传递给 key。重要的是,在前后两次渲染之间的 key 要具有“固定身份标识”的特点,以便 React 可以在添加、删除或重新排序 item 时,前后对应起来。理想情况下,key 应该从数据中获取,对应着唯一且固定的标识符,例如 post.id。

Ref

React 支持一个特殊的、可以附加到任何组件上的 ref 属性。此属性可以是一个由 React.createRef() 函数创建的对象、或者一个回调函数、或者一个字符串(遗留 API)。当 ref 属性是一个回调函数时,此函数会(根据元素的类型)接收底层 DOM 元素或 class 实例作为其参数。这能够让你直接访问 DOM 元素或组件实例。

谨慎使用 ref。如果你发现自己经常使用 ref 来在应用中“实现想要的功能”,你可以考虑去了解一下自上而下的数据流。

事件

使用 React 元素处理事件时,有一些语法上差异:

React 事件处理器使用 camelCase(驼峰式命名)而不使用小写命名。
通过 JSX,你可以直接传入一个函数,而不是传入一个字符串,来作为事件处理器。

协调

当组件的 props 或 state 发生变化时,React 通过将最新返回的元素与原先渲染的元素进行比较,来决定是否有必要进行一次实际的 DOM 更新。当它们不相等时,React 才会更新 DOM。这个过程被称为“协调”。、

Hook

Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。

import React,{useState} from 'react'

function Example(){
//声明一个新的叫做'count'的state变量
    const [count,setCount] = useState(0)
    return(
        <div>
          <p>
            You Clicked {count} times
          </p>
          <button onClick={()=>setCount(count + 1)}>Click Me</button>
        </div>
    )
}

React 16.8.0 是第一个支持 Hook 的版本。升级时,请注意更新所有的 package,包括 React DOM。 React Native 从 0.59 版本开始支持 Hook。

没有破坏性改动

在我们继续之前,请记住 Hook 是:

完全可选的。 你无需重写任何已有代码就可以在一些组件中尝试 Hook。但是如果你不想,你不必现在就去学习或使用 Hook。
100% 向后兼容的。 Hook 不包含任何破坏性改动。
现在可用。 Hook 已发布于 v16.8.0。
没有计划从 React 中移除 class。 你可以在本页底部的章节读到更多关于 Hook 的渐进策略。

Hook 不会影响你对 React 概念的理解。 恰恰相反,Hook 为已知的 React 概念提供了更直接的 API:props, state,context,refs 以及生命周期。稍后我们将看到,Hook 还提供了一种更强大的方式来组合他们。

在组件之间复用状态逻辑很难

React 没有提供将可复用性行为“附加”到组件的途径(例如,把组件连接到 store)。如果你使用过 React 一段时间,你也许会熟悉一些解决此类问题的方案,比如 render props 和 高阶组件。但是这类方案需要重新组织你的组件结构,这可能会很麻烦,使你的代码难以理解。如果你在 React DevTools 中观察过 React 应用,你会发现由 providers,consumers,高阶组件,render props 等其他抽象层组成的组件会形成“嵌套地狱”。尽管我们可以在 DevTools 过滤掉它们,但这说明了一个更深层次的问题:React 需要为共享状态逻辑提供更好的原生途径。

你可以使用 Hook 从组件中提取状态逻辑,使得这些逻辑可以单独测试并复用。Hook 使你在无需修改组件结构的情况下复用状态逻辑。 这使得在组件间或社区内共享 Hook 变得更便捷。

复杂组件变得难以理解

我们经常维护一些组件,组件起初很简单,但是逐渐会被状态逻辑和副作用充斥。每个生命周期常常包含一些不相关的逻辑。例如,组件常常在 componentDidMount 和 componentDidUpdate 中获取数据。但是,同一个 componentDidMount 中可能也包含很多其它的逻辑,如设置事件监听,而之后需在 componentWillUnmount 中清除。相互关联且需要对照修改的代码被进行了拆分,而完全不相关的代码却在同一个方法中组合在一起。如此很容易产生 bug,并且导致逻辑不一致。

在多数情况下,不可能将组件拆分为更小的粒度,因为状态逻辑无处不在。这也给测试带来了一定挑战。同时,这也是很多人将 React 与状态管理库结合使用的原因之一。但是,这往往会引入了很多抽象概念,需要你在不同的文件之间来回切换,使得复用变得更加困难。

为了解决这个问题,Hook 将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据),而并非强制按照生命周期划分。你还可以使用 reducer 来管理组件的内部状态,使其更加可预测。

我们将在使用 Effect Hook 中对此展开更多讨论。

难以理解的 class

除了代码复用和代码管理会遇到困难外,我们还发现 class 是学习 React 的一大屏障。你必须去理解 JavaScript 中 this 的工作方式,这与其他语言存在巨大差异。还不能忘记绑定事件处理器。没有稳定的语法提案,这些代码非常冗余。大家可以很好地理解 props,state 和自顶向下的数据流,但对 class 却一筹莫展。即便在有经验的 React 开发者之间,对于函数组件与 class 组件的差异也存在分歧,甚至还要区分两种组件的使用场景。

另外,React 已经发布五年了,我们希望它能在下一个五年也与时俱进。就像 Svelte,Angular,Glimmer等其它的库展示的那样,组件预编译会带来巨大的潜力。尤其是在它不局限于模板的时候。最近,我们一直在使用 Prepack 来试验 component folding,也取得了初步成效。但是我们发现使用 class 组件会无意中鼓励开发者使用一些让优化措施无效的方案。class 也给目前的工具带来了一些问题。例如,class 不能很好的压缩,并且会使热重载出现不稳定的情况。因此,我们想提供一个使代码更易于优化的 API。

为了解决这些问题,Hook 使你在非 class 的情况下可以使用更多的 React 特性。 从概念上讲,React 组件一直更像是函数。而 Hook 则拥抱了函数,同时也没有牺牲 React 的精神原则。Hook 提供了问题的解决方案,无需学习复杂的函数式或响应式编程技术。

示例

Hook 概览是开始学习 Hook 的不错选择。

渐进策略

总结:没有计划从 React 中移除 class。

大部分 React 开发者会专注于开发产品,而没时间关注每一个新 API 的发布。Hook 还很新,也许等到有更多示例和教程后,再考虑学习或使用它们也不迟。

我们也明白向 React 添加新的原生概念的门槛非常高。我们为好奇的读者准备了详细的征求意见文档,在文档中用更多细节深入讨论了我们推进这件事的动机,也在具体设计决策和相关先进技术上提供了额外的视角。

最重要的是,Hook 和现有代码可以同时工作,你可以渐进式地使用他们。 不用急着迁移到 Hook。我们建议避免任何“大规模重写”,尤其是对于现有的、复杂的 class 组件。开始“用 Hook 的方式思考”前,需要做一些思维上的转变。按照我们的经验,最好先在新的不复杂的组件中尝试使用 Hook,并确保团队中的每一位成员都能适应。在你尝试使用 Hook 后,欢迎给我们提供反馈,无论好坏。

我们准备让 Hook 覆盖所有 class 组件的使用场景,但是我们将继续为 class 组件提供支持。在 Facebook,我们有成千上万的组件用 class 书写,我们完全没有重写它们的计划。相反,我们开始在新的代码中同时使用 Hook 和 class。

State Hook

这个例子用来显示一个计数器。当你点击按钮,计数器的值就会增加:

import React,{useState} from 'react'
function Example(){
    //声明一个'count'的state变量
    const [count,setCount] = useState(0)
    return (
        <div>
          <p>You cliked {count} times</p>
          <button onClick={()=>setCount(count+1)}></button>
        </div>
    )
}

在这里,useState 就是一个 Hook (等下我们会讲到这是什么意思)。通过在函数组件里调用它来给组件添加一些内部 state。React 会在重复渲染时保留这个 state。useState 会返回一对值:当前状态和一个让你更新它的函数,你可以在事件处理函数中或其他一些地方调用这个函数。它类似 class 组件的 this.setState,但是它不会把新的 state 和旧的 state 进行合并。(我们会在使用 State Hook 里展示一个对比 useState 和 this.state 的例子)。

useState 唯一的参数就是初始 state。在上面的例子中,我们的计数器是从零开始的,所以初始 state 就是 0。值得注意的是,不同于 this.state,这里的 state 不一定要是一个对象 —— 如果你有需要,它也可以是。这个初始 state 参数只有在第一次渲染时会被用到。

声明多个 state 变量
你可以在一个组件中多次使用 State Hook:

function ExampleWithManyStates() {
  // 声明多个 state 变量!
  const [age, setAge] = useState(42);
  const [fruit, setFruit] = useState('banana');
  const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
  // ...
}

数组解构的语法让我们在调用 useState 时可以给 state 变量取不同的名字。当然,这些名字并不是 useState API 的一部分。React 假设当你多次调用 useState 的时候,你能保证每次渲染时它们的调用顺序是不变的。后面我们会再次解释它是如何工作的以及在什么场景下使用。

那么,什么是 Hook?
Hook 是一些可以让你在函数组件里“钩入” React state 及生命周期等特性的函数。Hook 不能在 class 组件中使用 —— 这使得你不使用 class 也能使用 React。(我们不推荐把你已有的组件全部重写,但是你可以在新组件里开始使用 Hook。)

React 内置了一些像 useState 这样的 Hook。你也可以创建你自己的 Hook 来复用不同组件之间的状态逻辑。我们会先介绍这些内置的 Hook。

Effect Hook

你之前可能已经在 React 组件中执行过数据获取、订阅或者手动修改过 DOM。我们统一把这些操作称为“副作用”,或者简称为“作用”。

useEffect 就是一个 Effect Hook,给函数组件增加了操作副作用的能力。它跟 class 组件中的 componentDidMount、componentDidUpdate 和 componentWillUnmount 具有相同的用途,只不过被合并成了一个 API。(我们会在使用 Effect Hook 里展示对比 useEffect 和这些方法的例子。)

例如,下面这个组件在 React 更新 DOM 后会设置一个页面标题:

import React,{useState,useEffect} from 'react'
function Example(){
    const [count,setCount] = useState(0)
    //相当于componentDidMount和componentDidUpdate;
    useEffect(()=>{
        //使用浏览器的API 更新页面标题
        document.title = `You clicked ${count} times`
    })
    return (
        <div>
          <p>You clicked {count} times</p>
          <button onClick={()=>setCount(count + 1)}>Click Me</button>
        </div>
    )
}

当你调用 useEffect 时,就是在告诉 React 在完成对 DOM 的更改后运行你的“副作用”函数。由于副作用函数是在组件内声明的,所以它们可以访问到组件的 props 和 state。默认情况下,React 会在每次渲染后调用副作用函数 —— 包括第一次渲染的时候。(我们会在使用 Effect Hook 中跟 class 组件的生命周期方法做更详细的对比。)

副作用函数还可以通过返回一个函数来指定如何“清除”副作用。例如,在下面的组件中使用副作用函数来订阅好友的在线状态,并通过取消订阅来进行清除操作:

import React,{useState,useEffect} from 'react'

function FriendStatus(props){
    const [isOnline,setIsOnline] = useState(null)
    function handleStatusChange(status){
        setIsOnline(status.isOnline)
    }
    useEffect(()=>{
        ChatAPI.subscribeToFriendStatus(props.friend.id,handleStatusChange)
        return ()=> {
            ChatAPI.unsubscribeFromFriendStatus(props.friend.id,handleStatusChange)
        }
    })
    if(isOnline === null){
        return 'loading...'
    }
    return isOnline ? 'Online':'Offline'
}

在这个示例中,React 会在组件销毁时取消对 ChatAPI 的订阅,然后在后续渲染时重新执行副作用函数。(如果传给 ChatAPI 的 props.friend.id 没有变化,你也可以告诉 React 跳过重新订阅。)

跟 useState 一样,你可以在组件中多次使用 useEffect :

function FriendStatusWithCounter(props){
    const [count,setCount] = useState(0)
    useEffect(()=>{
        document.title = `You clicked ${count} times`
    })
    const [isOnline,setIsOnline] = useState(null)
    useEffect(()=>{
        ChatAPI.subscribeToFriendStatus(props.friend.id,handleStatusChange)
        return ()=>{
        ChatAPI.unsubscribeFromFriendStatus(props.friend.id,handleStatusChange)
        }
    }) 
    function handleStatusChange(status){
        setInOnline(status.isOnline)
    }
}

通过使用 Hook,你可以把组件内相关的副作用组织在一起(例如创建订阅及取消订阅),而不要把它们拆分到不同的生命周期函数里。

Hook 使用规则

Hook 就是 JavaScript 函数,但是使用它们会有两个额外的规则:

  • 只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用。
  • 只能在 React 的函数组件中调用 Hook。不要在其他 JavaScript 函数中调用。(还有一个地方可以调用 Hook —— 就是自定义的 Hook 中,我们稍后会学习到。)

同时,我们提供了 linter 插件来自动执行这些规则。这些规则乍看起来会有一些限制和令人困惑,但是要让 Hook 正常工作,它们至关重要。

自定义 Hook

有时候我们会想要在组件之间重用一些状态逻辑。目前为止,有两种主流方案来解决这个问题:高阶组件和 render props。自定义 Hook 可以让你在不增加组件的情况下达到同样的目的。

前面,我们介绍了一个叫 FriendStatus 的组件,它通过调用 useState 和 useEffect 的 Hook 来订阅一个好友的在线状态。假设我们想在另一个组件里重用这个订阅逻辑。

首先,我们把这个逻辑抽取到一个叫做 useFriendStatus 的自定义 Hook 里:

import React,{useState,useEffect} from 'react'
function useFriendStatus(friendId){
    const [isOnline,setIsOnline] = useState()
    function handleStatusChange(status){
        setIsOnline(status.isOnline)
    }
    useEffect(()=>{
        ChatAPI.subscribeToFriendStatus(friendID,handleStatusChange)
        return ()=>{
            ChatAPI.unsubscribeFromFriendsStatus(friendID,handleStatusChange)
        }
    })
    return isOnline
}

它将 friendID 作为参数,并返回该好友是否在线:

现在我们可以在两个组件中使用它:

function FriendStatus(prop){
    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>
    )
}

这两个组件的 state 是完全独立的。Hook 是一种复用状态逻辑的方式,它不复用 state 本身。事实上 Hook 的每次调用都有一个完全独立的 state —— 因此你可以在单个组件中多次调用同一个自定义 Hook。

自定义 Hook 更像是一种约定而不是功能。如果函数的名字以 “use” 开头并调用其他 Hook,我们就说这是一个自定义 Hook。 useSomething 的命名约定可以让我们的 linter 插件在使用 Hook 的代码中找到 bug。

你可以创建涵盖各种场景的自定义 Hook,如表单处理、动画、订阅声明、计时器,甚至可能还有更多我们没想到的场景。我们很期待看到 React 社区会出现什么样的自定义 Hook。

其他 Hook

除此之外,还有一些使用频率较低的但是很有用的 Hook。比如,useContext 让你不使用组件嵌套就可以订阅 React 的 Context。

function Example() {
  const locale = useContext(LocaleContext);
  const theme = useContext(ThemeContext);
  // ...
}

另外 useReducer 可以让你通过 reducer 来管理组件本地的复杂 state。

function Todos() {
  const [todos, dispatch] = useReducer(todosReducer);
  // ...

使用 State Hook

Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。

Hook 简介章节中使用下面的例子介绍了 Hook:

import React,{useState} from 'react'
function Example(){
    //声明一个叫"count"的state变量
    const [count,setCount] = useState(0)
    return (
        <div>
          <p>You Clicked {count} times</p>
          <button onClick={()=> setCount(count+1)}>Click Me</button>
        </div>
    )
}

我们将通过将这段代码与一个等价的 class 示例进行比较来开始学习 Hook。

等价的 class 示例
如果你之前在 React 中使用过 class,这段代码看起来应该很熟悉:、

class Example extends React.Component{
    constructor(props){
        super(props)
        this.state = {
            count:0
        }
    }
    render(){
        return(
            <div>
              <p>You Cliked {this.state.count} times</p>
              <button onClick={()=> this.setState({count:this.state.count +1 })}>Cliked Me</button>
            </div>
        )
    }
}

state 初始值为 { count: 0 } ,当用户点击按钮后,我们通过调用 this.setState() 来增加 state.count。整个章节中都将使用该 class 的代码片段做示例。

注意

你可能想知道为什么我们在这里使用一个计数器例子而不一个更实际的示例。因为我们还只是初步接触 Hook ,这可以帮助我们将注意力集中到 API 本身。

Hook 和函数组件

复习一下, React 的函数组件是这样的:

const Example = (props) => {
  // 你可以在这使用 Hook
  return <div />;
}

或是这样:

function Example(props) {
  // 你可以在这使用 Hook
  return <div />;
}

你之前可能把它们叫做“无状态组件”。但现在我们为它们引入了使用 React state 的能力,所以我们更喜欢叫它”函数组件”。

Hook 在 class 内部是不起作用的。但你可以使用它们来取代 class 。

Hook 是什么?

在新示例中,首先引入 React 中 useState 的 Hook

import React, { useState } from 'react';

function Example() {
  // ...
}

Hook 是什么? Hook 是一个特殊的函数,它可以让你“钩入” React 的特性。例如,useState 是允许你在 React 函数组件中添加 state 的 Hook。稍后我们将学习其他 Hook。

什么时候我会用 Hook? 如果你在编写函数组件并意识到需要向其添加一些 state,以前的做法是必须将其它转化为 class。现在你可以在现有的函数组件中使用 Hook。

注意:

在组件中有些特殊的规则,规定什么地方能使用 Hook,什么地方不能使用。我们将在 Hook 规则中学习它们。

声明 State 变量

在 class 中,我们通过在构造函数中设置 this.state 为 { count: 0 } 来初始化 count state 为 0:

class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

在函数组件中,我们没有 this,所以我们不能分配或读取 this.state。我们直接在组件中调用 useState Hook:

import React, { useState } from 'react';

function Example() {
  // 声明一个叫 “count” 的 state 变量
  const [count, setCount] = useState(0);

我们声明了一个叫 count 的 state 变量,然后把它设为 0。React 会在重复渲染时记住它当前的值,并且提供最新的值给我们的函数。我们可以通过调用 setCount 来更新当前的 count。

注意

你可能想知道:为什么叫 useState 而不叫 createState?

“Create” 可能不是很准确,因为 state 只在组件首次渲染的时候被创建。在下一次重新渲染时,useState 返回给我们当前的 state。否则它就不是 “state”了!这也是 Hook 的名字总是以 use 开头的一个原因。我们将在后面的 Hook 规则中了解原因。

读取 State

当我们想在 class 中显示当前的 count,我们读取 this.state.count:

  <p>You clicked {this.state.count} times</p>

在函数中,我们可以直接用 count:

 <p>You clicked {count} times</p>
更新 State

在 class 中,我们需要调用 this.setState() 来更新 count 值:

  <button onClick={() => this.setState({ count: this.state.count + 1 })}>
    Click me
  </button>

在函数中,我们已经有了 setCount 和 count 变量,所以我们不需要 this:

<button onClick={() => setCount(count + 1)}>
    Click me
  </button>
总结
 import React, { 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 中的 useState Hook。它让我们在函数组件中存储内部 state。
第四行: 在 Example 组件内部,我们通过调用 useState Hook 声明了一个新的 state 变量。它返回一对值给到我们命名的变量上。我们把变量命名为 count,因为它存储的是点击次数。我们通过传 0 作为 useState 唯一的参数来将其初始化为 0。第二个返回的值本身就是一个函数。它让我们可以更新 count 的值,所以我们叫它 setCount。
第九行: 当用户点击按钮后,我们传递一个新的值给 setCount。React 会重新渲染 Example 组件,并把最新的 count 传给它。

提示:方括号有什么用?
你可能注意到我们用方括号定义了一个 state 变量

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

等号左边名字并不是 React API 的部分,你可以自己取名字:

  const [fruit, setFruit] = useState('banana');

这种 JavaScript 语法叫数组解构。它意味着我们同时创建了 fruit 和 setFruit 两个变量,fruit 的值为 useState 返回的第一个值,setFruit 是返回的第二个值。它等价于下面的代码:

var fruitStateVariable = useState('banana'); // 返回一个有两个元素的数组
  var fruit = fruitStateVariable[0]; // 数组里的第一个值
  var setFruit = fruitStateVariable[1]; // 数组里的第二个值

当我们使用 useState 定义 state 变量时候,它返回一个有两个值的数组。第一个值是当前的 state,第二个值是更新 state 的函数。使用 [0] 和 [1] 来访问有点令人困惑,因为它们有特定的含义。这就是我们使用数组解构的原因。

使用多个 state 变量

将 state 变量声明为一对 [something, setSomething] 也很方便,因为如果我们想使用多个 state 变量,它允许我们给不同的 state 变量取不同的名称:

function ExampleWithManyStates() {
  // 声明多个 state 变量
  const [age, setAge] = useState(42);
  const [fruit, setFruit] = useState('banana');
  const [todos, setTodos] = useState([{ text: '学习 Hook' }]);

在以上组件中,我们有局部变量 age,fruit 和 todos,并且我们可以单独更新它们:

function handleOrangeClick() {
    // 和 this.setState({ fruit: 'orange' }) 类似
    setFruit('orange');
  }

你不必使用多个 state 变量。State 变量可以很好地存储对象和数组,因此,你仍然可以将相关数据分为一组。然而,不像 class 中的 this.setState,更新 state 变量总是替换它而不是合并它。

React 提供的 useState Hook,有时候我们也叫它 “State Hook”。它让我们在 React 函数组件上添加内部 state —— 这是我们首次尝试。

我们还学到了一些知识比如什么是 Hook。Hook 是能让你在函数组件中“钩入” React 特性的函数。它们名字通常都以 use 开始

使用 Effect Hook

Effect Hook 可以让你在函数组件中执行副作用操作

import React,{useState,useEffect} from 'react'
function Example(){
    const [count,setCount] = useState(0)
    //Similar to componentDidMount and componentDidUpdate
    useEffect(()=>{
        document.title = `You Clicked ${count} times`
    })
    return(
        <div>
          <p>You Clicked {count} times</p>
          <button onClick={()=>setCount(count + 1)}>Click ME</button>
        </div>
    )
}

这段代码基于上一章节中的计数器示例进行修改,我们为计数器增加了一个小功能:将 document 的 title 设置为包含了点击次数的消息。

数据获取,设置订阅以及手动更改 React 组件中的 DOM 都属于副作用。不管你知不知道这些操作,或是“副作用”这个名字,应该都在组件中使用过它们。

提示

如果你熟悉 React class 的生命周期函数,你可以把 useEffect Hook 看做 componentDidMount,componentDidUpdate 和 componentWillUnmount 这三个函数的组合。

在 React 组件中有两种常见副作用操作:需要清除的和不需要清除的。我们来更仔细地看一下他们之间的区别。

无需清除的 effect

有时候,我们只想在 React 更新 DOM 之后运行一些额外的代码。比如发送网络请求,手动变更 DOM,记录日志,这些都是常见的无需清除的操作。因为我们在执行完这些操作之后,就可以忽略他们了。让我们对比一下使用 class 和 Hook 都是怎么实现这些副作用的。

使用 class 的示例

在 React 的 class 组件中,render 函数是不应该有任何副作用的。一般来说,在这里执行操作太早了,我们基本上都希望在 React 更新 DOM 之后才执行我们的操作。

这就是为什么在 React class 中,我们把副作用操作放到 componentDidMount 和 componentDidUpdate 函数中。回到示例中,这是一个 React 计数器的 class 组件。它在 React 对 DOM 进行操作之后,立即更新了 document 的 title 属性

class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  componentDidMount() {
    document.title = `You clicked ${this.state.count} times`;
  }
  componentDidUpdate() {
    document.title = `You clicked ${this.state.count} times`;
  }

  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click me
        </button>
      </div>
    );
  }
}

注意,在这个 class 中,我们需要在两个生命周期函数中编写重复的代码。

这是因为很多情况下,我们希望在组件加载和更新时执行同样的操作。从概念上说,我们希望它在每次渲染之后执行 —— 但 React 的 class 组件没有提供这样的方法。即使我们提取出一个方法,我们还是要在两个地方调用它。

现在让我们来看看如何使用 useEffect 执行相同的操作。

使用 Hook 的示例

我们在本章节开始时已经看到了这个示例,但让我们再仔细观察它:

import React, { useState, useEffect } from 'react';

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

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

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

useEffect 做了什么? 通过使用这个 Hook,你可以告诉 React 组件需要在渲染后执行某些操作。React 会保存你传递的函数(我们将它称之为 “effect”),并且在执行 DOM 更新之后调用它。在这个 effect 中,我们设置了 document 的 title 属性,不过我们也可以执行数据获取或调用其他命令式的 API。

为什么在组件内部调用 useEffect? 将 useEffect 放在组件内部让我们可以在 effect 中直接访问 count state 变量(或其他 props)。我们不需要特殊的 API 来读取它 —— 它已经保存在函数作用域中。Hook 使用了 JavaScript 的闭包机制,而不用在 JavaScript 已经提供了解决方案的情况下,还引入特定的 React API。

useEffect 会在每次渲染后都执行吗? 是的,默认情况下,它在第一次渲染之后和每次更新之后都会执行。(我们稍后会谈到如何控制它。)你可能会更容易接受 effect 发生在“渲染之后”这种概念,不用再去考虑“挂载”还是“更新”。React 保证了每次运行 effect 的同时,DOM 都已经更新完毕。

详细说明
现在我们已经对 effect 有了大致了解,下面这些代码应该不难看懂了:

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

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });
}

我们声明了 count state 变量,并告诉 React 我们需要使用 effect。紧接着传递函数给 useEffect Hook。此函数就是我们的 effect。然后使用 document.title 浏览器 API 设置 document 的 title。我们可以在 effect 中获取到最新的 count 值,因为他在函数的作用域内。当 React 渲染组件时,会保存已使用的 effect,并在更新完 DOM 后执行它。这个过程在每次渲染时都会发生,包括首次渲染。

经验丰富的 JavaScript 开发人员可能会注意到,传递给 useEffect 的函数在每次渲染中都会有所不同,这是刻意为之的。事实上这正是我们可以在 effect 中获取最新的 count 的值,而不用担心其过期的原因。每次我们重新渲染,都会生成新的 effect,替换掉之前的。某种意义上讲,effect 更像是渲染结果的一部分 —— 每个 effect “属于”一次特定的渲染。我们将在本章节后续部分更清楚地了解这样做的意义。

提示

与 componentDidMount 或 componentDidUpdate 不同,使用 useEffect 调度的 effect 不会阻塞浏览器更新屏幕,这让你的应用看起来响应更快。大多数情况下,effect 不需要同步地执行。在个别情况下(例如测量布局),有单独的 useLayoutEffect Hook 供你使用,其 API 与 useEffect 相同。

需要清除的 effect

之前,我们研究了如何使用不需要清除的副作用,还有一些副作用是需要清除的。例如订阅外部数据源。这种情况下,清除工作是非常重要的,可以防止引起内存泄露!现在让我们来比较一下如何用 Class 和 Hook 来实现。

使用 Class 的示例
在 React class 中,你通常会在 componentDidMount 中设置订阅,并在 componentWillUnmount 中清除它。例如,假设我们有一个 ChatAPI 模块,它允许我们订阅好友的在线状态。以下是我们如何使用 class 订阅和显示该状态:

import React from 'react'
 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':'Off'
     }
 }

你会注意到 componentDidMount 和 componentWillUnmount 之间相互对应。使用生命周期函数迫使我们拆分这些逻辑代码,即使这两部分代码都作用于相同的副作用。

使用 Hook 的示例

如何使用 Hook 编写这个组件。

你可能认为需要单独的 effect 来执行清除操作。但由于添加和删除订阅的代码的紧密性,所以 useEffect 的设计是在同一个地方执行。如果你的 effect 返回一个函数,React 将会在执行清除操作时调用它:

import React,{useState,useEffect} from 'react'

 function FriendStatus(props){
     const [isOnline,setIsOnline] = useState(null)
     useEffect(()=>{
         function handleStatusChange(status){
             setIsOnline(status.isOnline)
         }
         ChatAPI.subscribeToFriendStatus(props.friend.id,handleStatusChange)
         return function cleanup(){
             ChatAPI.unscribeFromFriendStatus(props.friend.id,handleStatusChange)
         }
     })
     if(isOnline === 'null'){
         return 'loading...'
     }
     return isOnline ? 'Online':'Off'
 }

为什么要在 effect 中返回一个函数? 这是 effect 可选的清除机制。每个 effect 都可以返回一个清除函数。如此可以将添加和移除订阅的逻辑放在一起。它们都属于 effect 的一部分。

React 何时清除 effect? React 会在组件卸载的时候执行清除操作。正如之前学到的,effect 在每次渲染的时候都会执行。这就是为什么 React 会在执行当前 effect 之前对上一个 effect 进行清除。我们稍后将讨论为什么这将助于避免 bug以及如何在遇到性能问题时跳过此行为。

注意

并不是必须为 effect 中返回的函数命名。这里我们将其命名为 cleanup 是为了表明此函数的目的,但其实也可以返回一个箭头函数或者给起一个别的名字。

解了 useEffect 可以在组件渲染后实现各种不同的副作用。有些副作用可能需要清除,所以需要返回一个函数:

  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

其他的 effect 可能不必清除,所以不需要返回。

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

effect Hook 使用同一个 API 来满足这两种情况。

使用 Effect 的提示

在本节中将继续深入了解 useEffect 的某些特性,有经验的 React 使用者可能会对此感兴趣。你不一定要在现在了解他们,你可以随时查看此页面以了解有关 Effect Hook 的更多详细信息。

提示: 使用多个 Effect 实现关注点分离
使用 Hook 其中一个目的就是要解决 class 中生命周期函数经常包含不相关的逻辑,但又把相关逻辑分离到了几个不同方法中的问题。下述代码是将前述示例中的计数器和好友在线状态指示器逻辑组合在一起的组件:

class FriendStatusWithCounter extends React.Component{
     constructor(props){
         super(props)
         this.state = {count:0,isOnline:null}
         this.handleStatusChange = this.handleStatusChange.bind(this)
     }
     componentDidMount(){
         document.title = `You Clicked ${this.state.count} times`
         ChatAPI.subscribeToFriendStatus(
             this.props.friend.id,
             this.handleStatusChange
         )
     }
     componentDidUpdate(){
         document.title = `You clicked ${this.state.count} times`
     }
     componentWillUnmount(){
         ChatAPI.unsubscribeFromFriendStatus(
             this.props.friend.id,
             this.handleStatusChange
         )
     }
     handleStatusChange(status){
         this.setState({
             isOnline:status.isOnline
         })
     }
 }

可以发现设置 document.title 的逻辑是如何被分割到 componentDidMount 和 componentDidUpdate 中的,订阅逻辑又是如何被分割到 componentDidMount 和 componentWillUnmount 中的。而且 componentDidMount 中同时包含了两个不同功能的代码。

那么 Hook 如何解决这个问题呢?就像你可以使用多个 state 的 Hook 一样,你也可以使用多个 effect。这会将不相关逻辑分离到不同的 effect 中:

function FriendStatusWithCounter(props){
     const [count,setCount] = useState(0)
     useEffect(()=>{
         document.title = `You Clicked ${count} times`
     })
     const [isOnline,setIsOnline] = useState(null)
     useEffect(()=>{
         function handleStatusChange(status){
             setIsOnline(status.isOnline)
         }
         ChatAPI.subscribeToFriendStatus(props.friend.id,handleStatusChange)
         return ()=>{
             ChatAPI.unsubscribeFromFriendStatus(props.friend.id,handleStatusChange)
         }
     })
 }

Hook 允许我们按照代码的用途分离他们, 而不是像生命周期函数那样。React 将按照 effect 声明的顺序依次调用组件中的每一个 effect。

解释: 为什么每次更新的时候都要运行 Effect

如果你已经习惯了使用 class,那么你或许会疑惑为什么 effect 的清除阶段在每次重新渲染时都会执行,而不是只在卸载组件的时候执行一次。让我们看一个实际的例子,看看为什么这个设计可以帮助我们创建 bug 更少的组件。

在本章节开始时,我们介绍了一个用于显示好友是否在线的 FriendStatus 组件。从 class 中 props 读取 friend.id,然后在组件挂载后订阅好友的状态,并在卸载组件的时候取消订阅:

  componentDidMount() {
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

但是当组件已经显示在屏幕上时,friend prop 发生变化时会发生什么? 我们的组件将继续展示原来的好友状态。这是一个 bug。而且我们还会因为取消订阅时使用错误的好友 ID 导致内存泄露或崩溃的问题。

在 class 组件中,我们需要添加 componentDidUpdate 来解决这个问题:

componentDidMount() {
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  componentDidUpdate(prevProps) {
    // 取消订阅之前的 friend.id
    ChatAPI.unsubscribeFromFriendStatus(
      prevProps.friend.id,
      this.handleStatusChange
    );
    // 订阅新的 friend.id
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

忘记正确地处理 componentDidUpdate 是 React 应用中常见的 bug 来源。

现在看一下使用 Hook 的版本:

function FriendStatus(props) {
  // ...
  useEffect(() => {
    // ...
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

它并不会受到此 bug 影响。(虽然我们没有对它做任何改动。)

并不需要特定的代码来处理更新逻辑,因为 useEffect 默认就会处理。它会在调用一个新的 effect 之前对前一个 effect 进行清理。为了说明这一点,下面按时间列出一个可能会产生的订阅和取消订阅操作调用序列:

// Mount with { friend: { id: 100 } } props
ChatAPI.subscribeToFriendStatus(100, handleStatusChange);     // 运行第一个 effect

// Update with { friend: { id: 200 } } props
ChatAPI.unsubscribeFromFriendStatus(100, handleStatusChange); // 清除上一个 effect
ChatAPI.subscribeToFriendStatus(200, handleStatusChange);     // 运行下一个 effect

// Update with { friend: { id: 300 } } props
ChatAPI.unsubscribeFromFriendStatus(200, handleStatusChange); // 清除上一个 effect
ChatAPI.subscribeToFriendStatus(300, handleStatusChange);     // 运行下一个 effect

// Unmount
ChatAPI.unsubscribeFromFriendStatus(300, handleStatusChange); // 清除最后一个 effect

此默认行为保证了一致性,避免了在 class 组件中因为没有处理更新逻辑而导致常见的 bug。

提示: 通过跳过 Effect 进行性能优化

在某些情况下,每次渲染后都执行清理或者执行 effect 可能会导致性能问题。在 class 组件中,我们可以通过在 componentDidUpdate 中添加对 prevProps 或 prevState 的比较逻辑解决:

componentDidUpdate(prevProps, prevState) {
  if (prevState.count !== this.state.count) {
    document.title = `You clicked ${this.state.count} times`;
  }
}

这是很常见的需求,所以它被内置到了 useEffect 的 Hook API 中。如果某些特定值在两次重渲染之间没有发生变化,你可以通知 React 跳过对 effect 的调用,只要传递数组作为 useEffect 的第二个可选参数即可:


 useEffect(()=>{
   document.title = `You Clicked ${count} times`
 },[count])

上面这个示例中,我们传入 [count] 作为第二个参数。这个参数是什么作用呢?如果 count 的值是 5,而且我们的组件重渲染的时候 count 还是等于 5,React 将对前一次渲染的 [5] 和后一次渲染的 [5] 进行比较。因为数组中的所有元素都是相等的(5 === 5),React 会跳过这个 effect,这就实现了性能的优化。

当渲染时,如果 count 的值更新成了 6,React 将会把前一次渲染时的数组 [5] 和这次渲染的数组 [6] 中的元素进行对比。这次因为 5 !== 6,React 就会再次调用 effect。如果数组中有多个元素,即使只有一个元素发生变化,React 也会执行 effect。

对于有清除操作的 effect 同样适用:

useEffect(()=>{
     function handleStatusChange(status){
         setIsOnline(status.isOnline)
     }
     ChatAPI.subscribeToFriendStatus(props.friend.id,handleStatusChange)
     return ()=>{
         ChatAPI.unsubscribeFromFriendStatus(props.friend.id,handleStatusChange)
     }
 },[props.friend.id])  //仅在props.friend.id发生改变时,重新订阅

未来版本,可能会在构建时自动添加第二个参数。

注意:

如果你要使用此优化方式,请确保数组中包含了所有外部作用域中会随时间变化并且在 effect 中使用的变量,否则你的代码会引用到先前渲染中的旧变量。参阅文档,了解更多关于如何处理函数以及数组频繁变化时的措施内容。

如果想执行只运行一次的 effect(仅在组件挂载和卸载时执行),可以传递一个空数组([])作为第二个参数。这就告诉 React 你的 effect 不依赖于 props 或 state 中的任何值,所以它永远都不需要重复执行。这并不属于特殊情况 —— 它依然遵循依赖数组的工作方式。

如果你传入了一个空数组([]),effect 内部的 props 和 state 就会一直拥有其初始值。尽管传入 [] 作为第二个参数更接近大家更熟悉的 componentDidMount 和 componentWillUnmount 思维模式,但我们有更好的方式来避免过于频繁的重复调用 effect。除此之外,请记得 React 会等待浏览器完成画面渲染之后才会延迟调用 useEffect,因此会使得额外操作很方便。

我们推荐启用 eslint-plugin-react-hooks 中的 exhaustive-deps 规则。此规则会在添加错误依赖时发出警告并给出修复建议。

Hook 规则

Hook 本质就是 JavaScript 函数,但是在使用它时需要遵循两条规则。我们提供了一个 linter 插件来强制执行这些规则:

只在最顶层使用 Hook

不要在循环,条件或嵌套函数中调用 Hook, 确保总是在你的 React 函数的最顶层调用他们。遵守这条规则,你就能确保 Hook 在每一次渲染中都按照同样的顺序被调用。这让 React 能够在多次的 useState 和 useEffect 调用之间保持 hook 状态的正确。(如果你对此感到好奇,我们在下面会有更深入的解释。)

只在 React 函数中调用 Hook
**不要在普通的 JavaScript 函数中调用 Hook。**你可以:

✅ 在 React 的函数组件中调用 Hook
✅ 在自定义 Hook 中调用其他 Hook (我们将会在下一页 中学习这个。)
遵循此规则,确保组件的状态逻辑在代码中清晰可见。

ESLint 插件

我们发布了一个名为 eslint-plugin-react-hooks 的 ESLint 插件来强制执行这两条规则。如果你想尝试一下,可以将此插件添加到你的项目中:

我们打算后续版本默认添加此插件到 Create React App 及其他类似的工具包中。

npm install eslint-plugin-react-hooks --save-dev

// 你的 ESLint 配置
{
  "plugins": [
    // ...
    "react-hooks"
  ],
  "rules": {
    // ...
    "react-hooks/rules-of-hooks": "error", // 检查 Hook 的规则
    "react-hooks/exhaustive-deps": "warn" // 检查 effect 的依赖
  }
}
说明

正如我们之前学到的,我们可以在单个组件中使用多个 State Hook 或 Effect 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;
  });

  // ...
}

那么 React 怎么知道哪个 state 对应哪个 useState?答案是 React 靠的是 Hook 调用的顺序。因为我们的示例中,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)     // 2. 替换保存 form 的 effect
useState('Poppins')        // 3. 读取变量名为 surname 的 state(参数被忽略)
useEffect(updateTitle)     // 4. 替换更新标题的 effect

// ...

只要 Hook 的调用顺序在多次渲染之间保持一致,React 就能正确地将内部 state 和对应的 Hook 进行关联。但如果我们将一个 Hook (例如 persistForm effect) 调用放到一个条件语句中会发生什么呢?

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

在第一次渲染中 name !== ‘’ 这个条件值为 true,所以我们会执行这个 Hook。但是下一次渲染时我们可能清空了表单,表达式值变为 false。此时的渲染会跳过该 Hook,Hook 的调用顺序发生了改变:

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

React 不知道第二个 useState 的 Hook 应该返回什么。React 会以为在该组件中第二个 Hook 的调用像上次的渲染一样,对应的是 persistForm 的 effect,但并非如此。从这里开始,后面的 Hook 调用都被提前执行,导致 bug 的产生。

这就是为什么 Hook 需要在我们组件的最顶层调用。如果我们想要有条件地执行一个 effect,可以将判断放到 Hook 的内部:

useEffect(function persistForm() {
    // 👍 将条件判断放置在 effect 中
    if (name !== '') {
      localStorage.setItem('formData', name);
    }
  });

注意:如果使用了提供的 lint 插件,就无需担心此问题。 不过你现在知道了为什么 Hook 会这样工作,也知道了这个规则是为了避免什么问题。

下一步

最后,接下来会学习如何编写自定义 Hook!自定义 Hook 可以将 React 中提供的 Hook 组合到定制的 Hook 中,以复用不同组件之间常见的状态逻辑。

自定义 Hook

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

在我们学习使用 Effect Hook 时,我们已经见过这个聊天程序中的组件,该组件用于显示好友的在线状态:

import React, { useState, useEffect } from 'react';

function FriendStatus(props) {
  const [isOnline, setIsOnline] = useState(null);
  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}

现在我们假设聊天应用中有一个联系人列表,当用户在线时需要把名字设置为绿色。我们可以把上面类似的逻辑复制并粘贴到 FriendListItem 组件中来,但这并不是理想的解决方案:

import React, { useState, useEffect } from 'react';

function FriendListItem(props) {
  const [isOnline, setIsOnline] = useState(null);
  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

  return (
    <li style={{ color: isOnline ? 'green' : 'black' }}>
      {props.friend.name}
    </li>
  );
}

相反,我们希望在 FriendStatus 和 FriendListItem 之间共享逻辑。

目前为止,在 React 中有两种流行的方式来共享组件之间的状态逻辑: render props 和高阶组件,现在让我们来看看 Hook 是如何在让你不增加组件的情况下解决相同问题的。

提取自定义 Hook
当我们想在两个函数之间共享逻辑时,我们会把它提取到第三个函数中。而组件和 Hook 都是函数,所以也同样适用这种方式。

自定义 Hook 是一个函数,其名称以 “use” 开头,函数内部可以调用其他的 Hook。 例如,下面的 useFriendStatus 是我们第一个自定义的 Hook:

import { useState, useEffect } from 'react';

function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);

  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
    };
  });

  return isOnline;
}

此处并未包含任何新的内容——逻辑是从上述组件拷贝来的。与组件中一致,请确保只在自定义 Hook 的顶层无条件地调用其他 Hook。

与 React 组件不同的是,自定义 Hook 不需要具有特殊的标识。我们可以自由的决定它的参数是什么,以及它应该返回什么(如果需要的话)。换句话说,它就像一个正常的函数。但是它的名字应该始终以 use 开头,这样可以一眼看出其符合 Hook 的规则。

此处 useFriendStatus 的 Hook 目的是订阅某个好友的在线状态。这就是我们需要将 friendID 作为参数,并返回这位好友的在线状态的原因。

function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);

  // ...

  return isOnline;
}

现在让我们看看应该如何使用自定义 Hook。

使用自定义 Hook
我们一开始的目标是在 FriendStatus 和 FriendListItem 组件中去除重复的逻辑,即:这两个组件都想知道好友是否在线。

现在我们已经把这个逻辑提取到 useFriendStatus 的自定义 Hook 中,然后就可以使用它了:

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

这段代码等价于原来的示例代码吗?等价,它的工作方式完全一样。如果你仔细观察,你会发现我们没有对其行为做任何的改变,我们只是将两个函数之间一些共同的代码提取到单独的函数中。自定义 Hook 是一种自然遵循 Hook 设计的约定,而并不是 React 的特性。

自定义 Hook 必须以 “use” 开头吗?必须如此。这个约定非常重要。不遵循的话,由于无法判断某个函数是否包含对其内部 Hook 的调用,React 将无法自动检查你的 Hook 是否违反了 Hook 的规则。

在两个组件中使用相同的 Hook 会共享 state 吗?不会。自定义 Hook 是一种重用状态逻辑的机制(例如设置为订阅并存储当前值),所以每次使用自定义 Hook 时,其中的所有 state 和副作用都是完全隔离的。

自定义 Hook 如何获取独立的 state?每次调用 Hook,它都会获取独立的 state。由于我们直接调用了 useFriendStatus,从 React 的角度来看,我们的组件只是调用了 useState 和 useEffect。 正如我们在之前章节中了解到的一样,我们可以在一个组件中多次调用 useState 和 useEffect,它们是完全独立的。

提示:在多个 Hook 之间传递信息
由于 Hook 本身就是函数,因此我们可以在它们之间传递信息。

我们将使用聊天程序中的另一个组件来说明这一点。这是一个聊天消息接收者的选择器,它会显示当前选定的好友是否在线:

const friendList = [

  { id: 1, name: 'Phoebe' },
  { id: 2, name: 'Rachel' },
  { id: 3, name: 'Ross' },
];

function ChatRecipientPicker() {
  const [recipientID, setRecipientID] = useState(1);
  const isRecipientOnline = useFriendStatus(recipientID);

  return (
    <>
      <Circle color={isRecipientOnline ? 'green' : 'red'} />
      <select
        value={recipientID}
        onChange={e => setRecipientID(Number(e.target.value))}
      >
        {friendList.map(friend => (
          <option key={friend.id} value={friend.id}>
            {friend.name}
          </option>
        ))}
      </select>
    </>
  );
}

我们将当前选择的好友 ID 保存在 recipientID 状态变量中,并在用户从 中选择其他好友时更新这个 state。

由于 useState 为我们提供了 recipientID 状态变量的最新值,因此我们可以将它作为参数传递给自定义的 useFriendStatus Hook:

const [recipientID, setRecipientID] = useState(1);
const isRecipientOnline = useFriendStatus(recipientID);
如此可以让我们知道当前选中的好友是否在线。当我们选择不同的好友并更新 recipientID 状态变量时,useFriendStatus Hook 将会取消订阅之前选中的好友,并订阅新选中的好友状态。

useYourImagination()
自定义 Hook 解决了以前在 React 组件中无法灵活共享逻辑的问题。你可以创建涵盖各种场景的自定义 Hook,如表单处理、动画、订阅声明、计时器,甚至可能还有其他我们没想到的场景。更重要的是,创建自定义 Hook 就像使用 React 内置的功能一样简单。

尽量避免过早地增加抽象逻辑。既然函数组件能够做的更多,那么代码库中函数组件的代码行数可能会剧增。这属于正常现象 —— 不必立即将它们拆分为 Hook。但我们仍鼓励你能通过自定义 Hook 寻找可能,以达到简化代码逻辑,解决组件杂乱无章的目的。

例如,有个复杂的组件,其中包含了大量以特殊的方式来管理的内部状态。useState 并不会使得集中更新逻辑变得容易,因此你可能更愿意使用 redux 中的 reducer 来编写。

function todosReducer(state, action) {
  switch (action.type) {
    case 'add':
      return [...state, {
        text: action.text,
        completed: false
      }];
    // ... other actions ...
    default:
      return state;
  }
}

Reducers 非常便于单独测试,且易于扩展,以表达复杂的更新逻辑。如有必要,您可以将它们分成更小的 reducer。但是,你可能还享受着 React 内部 state 带来的好处,或者可能根本不想安装其他库。

那么,为什么我们不编写一个 useReducer 的 Hook,使用 reducer 的方式来管理组件的内部 state 呢?其简化版本可能如下所示:

function useReducer(reducer, initialState) {
  const [state, setState] = useState(initialState);

  function dispatch(action) {
    const nextState = reducer(state, action);
    setState(nextState);
  }

  return [state, dispatch];
}
在组件中使用它,让 reducer 驱动它管理 state:

function Todos() {
  const [todos, dispatch] = useReducer(todosReducer, []);

  function handleAddClick(text) {
    dispatch({ type: 'add', text });
  }

  // ...
}

在复杂组件中使用 reducer 管理内部 state 的需求很常见,我们已经将 useReducer 的 Hook 内置到 React 中。你可以在 Hook API 索引中找到它使用,搭配其他内置的 Hook 一起使用。

Hook API 索引

本页面主要描述 React 中内置的 Hook API。

如果你刚开始接触 Hook,那么可能需要先查阅 Hook 概览。你也可以在 Hooks FAQ 章节中获取有用的信息。

基础 Hook

useState
useEffect
useContext

额外的 Hook

useReducer
useCallback
useMemo
useRef
useImperativeHandle
useLayoutEffect
useDebugValue
基础 Hook

useState
const [state, setState] = useState(initialState);
返回一个 state,以及更新 state 的函数。

在初始渲染期间,返回的状态 (state) 与传入的第一个参数 (initialState) 值相同。

setState 函数用于更新 state。它接收一个新的 state 值并将组件的一次重新渲染加入队列。

setState(newState);
在后续的重新渲染中,useState 返回的第一个值将始终是更新后最新的 state。

注意

React 会确保 setState 函数的标识是稳定的,并且不会在组件重新渲染时发生变化。这就是为什么可以安全地从 useEffect 或 useCallback 的依赖列表中省略 setState。

函数式更新
如果新的 state 需要通过使用先前的 state 计算得出,那么可以将函数传递给 setState。该函数将接收先前的 state,并返回一个更新后的值。下面的计数器组件示例展示了 setState 的两种用法:

function Counter({initialCount}) {
  const [count, setCount] = useState(initialCount);
  return (
    <>
      Count: {count}
      <button onClick={() => setCount(initialCount)}>Reset</button>
      <button onClick={() => setCount(prevCount => prevCount - 1)}>-</button>
      <button onClick={() => setCount(prevCount => prevCount + 1)}>+</button>
    </>
  );
}

“+” 和 “-” 按钮采用函数式形式,因为被更新的 state 需要基于之前的 state。但是“重置”按钮则采用普通形式,因为它总是把 count 设置回初始值。

如果你的更新函数返回值与当前 state 完全相同,则随后的重渲染会被完全跳过。

注意

与 class 组件中的 setState 方法不同,useState 不会自动合并更新对象。你可以用函数式的 setState 结合展开运算符来达到合并更新对象的效果。

setState(prevState => {
  // 也可以使用 Object.assign
  return {...prevState, ...updatedValues};
});

useReducer 是另一种可选方案,它更适合用于管理包含多个子值的 state 对象。

惰性初始 state
initialState 参数只会在组件的初始渲染中起作用,后续渲染时会被忽略。如果初始 state 需要通过复杂计算获得,则可以传入一个函数,在函数中计算并返回初始的 state,此函数只在初始渲染时被调用:

const [state, setState] = useState(() => {
  const initialState = someExpensiveComputation(props);
  return initialState;
});

跳过 state 更新
调用 State Hook 的更新函数并传入当前的 state 时,React 将跳过子组件的渲染及 effect 的执行。(React 使用 Object.is 比较算法 来比较 state。)

需要注意的是,React 可能仍需要在跳过渲染前渲染该组件。不过由于 React 不会对组件树的“深层”节点进行不必要的渲染,所以大可不必担心。如果你在渲染期间执行了高开销的计算,则可以使用 useMemo 来进行优化。

useEffect

useEffect(didUpdate);

该 Hook 接收一个包含命令式、且可能有副作用代码的函数。

在函数组件主体内(这里指在 React 渲染阶段)改变 DOM、添加订阅、设置定时器、记录日志以及执行其他包含副作用的操作都是不被允许的,因为这可能会产生莫名其妙的 bug 并破坏 UI 的一致性。

使用 useEffect 完成副作用操作。赋值给 useEffect 的函数会在组件渲染到屏幕之后执行。你可以把 effect 看作从 React 的纯函数式世界通往命令式世界的逃生通道。

默认情况下,effect 将在每轮渲染结束后执行,但你可以选择让它 在只有某些值改变的时候 才执行。

清除 effect
通常,组件卸载时需要清除 effect 创建的诸如订阅或计时器 ID 等资源。要实现这一点,useEffect 函数需返回一个清除函数。以下就是一个创建订阅的例子:

useEffect(() => {
  const subscription = props.source.subscribe();
  return () => {
    // 清除订阅
    subscription.unsubscribe();
  };
});

为防止内存泄漏,清除函数会在组件卸载前执行。另外,如果组件多次渲染(通常如此),则在执行下一个 effect 之前,上一个 effect 就已被清除。在上述示例中,意味着组件的每一次更新都会创建新的订阅。若想避免每次更新都触发 effect 的执行,请参阅下一小节。

effect 的执行时机

与 componentDidMount、componentDidUpdate 不同的是,在浏览器完成布局与绘制之后,传给 useEffect 的函数会延迟调用。这使得它适用于许多常见的副作用场景,比如设置订阅和事件处理等情况,因此不应在函数中执行阻塞浏览器更新屏幕的操作。

然而,并非所有 effect 都可以被延迟执行。例如,在浏览器执行下一次绘制前,用户可见的 DOM 变更就必须同步执行,这样用户才不会感觉到视觉上的不一致。(概念上类似于被动监听事件和主动监听事件的区别。)React 为此提供了一个额外的 useLayoutEffect Hook 来处理这类 effect。它和 useEffect 的结构相同,区别只是调用时机不同。

虽然 useEffect 会在浏览器绘制后延迟执行,但会保证在任何新的渲染前执行。React 将在组件更新前刷新上一轮渲染的 effect。

effect 的条件执行
默认情况下,effect 会在每轮组件渲染完成后执行。这样的话,一旦 effect 的依赖发生变化,它就会被重新创建。

然而,在某些场景下这么做可能会矫枉过正。比如,在上一章节的订阅示例中,我们不需要在每次组件更新时都创建新的订阅,而是仅需要在 source prop 改变时重新创建。

要实现这一点,可以给 useEffect 传递第二个参数,它是 effect 所依赖的值数组。更新后的示例如下:

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

此时,只有当 props.source 改变后才会重新创建订阅。

注意

如果你要使用此优化方式,请确保数组中包含了所有外部作用域中会发生变化且在 effect 中使用的变量,否则你的代码会引用到先前渲染中的旧变量。请参阅文档,了解更多关于如何处理函数 以及数组频繁变化时的措施 的内容。

如果想执行只运行一次的 effect(仅在组件挂载和卸载时执行),可以传递一个空数组([])作为第二个参数。这就告诉 React 你的 effect 不依赖于 props 或 state 中的任何值,所以它永远都不需要重复执行。这并不属于特殊情况 —— 它依然遵循输入数组的工作方式。

如果你传入了一个空数组([]),effect 内部的 props 和 state 就会一直持有其初始值。尽管传入 [] 作为第二个参数有点类似于 componentDidMount 和 componentWillUnmount 的思维模式,但我们有 更好的 方式 来避免过于频繁的重复调用 effect。除此之外,请记得 React 会等待浏览器完成画面渲染之后才会延迟调用 useEffect,因此会使得处理额外操作很方便。

我们推荐启用 eslint-plugin-react-hooks 中的 exhaustive-deps 规则。此规则会在添加错误依赖时发出警告并给出修复建议。

依赖项数组不会作为参数传给 effect 函数。虽然从概念上来说它表现为:所有 effect 函数中引用的值都应该出现在依赖项数组中。未来编译器会更加智能,届时自动创建数组将成为可能。

useContext
const value = useContext(MyContext);

接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider> 的 value prop 决定。

当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext provider 的 context value 值。即使祖先使用 React.memo 或 shouldComponentUpdate,也会在组件本身使用 useContext 时重新渲染。

别忘记 useContext 的参数必须是 context 对象本身:

正确: useContext(MyContext)
错误: useContext(MyContext.Consumer)
错误: useContext(MyContext.Provider)
调用了 useContext 的组件总会在 context 值变化时重新渲染。如果重渲染组件的开销较大,你可以 通过使用 memoization 来优化。

如果你在接触 Hook 前已经对 context API 比较熟悉,那应该可以理解,useContext(MyContext) 相当于 class 组件中的 static contextType = MyContext 或者 <MyContext.Consumer>。
useContext(MyContext) 只是让你能够读取 context 的值以及订阅 context 的变化。你仍然需要在上层组件树中使用 <MyContext.Provider> 来为下层组件提供 context。

把如下代码与 Context.Provider 放在一起

const themes = {
  light: {
    foreground: "#000000",
    background: "#eeeeee"
  },
  dark: {
    foreground: "#ffffff",
    background: "#222222"
  }
};

const ThemeContext = React.createContext(themes.light)

function App(){
    return (
        <ThemeContext.Provider value={themes.dark}>
            <Toolbar />
        </ThemeContext.Provider>
    )
}

function ThemedButton(){
    const theme = useContext(ThemeContext)
    return (
        <button style={{background:theme.background,color:theme.foreground}}>
            I am styled by theme context!
        </button>
    )
}

对先前 Context 高级指南中的示例使用 hook 进行了修改,你可以在链接中找到有关如何 Context 的更多信息。

额外的 Hook

以下介绍的 Hook,有些是上一节中基础 Hook 的变体,有些则仅在特殊情况下会用到。不用特意预先学习它们。

useReducer

useState 的替代方案。它接收一个形如 (state, action) => newState 的 reducer,并返回当前的 state 以及与其配套的 dispatch 方法。(如果你熟悉 Redux 的话,就已经知道它如何工作了。)

在某些场景下,useReducer 会比 useState 更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等。并且,使用 useReducer 还能给那些会触发深更新的组件做性能优化,因为你可以向子组件传递 dispatch 而不是回调函数 。

以下是用 reducer 重写 useState 一节的计数器示例:

const initialState = {count:0}
function reducer(state,action){
    switch(action.type){
        case:'increment':
            return {count:state.count + 1};
        case:'decrement':
            return {count:state.count -1 };
        default:
            throw new Error()
    }
}

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

React 会确保 dispatch 函数的标识是稳定的,并且不会在组件重新渲染时改变。这就是为什么可以安全地从 useEffect 或 useCallback 的依赖列表中省略 dispatch。

指定初始 state

有两种不同初始化 useReducer state 的方式,你可以根据使用场景选择其中的一种。将初始 state 作为第二个参数传入 useReducer 是最简单的方法:

const [state,dispatch] = useReducer(reducer,{count:initialCount})

React 不使用 state = initialState 这一由 Redux 推广开来的参数约定。有时候初始值依赖于 props,因此需要在调用 Hook 时指定。如果你特别喜欢上述的参数约定,可以通过调用 useReducer(reducer, undefined, reducer) 来模拟 Redux 的行为,但我们不鼓励你这么做。

惰性初始化

你可以选择惰性地创建初始 state。为此,需要将 init 函数作为 useReducer 的第三个参数传入,这样初始 state 将被设置为 init(initialArg)。

这么做可以将用于计算 state 的逻辑提取到 reducer 外部,这也为将来对重置 state 的 action 做处理提供了便利:

function init(initialCount){
    return {count:initialCount}
}
function reducer(state,action){
    switch(action.type){
        case 'increment':
            return {count:state.count + 1};
        case 'decrement':
            return {count:state.count -1 };
        case 'reset':
            return init(action.payload)
        default:
            throw new Error()
    }
}

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

跳过 dispatch
如果 Reducer Hook 的返回值与当前 state 相同,React 将跳过子组件的渲染及副作用的执行。(React 使用 Object.is 比较算法 来比较 state。)

需要注意的是,React 可能仍需要在跳过渲染前再次渲染该组件。不过由于 React 不会对组件树的“深层”节点进行不必要的渲染,所以大可不必担心。如果你在渲染期间执行了高开销的计算,则可以使用 useMemo 来进行优化。

useCallback
const memoizedcallback = useCallback(
    ()=>{
        doSomething(a,b)
    },
    [a,b]
)

返回一个 memoized 回调函数。

把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子组件时,它将非常有用。

useCallback(fn, deps) 相当于 useMemo(() => fn, deps)。

依赖项数组不会作为参数传给回调函数。虽然从概念上来说它表现为:所有回调函数中引用的值都应该出现在依赖项数组中。未来编译器会更加智能,届时自动创建数组将成为可能。

我们推荐启用 eslint-plugin-react-hooks 中的 exhaustive-deps 规则。此规则会在添加错误依赖时发出警告并给出修复建议。

useMemo
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

返回一个 memoized 值。

把“创建”函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。

记住,传入 useMemo 的函数会在渲染期间执行。请不要在这个函数内部执行与渲染无关的操作,诸如副作用这类的操作属于 useEffect 的适用范畴,而不是 useMemo。

如果没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值。

你可以把 useMemo 作为性能优化的手段,但不要把它当成语义上的保证。
将来,React 可能会选择“遗忘”以前的一些 memoized 值,并在下次渲染时重新计算它们,比如为离屏组件释放内存。先编写在没有 useMemo 的情况下也可以执行的代码 —— 之后再在你的代码中添加 useMemo,以达到优化性能的目的。
依赖项数组不会作为参数传给“创建”函数。虽然从概念上来说它表现为:所有“创建”函数中引用的值都应该出现在依赖项数组中。未来编译器会更加智能,届时自动创建数组将成为可能。

我们推荐启用 eslint-plugin-react-hooks 中的 exhaustive-deps 规则。此规则会在添加错误依赖时发出警告并给出修复建议。

useRef

const refContainer = useRef(initialValue);
useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内保持不变。

一个常见的用例便是命令式地访问子组件:

function TextInputWithFocusButton(){
    const inputEl = useRef(null)
    const onButtonClick = () => {
        //current指向已挂载到DOM上的文本输入元素
        inputEL.current.focus()
    }
    return(
        <>
         <input ref={inputEl} type="text"/>
         <button onClick={onButtonClick}>Focus the input</button>
        </>
    )
}

本质上,useRef 就像是可以在其 .current 属性中保存一个可变值的“盒子”。

你应该熟悉 ref 这一种访问 DOM 的主要方式。如果你将 ref 对象以

形式传入组件,则无论该节点如何改变,React 都会将 ref 对象的 .current 属性设置为相应的 DOM 节点。
然而,useRef() 比 ref 属性更有用。它可以很方便地保存任何可变值,其类似于在 class 中使用实例字段的方式。

这是因为它创建的是一个普通 Javascript 对象。而 useRef() 和自建一个 {current: …} 对象的唯一区别是,useRef 会在每次渲染时返回同一个 ref 对象。

请记住,当 ref 对象内容发生变化时,useRef 并不会通知你。变更 .current 属性不会引发组件重新渲染。如果想要在 React 绑定或解绑 DOM 节点的 ref 时运行某些代码,则需要使用回调 ref 来实现。

useImperativeHandle
useImperativeHandle(ref, createHandle, [deps])

useImperativeHandle 可以让你在使用 ref 时自定义暴露给父组件的实例值。在大多数情况下,应当避免使用 ref 这样的命令式代码。useImperativeHandle 应当与 forwardRef 一起使用:

function FancyInput(props,ref){
    const inputRef = useRef()
    useImperativeHandle(ref,()=>({
        focus:()=>{
            inputRef.current.focus()
        }
    }));
    return <input ref={inputRef}/>
}
FancyInput = forwardRef(FancyInput)

在本例中,渲染 的父组件可以调用 inputRef.current.focus()。

useLayoutEffect

其函数签名与 useEffect 相同,但它会在所有的 DOM 变更之后同步调用 effect。可以使用它来读取 DOM 布局并同步触发重渲染。在浏览器执行绘制之前,useLayoutEffect 内部的更新计划将被同步刷新。

尽可能使用标准的 useEffect 以避免阻塞视觉更新。

如果你正在将代码从 class 组件迁移到使用 Hook 的函数组件,则需要注意 useLayoutEffect 与 componentDidMount、componentDidUpdate 的调用阶段是一样的。但是,我们推荐你一开始先用 useEffect,只有当它出问题的时候再尝试使用 useLayoutEffect。

如果你使用服务端渲染,请记住,无论 useLayoutEffect 还是 useEffect 都无法在 Javascript 代码加载完成之前执行。这就是为什么在服务端渲染组件中引入 useLayoutEffect 代码时会触发 React 告警。解决这个问题,需要将代码逻辑移至 useEffect 中(如果首次渲染不需要这段逻辑的情况下),或是将该组件延迟到客户端渲染完成后再显示(如果直到 useLayoutEffect 执行之前 HTML 都显示错乱的情况下)。

若要从服务端渲染的 HTML 中排除依赖布局 effect 的组件,可以通过使用 showChild && 进行条件渲染,并使用 useEffect(() => { setShowChild(true); }, []) 延迟展示组件。这样,在客户端渲染完成之前,UI 就不会像之前那样显示错乱了。

useDebugValue
useDebugValue(value)

useDebugValue 可用于在 React 开发者工具中显示自定义 hook 的标签。

例如,“自定义 Hook” 章节中描述的名为 useFriendStatus 的自定义 Hook:

function useFriendStatus(friendID){
    const [isOnline,setIsOnline] = useState(null)
    //再开发工具中的这个HOOK旁边显示标签
    // e.g."FriendStatus:Online"
    useDebugValue(isOnline?'Online':'Offline')
    return isOnline
}

我们不推荐你向每个自定义 Hook 添加 debug 值。当它作为共享库的一部分时才最有价值。

延迟格式化 debug 值

在某些情况下,格式化值的显示可能是一项开销很大的操作。除非需要检查 Hook,否则没有必要这么做。

因此,useDebugValue 接受一个格式化函数作为可选的第二个参数。该函数只有在 Hook 被检查时才会被调用。它接受 debug 值作为参数,并且会返回一个格式化的显示值。

例如,一个返回 Date 值的自定义 Hook 可以通过格式化函数来避免不必要的 toDateString 函数调用:

useDebugValue(date, date => date.toDateString());

Hooks FAQ

哪个版本的 React 包含了 Hook?
从 16.8.0 开始,React 在以下模块中包含了 React Hook 的稳定实现:

React DOM
React Native
React DOM Server
React Test Renderer
React Shallow Renderer
请注意,要启用 Hook,所有 React 相关的 package 都必须升级到 16.8.0 或更高版本。如果你忘记更新诸如 React DOM 之类的 package,Hook 将无法运行。

React Native 0.59 及以上版本支持 Hook。


我需要重写所有的 class 组件吗?
不。我们并 没有计划 从 React 中移除 class —— 我们也需要不断地发布产品,重写成本较高。我们推荐在新代码中尝试 Hook。
有什么是 Hook 能做而 class 做不到的?
Hook 提供了强大而富有表现力的方式来在组件间复用功能。通过 「自定义 Hook」 这一节可以了解能用它做些什么。这篇来自一位 React 核心团队的成员的 文章 则更加深入地剖析了 Hook 解锁了哪些新的能力。


我的 React 知识还有多少是仍然有用的?
Hook 是使用你已经知道的 React 特性的一种更直接的方式 —— 比如 state,生命周期,context,以及 refs。它们并没有从根本上改变 React 的工作方式,你对组件,props, 以及自顶向下的数据流的知识并没有改变。

Hook 确实有它们自己的学习曲线。如果这份文档中遗失了一些什么,提一个 issue,我们会尽可能地帮你。


我应该使用 Hook,class,还是两者混用?
当你准备好了,我们鼓励你在写新组件的时候开始尝试 Hook。请确保你团队中的每个人都愿意使用它们并且熟知这份文档中的内容。我们不推荐用 Hook 重写你已有的 class,除非你本就打算重写它们。(例如:为了修复bug)。

你不能在 class 组件内部使用 Hook,但毫无疑问你可以在组件树里混合使用 class 组件和使用了 Hook 的函数组件。不论一个组件是 class 还是一个使用了 Hook 的函数,都只是这个组件的实现细节而已。长远来看,我们期望 Hook 能够成为人们编写 React 组件的主要方式。


Hook 能否覆盖 class 的所有使用场景?
我们给 Hook 设定的目标是尽早覆盖 class 的所有使用场景。目前暂时还没有对应不常用的 getSnapshotBeforeUpdate,getDerivedStateFromError 和 componentDidCatch 生命周期的 Hook 等价写法,但我们计划尽早把它们加进来。

目前 Hook 还处于早期阶段,一些第三方的库可能还暂时无法兼容 Hook
Hook 会替代 render props 和高阶组件吗?、
通常,render props 和高阶组件只渲染一个子节点。我们认为让 Hook 来服务这个使用场景更加简单。这两种模式仍有用武之地,(例如,一个虚拟滚动条组件或许会有一个 renderItem 属性,或是一个可见的容器组件或许会有它自己的 DOM 结构)。但在大部分场景下,Hook 足够了,并且能够帮助减少嵌套。
Hook 对于 Redux connect() 和 React Router 等流行的 API 来说,意味着什么?
你可以继续使用之前使用的 API;它们仍会继续有效。

React Redux 从 v7.1.0 开始支持 Hook API 并暴露了 useDispatch 和 useSelector 等 hook。

React Router 从 v5.1 开始支持 hook。

其它第三库也将即将支持 hook。
Hook 能和静态类型一起用吗?
Hook 在设计阶段就考虑了静态类型的问题。因为它们是函数,所以它们比像高阶组件这样的模式更易于设定正确的类型。最新版的 Flow 和 TypeScript React 定义已经包含了对 React Hook 的支持。

重要的是,在你需要严格限制类型的时候,自定义 Hook 能够帮你限制 React 的 API。React 只是给你提供了基础功能,具体怎么用就是你自己的事了。
如何测试使用了 Hook 的组件?

在 React 看来,一个使用了 Hook 的组件只不过是一个常规组件。如果你的测试方案不依赖于 React 的内部实现,测试带 Hook 的组件应该和你通常测试组件的方式没什么差别。

注意

测试技巧 中包含了许多可以拷贝粘贴的示例。

举个例子,比如我们有这么个计数器组件:

function Example(){
  const [count,setCount] = useState(0)
    useEffect(()=>{
        document.title = `You Clicked ${count} times`
    })
    return (
        <div>
         <p>You Clicked {count} times</p>
         <button onClick={()=>setCount(count + 1)}>Click me</button>
        </div>
    )
}

我们会使用 React DOM 来测试它。为了确保它表现得和在浏览器中一样,我们会把代码渲染的部分包裹起来,并更新为 ReactTestUtils.act() 调用:

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',()=>{
    //测试首次渲染和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')

    //测试第二次渲染和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')
})

对 act() 的调用也会清空它们内部的 effect。

如果你需要测试一个自定义 Hook,你可以在你的测试代码中创建一个组件并在其中使用你的 Hook。然后你就可以测试你刚写的组件了。

为了减少不必要的模板项目,我们推荐使用 React Testing Library,该项目旨在鼓励你按照终端用户使用组件的方式来编写测试。

欲了解更多,请参阅测试技巧一节。

lint 规则具体强制了哪些内容?

我们提供了一个 ESLint 插件 来强制 Hook 规范 以避免 Bug。它假设任何以 「use」 开头并紧跟着一个大写字母的函数就是一个 Hook。我们知道这种启发方式并不完美,甚至存在一些伪真理,但如果没有一个全生态范围的约定就没法让 Hook 很好的工作 —— 而名字太长会让人要么不愿意采用 Hook,要么不愿意遵守约定。

规范尤其强制了以下内容:

对 Hook 的调用要么在一个大驼峰法命名的函数(视作一个组件)内部,要么在另一个 useSomething 函数(视作一个自定义 Hook)中。
Hook 在每次渲染时都按照相同的顺序被调用。
还有一些其他的启发方式,但随着我们不断地调优以在发现 Bug 和避免伪真理之前取得平衡,这些方式随时会改变。

从 Class 迁移到 Hook

生命周期方法要如何对应到 Hook?

constructor:函数组件不需要构造函数。你可以通过调用 useState 来初始化 state。如果计算的代价比较昂贵,你可以传一个函数给 useState。
getDerivedStateFromProps:改为 在渲染时 安排一次更新。
shouldComponentUpdate:详见 下方 React.memo.
render:这是函数组件体本身。
componentDidMount, componentDidUpdate, componentWillUnmount:useEffect Hook 可以表达所有这些(包括 不那么 常见 的场景)的组合。
getSnapshotBeforeUpdate,componentDidCatch 以及 getDerivedStateFromError:目前还没有这些方法的 Hook 等价写法,但很快会被添加。

我该如何使用 Hook 进行数据获取?

该 demo 会帮助你开始理解。欲了解更多,请查阅 此文章 来了解如何使用 Hook 进行数据获取。

有类似实例变量的东西吗?

有!useRef() Hook 不仅可以用于 DOM refs。「ref」 对象是一个 current 属性可变且可以容纳任意值的通用容器,类似于一个 class 的实例属性。

你可以在 useEffect 内部对其进行写入:

有类似实例变量的东西吗?

有!useRef() Hook 不仅可以用于 DOM refs。「ref」 对象是一个 current 属性可变且可以容纳任意值的通用容器,类似于一个 class 的实例属性。

你可以在 useEffect 内部对其进行写入:

function Timer(){
    const intervalRef = useRef()
    useEffect(()=>{
        const id = setInterval(() => {
            
        });
        intervalRef.current = id
        return ()=>{
            clearInterval(intervalRef.current)
        }
    })
}

如果我们只是想设定一个循环定时器,我们不会需要这个 ref(id 可以是在 effect 本地的),但如果我们想要在一个事件处理器中清除这个循环定时器的话这就很有用了:

function handleCancelClick(){
    clearInterval(intervalRef.current)
}

从概念上讲,你可以认为 refs 就像是一个 class 的实例变量。除非你正在做 懒加载,否则避免在渲染期间设置 refs —— 这可能会导致意外的行为。相反的,通常你应该在事件处理器和 effects 中修改 refs。

我应该使用单个还是多个 state 变量?

如果你之前用过 class,你或许会试图总是在一次 useState() 调用中传入一个包含了所有 state 的对象。如果你愿意的话你可以这么做。这里有一个跟踪鼠标移动的组件的例子。我们在本地 state 中记录它的位置和尺寸:

function Box(){
    const [state,setState] = useState({left:0,top:0,width:100,height:100})
    // ...
}

现在假设我们想要编写一些逻辑以便在用户移动鼠标时改变 left 和 top。注意到我们是如何必须手动把这些字段合并到之前的 state 对象的:

useEffect(()=>{
 function handleWindowMouseMove(e){
     //展开 [...state] 以确保我们没有[丢失] width和height
     setState(state=>({...state,left:e.pageX,top:e.pageY}))
 }
 //注意:这个是简化版的实现
 window.addEventListener('mousemove',handleWindowMouseMove)
 return ()=> window.removeEventListener('mousemove',handleWindowMouseMove)
},[])

这是因为当我们更新一个 state 变量,我们会 替换 它的值。这和 class 中的 this.setState 不一样,后者会把更新后的字段 合并 入对象中。

如果你错过自动合并,你可以写一个自定义的 useLegacyState Hook 来合并对象 state 的更新。然而,我们推荐把 state 切分成多个 state 变量,每个变量包含的不同值会在同时发生变化。

举个例子,我们可以把组件的 state 拆分为 position 和 size 两个对象,并永远以非合并的方式去替换 position:

function Box(){
    const [position,setPosition] = useState({left:0,top:0})
    const [size,setSize] = useState({width:100,height:100})
    useEffect(()=>{
        function handleWindowMouseMove(e){
            setPosition({
                left:e.pageX,
                top:e.pageY
            })
        }
    })
}

把独立的 state 变量拆分开还有另外的好处。这使得后期把一些相关的逻辑抽取到一个自定义 Hook 变得容易,比如说:

function Box(){
    const position = useWindowPosition()
    const [size,setSize] = useState({width:100,height:100})
}
function useWindowPosition(){
    const [position,setPosition] = useState({left:0,top:0})
    useEffect(()=>{
    },[])
    return position
}

注意看我们是如何做到不改动代码就把对 position 这个 state 变量的 useState 调用和相关的 effect 移动到一个自定义 Hook 的。如果所有的 state 都存在同一个对象中,想要抽取出来就比较难了。
把所有 state 都放在同一个 useState 调用中,或是每一个字段都对应一个 useState 调用,这两方式都能跑通。当你在这两个极端之间找到平衡,然后把相关 state 组合到几个独立的 state 变量时,组件就会更加的可读。如果 state 的逻辑开始变得复杂,我们推荐 用 reducer 来管理它,或使用自定义 Hook。

我可以只在更新时运行 effect 吗?

这是个比较罕见的使用场景。如果你需要的话,你可以 使用一个可变的 ref 手动存储一个布尔值来表示是首次渲染还是后续渲染,然后在你的 effect 中检查这个标识。(如果你发现自己经常在这么做,你可以为之创建一个自定义 Hook。)

如何获取上一轮的 props 或 state?

目前,你可以 通过 ref 来手动实现:

function Counter(){
    const [count,setCount] = useState(0)
    const prevCountRef = useRef()
    useEffect(()=>{
        prevCountRef.current = count
    })
    const prevCount = prevCountRef.current
    return <h1>Now:{count},before:{prevCount}</h1>
}

这或许有一点错综复杂,但你可以把它抽取成一个自定义 Hook:

function Counter(){
    const [count,setCount] = useState(0)
    const prevCount = usePrevious(count)
    return <h1>Now:{count},Before:{prevCount}</h1>
}
function usePrevious(value){
    const ref = useRef()
    useEffect(()=>{
        ref.current = value
    })
    return ref.current
}

注意看这是如何作用于 props, state,或任何其他计算出来的值的。

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

  const calculation = count + 100;
  const prevCalculation = usePrevious(calculation);
  // ...

考虑到这是一个相对常见的使用场景,很可能在未来 React 会自带一个 usePrevious Hook。

参见 derived state 推荐模式.

为什么我会在我的函数中看到陈旧的 props 和 state ?

组件内部的任何函数,包括事件处理函数和 effect,都是从它被创建的那次渲染中被「看到」的。例如,考虑这样的代码:

function Example(){
    const [count,setCount] = useState(0)
    function handleAlertClick(){
        setTimeout(() => {
            alert('You clicked on:' + count)
        }, 3000);
    }
    return (
        <div>
           <p>You clicked {count} times</p>
           <button onClick={()=>setCount(count+1)}>Click Me</button>
           <button onClick={handleAlertClick}>show alert</button>
        </div>
    )
}

如果你先点击「Show alert」然后增加计数器的计数,那这个 alert 会显示在你点击『Show alert』按钮时的 count 变量。这避免了那些因为假设 props 和 state 没有改变的代码引起问题。

如果你刻意地想要从某些异步回调中读取 最新的 state,你可以用 一个 ref 来保存它,修改它,并从中读取。

最后,你看到陈旧的 props 和 state 的另一个可能的原因,是你使用了「依赖数组」优化但没有正确地指定所有的依赖。举个例子,如果一个 effect 指定了 [] 作为第二个参数,但在内部读取了 someProp,它会一直「看到」 someProp 的初始值。解决办法是要么移除依赖数组,要么修正它。 这里介绍了 你该如何处理函数,而这里介绍了关于如何减少 effect 的运行而不必错误的跳过依赖的 一些常见策略。

注意

我们提供了一个 exhaustive-deps ESLint 规则作为 eslint-plugin-react-hooks 包的一部分。它会在依赖被错误指定时发出警告,并给出修复建议。

我该如何实现 getDerivedStateFromProps?

尽管你可能 不需要它,但在一些罕见的你需要用到的场景下(比如实现一个 组件),你可以在渲染过程中更新 state 。React 会立即退出第一次渲染并用更新后的 state 重新运行组件以避免耗费太多性能。

这里我们把 row prop 上一轮的值存在一个 state 变量中以便比较:

function ScrollView({row}){
    const [isScrollingDown,setIsScollingDown] = useState(false)
    const [prevRow,setPrevRow] = useState(null)
    if(row !== prevRow){
        //Row自上次渲染以来发生过改变,更新isScollingDown
        setIsScollingDown(prevRow !== null && row > prevRow)
        setPrevRow(row)
    }
    return `Scolling down:${isScrollingDown}`
}

初看这或许有点奇怪,但渲染期间的一次更新恰恰就是 getDerivedStateFromProps 一直以来的概念。

有类似 forceUpdate 的东西吗?

如果前后两次的值相同,useState 和 useReducer Hook 都会放弃更新。原地修改 state 并调用 setState 不会引起重新渲染。

通常,你不应该在 React 中修改本地 state。然而,作为一条出路,你可以用一个增长的计数器来在 state 没变的时候依然强制一次重新渲染:

const [ignored,forceUpdate] = useReducer(x=>x+1,0)
function handleClick(){
    forceUpdate()
}

可能的话尽量避免这种模式。

我可以引用一个函数组件吗?
尽管你不应该经常需要这么做,但你可以通过 useImperativeHandle Hook 暴露一些命令式的方法给父组件。
我该如何测量 DOM 节点?

获取 DOM 节点的位置或是大小的基本方式是使用 callback ref。每当 ref 被附加到一个另一个节点,React 就会调用 callback。这里有一个 小 demo:

function MeasureExample(){
    const [height,setHeight] = useState(0)
     const measureRef = callback(node=>{
         if(node!==null){
             setHeight(node.getBoundingClientReact().height)
         }
     },[]);
     return (
         <>
           <h1 ref={measureRef}>Hello,world</h1>
           <h2>The above header is {Math.round(height)}px tall</h2>
         </>
     )
}

在这个案例中,我们没有选择使用 useRef,因为当 ref 是一个对象时它并不会把当前 ref 的值的 变化 通知到我们。使用 callback ref 可以确保 即便子组件延迟显示被测量的节点 (比如为了响应一次点击),我们依然能够在父组件接收到相关的信息,以便更新测量结果。

注意到我们传递了 [] 作为 useCallback 的依赖列表。这确保了 ref callback 不会在再次渲染时改变,因此 React 不会在非必要的时候调用它。

如果你愿意,你可以 把这个逻辑抽取出来作为 一个可复用的 Hook:

function MeasureExample(){
    const [react,ref] = useClientReact()
    return (
        <>
        <h1 ref={ref}>Hello,world</h1>
         {rect!==null && <h2>The above header is{Math.round(rect.height)}px tall</h2>}
        </>
    )
}

function useClientReact(){
    const [rect,setRect] = useState(null)
    const ref = useCallback(node=>{
        if(node !== null){
            setRect(node.getBoundingClientRect())
        }
    },[]);
    return [rect,ref]
}

性能优化

我可以在更新时跳过 effect 吗?

可以的。参见 条件式的发起 effect。注意,忘记处理更新常会 导致 bug,这也正是我们没有默认使用条件式 effect 的原因。

在依赖列表中省略函数是否安全?

一般来说,不安全。

function Example({someProp}){
    function doSomething(){
        console.log(someProp)
    }
    useEffect(()=>{
        doSomething()//这样不安全(它调用的doSomething 函数使用了someProp)
    },[])
}

要记住 effect 外部的函数使用了哪些 props 和 state 很难。这也是为什么 通常你会想要在 effect 内部 去声明它所需要的函数。 这样就能容易的看出那个 effect 依赖了组件作用域中的哪些值:

function Example({someProp}){
    useEffect(()=>{
        function doSomething(){
            console.log(someProp);
        }
        doSomething()
    },[someProp])  //安全,我们的effect仅用到了someProp
}

如果这样之后我们依然没用到组件作用域中的任何值,就可以安全地把它指定为 []:

useEffect(()=>{
    function doSomething(){
        console.log('hello');
    }
    doSomething()
},[]) //在这个例子中是安全的 因为我们没有用到组件作用域中的任何值

我们提供了一个 exhaustive-deps ESLint 规则作为 eslint-plugin-react-hooks 包的一部分。它会帮助你找出无法一致地处理更新的组件。

让我们来看看这有什么关系。

如果你指定了一个 依赖列表 作为 useEffect、useMemo、useCallback 或 useImperativeHandle 的最后一个参数,它必须包含回调中的所有值,并参与 React 数据流。这就包括 props、state,以及任何由它们衍生而来的东西。

只有 当函数(以及它所调用的函数)不引用 props、state 以及由它们衍生而来的值时,你才能放心地把它们从依赖列表中省略。下面这个案例有一个 Bug:

function ProductPage({productId}){
    const [product,setProduct] = useState(null)
    async function fetchProduct(){
        const response = await fetch('http://myaoi/product'+ productId) //使用了productId prop
        const json = await response.json()
        setProduct(json)
    }
    useEffect(()=>{
        fetchProduct() //这样是无效的,因为fetchProduct,使用了productId
    },[])
}

推荐的修复方案是把那个函数移动到你的 effect 内部。这样就能很容易的看出来你的 effect 使用了哪些 props 和 state,并确保它们都被声明了:

function ProductPage({productId}){
    const [product,setProduct] = useState(null)
    useEffect(()=>{
        //把这个函数移动到effect内部后,我们可以清楚地看到它用到的值
        async function fetchProduct(){
            const response = await fetch('http://myapi/product/'+ productId)
            const json = await response.json()
            setProduct(json)
        }
        fetchProduct()
    },[productId])  //有效,因为我们的effect只用到了productId
}

这同时也允许你通过 effect 内部的局部变量来处理无序的响应:

useEffect(()=>{
    let ignore = false
    async function fetchProduct(){
        const response = await fetch('http://myapi/product/'+ productId)
        const json = await response.json()
        if(!ignore) setProduct(json)
    }
    fetchProduct();
    return()=>{ignore = true}
},[productId])

我们把这个函数移动到 effect 内部,这样它就不用出现在它的依赖列表中了。
如果处于某些原因你 无法 把一个函数移动到 effect 内部,还有一些其他办法

  • 你可以尝试把那个函数移动到你的组件之外。那样一来,这个函数就肯定不会依赖任何 props 或 state,并且也不用出现在依赖列表中了。
  • 如果你所调用的方法是一个纯计算,并且可以在渲染时调用,你可以 转而在 effect 之外调用它, 并让 effect 依赖于它的返回值。
  • 万不得已的情况下,你可以 把函数加入 effect 的依赖但 把它的定义包裹 进 useCallback Hook。这就确保了它不随渲染而改变,除非 它自身 的依赖发生了改变:
function ProductPage({productId}){
    //用useCallback包裹以避免随渲染发生改变
    const fetchProduct = useCallback(()=>{
      //does something with productId
    },[productId]) //useCallback 的所有依赖都被指定了
    return <ProductDetails fetchProduct={fetchProduct}/>
}

function ProductDetails({fetchProduct}){
    useEffect(()=>{
        fetchProduct()
    },[fetchProduct])//useEffect的所有依赖都被指定了
    //...
}

注意在上面的案例中,我们 需要 让函数出现在依赖列表中。这确保了 ProductPage 的 productId prop 的变化会自动触发 ProductDetails 的重新获取。

如果我的 effect 的依赖频繁变化,我该怎么办?

有时候,你的 effect 可能会使用一些频繁变化的值。你可能会忽略依赖列表中 state,但这通常会引起 Bug:

function Counter(){
    const [count,setCount] = useState(0)
    useEffect(()=>{
        const id = setInterval(() => {
            setCount(count + 1) //这个effect依赖与 count state
        }, 1000);
        return ()=>clearInterval(id)
    },[]) //bug  count没有被指定为依赖
    return <h1>{count}</h1>
}

传入空的依赖数组 [],意味着该 hook 只在组件挂载时运行一次,并非重新渲染时。但如此会有问题,在 setInterval 的回调中,count 的值不会发生变化。因为当 effect 执行时,我们会创建一个闭包,并将 count 的值被保存在该闭包当中,且初值为 0。每隔一秒,回调就会执行 setCount(0 + 1),因此,count 永远不会超过 1。

指定 [count] 作为依赖列表就能修复这个 Bug,但会导致每次改变发生时定时器都被重置。事实上,每个 setInterval 在被清除前(类似于 setTimeout)都会调用一次。但这并不是我们想要的。要解决这个问题,我们可以使用 setState 的函数式更新形式。它允许我们指定 state 该 如何 改变而不用引用 当前 state:

function Counter(){
    const [count,setCount] = useState(0)
    useEffect(()=>{
        const id = setInterval(() => {
            setCount(c => c + 1) //在这不依赖于外部的count变量
        }, 1000);
        return () => clearInterval(id)
    },[]) //我们的effect不适用组件作用域中的任何变量
    return <h1>{count}</h1>
}

(setCount 函数的身份是被确保稳定的,所以可以放心的省略掉)
此时,setInterval 的回调依旧每秒调用一次,但每次 setCount 内部的回调取到的 count 是最新值(在回调中变量命名为 c)。

在一些更加复杂的场景中(比如一个 state 依赖于另一个 state),尝试用 useReducer Hook 把 state 更新逻辑移到 effect 之外。这篇文章 提供了一个你该如何做到这一点的案例。 useReducer 的 dispatch 的身份永远是稳定的 —— 即使 reducer 函数是定义在组件内部并且依赖 props。

万不得已的情况下,如果你想要类似 class 中的 this 的功能,你可以 使用一个 ref 来保存一个可变的变量。然后你就可以对它进行读写了。举个例子:

function Example(props) {
    //把最新的props保存在一个ref中
    const latestProps = useRef(props)
    useEffect(()=>{
        latestProps.current = props
    });
    useEffect(()=>{
        function tick(){
            //在任何时候读取最新的props
            console.log(latestProps.current);
        }
        const id = setInterval(tick,1000)
        return ()=> clearInterval(id)
    },[]) //这个effect 从不会重新执行
}

仅当你实在找不到更好办法的时候才这么做,因为依赖于变更会使得组件更难以预测。如果有某些特定的模式无法很好地转化成这样,发起一个 issue 并配上可运行的实例代码以便,我们会尽可能帮助你。

我该如何实现 shouldComponentUpdate?

你可以用 React.memo 包裹一个组件来对它的 props 进行浅比较:

const Button = React.memo((props)=>{
    //你的组件
})

这不是一个 Hook 因为它的写法和 Hook 不同。React.memo 等效于 PureComponent,但它只比较 props。(你也可以通过第二个参数指定一个自定义的比较函数来比较新旧 props。如果函数返回 true,就会跳过更新。)

React.memo 不比较 state,因为没有单一的 state 对象可供比较。但你也可以让子节点变为纯组件,或者 用 useMemo 优化每一个具体的子节点。

如何记忆计算结果?

useMemo Hook 允许你通过「记住」上一次计算结果的方式在多次渲染的之间缓存计算结果:

const memoizedValue = useMemo(()=>computeExpensiveValue(a,b),[a,b])

这行代码会调用 computeExpensiveValue(a, b)。但如果依赖数组 [a, b] 自上次赋值以来没有改变过,useMemo 会跳过二次调用,只是简单复用它上一次返回的值。

记住,传给 useMemo 的函数是在渲染期间运行的。不要在其中做任何你通常不会在渲染期间做的事。举个例子,副作用属于 useEffect,而不是 useMemo。

你可以把 useMemo 作为一种性能优化的手段,但不要把它当做一种语义上的保证。未来,React 可能会选择「忘掉」一些之前记住的值并在下一次渲染时重新计算它们,比如为离屏组件释放内存。建议自己编写相关代码以便没有 useMemo 也能正常工作 —— 然后把它加入性能优化。(在某些取值必须 从不 被重新计算的罕见场景,你可以 惰性初始化 一个 ref。)
方便起见,useMemo 也允许你跳过一次子节点的昂贵的重新渲染:

function Parent({a,b}){
    //Only re-rendered if 'a' changes:
    const child1 = useMemo(()=><Child1 a={a}/>,[a])
    const child2 = useMemo(()=><Child2 b={b}/>,[b])
    return (
        <>
            {child1}
            {child2}
        </>
    )
}

注意这种方式在循环中是无效的,因为 Hook 调用 不能 被放在循环中。但你可以为列表项抽取一个单独的组件,并在其中调用 useMemo。

如何惰性创建昂贵的对象?

如果依赖数组的值相同,useMemo 允许你 记住一次昂贵的计算。但是,这仅作为一种提示,并不 保证 计算不会重新运行。但有时候需要确保一个对象仅被创建一次。
第一个常见的使用场景是当创建初始 state 很昂贵时:

function Table(props){
    //createRows() 每次渲染都会被调用
    const [rows,setRows] = useState(createRows(props.count))
    //...
}

为避免重新创建被忽略的初始 state,我们可以传一个 函数 给 useState:

function Table(props){
    //createRows()只会被调用一次
    const [rows,setRows] = useState(()=>createRows(props.count))
}

React 只会在首次渲染时调用这个函数。参见 useState API 参考。

你或许也会偶尔想要避免重新创建 useRef() 的初始值。举个例子,或许你想确保某些命令式的 class 实例只被创建一次:

function Image(props){
    // IntersectionObserver 在每次渲染都会被创建
    const ref = useRef(new IntersectionObserver(onIntersect))
    //...
}

useRef 不会 像 useState 那样接受一个特殊的函数重载。相反,你可以编写你自己的函数来创建并将其设为惰性的:

function Image(props){
    const ref = useRef(null)
    //IntersectionObserver  只会被惰性创建一次
    function getObserver(){
        if(ref.current === null){
            ref.current = new IntersectionObserver(onIntersect)
        }
        return ref.current
    }

    //当你需要时 调用getObserver()
    //...
}

这避免了我们在一个对象被首次真正需要之前就创建它。如果你使用 Flow 或 TypeScript,你还可以为了方便给 getObserver() 一个不可为 null 的类型。

Hook 会因为在渲染时创建函数而变慢吗?

不会。在现代浏览器中,闭包和类的原始性能只有在极端场景下才会有明显的差别。

除此之外,可以认为 Hook 的设计在某些方面更加高效:

*Hook 避免了 class 需要的额外开支,像是创建类实例和在构造函数中绑定事件处理器的成本。

  • 符合语言习惯的代码在使用 Hook 时不需要很深的组件树嵌套。这个现象在使用高阶组件、render props、和 context 的代码库中非常普遍。组件树小了,React 的工作量也随之减少。

传统上认为,在 React 中使用内联函数对性能的影响,与每次渲染都传递新的回调会如何破坏子组件的 shouldComponentUpdate 优化有关。Hook 从三个方面解决了这个问题。

useCallback Hook 允许你在重新渲染之间保持对相同的回调引用以使得 shouldComponentUpdate 继续工作:

//除非 a 或b 改变,否则不会变
const memoizedCallback = useCallback(()=>{
    doSomething(a,b);
},[a,b])

useMemo Hook 使得控制具体子节点何时更新变得更容易,减少了对纯组件的需要。
最后,useReducer Hook 减少了对深层传递回调的依赖,正如下面解释的那样。

如何避免向下传递回调?

我们已经发现大部分人并不喜欢在组件树的每一层手动传递回调。尽管这种写法更明确,但这给人感觉像错综复杂的管道工程一样麻烦。

在大型的组件树中,我们推荐的替代方案是通过 context 用 useReducer 往下传一个 dispatch 函数:

const TodosDispatch = React.createContext(null)
function TodosApp(){
    //提示:dispatch 不会在重新渲染之间变化
    const [todos,dispatch] = useReducer(todosReducer)
    return (
        <TodosDispatch.provider value={dispatch}>
            <DeepTree todos={todos}/>
        </TodosDispatch.provider>
    )
}

TodosApp 内部组件树里的任何子节点都可以使用 dispatch 函数来向上传递 actions 到 TodosApp:

function DeepChild(props){
     //如果我们想要执行一个action 我们可以从context中获取dispatch
     const dispatch = useContext(TodosDispatch);
     function handleClick(){
         dispatch({
             type:'add',
             text:'hello'
         })
     }
     return (
         <button onClick={handleClick}>Add </button>
     )
 }

总而言之,从维护的角度来这样看更加方便(不用不断转发回调),同时也避免了回调的问题。像这样向下传递 dispatch 是处理深度更新的推荐模式。

注意,你依然可以选择是要把应用的 state 作为 props 向下传递(更显明确)还是作为作为 context(对很深的更新而言更加方便)。如果你也使用 context 来向下传递 state,请使用两种不同的 context 类型 —— dispatch context 永远不会变,因此组件通过读取它就不需要重新渲染了,除非它们还需要应用的 state。

如何从 useCallback 读取一个经常变化的值?

我们推荐 在 context 中向下传递 dispatch 而非在 props 中使用独立的回调。下面的方法仅仅出于文档完整性考虑,以及作为一条出路在此提及。

同时也请注意这种模式在 并行模式 下可能会导致一些问题。我们计划在未来提供一个更加合理的替代方案,但当下最安全的解决方案是,如果回调所依赖的值变化了,总是让回调失效。

在某些罕见场景中,你可能会需要用 useCallback 记住一个回调,但由于内部函数必须经常重新创建,记忆效果不是很好。如果你想要记住的函数是一个事件处理器并且在渲染期间没有被用到,你可以 把 ref 当做实例变量 来用,并手动把最后提交的值保存在它当中:

function From(){
    const [text,updateText] = useState('')
    const textRef = useRef()

    useEffect(()=>{
        textRef.current = text //把它写入ref
    })

    const handleSubmit = useCallback(()=>{
        const currentText = textRef.current //从ref读取它
        alert(currentText)
    },[textRef])//不要像[text] 那样重新创建handleSubmit
    
    return(
        <>
         <input value={text} onChange={e=> updateText(e.target.value)}/>
         <ExpensiveTree onSubmit={handleSubmit}/>
        </>
    )
}

这是一个比较麻烦的模式,但这表示如果你需要的话你可以用这条出路进行优化。如果你把它抽取成一个自定义 Hook 的话会更加好受些:

function From(){
    const [text,updateText] = useState('')
    //即便 'text'变了也会被记住
    const handleSubmit = useEventCallback(()=>{
        alert(text)
    },[text])
    return(
        <>
          <input value={text} onChange={e=>updateText(e.target.value)}/>
          <ExpensiveTree onSubmit={handleSubmit}/>
        </>
    )
}

function ExpensiveTree(fn,dependencies){
    cosnt ref = useRef(()=>{
        throw new Error('Cannot call an event handler while rendering.')
    })
    useEffect(()=>{
        ref.current = fn
    },[fn,...dependencies])
    
    return useCallback(()=>{
        const fn = ref.current
        return fn()
    },[ref])
}

无论如何,我们都 不推荐使用这种模式 ,只是为了文档的完整性而把它展示在这里。相反的,我们更倾向于 避免向下深入传递回调。

底层原理

React 是如何把对 Hook 的调用和组件联系起来的?

React 保持对当先渲染中的组件的追踪。多亏了 Hook 规范,我们得知 Hook 只会在 React 组件中被调用(或自定义 Hook —— 同样只会在 React 组件中被调用)。
每个组件内部都有一个「记忆单元格」列表。它们只不过是我们用来存储一些数据的 JavaScript 对象。当你用 useState() 调用一个 Hook 的时候,它会读取当前的单元格(或在首次渲染时将其初始化),然后把指针移动到下一个。这就是多个 useState() 调用会得到各自独立的本地 state 的原因。

Hook 使用了哪些现有技术?

Hook 由不同的来源的多个想法构成:

  • react-future 这个仓库中包含我们对函数式 API 的老旧实验。
  • React 社区对 render prop API 的实验,其中包括 Ryan Florence 的 Reactions Component 。
  • Dominic Gannaway 的用 adopt 关键字 作为 render props 的语法糖的提案。
  • DisplayScript 中的 state 变量和 state 单元格。
  • ReasonReact 中的 Reducer components。
    Rx 中的 Subscriptions。
  • Multicore OCaml 提到的 Algebraic effects。

Sebastian Markbåge 想到了 Hook 最初的设计,后来经过 Andrew Clark,Sophie Alpert,Dominic Gannaway,和 React 团队的其它成员的提炼。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值