概述
useState 可以使函数组件像类组件一样拥有 state,也就说明函数组件可以通过 useState 改变 UI 视图。那么 useState 到底应该如何使用,底层又是怎么运作的呢,首先一起看一下 useState 。
问题:Hook 是什么? 一个 Hook 就是一个特殊的函数,让你在函数组件中获取状态等 React 特性
使用模式:函数组件 + Hooks
特点:从名称上看,Hook 都以 use 开头
基本使用
- 使用场景:当你想要在函数组件中,使用组件状态时,就要使用 useState Hook 了
- 作用:为函数组件提供状态(state)
- 使用步骤:
-
- 导入
useState
函数:import { useState} from 'react'
- 调用
useState
函数,并传入状态的初始值 - 从
useState
函数的返回值中,拿到状态和修改状态的函数 - 在 JSX 中展示状态
- 在按钮的点击事件中调用修改状态的函数,来更新状态
- 导入
- 规则:
-
- 只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用
- 只能在 React 的函数组件中调用 Hook。不要在其他 JavaScript 函数中调用
- useState接受唯一一个参数,在第一次组件被调用时使用来作为初始化值
使用数组解构简化
比如,要获取数组中的元素:
- 原始方式:索引访问
const arr = ['aaa', 'bbb']
const a = arr[0] // 获取索引为 0 的元素
const b = arr[1] // 获取索引为 1 的元素
- 简化方式:数组解构
-
- 相当于创建了两个变量(可以是任意的变量名称)分别获取到对应索引的数组元素
const arr = ['aaa', 'bbb']
const [a, b] = arr
const [state, setState] = arr
- 使用数组解构简化
useState
的使用
-
- 约定:修改状态的函数名称以 set 开头,后面跟上状态的名称
// 解构出来的名称可以是任意名称
const [state, setState] = useState(0)
const [age, setAge] = useState(0)
const [count, setCount] = useState(0)
演示示例
父传子
App.js--父组件
import React from 'react'
import UseFun from './components/useFun'
export default function App() {
let msg = '132'
return (
<div>
<UseFun msg={msg}/>
</div>
)
}
src/components/useFun.js--子组件
import React from 'react'
import PropTypes from 'prop-types'
function useFun(props) {
return (
<div>{props.msg}</div>
)
}
useFun.defaultProps = {
msg: '456'
}
useFun.propTypes = {
msg: PropTypes.string
}
export default useFun
计数器
App.js
import React from 'react'
import UseFun from './components/UseFun'
import CountFun from './components/CountFun'
export default function App() {
let msg = '132'
return (
<div>
<CountFun />
</div>
)
}
components/CountFun.jsx
import React from 'react'
import { useState } from 'react';
export default function CountFun() {
let [count, setCount] = useState(0)
function changeCount(){
setCount(count + 1)
}
return (
<div>
{/* 展示状态值 */}
<h1>useState Hook - {count}</h1>
{/* 点击按钮,让状态值 +1 */}
<button onClick={changeCount}>+1</button>
<button onClick={() => changeCount()}>+1</button>
</div>
)
}
或下面的做法也可以
import { useState } from 'react'
export default function const CountFun = () => {
// 返回值是一个数组
const stateArray = useState(0)
// 状态值 -> 0
const state = stateArray[0]
// 修改状态的函数
const setState = stateArray[1]
return (
<div>
{/* 展示状态值 */}
<h1>useState Hook -> {state}</h1>
{/* 点击按钮,让状态值 +1 */}
<button onClick={() => setState(state + 1)}>+1</button>
</div>
)
}
- 参数:状态初始值。比如,传入 0 表示该状态的初始值为 0
-
- 注意:此处的状态可以是任意值(比如,数值、字符串等),而 class 组件中的 state 必须是对象
- 返回值:数组,包含两个值:1 状态值(state) 2 修改该状态的函数(setState)
useState详解
概述
状态的读取和修改:
状态的使用:1 读取状态 2 修改状态
- 读取状态:该方式提供的状态,是函数内部的局部变量,可以在函数内的任意位置使用
- 修改状态:
setCount(newValue)
是一个函数,参数表示:新的状态值- 调用该函数后,将使用新的状态值
替换
旧值 - 修改状态后,因为状态发生了改变,所以,该组件会重新渲染
组件的更新过程:
函数组件使用 useState hook 后的执行过程,以及状态值的变化:
- 组件第一次渲染:
-
- 从头开始执行该组件中的代码逻辑
- 调用
useState(0)
将传入的参数作为状态初始值,即:0 - 渲染组件,此时,获取到的状态 count 值为: 0
- 组件第二次渲染:
-
- 点击按钮,调用
setCount(count + 1)
修改状态,因为状态发生改变,所以,该组件会重新渲染 - 组件重新渲染时,会再次执行该组件中的代码逻辑
- 再次调用
useState(0)
,此时 React 内部会拿到最新的状态值而非初始值,比如,该案例中最新的状态值为 1 - 再次渲染组件,此时,获取到的状态 count 值为:1
- 点击按钮,调用
注意:useState 的初始值(参数)只会在组件第一次渲染时生效。
也就是说,以后的每次渲染,useState 获取到都是最新的状态值。React 组件会记住每次最新的状态值!
注意点:
- 在普通函数(方法)中,不能使用hooks
- 在自定义的hooks中可以使用,自定义hooks必须以use开头
基本数据类型
引入useState
import { useState } from 'react';
在函数组件内部定义state数据
let [count, setCount] = useState(0);
let [name, setName] = useState('张三')
useState会返回一个数组,数组有两个值
- 第一个值:代表state数据
- 第二个值:更新这个state数据的一个函数,一般函数变量取名字通常是
setXxx
可以定义多个 state值
修改数据:
function changeName() {
setName('李四');
}
完整代码:
import React from 'react'
import { useState } from 'react';
function useFun(props) {
let [msg, setMsg] = useState('张三')
function changeMsg(){
setMsg('李四')
}
return (
<div>
<p>msg:{msg}</p>
<button onClick={changeMsg}>修改msg</button>
</div>
)
}
export default useFun
引用数据类型
定义对象数据
let [user, setUser] = useState({
name: 'zs',
age: 20
})
修改对象
function changeUserName() {
setUser({
name: 'ls',
})
}
这样修改对象,会导致其他属性丢失,因为它没有自动合并对象
正确方式:
function changeUserName() {
setUser({
...user,
name: 'ls',
})
}
可以将整个对象解构扩展,然后将修改的属性覆盖原来属性
注意:
修改state数据第一个参数可以是函数
setUser((prevState) => {
console.log(prevState)
return {
...prevState,
name: 'ls'
}
})
这个函数和类组件的setState的第一个参数是一致的
修改数据函数没有第二个参数
修改数据的函数仍然是异步的,不是同步的。
完整代码:
import React from "react";
import { useState } from "react";
function useFun(props) {
let [msg, setMsg] = useState("张三");
let [user, setName] = useState({
name: "zs",
age: 20,
});
function changeMsg() {
setMsg("李四");
}
const changeName = () => {
// setName({
// ...user,
// name: 'ls'
// })
//或如下写法:
setName((prevState) => {
console.log(prevState);
return {
...prevState,
name: "ls",
};
});
};
return (
<div>
<p>msg:{msg}</p>
<button onClick={changeMsg}>修改msg</button>
<hr />
<p>name::{user.name}</p>
<p>age: {user.age}</p>
<button onClick={changeName}>修改name</button>
</div>
);
}
export default useFun;
为函数组件添加多个状态
问题:如果一个函数组件需要多个状态,该如何处理?
回答:调用 useState
Hook 多次即可,每调用一次 useState Hook 可以提供一个状态。
注意:useState Hook 多次调用返回的 [state, setState] 相互之间,互不影响。
注意:React Hooks 只能直接出现在 函数组件 中,不能嵌套在 if/for/其他函数中!
否则就会报错:React Hook "useState" is called conditionally. React Hooks must be called in the exact same order in every component render
React 的 useState 这个 Hook 被条件性(放在一个条件判断中)的调用了。
React Hooks 必须要每次组件渲染时,按照相同的顺序来调用所有的 Hooks。
- 为什么会有这样的规则? 因为 React 是按照 Hooks 的调用顺序来识别每一个 Hook,如果每次调用的顺序不同,导致 React 无法知道是哪一个 Hook
- 通过开发者工具可以查看到。
如何监听 state 变化?
类组件 setState 中,有第二个参数 callback 或者是生命周期componentDidUpdate 可以检测监听到 state 改变或是组件更新。
那么在函数组件中,如何怎么监听 state 变化呢?这个时候就需要 useEffect 出场了,通常可以把 state 作为依赖项传入 useEffect 第二个参数 deps ,但是注意 useEffect 初始化会默认执行一次。
具体可以参考如下 Demo :
dispatch更新特点
上述讲的批量更新和 flushSync ,在函数组件中,dispatch 更新效果和类组件是一样的,但是 useState 有一点值得注意,就是当调用改变 state 的函数dispatch,在本次函数执行上下文中,是获取不到最新的 state 值的,把上述demo 如下这么改:
结果:0,0,0
原因很简单,函数组件更新就是函数的执行,在函数一次执行过程中,函数内部所有变量重新声明,所以改变的 state ,只有在下一次函数组件执行时才会被更新。所以在如上同一个函数执行上下文中,number 一直为0,无论怎么打印,都拿不到最新的 state 。
useState注意事项
在使用 useState 的 dispatchAction 更新 state 的时候,记得不要传入相同的 state,这样会使视图不更新。比如下面这么写:
如上例子🌰中,当点击按钮后,发现视图没有改变,为什么会造成这个原因呢?
在 useState 的 dispatchAction 处理逻辑中,会浅比较两次 state ,发现 state 相同,不会开启更新调度任务; demo 中两次 state 指向了相同的内存空间,所以默认为 state 相等,就不会发生视图更新了。
解决问题: 把上述的 dispatchState 改成 dispatchState({...state}) 根本解决了问题,浅拷贝了对象,重新申请了一个内存空间。