Hooks 二三事 (1) | 十分钟快速入门 React Hooks

原文链接:https://medium.com/free-code-camp/learn-the-basics-of-react-hooks-in-10-minutes-b2898287fe5d

2019 年初,React 团队在 React 16.8.0 版本中新增了一个特性—— Hooks(钩子)。如果说 React 是一大碗糖果,那么 Hooks 就是最新款的、耐嚼的超美味糖果。

那么 Hooks 到底是什么呢?为什么会触发前端程序员的 ”真香“ 定律呢?

简介

React Hooks 诞生的初衷是为了提供一种更强大、更具表现力的方式来编写和复用组件中的通用功能。

从长远来看,我们希望 Hooks 成为人们编写 React 组件的主要方式。——React 团队

既然 Hooks 这么重要,为什么不以一种有趣的方式来学习它呢?

你可以借助这个具备实时编辑示例的 Hooks API 学习工具 来快速学习

糖果

想象一下,React 是一碗漂亮的糖果。这碗糖果能够适应世界上很多人的口味,但是糖果的厂家意识到碗内的有几块糖果虽然吃起来很美味,但是吃的过程十分麻烦(想想渲染参数和高阶组件)。于是他们做出了改革,并不是直接扔掉原来的糖果,而是加入了一些新的糖果,名字叫做「Hooks」。

这个糖果存在的目的就是让你更容易做你已经在做的事情,比如更容易的享受美味。这些糖果一点都不特别,如果你仔细品尝,你还能发现一些熟悉的味道—— JavaScript 函数。

状态 Hook

如上一章节谈及的,Hook 本质就是函数。官方发布的钩子函数总计有 10 个,这10 个 API 简化了组件中的功能实现和逻辑复用。首先,我们来看第一个 Hook —— useState

在过去很长一段时间里,我们没有办法在函数组件中使用本地状态值,Hooks 的出现解决了该问题。使用 useState 可以方便快捷地在函数组件中声明或更新本地状态,是不是很神奇呢?如下是一个简单的计数器应用:

Comp1.gif

一般来讲,我们会声明 Couter 组件如下:

import React, { Component } from 'react';

class Counter extends Component {
  constructor() {
    super();
    this.state = {
      count: 1
    };
  }

  handleClick = () => {
    this.setState(prevState => ({count: prevState.count + 1}))
  }

  render() {
    const { count } = this.state;
    return (
      <div>
        <h3 className="center">
            Welcome to the Counter of Life 
        </h3>
        <button className="center-block" onClick={this.handleClick}>
            {count}
        </button>
      </div>
    );
  }
}

之所以使用类声明的方式来创建组件是为了跟踪组件内的 count 状态。现在,使用 useState 钩子函数,也可以使用函数声明的方式来创建 Count 组件。

hook.gif

两者有什么不同呢?

函数组件的声明,不需要使用 Class extends 关键字,并且不需要一个 render 的内部方法。

function CounterHooks() {
  
  return (
      <div>
        <h3 className="center">Welcome to the Counter of Life </h3>
        <button 
          className="center-block" 
          onClick={this.handleClick}> {count} </button>
      </div>
    ); 
}

上述代码存在两个问题:

  • 不应该在函数组件中使用 this 关键字;
  • count 状态尚未在函数体中定义。

针对第一个问题,可以将 handleClick 作为函数体内部的独立函数:

function CounterHooks() {
  const handleClick = () => {
    
  }
  return (
      <div>
        <h3 className="center">Welcome to the Counter of Life </h3>
        <button 
          className="center-block" 
          onClick={handleClick}> {count} </button>
      </div>
    ); 
}

在类组件中,count 来自于组件内的 state 对象。而在函数组件中,可以通过调用 useState 方法(钩子)产生。

调用 useState 时,可以传入一个参数,用于初始化某一状态。例如 useState(0) 中,0 就是要追踪的那个状态的初始值。该函数返回一个包含两个元素的数组,第一个元素为状态值,第二个是用于更新状态的函数,可以将两者看做是 statesetState 的变异体(并不完全相同)。于是,我们便可以对 Count 组件进行重构了:

