创建组件时,我们可以通过 props 接收外部传入的数据,该数据可以称之为组件外部数据。除此之外,React还有一个命脉知识点 -> 组件内部数据:state.
使用函数创建组件时,内部数据 state 通过 useState
定义。
function Coutner() { // 利用数组结构得到两个变量 // count:表示定义的数据 // setCount:修改该数据的方法 // useState:从闭包数据中取出 count 的值,0 仅表示默认值 const [count, setCount] = useState(0) return ( <div>{count}</div> ) }
由于目前函数组件足以支撑所有场景的实现,因此写法更为复杂的 class 组件相关知识本系列将不会涉及,有兴趣的同学可以阅读官网了解。现在主流的项目也基本全面采用函数式组件相关解决方案。只有部分落后的项目团队依然在坚持 class 组件。React 提供了方便平滑的升级模式,还在维护老项目的同学可以跟着本系列学习函数组件并逐步重构项目
state 属于被监控的数据,它是 React 实现数据驱动 UI 的核心。当 state 值发生变化时,组件会尝试重新渲染,因此,函数会重新执行一次。函数重新执行后,此时 count 的数据已经是变化后的结果,因此渲染到 UI 的结果也会发生变化。
import { useState } from 'react'; export default function Counter() { const [count, setCount] = useState(0) return ( <div> <div>{count}</div> <button onClick={() => setCount(count + 1)}>递增</button> </div> ) }
我在带学生的过程中,遇到一个非常有趣的现象,我期望
count
的值能递增,于是这样写setCount(count++)
,你们猜 count 会按照预想的结果发生变化吗?
在 TypeScript 中使用 useState 时,我们应该使用如下的方式声明 state 的数据类型
const [count, setCount] = useState<number>(0);
但是通常情况下,基础数据类型能够通过默认值轻松推导出来,因此我们不需要专门设置,只有在相对复杂的场景下才会需要专门声明。
// 能根据 0 推导为 number 类型 const [count, setCount] = useState(0); // 能根据 false 推导为 boolean 类型 const [visible, setVisible] = useState(false); // 能根据 [] 推导为 any[] 类型,因此此时还需要专门声明any为何物 const [arr, setArr] = useState<number[]>([]);
需要注意的是,state 使用浅比较对比新旧数据。也就意味着,当 state 为引用数据类型时,如果你的新数据与旧数据引用相同,那么 React 无法感知到你的数据发生了变化。
例如
import { useState } from 'react'; export default function Counter() { const [arr, setArr] = useState<number[]>([]) function incrementHandle() { let newArr = arr newArr.push((arr[arr.length - 1] || 0) + 1) setArr(newArr) } return ( <div> {arr.map((item) => ( <div key={item}>{item}</div> ))} <button onClick={incrementHandle}>递增</button> </div> ) }
重点关注该例子中的 incrementHandle
方法。新的数组与旧的数组引用一样,因此就算更改了数组内容,但是 React 无法感知,组件也就不会重新渲染。
因此,正确的方式应该要想办法让新旧数据的引用不同
function incrementHandle() { let newArr = [...arr] newArr.push((arr[arr.length - 1] || 0) + 1) setArr(newArr) }
当 state 的数据变得复杂,我们可以借助
immer
等不可变数据集来帮助我们。详情可阅读相关文档注意,state 是被监控的数据,它与 UI 的变化息息相关。在实践中,还有很多其他的数据与 UI 变化无关,他们不应该放在 state 中来管理,而应该想其他办法。
单向数据流
一个完整的 React 项目由多个组件组合而成。每个组件都是独立的,都可以有自己的外部数据与内部数据。对于父组件来说,它可以把自己的 state 作为 props 向下传递给它的子组件。
这种自上而下的数据流动,我们称之为单向数据流
.
任何一个组件的内部数据 state 发生变化,就会影响组件树中低于它的组件。
如果你把一个以组件构成的树想象成一个 props 的数据瀑布的话,那么每一个组件的 state 就像是在任意一点上给瀑布增加额外的水源,但是它只能向下流动。
在实践中,为了避免额外的性能消耗,我们需要精准的把握每一次 state 的更新会影响哪些组件,掌握单向数据流的特性对此非常有帮助。
如果你想要在子组件中,修改父组件传递而来的状态,只能通过修改父组件 state 的方式,修改方法通常也由父组件传递给子组件。
合并
当同一个 state 数据被修改多次时,他们会合并成一次修改。如下面例子,我们调用两次 setCount
,执行一次之后,count 变成 2,而不会变成 3
import { useState } from 'react'; export default function Counter() { const [count, setCount] = useState(0) function handle() { setCount(count + 1) setCount(count + 2) } return ( <div> <div>{count}</div> <button onClick={handle}>递增</button> </div> ) }
当我们同时修改多个 state 时,也会合并起来,被认为是一次修改,组件只会重新渲染一次。
import { useState } from 'react'; export default function Counter() { const [count, setCount] = useState(0) const [other, setOther] = useState(0) function handle() { setCount(count + 1) setOther(other + 1) } return ( <div> <div>{count}</div> <div>{other}</div> <button onClick={handle}>递增</button> </div> ) }
如果同时修改多个 state 的行为发生在异步回调里,React 18 也会把它们合并成为一次 state 操作。
import { useState } from 'react'; export default function Counter() { const [count, setCount] = useState(0) const [other, setOther] = useState(0) function handle() { setTimeout(() => { setCount(count + 1) setOther(other + 1) }, 500) } return ( <div> <div>{count}</div> <div>{other}</div> <button onClick={handle}>递增</button> </div> ) }
正确识别闭包
在函数组件中,如果我们在回调函数中使用了 state 的值,那么闭包就会产生。闭包在函数创建时产生,他会缓存创建时的 state 的值。
在很多文章中,把这种现象称为“闭包陷阱”,它是一种正常现象,但是如果你在使用时无法正确识别它,那么会给你带来麻烦。
import { useState } from 'react'; export default function Counter() { const [count, setCount] = useState(0) function handle() { setCount(count + 1) // 当 setTimeout 执行时, // 回调函数的 count 值不是 1,而是 0 setTimeout(() => { setCount(count + 2) }, 0) } return ( <div> <div>{count}</div> <button onClick={handle}>递增</button> </div> ) }
异步写法
如果我们要在 setTimeout 回调函数中,正确的拿到当前 state 的值,我们可以使用如下的写法来达到目的
import { useState } from 'react'; export default function Counter() { const [count, setCount] = useState(0) function handle() { setCount(count + 1) setTimeout(() => { - setCount(count + 2) + setCount(count => count + 2) }, 0) } return ( <div> <div>{count}</div> <button onClick={handle}>递增</button> </div> ) }
实践
接下来,我们完成一个稍微复杂一点的例子
多个滑动条控制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> ) }
聊点高级的
原则上来说,state 的应用知识基本上已经聊完了。不过,作为 React 专家,我还能跟大家聊一点高级的。
state 的变化,是异步的。
如果看过我《JavaScript 核心进阶》,对事件循环机制了解比较深刻的,那么 state 异步潜藏的危机就容易被意识并解决它。如果不了解,你可能会遇到大坑。
状态异步,也就意味着,当你想要在setCount
之后立即去使用它时,你无法拿到状态最新的值,而到下一个事件循环周期执行时,状态才是最新值。
const [count, setCount] = useState(10); setCount(20); console.log(count); // 此时counter的值,并不是20,而是10
实践中有许多错误的使用,会因为这个异步问题出现 bug。
例如我们想要用一个接口,去请求一堆数据,而这个接口接收多个参数。
当改变各种过滤条件,那么就势必会改变传入的参数,并在参数改变时,立即重新去请求一次数据。
我们会很自然的想到使用如下的方式
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
并没有改变。请求的结果,自然无法达到预期。
那么,如何解决这个问题呢?提示:我们要首先考虑一个数据是否一定要把他定义为 state?想明白这个问题,继续学习后面的章节,相信你能找到答案!