尽管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.