1.使用 useState() 进行状态管理
useState()是改变状态的开关,将状态添加到函数组件需要4个步骤:启用状态、初始化、读取和更新。
1.1 启用状态
要将<Bulbs> 转换为有状态组件,需要告诉 React:从'react'包中导入useState钩子,然后在组件函数的顶部调用useState()。
import React, { useState } from 'react';
function Bulbs() {
... = useState(...);
return <div className="bulb-off" />;
}
在Bulbs函数的第一行调用useState(),在组件内部调用会使该函数成为有状态的函数组件。
启用状态后,下一步是初始化。
1.2初始化状态
import React, { useState } from 'react';
function Bulbs() {
... = useState(false);
return <div className="bulb-off" />;
}
useState(false)用false初始化状态。
1.3 读取状态
import React, { useState } from 'react';
function Bulbs() {
const [on] = useState(false);
return <div className={on ? 'bulb-on' : 'bulb-off'} />;
}
on状态变量保存状态值。
状态已经启用并初始化,现在可以读取它了。但是如何更新呢?再来看看useState(initialState)返回什么。
1.4 更新状态
用值更新状态
useState(initialState)返回一个数组,其中第一项是状态值,第二项是一个更新状态的函数。
import React, { useState } from 'react';
function Bulbs() {
const [on, setOn] = useState(false);
const lightOn = () => setOn(true);
const lightOff = () => setOn(false);
return (
<>
<div className={on ? 'bulb-on' : 'bulb-off'} />
<button onClick={lightOn}>开</button>
<button onClick={lightOff}>关</button>
</>
);
}
状态一旦改变,React 就会重新渲染组件,on变量获取新的状态值。
状态更新作为对提供一些新信息的事件的响应。这些事件包括按钮单击、HTTP 请求完成等,确保在事件回调或其他回调中调用状态更新函数。
使用回调更新状态
import React, { useState } from 'react';
function Bulbs() {
const [on, setOn] = useState(false);
const lightSwitch = () => setOn(on => !on);
return (
<>
<div className={on ? 'bulb-on' : 'bulb-off'} />
<button onClick={lightSwitch}>开/关</button>
</>
);
}
setOn(on => !on)使用函数更新状态。
2. 多种状态
通过多次调用useState(),一个函数组件可以拥有多个状态。
import React, { useState } from 'react';
function Bulbs() {
const [on, setOn] = useState(false);
const [count, setCount] = useState(1);
const lightSwitch = () => setOn(on => !on);
const addBulbs = () => setCount(count => count + 1);
const bulb = <div className={on ? 'bulb-on' : 'bulb-off'} />;
const bulbs = Array(count).fill(bulb);
return (
<>
<div className="bulbs">{bulbs}</div>
<button onClick={lightSwitch}>开/关</button>
<button onClick={addBulbs}>添加数量</button>
</>
);
}
[on, setOn] = useState(false) 管理开/关状态
[count, setCount] = useState(1)管理数量。
多个状态可以在一个组件中正确工作
3.状态的延迟初始化
每当 React 重新渲染组件时,都会执行useState(initialState)。 如果初始状态是原始值(数字,布尔值等),则不会有性能问题。
当初始状态需要昂贵的性能方面的操作时,可以通过为useState(computeInitialState)提供一个函数来使用状态的延迟初始化,如下所示:
import React, { useState } from 'react';
function MyComponent({ bigJsonData }) {
const [value, setValue] = useState(function getInitialState() {
const object = JSON.parse(bigJsonData);
return object.initialValue;
});
}
getInitialState()仅在初始渲染时执行一次,以获得初始状态。在以后的组件渲染中,不会再调用getInitialState(),从而跳过昂贵的操作。
4. useState() 中的坑
4.1 在哪里调用 useState()
在使用useState() 时,必须遵循的规则
1、仅顶层调用:不能在循环,条件,嵌套函数等中调用useState().在多个useState()调用中,渲染之间的调用顺序必须相同。
2、仅从React 函数调用 :必须仅在函数组件或自定义钩子内部调用useState()。
下面看下useState()的正确用法和错误用法的例子。
有效调用useState()
useState()在函数组件的顶层被正确调用
import React, { useState } from 'react';
function Bulbs() {
const [on, setOn] = useState(false);
}
以相同的顺序正确地调用多个useState()调用:
import React, { useState } from 'react';
function Bulbs() {
const [on, setOn] = useState(false);
const [count, setCount] = useState(1);
}
useState()在自定义钩子的顶层被正确调用
import React, { useState } from 'react';
function useToggleHook(initial) {
const [on, setOn] = useState(initial);
return [on, () => setOn(!on)];
}
function Bulbs() {
const [on, toggle] = useToggleHook(false);
}
useState() 的无效调用
在条件中调用useState()是不正确的
import React, { useState } from 'react';
function Switch({ isSwitchEnabled }) {
if (isSwitchEnabled) {
const [on, setOn] = useState(false);
}
}
在嵌套函数中调用useState()也是不对的
import React, { useState } from 'react';
function Switch() {
let on = false;
let setOn = () => {};
function enableSwitch() {
// Bad
[on, setOn] = useState(false);
}
return (
<button onClick={enableSwitch}>
Enable light switch state
</button>
);
}
4.2 过时状态
闭包是一个从外部作用域捕获变量的函数。
闭包(例如事件处理程序,回调)可能会从函数组件作用域中捕获状态变量。 由于状态变量在渲染之间变化,因此闭包应捕获具有最新状态值的变量。否则,如果闭包捕获了过时的状态值,则可能会遇到过时的状态问题。
来看看一个过时的状态是如何表现出来的。组件<DelayedCount>延迟3秒计数按钮点击的次数
import React, { useState } from 'react';
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状态:
import React, { useState } from 'react';
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 能正确表示点击次数。
4.3 复杂状态管理
useState()用于管理简单状态。对于复杂的状态管理,可以使用useReducer() 。它为需要多个状态操作的状态提供了更好的支持。
假设需要编写一个最喜欢的电影列表。用户可以添加电影,也可以删除已有的电影,实现方式大致如下:
import React, { useState } from 'react';
function FavoriteMovies() {
const [movies, setMovies] = useState([{ name: "Heat" }]);
const [newMovie, setNewMovie] = useState("");
const add = movie => setMovies([...movies, movie]);
const remove = index => {
setMovies([...movies.slice(0, index), ...movies.slice(index + 1)]);
};
const handleAddClick = () => {
if (newMovie === "") {
return;
}
add({ name: newMovie });
setNewMovie("");
};
return (
<>
<div className="movies">
{movies.map((movie, index) => {
return <Movie movie={movie} onRemove={() => remove(index)} />;
})}
</div>
<div className="add-movie">
<input
type="text"
value={newMovie}
onChange={event => setNewMovie(event.target.value)}
/>
<button onClick={handleAddClick}>Add movie</button>
</div>
</>
);
}
function Movie({ movie, onRemove }) {
return (
<div className="movie">
<span>{movie.name}</span>
<button onClick={onRemove}>Remove</button>
</div>
);
}
function App() {
return (
<div className="App">
<h2>My favorite movies</h2>
<FavoriteMovies />
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
状态列表需要几个操作:添加和删除电影,状态管理细节使组件混乱。
更好的解决方案是将复杂的状态管理提取到reducer中:
reducer 接受两个参数一个是 state 另一个是 action 。然后返回一个状态 count 和 dispath,count 是返回状态中的值,而 dispatch 是一个可以发布事件来更新 state 的
在 useReducer 传入 reducer 函数根据 action 来更新 state,如果 action 为 add 正增加 state 也就是增加 count。
在 button 中调用 dispatch 发布 add 事件,发布 add 事件后就会在 reducer 根据其类型对 state 进行对应操作,更新 state。
import React, { useReducer } from 'react';
function reducer(state, action) {
switch (action.type) {
case "add":
return [...state, action.item];
case "remove":
return [
...state.slice(0, action.index),
...state.slice(action.index + 1)
];
default:
throw new Error();
}
}
function FavoriteMovies() {
const [movies, dispatch] = useReducer(reducer, [{ name: "Heat" }]);
const [newMovie, setNewMovie] = useState("");
const handleAddClick = () => {
if (newMovie === "") {
return;
}
dispatch({ type: "add", item: { name: newMovie } });
setNewMovie("");
};
return (
<>
<div className="movies">
{movies.map((movie, index) => {
return (
<Movie
movie={movie}
onRemove={() => dispatch({ type: "remove", index })}
/>
);
})}
</div>
<div className="add-movie">
<input
type="text"
value={newMovie}
onChange={event => setNewMovie(event.target.value)}
/>
<button onClick={handleAddClick}>Add movie</button>
</div>
</>
);
}
function Movie({ movie, onRemove }) {
return (
<div className="movie">
<span>{movie.name}</span>
<button onClick={onRemove}>Remove</button>
</div>
);
}
function App() {
return (
<div className="App">
<h2>My favorite movies</h2>
<FavoriteMovies />
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
reducer
管理电影的状态,有两种操作类型:
"add"
将新电影插入列表"remove"
从列表中按索引删除电影
注意组件功能没有改变。但是这个版本的<FavoriteMovies>
更容易理解,因为状态管理已经被提取到reducer
中。
还有一个好处:可以将reducer
提取到一个单独的模块中,并在其他组件中重用它。另外,即使没有组件,也可以对reducer
进行单元测试。
这就是关注点分离的威力:组件渲染UI并响应事件,而reducer
执行状态操作。
查看效果:https://codesandbox.io/s/react-usestate-complex-state-usereducer-gpw87
4.4 状态 vs 引用
考虑这样一个场景:咱们想要计算组件渲染的次数。
一种简单的实现方法是初始化countRender状态,并在每次渲染时更新它(使用useEffect())
函数组件中没有生命周期,那么可以使用 useEffect 来替代。如果你熟悉 React class 的生命周期函数,你可以把 useEffect 看做 componentDidMount,componentDidUpdate 和 componentWillUnmount 这三个函数的组合。
import React, { useEffect } from 'react';
function CountMyRenders() {
const [countRender, setCountRender] = useState(0);
useEffect(function afterRender() {
setCountRender(countRender => countRender + 1);
});
return (
<div>I've rendered {countRender} times</div>
);
}
useEffect()在每次渲染后调用afterRender()回调。但是一旦countRender状态更新,组件就会重新渲染。这将触发另一个状态更新和另一个重新渲染,依此类推。
可变引用useRef()保存可变数据,这些数据在更改时不会触发重新渲染,使用可变的引用改造一下<CountMyRenders> :
import React, { useState, useEffect,useRef } from 'react';
function CountMyRenders() {
const countRenderRef = useRef(1);
useEffect(function afterRender() {
countRenderRef.current++;
});
return (
<div>I've rendered {countRenderRef.current} times</div>
);
}
function App() {
const [count, setCount] = useState(0);
return (
<div className="App">
<CountMyRenders />
<button onClick={() => setCount(count => count + 1)}>
Click to re-render
</button>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
每次渲染组件时,countRenderRef可变引用的值都会使countRenderRef.current ++递增。 重要的是,更改不会触发组件重新渲染。
打开例子:https://codesandbox.io/s/react-usestate-vs-useref-g6qv3?file=/src/index.js
5. 总结
要使函数组件有状态,请在组件的函数体中调用useState()。
useState(initialState)的第一个参数是初始状态。返回的数组有两项:当前状态和状态更新函数。
const [state, setState] = useState(initialState);
使用 setState(newState)来更新状态值。 另外,如果需要根据先前的状态更新状态,可以使用回调函数setState(prevState => newState)。
在单个组件中可以有多个状态:调用多次useState()。
当初始状态开销很大时,延迟初始化很方便。使用计算初始状态的回调调用useState(computeInitialState),并且此回调仅在初始渲染时执行一次。
必须确保使用useState()遵循规则。
当闭包捕获过时的状态变量时,就会出现过时状态的问题。可以通过使用一个回调来更新状态来解决这个问题,这个回调会根据先前的状态来计算新的状态。
使用useState()来管理一个简单的状态。为了处理更复杂的状态,一个更好的的选择是使用useReducer() 。