这篇文章解释了React HooksuseState
和useRef
。你将学习它们的基本用法,并了解这两个Hooks的不同使用情况。
你可以找到作为CodeSandbox的一部分的例子。要想看到不同的例子,只需改编以下一行,在App.js:
export default AppDemo6; // change to AppDemo<Nr>
了解useState
钩子
钩子 [useState](https://reactjs.org/docs/hooks-state.html)
钩子能够为功能组件开发组件状态。在React 16.8之前,只有基于类的组件才能实现组件的本地状态。
看一下下面的代码。
import { useState } from "react";
function AppDemo1() {
const stateWithUpdater = useState(true);
const darkMode = stateWithUpdater[0];
const darkModeUpdater = stateWithUpdater[1];
return (
<div>
<p>{darkMode ? "dark mode on" : "dark mode off"}</p>
<button onClick={() => darkModeUpdater(!darkMode)}>
toggle dark mode
</button>
</div>
);
}
useState
钩子返回一个有两个项目的数组。在这个例子中,我们实现了一个布尔组件的状态,并且我们用true
来初始化我们的Hook。
useState
的这个单一参数只在初始渲染周期中被考虑。然而,如果你需要一个计算复杂的初始值,那么你可以传递一个回调函数以达到性能优化的目的。
第一个数组项代表实际状态,第二个项构成状态更新函数。onClick
处理器演示了如何使用更新器函数 (darkModeUpdate
) 来改变状态变量 (darkMode
) 。像这样准确地更新你的状态是很重要的。下面的代码是非法的。
darkMode = true;
如果你对useState
Hook有一些经验,你可能会对我的例子的语法感到疑惑。默认的用法是在数组析构的帮助下利用返回的数组项。
const [darkMode, setDarkMode] = useState(true);
作为提醒,在使用任何Hook时,遵循Hook的规则是至关重要的,不仅仅是useState
或useRef
。
- Hooks只能从你的React函数的最高层调用
- Hooks不能从嵌套代码中调用(如循环、条件)。
- 钩子也可以在顶层从自定义钩子中调用
现在我们已经涵盖了基础知识,让我们通过下面的示例代码来看看Hook的各个方面。
import { useState } from "react";
import "./styles.css";
function AppDemo2() {
console.log("render App");
const [darkMode, setDarkMode] = useState(false);
return (
<div className={`App ${darkMode && "dark-mode"}`}>
<h1>The useState hook</h1>
<h2>Click the button to toggle the state</h2>
<button
onClick={() => {
setDarkMode(!darkMode);
}}
>
toggle dark mode
</button>
</div>
);
}
如果darkMode
被设置为true
,那么在className
中加入了一个额外的CSS类(dark-mode
),背景和文本的颜色被颠倒了。你可以从录音中的控制台输出看到,每次状态改变,相应的组件都会被重新渲染。
每一个状态的变化都会重新渲染App
组件。
React DevTools在这里特别有帮助,可以直观地突出组件渲染时的更新。在上一段录音中,你可以看到组件周围闪烁的边框,通知你另一个组件的渲染周期。
视觉上突出显示重新渲染的选项。
在下一个例子中,标题被提取到一个单独的React组件(Description
)。
import { useState } from "react";
import "./styles.css";
function AppDemo3() {
console.log("render App");
const [darkMode, setDarkMode] = useState(false);
return (
<div className={`App ${darkMode && "dark-mode"}`}>
<Description />
<button
onClick={() => {
setDarkMode(!darkMode);
}}
>
toggle dark mode
</button>
</div>
);
}
const Description = () => {
console.log("render Description");
return (
<>
<h1>The useState hook</h1>
<h2>Click the button to toggle the state</h2>
</>
);
};
只要用户点击按钮,App
组件就会被渲染,因为相应的点击处理程序会更新darkMode
状态变量。此外,子组件Description
也会被渲染。
每一个状态变化都会重新渲染App
和子组件。
下图说明了一个状态变化会导致一个渲染周期。
一个状态的更新会重新渲染相应的组件。
为什么理解React Hooks的生命周期很重要?一方面,只要你不通过updater函数更新状态,状态就会在渲染过程中被保留下来,这本身就会触发一个新的渲染周期。
使用useState
Hook与useEffect
另一个需要理解的重要概念是 [useEffect](https://reactjs.org/docs/hooks-effect.html)
Hook,你很可能要在你的应用程序中使用它来调用异步代码(例如,获取数据)。正如你在前面的图中看到的,useState
和useEffect
Hooks 是紧密耦合的,因为状态变化可能会调用效果。
让我们看一下下面的例子。我们引入了两个额外的状态变量:loading
和lang
。每当url
道具发生变化时,该效果就会被调用。它获取一个语言字符串(en
或de
)并通过setLang
更新器函数更新状态。
根据语言的不同,标题内的英语或德语字符串会被呈现出来。此外,在获取的过程中,loading
状态被设置,根据其值(true
或false
),一个加载指示器被呈现出来,而不是标题。
import { useEffect, useState } from "react";
import "./styles.css";
function App4({ url }) {
console.log("render App");
const [loading, setLoading] = useState(true);
const [lang, setLang] = useState("de");
const [darkMode, setDarkMode] = useState(false);
useEffect(() => {
console.log("useEffect");
const fetchData = async function () {
try {
setLoading(true);
const response = await axios.get(url);
if (response.status === 200) {
const { language } = response.data;
setLang(language);
}
} catch (error) {
throw error;
} finally {
setLoading(false);
}
};
fetchData();
}, [url]);
return (
<div className={`App ${darkMode && "dark-mode"}`}>
{loading ? (
<div>Loading...</div>
) : (
<>
<h1>
{lang === "en"
? "The useState hook is awesome"
: "Der useState Hook ist toll"}
</h1>
<button
onClick={() => {
setDarkMode(!darkMode);
}}
>
toggle dark mode
</button>
</>
)}
</div>
);
}
在useEffect
内设置加载和lang状态。
让我们假设我们想在获取了当前语言的时候切换黑暗模式。我们在更新语言后,添加一个对setDarkMode
更新器的调用。此外,我们需要将darkMode
状态作为一个依赖项添加到效果的依赖数组中。
为什么必须这样做超出了本文的范围,但你可以在我以前的文章中详细了解
useEffect
Hook。
import { useEffect, useState } from "react";
import "./styles.css";
function AppDemo5({ url }) {
console.log("render App");
const [loading, setLoading] = useState(true);
const [lang, setLang] = useState("de");
const [darkMode, setDarkMode] = useState(false);
useEffect(() => {
console.log("useEffect");
const fetchData = async function () {
try {
setLoading(true);
const response = await axios.get(url);
if (response.status === 200) {
const { language } = response.data;
setLang(language);
setDarkMode(!darkMode);
}
} catch (error) {
throw error;
} finally {
setLoading(false);
}
};
fetchData();
}, [url, darkMode]);
return (
<div className={`App ${darkMode && "dark-mode"}`}>
{loading ? (
<div>Loading...</div>
) : (
<>
<h1>
{lang === "en"
? "The useState hook is awesome"
: "Der useState Hook ist toll"}
</h1>
<button
onClick={() => {
setDarkMode(!darkMode);
}}
>
toggle dark mode
</button>
</>
)}
</div>
);
}
不幸的是,我们已经造成了一个无限的循环。
错误地使用状态与useEffect
,会导致无限循环。
这是为什么呢?因为我们在效果的依赖数组中添加了darkMode
,我们在效果中更新了这个确切的状态,效果再次被调用,再次更新状态,这样一直持续下去。
但是有一个办法!我们可以通过从以前的状态中计算新的状态来避免darkMode
作为效果的依赖。我们通过传递一个以先前状态为参数的函数,以不同方式调用setDarkMode
更新器。
修改后的useEffect
实现看起来像这样。
useEffect(() => {
console.log("useEffect");
const fetchData = async function () {
try {
setLoading(true);
const response = await axios.get(url);
if (response.status === 200) {
const { language } = response.data;
setLang(language);
setDarkMode((previous) => !previous); // no access of darkMode state
}
} catch (error) {
throw error;
} finally {
setLoading(false);
}
};
fetchData();
}, [url]); // no darkMode dependency
与基于类的组件的区别
如果你已经使用React很久了,或者你目前正在处理遗留的代码,你知道基于类的组件。在基于类的组件中,你有一个代表组件状态的单一对象。要更新整体状态的一个片断,你可以利用通用的 [setState]([https://reactjs.org/docs/state-and-lifecycle.html](https://reactjs.org/docs/state-and-lifecycle.html))
方法。
想象一下,我们只想更新darkMode
的状态变量。那么你可以只把更新的属性放到对象中;其余的状态不受影响。
this.setState({darkMode: false});
然而,对于功能组件,首选的方式是使用可以单独更新的原子状态变量。否则,你会很快发现自己处于泪水的谷底。
与AppDemo6
相比,下面这个组件(AppDemo7
)只在状态管理方面进行了重构。我们使用一个状态对象(state
),而不是三个具有原始数据类型的原子状态变量。
import { useEffect, useState } from "react";
import "./styles.css";
function AppDemo7({ url }) {
const initialState = {
loading: true,
lang: "de",
darkMode: true
};
const [state, setState] = useState(initialState);
console.log("render App", state);
useEffect(() => {
console.log("useEffect");
const fetchData = async function () {
try {
setState((prev) => ({
loading: true,
lang: prev.lang,
darkMode: prev.darkMode
}));
const response = await axios.get(url);
if (response.status === 200) {
const { language } = response.data;
setState((prev) => ({
lang: language,
darkMode: !prev.darkMode,
loading: prev.loading
}));
}
} catch (error) {
throw error;
} finally {
setState((prev) => ({
loading: false,
lang: prev.lang,
darkMode: prev.darkMode
}));
}
};
fetchData();
}, [url]);
return (
<div className={`App ${state.darkMode && "dark-mode"}`}>
{state.loading ? (
<div>Loading...</div>
) : (
<>
<h1>
{state.lang === "en"
? "The useState hook is awesome"
: "Der useState Hook ist toll"}
</h1>
<button
onClick={() => {
setState((prev) => ({
darkMode: !prev.darkMode,
// lang: prev.lang,
loading: prev.loading
}));
}}
>
toggle dark mode
</button>
</>
)}
</div>
);
}
正如你所看到的,这段代码很乱,很难维护。它还包括一个在onClick
处理程序中被注释掉的属性所说明的错误。当用户点击按钮时,整体状态没有被正确计算。
在这种情况下,lang
属性是不存在的。这导致了一个错误,即导致文本以德语呈现,因为state.lang
是undefined
。我希望我已经明确地表明,这是一个坏主意。顺便说一下,React团队也不建议这样做。
了解useRef
钩子
钩子 [useRef](https://reactjs.org/docs/hooks-reference.html#useref)
Hook类似于useState
,但不同的是 。在明确这一点之前,我将解释它的基本用法。
import { useRef } from 'react';
const AppDemo8 = () => {
const ref1 = useRef();
const ref2 = useRef(2021);
console.log("render");
console.log(ref1, ref2);
return (
<div>
<h2>{ref1.current}</h2>
<h2>{ref2.current}</h2>
</div>
);
};
结果并不引人注目,但显示了问题的关键所在。
这些值被存储在current
属性中。
我们通过调用初始化了两个引用(又称refs)。钩子调用返回一个对象,该对象有一个属性current
,它存储了实际值。如果你向useRef(initialValue)
传递一个参数initialValue
,那么这个值就存储在current
。
这就是为什么第一个console.log
输出存储了undefined
:因为我们调用Hook时没有任何参数。不要担心,我们可以在以后分配值。
要访问一个ref的值,你需要访问它的current
属性,就像我们在JJSX部分所做的那样。Refs在被定义后可以直接在初始渲染中使用。
但是为什么我们需要useRef
?为什么不使用普通的let
变量来代替呢?请稍安勿躁--我们会回来讨论这个问题。
浏览器的常见使用情况useRef
让我们看一下下面的例子。
import { useRef } from "react";
import "./styles.css";
const AppDemo9 = () => {
const countRef = useRef(0);
console.log("render");
return (
<div className="App">
<h2>count: {countRef.current}</h2>
<button
onClick={() => {
countRef.current = countRef.current + 1;
console.log(countRef.current);
}}
>
increase count
</button>
</div>
);
};
我们的目标是定义一个叫做countRef
的Ref,用0
来初始化这个值,并在每次点击按钮时增加这个计数器变量。渲染的计数值应该更新。不幸的是,这并不奏效--甚至控制台的输出也证明了current
属性持有正确的更新。
点击按钮时,计数没有更新。
正如你从我们的其他控制台输出渲染中看到的,我们的组件没有重新渲染。我们可以利用useState
来代替这种行为。
什么?所以useRef
是非常无用的?并非如此--它与其他触发重现的钩子结合使用很方便,例如useState
, [useReducer](https://reactjs.org/docs/hooks-reference.html#usereducer)
,以及 [useContext](https://reactjs.org/docs/hooks-reference.html#usecontext)
.
你必须把useRef
作为你工具箱中的另一个工具,你必须了解何时使用它。还记得上面的组件生命周期图吗?在整个渲染周期中,Refs的值会持续存在(特别是current
属性)。这不是一个错误,而是一个特点。
考虑一下这样的情况:你想更新一个组件的数据(即它的状态变量)来触发一次渲染,以便更新用户界面。你也可以有这样的情况:你希望有同样的行为,但有一个例外:你不希望触发一个渲染周期,因为这可能会导致bug、尴尬的用户体验(例如,闪烁)或性能问题。
你可以把Refs看作是基于类的组件的实例变量。一个ref是一个通用的容器,用来存储任何种类的数据,比如原始数据或对象。
很好,我们将展示一个有用的例子。
import { useState } from "react";
import "./styles.css";
const AppDemo10 = () => {
const [value, setValue] = useState("");
console.log("render");
const handleInputChange = (e) => {
setValue(e.target.value);
};
return (
<div className="App">
<input value={value} onChange={handleInputChange} />
</div>
);
};
从下面的录音中可以看到,这个组件只是渲染了一个输入字段,并将其值存储在value
状态变量中。控制台的输出显示,AppDemo10
组件在每次按键时都会被重新渲染。
这可能是你想要的行为,例如,在每个字符上执行一个操作,如搜索。这被称为受控组件。然而,这可能正好相反,渲染变得有问题。那么你就需要一个不受控的组件。
受控组件在每个按键上进行渲染。
让我们重写这个例子,使用一个不受控制的组件,useRef
。因此,我们需要一个按钮来更新组件的状态,并存储完全填充的输入字段。
import { useState, useRef } from "react";
import "./styles.css";
const AppDemo11 = () => {
const [value, setValue] = useState("");
const valueRef = useRef();
console.log("render");
const handleClick = () => {
console.log(valueRef);
setValue(valueRef.current.value);
};
return (
<div className="App">
<h4>Value: {value}</h4>
<input ref={valueRef} />
<button onClick={handleClick}>click</button>
</div>
);
};
有了这个解决方案,我们就不会在每个按键上引起渲染循环。在另一方面,我们需要用一个按钮来 "提交 "输入,以更新状态变量value
。你可以从控制台的输出中看到,第二次渲染首先发生在按钮的点击上。
一个不受控制的组件不会在变化时触发重新渲染。
顺便说一下,上面的例子显示了refs的第二个用例。
<input ref={valueRef} />
通过ref
属性,React提供了对React组件或HTML元素的直接访问。控制台输出显示,我们确实可以访问input
元素。该引用被存储在current
属性中。
这构成了useRef
的第二个用例,除了利用它作为一个通用容器在整个组件生命周期中持久化数据。如果你需要直接访问一个DOM元素,你可以利用ref
道具。下一个例子显示了如何在组件初始化后聚焦输入字段。
import { useEffect, useRef } from "react";
import "./styles.css";
const AppDemo12 = () => {
const inputRef = useRef();
console.log("render");
useEffect(() => {
console.log("useEffect");
inputRef.current.focus();
}, []);
return (
<div className="App">
<input ref={inputRef} placeholder="input" />
</div>
);
};
在useEffect
的回调中,我们调用本地的 [focus](https://developer.mozilla.org/en-US/docs/Web/API/HTMLOrForeignElement/focus)
方法。
在ref的帮助下聚焦一个输入字段。
在React项目中,当你需要直接访问DOM元素时,这种技术也被广泛用于与第三方(非React)组件相结合。
另一个常见的用例是当你需要前一个渲染周期的状态值时。下面的例子展示了如何做到这一点。当然,你也可以把这些逻辑提取到一个自定义的 [usePrevious](https://usehooks.com/usePrevious/)
钩子。
import { useEffect, useState, useRef } from "react";
import "./styles.css";
const AppDemo13 = () => {
console.log("render");
const [count, setCount] = useState(0);
// Get the previous value (was passed into hook on last render)
const ref = useRef();
// Store current value in ref
useEffect(() => {
console.log("useEffect");
ref.current = count;
}, [count]); // Only re-run if value changes
return (
<div className="App">
<h1>
Now: {count}, before: {ref.current}
</h1>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
};
在最初的渲染之后,执行一个效果,将状态变量count
赋给ref.current
。因为没有发生额外的渲染,渲染的值是undefined
。由于对setCount
的调用,对按钮的点击引发了状态的更新。
接下来,用户界面被重新渲染,之前的标签显示了正确的值(0
)。渲染之后,另一个效果被调用。现在1
被分配给我们的ref,以此类推。
在useRef
的帮助下访问以前的状态。
需要注意的是,所有的引用都需要在useEffect
回调或处理程序中得到更新。在渲染过程中突变 ref,即从刚才提到的那些地方以外的地方突变 ref,可能会带来bug。这一点也适用于useState
,也是如此。
为什么let
不能替代useRef
现在我还欠你一个解决方案,即为什么let
变量不能取代ref的概念。下一个例子从useEffect
Hook里面用一个普通的JavaScript变量赋值代替了useRef
的使用。
import { useEffect, useState } from "react";
import "./styles.css";
const AppDemo14 = () => {
console.log("render");
const [count, setCount] = useState(0);
let prevCount;
useEffect(() => {
console.log("useEffect", prevCount);
prevCount = count;
}, [count]);
return (
<div className="App">
<h1>
Now: {count}, before: {prevCount}
</h1>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
};
然而,下面的记录会显示这并不奏效。控制台的输出加强了这个问题,因为useEffect
里面的赋值在每个新的渲染周期都会被覆盖。undefined
因为let prevCount;
而被隐式赋值。
一个正常的变量赋值不能取代useRef。
甚至强大的ESLintRules of Hooks插件也告诉你,我们应该利用useRef
。
ESLint插件警告你要使用变量而不是Refs。
useRef
和useState
之间的区别一目了然
下面的区别已经被详细讨论过了,但在这里再一次以简洁的总结的形式呈现。
- 两者都在渲染周期和UI更新期间保留其数据,但只有
useState
Hook及其更新函数会导致重新渲染 useRef
会返回一个对象,该对象有一个current
的属性来保存实际值。相比之下,useState
返回一个有两个元素的数组:第一项构成状态,第二项代表状态更新器函数useRef
'的current
属性是可变的,但useState
'的状态变量不是。与useRef
的current
属性不同,你不应该直接给useState
的状态变量赋值。相反,总是使用updater函数(即第二个数组项)。正如React团队在基于类的组件中的setState
文档中所建议的那样(但对于函数组件来说仍然如此),将状态视为不可变的变量useState
和useRef
可以被认为是数据钩子,但只有useRef
可以用于另一个应用领域:获得对 React 组件或 DOM 元素的直接访问
结语
这篇文章讨论了useState
和useRef
Hooks。在这一点上应该很清楚,没有所谓的好或坏的Hook。你的React应用需要这两种Hooks,因为它们是为不同的应用设计的。
如果你想更新数据并导致UI更新,useState
是你的Hook。如果你在整个组件的生命周期中需要某种数据容器,而不会在突变你的变量时引起渲染周期,那么useRef
是你的解决方案。
The postuseState
vs. useRef
:相似性、差异性和使用案例首次出现在LogRocket博客上。