function CounterHooks() {
  // 🦄 
  const [count, setCount] = useState(0);

  const handleClick = () => {
    setCount(count + 1)
  }

  return (
      <div>
        <h3 className="center">Welcome to the Counter of Life </h3>
        <button 
          className="center-block" 
          onClick={handleClick}> {count} </button>
      </div>
    ); 
}

除了代码比较少之外,还有几个点需要注意:

第一,由于 useState 返回的是一个数组,可以通过“解构赋值”的方法,快速拆分为两个变量:

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

第二,重构代码中的 handleClick 函数不需要引用 prevState 或类似的过去值,只需要向 setCount 传入新的状态值即可。

const handleClick = () => {
    setCount(count + 1)
}

由于钩子函数保证了 count 状态值会在前后两次渲染中正确传递,所以当需要更新 count 值时,仅仅需要调用 setCount 并传入新的值即可,如 setCount(count + 1)

使用 Hooks 创建组件是不是很简单呢?虽然这仅仅是一个虚构的组件,但也是一个美好的开始。

注 :状态的更新函数也支持传入一个函数作为参数,比如当新的状态值依赖于旧的状态值并需要做进一步处理时,可以使用

setCount(prevCount => {
 if(prevCount >= 0) {
   return prevCount;
 } else {
   return -prevCount;
 }
})

多状态场景

在类组件中,我们总是习惯在一个对象(state)中设置所有的状态值,无论是多个属性还是仅有一个属性。

// 单个属性
state = {
  count: 0
}

// 多个属性 
state = {
 count: 0,
 time: '07:00'
}

使用 useState 时,有着细微的差别。在上面的计数器案例中,只使用了一个数值来初始化 count 状态,而非使用一个对象来存储状态。

那么,如果我们需要添加另一个状态呢?是否可以调用多个 useState ?我们使用下面这个组件来分析,该组件与 Count 组件基本一致,但是会使用点击次数来更新时间。

Comp2.gif
function CounterHooks() {
  const [count, setCount] = useState(0);
  const [time, setTime] = useState(new Date())

  const handleClick = () => {
    setCount(count + 1);
    setTime(new Date())
  }

  return (
      <div>
        <h3 className="center">Welcome to the Counter of Life </h3>
        <button 
          className="center-block" 
          onClick={handleClick}>{count}</button>
        <p className="center"> 
          at: {`${time.getHours()} : ${time.getMinutes()} : ${time.getSeconds()}`}</p>
      </div>
    ); 
}

如你所见,组件内钩子的用法基本一致,只是增多了一次 useState 的调用:

const [time, setTime] = useState(new Date())

如此,time 状态便可以直接在 JSX 渲染块中使用,记录点击按钮那一刻的时、分、秒。

但是,是否可以使用「useState + 对象」统一管理所有的状态,而不是多次调用 useState 呢?答案是肯定的,但是在更新 state 时,需要注意,与类组件中的 setState 不同,传入 useState 的对象将替代原来的状态对象,而 setState 则会合并两个对象属性。

// 🐢 合并 (setState) vs 替代 (useState)
// 假设初始状态对象为 {name: "Ohans"}

setState({age: "unknown"})
// 新的状态对象将是 
// {name: "Ohans", age: "unknown"}

setStateGeneratedByUseState({age: "unknown"})
// 新的状态对象将是
// {age: "unknown"} - 初始状态值被替代了

副作用钩子

在类组件中,我们经常会使用日志打印、数据请求或管理订阅等副作用。这些副作用可以看做是不包含在渲染逻辑中的操作,而 useEffect 钩子就是为此创建的,怎么使用呢?

useEffect 钩子中需要传入一个函数,在该函数中,可以执行一系列的副作用,来看一个简单的例子。

useEffect(() => {
  // 🐢 你可以在这里设置副作用
  console.log("useEffect first timer here.")
})

useEffect 中,我传入了一个匿名函数,并在函数中调用一些副作用方法。

那么下一个问题是:什么时候该使用 useEffect 呢?

