尽管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 propsfunction PageFirst(user) { return user.name;}// Passing propsfunction 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 contextconst UserContext = createContext();// That component separates user context from app, so we don't pollute itfunction 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.