超性感的React Hooks(三):useState

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

索性拉了一个React Hooks的讨论群,抽空时间在群里纠正大家的使用方式。如果持续关注群消息,能够学到许多正确的使用方式。感兴趣的同学速度进群,如果提示满人,公众号回复React Hooks获取进群方式。

超性感的React Hooks(三):useState_第1张图片

后进群的不用担心,每天的讨论的内容,我都会记录在语雀讨论组中,任何时候进群都能够访问到

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

单向数据流

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

超性感的React Hooks(三):useState_第2张图片

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

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

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

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

函数式组件

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

function Hello() {
  return <div>hello world.</div>
}

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

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

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

import { useState } from 'react';

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

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

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

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

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是一个引用类型。

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

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

const [counter, setCounter] = useState({ a: 1, b: 2 });
// 修改counter的值
counter.b = 4;
setCounter(counter);

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

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

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

const a = 10;
const b = 20

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

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

const [counter, setCounter] = useState<number>(0);

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

// 能根据 0 推导为number类型
const [counter, setCounter] = useState(0);

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

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

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

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

代码如下:

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基础进阶。详解事件循环

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

const [counter, setCounter] = useState(10);
setCounter(20);
console.log(counter);  // 此时counter的值,并不是20,而是10

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

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

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

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

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而言没有影响,此时将他定义为一个异步变量并不明智。好的方式是将其定义为一个同步变量。

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也就被重置了。状态无法得到缓存。

那么怎么办?

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

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

// 定义为同步变量
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去改变它的值,如果我们换一种思路呢?仔细体会一下代码就知道了。

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行左右!!快到群里来!

超性感的React Hooks(三):useState_第3张图片

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

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

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

你可能感兴趣的:(冲吧React)