前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >超性感的React Hooks(三):useState

超性感的React Hooks(三):useState

作者头像
用户6901603
发布2020-07-23 15:54:19
2.3K0
发布2020-07-23 15:54:19
举报
文章被收录于专栏:不知非攻不知非攻

这几天和许多同学聊了使用React Hooks的感受。总体感觉是,学会使用并不算难,但能用好却并不简单。

今天分享的内容,是React Hooks第一个api,useState,阅读本文需要有具备最基础的React知识。

单向数据流

和angular双向绑定不同,React采用自上而下单向数据流的方式,管理自身的数据与状态。在单向数据流中,数据只能由父组件触发,向下传递到子组件。

我们可以在父组件中定义state,并通过props的方式传递到子组件。如果子组件想要修改父组件传递而来的状态,则只能给父组件发送消息,由父组件改变,再重新传递给子组件。

在React中,state与props的改变,都会引发组件重新渲染。如果是父组件的变化,则父组件下所有子组件都会重新渲染。

在class组件中,组件重新渲染,是执行render方法。

而在函数式组件中,是整个函数重新执行。

函数式组件

函数式组件与普通的函数几乎完全一样。只不过函数执行完毕时,返回的是一个JSX结构。

代码语言:javascript
复制
function Hello() {
  return <div>hello world.</div>
}

函数式组件非常简单,也正因如此,一些特性常常被忽略,而这些特性,是掌握React Hooks的关键。

1. 函数式组件接收props作为自己的参数

代码语言:javascript
复制
import React from 'react';

interface Props {
  name: string,
  age: number
}

function Demo({ name, age }: Props) {
  return [
    <div>name: {name}</div>,
    <div>age: {age}</div>
  ]
}

export default Demo;

2. props的每次变动,组件都会重新渲染一次,函数重新执行

3. 没有this。那么也就意味着,之前在class中由于this带来的困扰就自然消失了。

Hooks

Hooks并不是神秘,它就是函数式组件。更准确的概述是:有状态的函数式组件。

useState

每次渲染,函数都会重新执行。我们知道,每当函数执行完毕,所有的内存都会被释放掉。因此想让函数式组件拥有内部状态,并不是一件理所当然的事情。

当然,也不是完全没有办法,useState就是帮助我们做这个事情。

从上一章再谈闭包中我们知道,useState利用闭包,在函数内部创建一个当前函数组件的状态。并提供一个修改该状态的方法。

我们从react中引入useState

代码语言:javascript
复制
import { useState } from 'react';

利用数组解构的方式得到一个状态与修改状态的方法。

代码语言:javascript
复制
// 利用数组解构的方式接收状态名及其设置方法
// 传入0作为状态 counter的初始值
const [counter, setCounter] = useState(0);

每当setCounter执行,就会改变counter的值。

基于这个知识点,我们可以创建一个最简单的,有内部状态的函数式组件。

代码语言:javascript
复制
import React, { useState } from 'react';

export default function Counter() {
  const [counter, setCounter] = useState(0);

  return [
    <div key="a">{counter}</div>,
    <button key="b" onClick={() => setCounter(counter + 1)}>
      点击+1
    </button>
  ]
}

利用useState声明状态,每当点击时,setCounter执行,counter递增。

需要注意的是,setCounter接收的值可以是任意类型,无论是什么类型,每次赋值,counter得到的,都是新传入setCounter中的值。

举个例子,如果counter是一个引用类型。

代码语言:javascript
复制
// counter默认值为 { a: 1, b: 2 }
const [counter, setCounter] = useState({ a: 1, b: 2 });

// 此时counter的值被改为了 { b: 4 }, 而不是 { a: 1, b: 4 }
setCounter({ b: 4 });

// 如果想要得到 { a: 1, b: 4 }的结果,就必须这样
setCounter({ ...counter, b: 4 });

那么一个思考题:用下面的例子修改状态,会让组件重新渲染吗?

代码语言:javascript
复制
const [counter, setCounter] = useState({ a: 1, b: 2 });
// 修改counter的值
counter.b = 4;
setCounter(counter);

useState接收一个值作为当前定义的state的初始值。并且初始操作只有组件首次渲染才会执行。

代码语言:javascript
复制
// 首次执行,counter初始值为10
// 再次执行,因为在后面因为某种操作改变了counter,则获取到的便不再是初始值,而是闭包中的缓存值
const [counter, setCounter] = useState(10);
setCounter(20);

