头图

虽然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是一个非常强大的工具,不要忘记使用它们!对于这个问题我之前发了专门的文章来阐述

资源


晚安啦
36 声望1 粉丝