新手最常犯的 6 个 React Hook 错误
学习 React 最难的部分其实不是学习如何使用 React,而是学习如何编写简洁优秀的 React 代码。
在本文中,我将讨论几乎所有人在使用 useState 和 useEffect hook 时都会犯的 6 个错误。
错误1:在不需要的时候使用状态
我想谈的第一个错误就是在实际上不需要任何状态的情况下使用状态。
我们来看一个例子。
import {useState} from "react";
const App = () => {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
function onSubmit(e) {
e.preventDefault();
console.log({ email, password });
}
return (
<form onSubmit={onSubmit}>
<label htmlFor="email">Email</label>
<input
value={email}
onChange={e => setEmail(e.target.value)}
type="email"
id="email"
/>
<label htmlFor="password">Password</label>
<input
value={password}
onChange={e => setPassword(e.target.value)}
type="password"
id="password"
/>
<button type="submit">Submit</button>
</form>
)
};
export default App;
这里我们使用了电子邮件和密码状态,但问题是我们只关心表单提交时的电子邮件和密码值,电子邮件和密码更新时不需要重新渲染,所以与其跟踪状态并在每次输入字符时重新渲染,不如将它们存储在 ref 中。
import { useRef } from "react";
const App = () => {
const emailRef = useRef();
const passwordRef = useRef();
function onSubmit(e) {
e.preventDefault();
console.log({
email: emailRef.current.value,
password: passwordRef.current.value
});
}
return (
<form onSubmit={onSubmit}>
<label htmlFor="email">Email</label>
<input
ref={emailRef}
type="email"
id="email"
/>
<label htmlFor="password">Password</label>
<input
ref={passwordRef}
type="password"
id="password"
/>
<button type="submit">Submit</button>
</form>
)
};
export default App;
如果你点击提交按钮,然后再次查看控制台,它会打印出这些值。你完全不需要任何状态信息。
所以给你的第一个建议是思考你是否真的需要使用状态,并在每次状态改变时重新渲染组件,或者如果你不需要重新渲染组件,是否可以使用 ref。
此外,您还可以直接在表单中访问数据,因此也不需要引用。
import { useRef } from "react";
const App = () => {
function onSubmit(event) {
event.preventDefault();
const data = new FormData(event.target);
console.log(data.get('email'));
console.log(data.get('password'));
fetch('/api/form-submit-url', {
method: 'POST',
body: data,
});
}
return (
<form onSubmit={onSubmit}>
<label htmlFor="email">Email</label>
<input
type="email"
name="email"
id="email"
/>
<label htmlFor="password">Password</label>
<input
type="password"
name="password"
id="password"
/>
<button type="submit">Submit</button>
</form>
)
};
export default App;
错误 2:未使用 useState 的函数版本
我们来看一个例子。
import { useState } from "react";
export function Counter() {
const [count, setCount] = useState(0);
function adjustCount(amount) {
setCount(count + amount);
setCount(count + amount);
}
return (
<>
<button onClick={ () => adjustCount(-1) }> - </button>
<span> {count} </span>
<button onClick={ () => adjustCount(1) }> + </button>
</>
)
}
即使您点击两次按钮,计数也只会更新一次setCount。这是因为当第二次调用 setCount 时,第一次 setCount 尚未完成,计数值不会立即更新。
您应该使用版本控制函数来解决这个问题。
https://legacy.reactjs.org/docs/hooks-reference.html#functional-updates
import { useState } from "react";
export function Counter() {
const [count, setCount] = useState(0);
function adjustCount(amount) {
setCount(prevCount => prevCount + amount);
setCount(prevCount => prevCount + amount);
}
return (
<>
<button onClick={ () => adjustCount(-1) }> - </button>
<span> {count} </span>
<button onClick={ () => adjustCount(1) }> + </button>
</>
)
}
错误3:状态未立即更新
我们来看一个例子。
import { useState } from "react";
export function Counter() {
const [count, setCount] = useState(0);
function adjustCount(amount) {
setCount(prevCount => prevCount + amount);
// count is the value before setCount
console.log(count);
}
return (
<>
<button onClick={ () => adjustCount(-1) }> - </button>
<span> {count} </span>
<button onClick={ () => adjustCount(1) }> + </button>
</>
)
}
当你更新状态变量时,它实际上不会立即改变,而是在下次渲染时才会改变,所以你应该使用 useEffect 而不是在状态设置器之后添加代码。
import {useEffect, useState} from "react";
export function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log(count);
}, [count])
function adjustCount(amount) {
setCount(prevCount => prevCount + amount);
}
return (
<>
<button onClick={ () => adjustCount(-1) }> - </button>
<span> {count} </span>
<button onClick={ () => adjustCount(1) }> + </button>
</>
)
}
错误 4,不必要的使用效果
我们来看一个例子。
import { useEffect, useState } from "react";
const App = () => {
const [firstName, setFirstName] = useState('')
const [lastName, setLastName] = useState('')
const [fullName, setFullName] = useState('')
useEffect(() => {
setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName])
return (
<>
<input value={firstName} onChange={ e => setFirstName(e.target.value) } />
<input value={lastName} onChange={ e => setLastName(e.target.value) } />
{fullName}
</>
)
};
export default App;
问题是,当我们更新 firstName 或 lastName 时,状态 fullName 会更新并再次重新渲染组件,导致组件重新渲染两次。
如何使其达到最佳状态。
import { useState } from "react";
const App = () => {
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const fullName = `${firstName} ${lastName}`;
return (
<>
<input value={firstName} onChange={ e => setFirstName(e.target.value) } />
<input value={lastName} onChange={ e => setLastName(e.target.value) } />
{fullName}
</>
)
};
export default App;
错误 5,参照相等性错误
我们来看一个例子。
import {useEffect, useState} from "react";
const App = () => {
const [age, setAge] = useState(0);
const [name, setName] = useState('');
const [darkMode, setDarkMode] = useState(false);
const person = { age, name };
useEffect(() => {
console.log(person)
}, [person])
return (
<div style={{ background: darkMode ? "#333" : "#fff" }}>
Age: {" "}
<input value={age} type="number" onChange={ e => setAge(e.target.value)} />
<br/>
Name: <input value={name} onChange={ e => setName(e.target.value) } />
<br/>
Dark Mode: {" "}
<input type="checkbox" value={darkMode} onChange={e => setDarkMode(e.target.checked)} />
</div>
)
};
export default App;
如果更改年龄或姓名,useEffect 函数将被触发,但如果切换 darkMode,useEffect 函数也会被触发,这不是我们预期的结果。
要实现这一点,有两个关键点。
首先,每个渲染对象都有自己的 props 和 state,因此变量 person 会被初始化为一个新的值。如果您想详细了解这方面的内容,请查看这篇文章:https://overreacted.io/a-complete-guide-to-useeffect/
第二点是引用相等性,https://barker.codes/blog/referential-equality-in-javascript/
因此,当 darkMode 更新时,变量 person 的值不再相等。
我们可以使用useMemo来解决这个问题。
import {useEffect, useMemo, useState} from "react";
const App = () => {
const [age, setAge] = useState(0);
const [name, setName] = useState('');
const [darkMode, setDarkMode] = useState(false);
const person = useMemo(() => {
return { age, name }
}, [age, name])
useEffect(() => {
console.log(person)
}, [person])
return (
<div style={{ background: darkMode ? "#333" : "#fff" }}>
Age: {" "}
<input value={age} type="number" onChange={ e => setAge(e.target.value)} />
<br/>
Name: <input value={name} onChange={ e => setName(e.target.value) } />
<br/>
Dark Mode: {" "}
<input type="checkbox" value={darkMode} onChange={e => setDarkMode(e.target.checked)} />
</div>
)
};
export default App;
请注意,在 React 19 版本中,您可以使用React Compiler代替 useMemo。
错误 6:未中止获取请求
我们来看一个例子。
import {useEffect, useState} from "react";
export function useFetch(url) {
const [loading, setLoading] = useState(true);
const [data, setData] = useState();
const [error, setError] = useState();
useEffect(() => {
setLoading(true)
fetch(url)
.then(setData)
.catch(setError)
.finally(() => setLoading(false))
}, [url])
}
这个例子中的问题在于,当组件卸载或 URL 更改时,之前的 fetch 请求仍在后台运行,这在很多情况下都会导致无法按预期工作。详情请参阅这篇文章:https://plainenglish.io/community/how-to-cancel-fetch-and-axios-requests-in-react-useeffect-hook
我们可以使用 AbortController 来解决这个问题。
import {useEffect, useState} from "react";
export function useFetch(url) {
const [loading, setLoading] = useState(true);
const [data, setData] = useState();
const [error, setError] = useState();
useEffect(() => {
const controller = new AbortController();
setLoading(true)
fetch(url, { signal: controller.signal })
.then(setData)
.catch(setError)
.finally(() => setLoading(false))
return () => {
controller.abort();
}
}, [url])
}
本文基于以下 YouTube 视频:https://www.youtube.com/watch?v =GGo3MVBFr1A
文章来源:https://dev.to/markliu2013/top-6-react-hook-mistakes-beginners-make-1135