如果初始值需要通过较为复杂的计算得出,则可以传入一个函数作为参数,函数返回值为初始值。该函数也只会在组件首次渲染时执行一次。

代码语言:javascript
复制
const a = 10;
const b = 20

// 初始值为a、b计算之和
const [counter, setCounter] = useState(() => {
  return a + b;
})

如果是在typescript中使用,我们可以用如下的方式声明状态的类型。

代码语言:javascript
复制
const [counter, setCounter] = useState<number>(0);

但是通常情况下,基础数据类型typescript能够很容易推导出来,因此我们不需要专门设置,只有在相对复杂的场景下才会需要专门声明。

代码语言:javascript
复制
// 能根据 0 推导为number类型
const [counter, setCounter] = useState(0);

// 能根据 false 推导为 boolean 类型
const [visible, setVisible] = useState(false);

// 能根据 [] 推导为 any[] 类型,因此此时还需要专门声明any为何物
const [arr, setArr] = useState<number[]>([]);

实践

接下来,我们完成一个稍微复杂一点的例子。文章头部的动态图还有印象吗?

多个滑动条控制div元素的不同属性,如果使用useState来实现,应该怎么做?

代码如下:

代码语言:javascript
复制
import React, { useState } from 'react';
import { Slider } from 'antd-mobile';
import './index.scss';

interface Color {
  r: number,
  g: number,
  b: number
}

export default function Rectangle() {
  const [height, setHeight] = useState(10);
  const [width, setWidth] = useState(10);
  const [color, setColor] = useState<Color>({ r: 0, g: 0, b: 0 });
  const [radius, setRadius] = useState<number>(0);

  const style = {
    height: `${height}px`,
    width: `${width}px`,
    backgroundColor: `rgb(${color.r}, ${color.g}, ${color.b})`,
    borderRadius: `${radius}px`
  }

  return (
    <div className="container">
      <p>height:</p>
      <Slider
        max={300}
        min={10}
        onChange={(n) => setHeight(n || 0)}
      />
      <p>width:</p>
      <Slider
        max={300}
        min={10}
        onChange={(n) => setWidth(n || 0)}
      />

      <p>color: R:</p>
      <Slider
        max={255}
        min={0}
        onChange={(n = 0) => setColor({ ...color, r: n })}
      />

      <p>color: G:</p>
      <Slider
        max={255}
        min={0}
        onChange={(n = 0) => setColor({ ...color, g: n })}
      />

      <p>color: B:</p>
      <Slider
        max={255}
        min={0}
        onChange={(n = 0) => setColor({ ...color, b: n })}
      />
      <p>Radius:</p>
      <Slider
        max={150}
        min={0}
        onChange={(n = 0) => setRadius(n)}
      />
      <div className="reatangle" style={style} />
    </div>
  )
}

仔细体会一下,代码是不是比想象中更简单?需要注意观察的地方是,当状态被定义为引用数据类型时,例子中是如何修改的。

原则上来说,useState的应用知识差不多都聊完了。不过,还能聊点高级的。

无论是在class中,还是hooks中,state的改变,都是异步的。

如果对事件循环机制了解比较深刻,那么异步状态潜藏的危机就很容易被意识到并解决它。如果不了解,可以翻阅我的JS基础进阶。详解事件循环[1]

状态异步,也就意味着,当你想要在setCounter之后立即去使用它时,你无法拿到状态最新的值,而之后到下一个事件循环周期执行时,状态才是最新的值。

代码语言:javascript
复制
const [counter, setCounter] = useState(10);
setCounter(20);
console.log(counter);  // 此时counter的值,并不是20,而是10

实践中有许多的错误使用,因为异步问题而出现bug。

例如我们想要用一个接口,去请求一堆数据,而这个接口接收多个参数。

当改变各种过滤条件,那么就势必会改变传入的参数,并在参数改变时,立即重新去请求一次数据。

利用hooks,会很自然的想到使用如下的方式。

代码语言:javascript
复制
import React, { useState } from 'react';

interface ListItem {
  name: string,
  id: number,
  thumb: string
}

// 一堆各种参数
interface Param {
  current?: number,
  pageSize?: number,
  name?: string,
  id?: number,
  time?: Date
}

