做者:Dmitri Pavlutinhtml
译者:前端小智前端
来源:dmitripavlutin.comreact
阿里云最近在作活动,低至2折,真心以为很划算了,能够点击本条内容或者连接进行参与: promotion.aliyun.com/ntms/yunpar…git
腾讯云最近在作活动,百款云产品低至 1 折,能够点击本条内容或者连接进行参与github
状态是隐藏在组件中的信息,组件能够在父组件不知道的状况下修改其状态。我更偏心函数组件,由于它们足够简单,要使函数组件具备状态管理,能够useState()
Hook。数组
本文会逐步讲解如何使用useState()
Hook。此外,还会介绍一些常见useState()
坑。微信
useState()
进行状态管理无状态的函数组件没有状态,以下所示(部分代码):闭包
import React from 'react';
function Bulbs() {
return <div className="bulb-off" />;
}
复制代码
能够找 codesandbox 尝试一下。less
运行效果:async
这时,要如何添加一个按钮来打开/关闭灯泡呢? 为此,我们须要具备状态的函数组件,也就是有状态函数组件。
useState()
是实现灯泡开关状态的 Hoook,将状态添加到函数组件须要4
个步骤:启用状态、初始化、读取和更新。
要将<Bulbs>
转换为有状态组件,须要告诉 React:从'react'
包中导入useState
钩子,而后在组件函数的顶部调用useState()
。
大体以下所示:
import React, { useState } from 'react';
function Bulbs() {
... = useState(...);
return <div className="bulb-off" />;
}
复制代码
在Bulbs
函数的第一行调用useState()
(暂时不要考Hook的参数和返回值)。 重要的是,在组件内部调用 Hook 会使该函数成为有状态的函数组件。
启用状态后,下一步是初始化它。
始时,灯泡关闭,对应到状态应使用false
初始化 Hook:
import React, { useState } from 'react';
function Bulbs() {
... = useState(false);
return <div className="bulb-off" />;
}
复制代码
useState(false)
用false
初始化状态。
启用和初始化状态以后,如何读取它?来看看useState(false)
返回什么。
当 hook useState(initialState)
被调用时,它返回一个数组,该数组的第一项是状态值
const stateArray = useState(false);
stateArray[0]; // => 状态值
复制代码
我们读取组件的状态
function Bulbs() {
const stateArray = useState(false);
return <div className={stateArray[0] ? 'bulb-on' : 'bulb-off'} />;
}
复制代码
<Bulbs>
组件状态初始化为false
,能够打开 codesandbox 看看效果。
useState(false)
返回一个数组,第一项包含状态值,该值当前为false
(由于状态已用false
初始化)。
我们可使用数组解构来将状态值提取到变量on
上:
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)
返回一个数组,其中第一项是状态值,第二项是一个更新状态的函数。
const [state, setState] = useState(initialState);
// 将状态更改成 'newState' 并触发从新渲染
setState(newState);
// 从新渲染`state`后的值为`newState`
复制代码
要更新组件的状态,请使用新状态调用更新器函数setState(newState)
。组件从新渲染后,状态接收新值newState
。
当点击开灯
按钮时将灯泡开关状态更新为true
,点击关灯
时更新为 false
。
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>
</>
);
}
复制代码
打开 codesandbox 自行尝试一下。
单击开灯按钮时,lightOn()
函数将on
更新为true
: setOn(true)
。单击关灯时也会发生相同的状况,只是状态更新为false
。
状态一旦改变,React 就会从新渲染组件,on
变量获取新的状态值。
状态更新做为对提供一些新信息的事件的响应。这些事件包括按钮单击、HTTP 请求完成等,确保在事件回调或其余 Hook 回调中调用状态更新函数。
使用回调更新状态
当使用前一个状态计算新状态时,可使用回调更新该状态:
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']);
复制代码
接着,经过这种方式从新实现上面电灯的示例:
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>
</>
);
}
复制代码
打开 codesandbox 自行尝试一下。
setOn(on => !on)
使用函数更新状态。
调用useState()
Hook 来启用函数组件中的状态。
useState(initialValue)
的第一个参数initialValue
是状态的初始值。
[state, setState] = useState(initialValue)
返回一个包含2
个元素的数组:状态值和状态更新函数。
使用新值调用状态更新器函数setState(newState)
更新状态。或者,可使用一个回调setState(prev => next)
来调用状态更新器,该回调将返回基于先前状态的新状态。
调用状态更新器后,React 确保从新渲染组件,以使新状态变为当前状态。
经过屡次调用useState()
,一个函数组件能够拥有多个状态。
function MyComponent() {
const [state1, setState1] = useState(initial1);
const [state2, setState2] = useState(initial2);
const [state3, setState3] = useState(initial3);
// ...
}
复制代码
须要注意的,要确保对useState()
的屡次调用在渲染之间始终保持相同的顺序(后面会讲)。
咱们添加一个按钮添加灯泡
,并添加一个新状态来保存灯泡数量,单击该按钮时,将添加一个新灯泡。
新的状态count
包含灯泡的数量,初始值为1
:
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>
</>
);
}
复制代码
打开演示,而后单击添加灯泡按钮:灯泡数量增长,单击开/关按钮可打开/关闭灯泡。
[count, setCount] = useState(1)
管理灯泡数量。多个状态能够在一个组件中正确工做。
每当 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()
,从而跳过昂贵的操做。
如今我们基本已经初步掌握了如何使用useState()
,尽管如此,我们必须注意在使用useState()
时可能遇到的常见问题。
useState()
在使用useState()
Hook 时,必须遵循 Hook 的规则
仅顶层调用 Hook :不能在循环,条件,嵌套函数等中调用useState()
。在多个useState()
调用中,渲染之间的调用顺序必须相同。
仅从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>
);
}
复制代码
如今etCount(count => count + 1)
在delay()
中正确更新计数状态。React 确保将最新状态值做为参数提供给更新状态函数,过期闭包的问题解决了。
打开演示,快速单击按钮。 延迟过去后,count
能正确表示点击次数。
useState()
用于管理简单状态。对于复杂的状态管理,可使用useReducer()
hook。它为须要多个状态操做的状态提供了更好的支持。
假设须要编写一个最喜欢的电影列表。用户能够添加电影,也能够删除已有的电影,实现方式大体以下:
import React, { useState } from 'react';
function FavoriteMovies() {
const [movies, setMovies] = useState([{ name: 'Heat' }]);
const add = movie => setMovies([...movies, movie]);
const remove = index => {
setMovies([
...movies.slice(0, index),
...movies.slice(index + 1)
]);
}
return (
// Use add(movie) and remove(index)...
);
}
复制代码
尝试演示:添加和删除本身喜欢的电影。
状态列表须要几个操做:添加和删除电影,状态管理细节使组件混乱。
更好的解决方案是将复杂的状态管理提取到reducer
中:
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 [state, dispatch] = useReducer(reducer, [{ name: 'Heat' }]);
return (
// Use dispatch({ type: 'add', item: movie })
// and dispatch({ type: 'remove', index })...
);
}
复制代码
reducer
管理电影的状态,有两种操做类型:
"add"
将新电影插入列表
"remove"
从列表中按索引删除电影
尝试演示并注意组件功能没有改变。可是这个版本的<FavoriteMovies>
更容易理解,由于状态管理已经被提取到reducer
中。
还有一个好处:能够将reducer
提取到一个单独的模块中,并在其余组件中重用它。另外,即便没有组件,也能够对reducer
进行单元测试。
这就是关注点分离的威力:组件渲染UI并响应事件,而reducer
执行状态操做。
考虑这样一个场景:我们想要计算组件渲染的次数。
一种简单的实现方法是初始化countRender
状态,并在每次渲染时更新它(使用useEffect()
hook)
import React, { useState, 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, { useRef, useEffect } from 'react';
function CountMyRenders() {
const countRenderRef = useRef(1);
useEffect(function afterRender() {
countRenderRef.current++;
});
return (
<div>I've rendered {countRenderRef.current} times</div>
);
}
复制代码
打开演示并单击几回按钮来触发从新渲染。
每次渲染组件时,countRenderRef
可变引用的值都会使countRenderRef.current ++
递增。 重要的是,更改不会触发组件从新渲染。
要使函数组件有状态,请在组件的函数体中调用useState()
。
useState(initialState)
的第一个参数是初始状态。返回的数组有两项:当前状态和状态更新函数。
const [state, setState] = useState(initialState);
复制代码
使用 setState(newState)
来更新状态值。 另外,若是须要根据先前的状态更新状态,可使用回调函数setState(prevState => newState)
。
在单个组件中能够有多个状态:调用屡次useState()
。
当初始状态开销很大时,延迟初始化很方便。使用计算初始状态的回调调用useState(computeInitialState)
,而且此回调仅在初始渲染时执行一次。
必须确保使用useState()
遵循 Hook 规则。
当闭包捕获过期的状态变量时,就会出现过期状态的问题。能够经过使用一个回调来更新状态来解决这个问题,这个回调会根据先前的状态来计算新的状态。
最后,您将使用useState()
来管理一个简单的状态。为了处理更复杂的状态,一个更好的的选择是使用useReducer()
hook。
原文:dmitripavlutin.com/react-usest…
代码部署后可能存在的BUG无法实时知道,过后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给你们推荐一个好用的BUG监控工具 Fundebug。
干货系列文章汇总以下,以为不错点个Star,欢迎 加群 互相学习。
由于篇幅的限制,今天的分享只到这里。若是你们想了解更多的内容的话,能够去扫一扫每篇文章最下面的二维码,而后关注我们的微信公众号,了解更多的资讯和有价值的内容。