Skip to main content

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);
s 说明

左边的变量名可以随便定义,它并不是react api中的一部分

 const [fruit, setFruit] = useState('banana');

左边使用方括号[]的这种语法叫做数组解析, 意思是我们声明了2个变量fruitsetFruit, 分别赋值给了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的案例:

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;

刷新浏览器,发现 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 能正确表示点击次数。

总结

  1. 要使函数组件有状态,请在组件的函数体中调用useState()
  2. useState(initialState)的第一个参数是初始状态。返回的数组有两项:当前状态和状态更新函数。
    const [state, setState] = useState(initialState);
  3. 在单个组件中可以有多个状态:调用多次useState()
  4. 当初始状态开销很大时,延迟初始化很方便。使用计算初始状态的回调调用useState(computeInitialState),并且此回调仅在初始渲染时执行一次。
  5. 必须确保使用useState()遵循 Hook 规则。
  6. 当闭包捕获过时的状态变量时,就会出现过时状态的问题。可以通过使用一个回调来更新状态来解决这个问题,这个回调会根据先前的状态来计算新的状态。
  7. useState()用于管理简单的状态。当状态复杂时,建议使用 useReducer() hook

相关文章