export default function AsyncDemo() {
  const [listData, setListData] = useState<ListItem[]>([]);

  // 定义一个状态缓存参数,确保每次改变后都能缓存完整的参数
  const [param, setParam] = useState<Param>({});

  function fetchListData() {
    // @ts-ignore
    listApi(param).then(res => {
      setListData(res.data);
    })
  }

  function searchByName(name: string) {
    setParam({ ...param, name });
    // 改变param之后立即执行请求数据的代码
    // 这里的问题是,因为异步的原因,param并不会马上发生变化,
    // 此时直接发送请求无法拿到最新的参数
    fetchListData();
  }

  return [
    <div>data list</div>,
    <button onClick={() => searchByName('Jone')}>search by name</button>
  ]
}

这是一个不完整的示例。需要大家在阅读时结合自身开发经验去意会。

关键的代码在于searchByName方法。当使用setParam改变了param之后,立即去请求数据,在当前事件循环周期,param并没有改变。请求的结果,自然无法达到预期。

如何解决呢?

首先我们要考虑的一个问题是,什么样的变量适合使用useState去定义?

当然是能够直接影响DOM的变量,这样我们才会将其称之为状态。

因此param这个变量对于DOM而言没有影响,此时将他定义为一个异步变量并不明智。好的方式是将其定义为一个同步变量。

代码语言:javascript
复制
export default function AsyncDemo() {
  const [listData, setListData] = useState<ListItem[]>([]);

  // 定义为同步变量
  let param: Param = {}

  function fetchListData() {
    // @ts-ignore
    listApi(param).then(res => {
      setListData(res.data);
    })
  }

  function searchByName(name: string) {
    param = { ...param, name };
    fetchListData();
  }

  return [
    <div>data list</div>,
    <button onClick={() => searchByName('Jone')}>search by name</button>
  ]
}

不过,等一下,这样好像也有一点问题

还记得函数式组件的特性吗?每次状态改变,函数都会重新执行一次,那么此时param也就被重置了。状态无法得到缓存。

那么怎么办?

好吧,利用闭包。上一篇文章我们知道,每一个模块,都是一个执行上下文。因此,我们只要在这个模块中定义一个变量,并且在函数组件中访问,那么闭包就有了。

因此,将变量定义到函数的外面。如下

代码语言:javascript
复制
// 定义为同步变量
let param: Param = {}

export default function AsyncDemo() {
  const [listData, setListData] = useState<ListItem[]>([]);

  function fetchListData() {
    // @ts-ignore
    listApi(param).then(res => {
      setListData(res.data);
    })
  }

  function searchByName(name: string) {
    param = { ...param, name };
    fetchListData();
  }

  return [
    <div>data list</div>,
    <button onClick={() => searchByName('Jone')}>search by name</button>
  ]
}

这样似乎能够解决一些问题。

但也不是完全没有隐患,因为善后工作还没有做,因为这个闭包中的变量,即使在组件被销毁了,它的值还会存在。当新的组件实例被渲染,param就无法得到初始值了。因此这样的方式,我们必须在每一个组件被销毁时,做好善后工作。

那还有没有更好的方式呢?答案就藏在我们上面的知识点中。

我们知道useState其实也是利用闭包缓存了状态,并且即使函数多次执行,也只会初始化一次。之前的问题在于我们使用了setParam去改变它的值,如果我们换一种思路呢?仔细体会一下代码就知道了。

代码语言:javascript
复制
export default function AsyncDemo() {
  const [param] = useState<Param>({});
  const [listData, setListData] = useState<ListItem[]>([]);

  function fetchListData() {
    // @ts-ignore
    listApi(param).then(res => {
      setListData(res.data);
    })
  }

  function searchByName(name: string) {
    param.name = name;
    fetchListData();
  }

  return [
    <div>data list</div>,
    <button onClick={() => searchByName('Jone')}>search by name</button>
  ]
}

没有想到吧,useState还能这么用!

OK,useState相关的应用知识就基本分享完了,接下来的文章聊聊useEffect。

今天帮助一位同学优化了hooks实践代码,同样的功能,优化结果代码量减少了40行左右!!快到群里来!

本系列文章的所有案例,都可以在下面的地址中查看

https://github.com/advance-course/react-hooks

本系列文章为原创,请勿私自转载,转载请务必私信我

References

[1] 详解事件循环: [https://www.jianshu.com/p/12b9f73c5a4f](https://www.jianshu.com/p/12b9f73c5a4f)

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-11-06,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 不知非攻 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 单向数据流
  • 函数式组件
  • Hooks
  • useState
  • 实践
  • References
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档