在类组件中,会有 componentDidMountcomponentDidUpdate 等生命周期方法,而函数组件是不具备生命周期函数方法的,故而 useEffect 可以起到一定的补偿效果。因此,在上述例子中,但组件挂载(componentDidMount)或组件更新(componentDidUpdate)时,将调用 useEffect 中的方法(也称为「副作用方法」)。

将上面的 useEffect 逻辑加入到我们的 Counter 组件中,得到如下结果:

Dom2.gif
function CounterHooks() {
  const [count, setCount] = useState(0);
  const [time, setTime] = useState(new Date())
  
  // 🐢 look here.
  useEffect(() => {
    console.log("useEffect first timer here.")
  }, [count])


  const handleClick = () => {
    setCount(count + 1);
    setTime(new Date())
  }

  return (
      <div>
        <h3 className="center">Welcome to the Counter of Life </h3>
        <button className="center-block" 
          onClick={handleClick}>{count}</button>
        <p className="center"> at: {`${time.getHours()} : ${time.getMinutes()} : ${time.getSeconds()}`}</p>
      </div>
    ); 
}

注意:useEffect 钩子并不完全等价于 componentDidMount + componentDidUpdate。可以这么理解,但并不完全等价,二者之间存在一些细微差别。

在上面的案例中,每次组件更新都会打印信息,并不是我们理想中的效果。如果你只是想在组件挂在时打印信息,应该怎么办呢?

看向 useEffect 的第二个参数,这是「副作用方法」的依赖数组,当数组内任一元素发生变化时,都会重新「副作用方法」。故而在上述案例中,「副作用方法」将在组件挂载和 count 状态值发生变化时执行。

如果你传入一个空数组,「副作用方法」将仅在组件挂载时执行,组件更新时将不再触发。

useEffect(() => {
    console.log("useEffect first timer here.")
}, []) 
Dom3.gif

订阅

在很多应用中,订阅和取消订阅都是很常用的副作用。那么 useEffect 中又应怎么使用呢?

useEffect(() => {
  const clicked = () => console.log('window clicked');
  window.addEventListener('click', clicked);
}, [])

在上面的代码中,当组件挂载时,window 上将绑定一个点击事件。那么又该如何取消订阅呢?

useEffect 中考虑了这一需求。如果在「副作用方法」中返回一个函数,那么卸载组件时将会调用该函数。因此,这是取消订阅的最佳位置,如下所示:

useEffect(() => {
    const clicked = () => console.log('window clicked');
    window.addEventListener('click', clicked);

    return () => {
      window.removeEventListener('click', clicked)
    }
 }, [])

使用 useEffect 钩子可以完成很多的行为,如 API 调用等。

自定义 Hooks

上面所有章节讲述的都是从 React 糖果盒中获取并使用糖果,然而 React 也为你提供了一种定制化糖果的方法——自定义 Hooks。

自定义 Hooks 本质上也只是一个普通方法,但是命名上要求以 "use" 开头,如有必要,在其内部,可以调用 React 内置的所有钩子函数。下面是个简单的例子:

// 🐢 自定义 hook - 以 "use" 作为前缀 (useMedium)
useMedium(() => {
  const URI = "https://some-path-api";
  // 🦄 内部可以使用内置的钩子函数
  useEffect(() => {
    fetch(URI)
  },[])
})

Hooks 规则

在使用 Hooks 时,有两条规则需要遵守:

  • 仅在顶层调用钩子,即不可以在条件、循环或嵌套函数中调用;
  • 仅在 React 函数中调用钩子,即函数组件和自定义钩子。

ESLint 插件可以更好地协助你遵守以上规则。

译者言

本专题主要分享国外关于 hooks 的一些博客或讨论,主要会分成三个部分:话题引入、基础知识、实际应用。
本文简单介绍了 React Hooks 的部分特性,但还有很多没有提及。在后面的系列文章中,还会详细介绍 Hooks 中的各 API。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,376评论 6 491
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,126评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,966评论 0 347
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,432评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,519评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,792评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,933评论 3 406
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,701评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,143评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,488评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,626评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,292评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,896评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,742评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,977评论 1 265
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,324评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,494评论 2 348