共计 6743 个字符,预计需要花费 17 分钟才能阅读完成。
尽管 useState 是一个简略易用的工具,但仍有许多开发人员在应用它时犯了谬误。在代码审查中,我常常看到即便是有教训的开发人员也会犯这些谬误。
在本文中,我将通过简略实用的示例向您展现如何防止这些谬误。
谬误地获取上一个值
在应用 setState 时,能够将上一个状态作为回调的参数进行拜访。不应用它可能会导致意外的状态更新。咱们将通过一个典型的计数器示例来阐明这个谬误。
import {useCallback, useState} from "react";
export default function App() {const [counter, setCounter] = useState(0);
const handleIncrement = useCallback(() => {setCounter(counter + 1);
}, [counter]);
const handleDelayedIncrement = useCallback(() => {
// 这里的 counter +1 就是一个问题当 setTimeout 进行回调的时候 counter 值可能曾经变动了
setTimeout(() => setCounter(counter + 1), 1000);
}, [counter]);
return (
<div>
<h1>{`Counter is ${counter}`}</h1>
{/* This handler works just fine */}
<button onClick={handleIncrement}>Instant increment</button>
{/* Multi-clicking that handler causes unexpected states updates */}
<button onClick={handleDelayedIncrement}>Delayed increment</button>
</div>
);
}
当初让咱们在设置状态时应用回调函数。请留神,这也将帮忙咱们从 useCallback 中删除不必要的依赖项。请记住这个解决方案! 这个问题在面试中常常被问到 。
import {useCallback, useState} from "react";
export default function App() {const [counter, setCounter] = useState(0);
const handleIncrement = useCallback(() => {setCounter((prev) => prev + 1);
// Dependency removed!
}, []);
const handleDelayedIncrement = useCallback(() => {
// 应用回调函数无效的帮咱们解决 state 状态不统一的问题
setTimeout(() => setCounter((prev) => prev + 1), 1000);
// Dependency removed!
}, []);
return (
<div>
<h1>{`Counter is ${counter}`}</h1>
<button onClick={handleIncrement}>Instant increment</button>
<button onClick={handleDelayedIncrement}>Delayed increment</button>
</div>
);
}
在 useState 中存储全局状态
useState 只适宜存储组件的部分状态。这能够包含输出值、切换标记等。全局状态属于整个应用程序,不仅仅与一个特定的组件相干。如果您的数据在多个页面或小部件中应用,请思考将其放入全局状态中(如 React Context、Redux、MobX 等)。
让咱们通过一个示例来阐明。这个示例非常简单,然而假如咱们行将领有一个更加简单的应用程序。因而,组件档次将十分深,用户状态将在整个应用程序中应用。在这种状况下,咱们应该将咱们的状态拆散到全局范畴,这样它能够轻松地从应用程序的任何中央拜访(而且咱们不用将 props 传递到 20-40 级别)。
import React, {useState} from "react";
// Passing props
function PageFirst(user) {return user.name;}
// Passing props
function PageSecond(user) {return user.surname;}
export default function App() {
// User state 将会到处被应用,而且组件嵌套层级也会很深
const [user] = useState({name: "Pavel", surname: "Pogosov"});
return (
<>
<PageFirst user={user} />
<PageSecond user={user} />
</>
);
}
在这里,咱们应该优先应用全局状态,而不是应用部分状态。让咱们应用 React Context 重写这个示例。
import React, {createContext, useContext, useMemo, useState} from "react";
// Created context
const UserContext = createContext();
// That component separates user context from app, so we don't pollute it
function UserContextProvider({children}) {const [name, setName] = useState("Pavel");
const [surname, setSurname] = useState("Pogosov");
// We want to remember value reference, otherwise we will have unnecessary rerenders
const value = useMemo(() => {
return {
name,
surname,
setName,
setSurname
};
}, [name, surname]);
return <UserContext.Provider value={value}>{children}</UserContext.Provider>;
}
function PageFirst() {const { name} = useContext(UserContext);
return name;
}
function PageSecond() {const { surname} = useContext(UserContext);
return surname;
}
export default function App() {
return (
<UserContextProvider>
<PageFirst />
<PageSecond />
</UserContextProvider>
);
}
当初咱们能够轻松地从应用程序的任何局部拜访全局状态。这比应用纯 useState 要不便和清晰得多。
遗记初始化状态
这个谬误可能会在代码执行过程中引起谬误。您可能曾经看到了这种类型的谬误,它被命名为“无奈读取未定义的属性”。
import React, {useEffect, useState} from "react";
// Fetch users func. I don't handle error here, but you should always do it!
async function fetchUsers() {
const usersResponse = await fetch(`https://jsonplaceholder.typicode.com/users`);
const users = await usersResponse.json();
return users;
}
export default function App() {
// No initial state here, so users === undefined, until setUsers
const [users, setUsers] = useState();
useEffect(() => {fetchUsers().then(setUsers);
}, []);
return (
<div>
{/* Error, can't read properties of undefined */}}
{users.map(({id, name, email}) => (<div key={id}>
<h4>{name}</h4>
<h6>{email}</h6>
</div>
))}
</div>
);
}
纠正这个谬误和犯这个谬误一样容易!咱们应该将咱们的状态设置为一个空数组。如果您想不出任何初始状态,您能够搁置 null 并解决它。
import React, {useEffect, useState} from "react";
async function fetchUsers() {
const response = await fetch(`https://jsonplaceholder.typicode.com/users`);
const users = await response.json();
return users;
}
export default function App() {// If it doesn't cause errors in your case, it's still a good tone to always initialize it (even with null)
const [users, setUsers] = useState([]);
useEffect(() => {fetchUsers().then(setUsers);
}, []);
// 如果想要有更好的用户体验能够加上 loading
// if (users.length === 0) return <Loading />
return (
<div>
{users.map(({id, name, email}) => (<div key={id}>
<h4>{name}</h4>
<h6>{email}</h6>
</div>
))}
</div>
);
}
扭转属性值而不是返回新的状态
在任何时候都不应该扭转 state 对象的属性值,因为 react 更新的时候对于简单数据类型是做的浅比拟。
import {useCallback, useState} from "react";
export default function App() {
// Initialize State
const [userInfo, setUserInfo] = useState({
name: "Pavel",
surname: "Pogosov"
});
// field is either name or surname
const handleChangeInfo = useCallback((field) => {
// e is input onChange event
return (e) => {setUserInfo((prev) => {
// Here we are mutating prev state.
// That simply won't work as React doesn't recognise the change
prev[field] = e.target.value;
return prev;
});
};
}, []);
return (
<div>
<h2>{`Name = ${userInfo.name}`}</h2>
<h2>{`Surname = ${userInfo.surname}`}</h2>
<input value={userInfo.name} onChange={handleChangeInfo("name")} />
<input value={userInfo.surname} onChange={handleChangeInfo("surname")} />
</div>
);
}
解决办法非常简单。咱们应该防止扭转属性值,而是返回一个新状态。
import {useCallback, useState} from "react";
export default function App() {const [userInfo, setUserInfo] = useState({
name: "Pavel",
surname: "Pogosov"
});
const handleChangeInfo = useCallback((field) => {return (e) => {
// Now it works!
setUserInfo((prev) => ({
// So when we update name, surname stays in state and vice versa
...prev,
[field]: e.target.value
}));
};
}, []);
return (
<div>
<h2>{`Name = ${userInfo.name}`}</h2>
<h2>{`Surname = ${userInfo.surname}`}</h2>
<input value={userInfo.name} onChange={handleChangeInfo("name")} />
<input value={userInfo.surname} onChange={handleChangeInfo("surname")} />
</div>
);
}
Hooks 逻辑的复制粘贴
所有的 React hooks 都是可组合的,意味着它们能够组合在一起来封装特定的逻辑。这使您能够构建自定义 hooks,而后在整个应用程序中应用它们。
看一下上面的示例。对于这个简略的逻辑来说,它不是有点冗余吗?
import React, {useCallback, useState} from "react";
export default function App() {const [name, setName] = useState("");
const [surname, setSurname] = useState("");
const handleNameChange = useCallback((e) => {setName(e.target.value);
}, []);
const handleSurnameChange = useCallback((e) => {setSurname(e.target.value);
}, []);
return (
<div>
<input value={name} onChange={handleNameChange} />
<input value={surname} onChange={handleSurnameChange} />
</div>
);
}
咱们如何简化咱们的代码?基本上,咱们在这里做了两次雷同的事件——申明部分状态,并解决 onChange 事件。这能够轻松地拆散为一个自定义 hook,让咱们称其为 useInput!
import React, {useCallback, useState} from "react";
function useInput(defaultValue = "") {
// We declare this state only once!
const [value, setValue] = useState(defaultValue);
// We write this handler only once!
const handleChange = useCallback((e) => {setValue(e.target.value);
}, []);
// Cases when we need setValue are also possible
return [value, handleChange, setValue];
}
export default function App() {const [name, onChangeName] = useInput("Pavel");
const [surname, onChangeSurname] = useInput("Pogosov");
return (
<div>
<input value={name} onChange={onChangeName} />
<input value={surname} onChange={onChangeSurname} />
</div>
);
}
咱们将输出逻辑拆散到了专用的 hook 中,当初应用起来更加不便了。React hooks 是一个十分弱小的工具,不要遗记应用它们!对于这个问题我之前发了专门的文章来论述
资源
- React hooks official documentation.
- React Context official documentation.
- Awesome hooks library for React.