useState
hooks简介
Hooks are a new addition in React 16.8. They let you use state and other React features without writing a class.
hooks是react16.8
新增的特性,它可以让你使用state和其它react特性而无需写繁琐的class
下面是个简单的例子:
import React, { useState } from 'react';
function Example() {
// Declare a new state variable, which we'll call "count"
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
其等价于:
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Click me
</button>
</div>
);
}
}
当我们点击Click me button的时候,count会自增1
可以看到前者的实现方式更加简洁,我们叫它函数式组件
Hooks and Function Components
在react中我们可以像这样定义组件:
const Example = (props) => {
// You can use Hooks here!
return <div />;
}
或者这样:
function Example(props) {
// You can use Hooks here!
return <div />;
}
这样的组件没有任何状态管理,我们称之为无状态组件
We’re now introducing the ability to use React state from these, so we prefer the name “function components”.
像这样的组件还可以操作state,我们称之为函数式组件
什么是hook?
A Hook is a special function that lets you “hook into” React features. For example, useState
is a Hook that lets you add React state to function components.
hook是一种特殊的生命周期函数能够让你使用react特性而无需写class
比如下面这个hook - useState
可以让你在函数式组件中使用state
用useState()
hook 声明state
在class中我们是这样声明state的
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
}
在函数式组件中 由于没有this,所以我们不能使用this.state
这样的语法
我们可以在组件中直接使用useState
这个钩子函数
import React, { useState } from 'react';
function Example() {
// Declare a new state variable, which we'll call "count"
const [count, setCount] = useState(0);
}
分析:useState函数到底做了什么?
- 1, 初始化state - count
- 2, 返回一个数组:数组第一项返回的是初始值,第二项是个函数(setter),用于更新state
读state
class中:
<p>You clicked {this.state.count} times</p>
函数组件中:
<p>You clicked {count} times</p>
写state
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Click me
</button>
函数组件中:
<button onClick={() => setCount(count + 1)}>
Click me
</button>
[]
语法解析:
细心的朋友可能注意到了,我们定义state的时候左边使用了方括号[]
const [count, setCount] = useState(0);
左边的变量名可以随便定义,它并不是react api中的一部分
const [fruit, setFruit] = useState('banana');
左边使用方括号[]
的这种语法叫做数组解析, 意思是我们声明了2个变量fruit
和 setFruit
, 分别赋值给了useState
的返回值,它和下面是等价的:
var fruitStateVariable = useState('banana'); // Returns a pair
var fruit = fruitStateVariable[0]; // First item in a pair
var setFruit = fruitStateVariable[1]; // Second item in a pair
使用多个state变量
如果想声明不同的state,就要设置不同的变量名,就像下面这样
function ExampleWithManyStates() {
// Declare multiple state variables!
const [age, setAge] = useState(42);
const [fruit, setFruit] = useState('banana');
const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
}
想要更新state的话就要单独去设置
function handleOrangeClick() {
// Similar to this.setState({ fruit: 'orange' })
setFruit('orange');
}
使用回调更新状态
当使用前一个状态计算新状态时,可以使用回调更新该状态:
const [state, setState] = useState(initialState);
...
setState(prevState => nextState);
...
下面是一些事例:
// Toggle a boolean
const [toggled, setToggled] = useState(false);
setToggled(toggled => !toggled);
// Increase a counter
const [count, setCount] = useState(0);
setCount(count => count + 1);
// Add an item to array
const [items, setItems] = useState([]);
setItems(items => [...items, 'New Item']);
状态的延迟初始化
每当 React 重新渲染组件时,都会执行useState(initialState)
。 如果初始状态是基本数据类型(数字,布尔值等),则不会有性能问题。
当初始状态需要昂贵的性能方面的操作时,可以通过为useState(computeInitialState)
提供一个函数来使用状态的延迟初始化,如下所示:
function MyComponent({ bigJsonData }) {
const [value, setValue] = useState(function getInitialState() {
const object = JSON.parse(bigJsonData); // expensive operation
return object.initialValue;
});
// ...
}
getInitialState()
仅在初始渲染时执行一次,以获得初始状态。在以后的组件渲染中,不会再调用getInitialState()
,从而跳过昂贵的操作。
来看下面todoList
的案例:
- src/App.js
- src/components/Todo/index.js
import Todo from './components/Todo';
function getBigData() {
//模拟网络请求
setTimeout(()=>{},1000)
return [
{
id:1,
desc: "去万达购物"
},
{
id:2,
desc: "约客户吃饭"
},
{
id:3,
desc: "陪领导游泳"
},
{
id:4,
desc: "去幼儿园接孩子"
},
]
}
function App() {
return (
<div className="App">
<Todo bigData ={getBigData} />
</div>
);
}
export default App;
import React, { useRef, useState } from 'react'
import { UUID } from 'uuidjs'
function Todo({bigData}) {
const [todos,setTodo] = useState(()=>{
console.log("function was called")
return bigData()
})
const domeRef = useRef()
const addTodo = value =>{
const item = {
id: UUID.generate(),
desc: value
}
setTodo([...todos,item])
domeRef.current.value = '' //clear input value
}
const deleteItem = id => {
setTodo(todos.filter(item=> item.id!=id))
}
return (
<div>
<input type='text' ref={domeRef} onChange={()=>{
console.log("domeRef:",domeRef)
console.log("domeRef.current:",domeRef.current)
console.log("domeRef.current.value:",domeRef.current.value)
}}/>
<button onClick={()=>addTodo(domeRef.current.value)}>Add Todo</button>
<ul>
{
todos.map(item=>{
return (
<li key={item.id}>
<span>{item.desc}</span>
<button onClick={()=>deleteItem(item.id)}>delete</button>
</li>
)
})
}
</ul>
</div>
)
}
export default Todo
刷新浏览器,发现 console.log("function was called")
被执行了2次
这是由于react的stric mode所致,去掉就好了
在input框中输入 "hello":
点击 Add Todo
:
可以看到
hello
已正常添加到todo列表中,虽然state 发生了改变,但是 useState的回调函数只会在组件初始化时执行
再点击去幼儿园接孩子
那一项的delete按钮:
可以看到item被正确删除
useState() 中的坑
现在我们已经基本初步掌握了如何使用useState()
,尽管如此,还必须注意在使用useState()
时可能遇到的常见问题。
在哪里调用 useState()
在使用useState()
Hook 时,必须遵循 Hook 的规则
- 1, 仅顶层调用 Hook :不能在循环,条件,嵌套函数等中调用
useState()
。在多个useState()
调用中,渲染之间的调用顺序必须相同。 - 2, 仅从React 函数调用 Hook:必须仅在函数组件或自定义钩子内部调用
useState()
。
来看看useState()
的正确用法和错误用法的例子。
有效调用useState()
useState()
在函数组件的顶层被正确调用
function Bulbs() {
// Good
const [on, setOn] = useState(false);
// ...
}
以相同的顺序正确地调用多个useState()
调用:
function Bulbs() {
// Good
const [on, setOn] = useState(false);
const [count, setCount] = useState(1);
// ...
}
useState()
在自定义钩子的顶层被正确调用
function toggleHook(initial) {
// Good
const [on, setOn] = useState(initial);
return [on, () => setOn(!on)];
}
function Bulbs() {
const [on, toggle] = toggleHook(false);
// ...
}
useState()
的无效调用
在条件中调用useState()
是不正确的:
function Switch({ isSwitchEnabled }) {
if (isSwitchEnabled) {
// Bad
const [on, setOn] = useState(false);
}
// ...
}
在嵌套函数中调用useState()
也是不对的
function Switch() {
let on = false;
let setOn = () => {};
function enableSwitch() {
// Bad
[on, setOn] = useState(false);
}
return (
<button onClick={enableSwitch}>
Enable light switch state
</button>
);
}
过时状态
闭包是一个从外部作用域捕获变量的函数。
闭包(例如事件处理程序,回调)可能会从函数组件作用域中捕获状态变量。 由于状态变量在渲染之间变化,因此闭包应捕获具有最新状态值的变量。否则,如果闭包捕获了过时的状态值,则可能会遇到过时的状态问题。
来看看一个过时的状态是如何表现出来的。组件<DelayedCount>
延迟3秒计数按钮点击的次数。
function DelayedCount() {
const [count, setCount] = useState(0);
const handleClickAsync = () => {
setTimeout(function delay() {
setCount(count + 1);
}, 3000);
}
return (
<div>
{count}
<button onClick={handleClickAsync}>Increase async</button>
</div>
);
}
快速多次点击按钮。count 变量不能正确记录实际点击次数,有些点击被吃掉。
delay()
是一个过时的闭包,它从初始渲染(使用0初始化时)中捕获了过时的count变量。
为了解决这个问题,使用函数方法来更新count状态:
function DelayedCount() {
const [count, setCount] = useState(0);
const handleClickAsync = () => {
setTimeout(function delay() {
setCount(count => count + 1);
}, 3000);
}
return (
<div>
{count}
<button onClick={handleClickAsync}>Increase async</button>
</div>
);
}
现在setCount(count => count + 1)
在delay()
中正确更新计数状态。React 确保将最新状态值作为参数提供给更新状态函数,过时闭包的问题解决了。
快速单击按钮。 延迟过去后,count 能正确表示点击次数。
总结
- 要使函数组件有状态,请在组件的函数体中调用
useState()
。 useState(initialState)
的第一个参数是初始状态。返回的数组有两项:当前状态和状态更新函数。const [state, setState] = useState(initialState);
- 在单个组件中可以有多个状态:调用多次
useState()
- 当初始状态开销很大时,延迟初始化很方便。使用计算初始状态的回调调用
useState(computeInitialState)
,并且此回调仅在初始渲染时执行一次。 - 必须确保使用
useState()
遵循 Hook 规则。 - 当闭包捕获过时的状态变量时,就会出现过时状态的问题。可以通过使用一个回调来更新状态来解决这个问题,这个回调会根据先前的状态来计算新的状态。
- useState()用于管理简单的状态。当状态复杂时,建议使用 useReducer() hook。