cover_image

超性感的React Hooks(三):useState

这波能反杀丶 这波能反杀
2019年11月06日 13:12

图片

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

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

图片

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

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

单向数据流

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

图片

我们可以在父组件中定义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基础进阶。详解事件循环[1]

状态异步,也就意味着,当你想要在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行左右!!快到群里来!

图片

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

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

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

图片

关于如何学好JavaScript,我写了一本书,感兴趣的同学可点击阅读原文查看详情。

References

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


React Hooks 最强导读 · 目录
上一篇超性感的React Hooks(四):useEffect下一篇超性感的React Hooks(二)再谈闭包
继续滑动看下一个
这波能反杀
向上滑动看下一个