简介

开发任何应用程序最具挑战性的方面通常是管理其状态。但是,我们经常需要在应用程序中管理多个状态,例如从外部服务器检索数据或在应用程序中更新数据时。

状态管理的困难是今天存在如此多状态管理库的原因——而且还有更多的库仍在开发中。值得庆幸的是,React 有几个以钩子形式用于状态管理的内置解决方案,这使得 React 中的状态管理更加容易。

众所周知,钩子在 React 组件开发中变得越来越重要,尤其是在功能组件中,因为它们完全取代了对基于类的组件的需求,这是管理有状态组件的传统方式。useState钩子是 React 中引入的众多钩子之一,但是尽管useState钩子已经存在了几年,但由于理解不足,开发人员仍然容易犯常见错误。

useState钩子可能很难理解,尤其是对于较新的 React 开发人员或从基于类的组件迁移到功能组件的开发人员。在本指南中,我们将探讨 React 开发人员经常犯的 5 个常见的useState错误以及如何避免这些错误。

我们将介绍的步骤:

  • 初始化 useState 错误

  • 不使用可选链接

  • 直接更新useState

  • 更新特定对象属性

  • 管理表单中的多个输入字段

初始化 useState 错误

错误地启动 useState 钩子是开发人员在使用它时最常犯的错误之一。初始化useState是什么意思?初始化某物就是设置它的初始值。

问题是 useState 允许你使用任何你想要的东西来定义它的初始状态。但是,没有人直接告诉您的是,根据您的组件在该状态下的期望,使用错误的日期类型值来初始化您的 useState 可能会导致您的应用程序出现意外行为,例如无法呈现 UI,从而导致黑屏错误。

例如,我们有一个组件需要一个包含用户姓名、图像和简历的用户对象状态——在这个组件中,我们正在渲染用户的属性。

使用不同的数据类型(例如空状态或空值)初始化 useState 会导致空白页错误,如下所示。

import { useState } from "react";

function App() {
  // Initializing state
  const [user, setUser] = useState();

  // Render UI
  return (
    <div className='App'>
      <img src={user.image} alt='profile image' />
      <p>User: {user.name}</p>
      <p>About: {user.bio}</p>
    </div>
  );
}

export default App;

进入全屏模式 退出全屏模式

输出:

检查控制台会抛出类似的错误,如下所示:

较新的开发人员在初始化他们的状态时经常犯这个错误,特别是在从服务器或数据库获取数据时,因为检索到的数据预计会使用实际的用户对象更新状态。但是,这是一种不好的做法,可能会导致预期的行为,如上所示。

初始化useState的首选方法是将预期的数据类型传递给它,以避免潜在的空白页错误。例如,可以将如下所示的空对象传递给状态:

import { useState } from "react";

function App() {
  // Initializing state with expected data type
  const [user, setUser] = useState({});

  // Render UI
  return (
    <div className='App'>
      <img src={user.image} alt='profile image' />
      <p>User: {user.name}</p>
      <p>About: {user.bio}</p>
    </div>
  );
}

export default App;

进入全屏模式 退出全屏模式

输出:

我们可以通过在初始化状态时定义用户对象的预期属性来更进一步。

import { useState } from "react";

function App() {
  // Initializing state with expected data type
  const [user, setUser] = useState({
    image: "",
    name: "",
    bio: "",
  });

  // Render UI
  return (
    <div className='App'>
      <img src={user.image} alt='profile image' />
      <p>User: {user.name}</p>
      <p>About: {user.bio}</p>
    </div>
  );
}

export default App;

进入全屏模式 退出全屏模式

不使用可选链接

有时仅使用预期的数据类型初始化 useState 通常不足以防止意外的空白页错误。当试图访问深埋在相关对象链中的深层嵌套对象的属性时,尤其如此。

您通常尝试通过使用点 (.) 链接运算符链接相关对象来访问此对象,例如user.names.firstName。但是,如果缺少任何链接的对象或属性,我们就会遇到问题。页面将中断,用户将收到空白页错误。

例如:

import { useState } from "react";

function App() {
  // Initializing state with expected data type
  const [user, setUser] = useState({});

  // Render UI
  return (
    <div className='App'>
      <img src={user.image} alt='profile image' />
      <p>User: {user.names.firstName}</p>
      <p>About: {user.bio}</p>
    </div>
  );
}

export default App;

进入全屏模式 退出全屏模式

输出错误:

这个错误和 UI 不渲染的典型解决方案是使用条件检查来验证状态的存在,以在渲染组件之前检查它是否可访问,例如user.names && user.names.firstName,它仅在左表达式为真时评估右表达式(如果user.names存在)。然而,这个解决方案是一个混乱的解决方案,因为它需要对每个对象链进行多次检查。

使用可选的链接运算符(?.),您可以读取深埋在相关对象链中的属性的值,而无需验证每个引用的对象是否有效。可选链运算符(?.)与点链运算符(.)类似,只是如果缺少引用的对象或属性(即 null 或未定义),则表达式会短路并返回未定义的值。简而言之,如果缺少任何链接对象,它就不会继续进行链接操作(短路)。

例如,user?.names?.firstName不会抛出任何错误或破坏页面,因为一旦它检测到用户或名称对象丢失,它会立即终止操作。

import { useState } from "react";

function App() {
  // Initializing state with expected data type
  const [user, setUser] = useState({});

  // Render UI
  return (
    <div className='App'>
      <img src={user.image} alt='profile image' />
      <p>User: {user?.names?.firstName}</p>
      <p>About: {user.bio}</p>
    </div>
  );
}

export default App;

进入全屏模式 退出全屏模式

在访问状态中的链接属性时,利用可选的链接运算符可以简化和缩短表达式,这在探索可能事先不知道其引用的对象的内容时非常有用。

直接更新useState

缺乏对 React 如何调度和更新状态的正确理解很容易导致更新应用程序状态时出现错误。在使用useState时,我们一般会定义一个状态,直接使用set state函数更新状态。

例如,我们创建一个计数状态和一个附加到按钮的处理函数,该按钮在单击时将状态加一 (+1):

import { useState } from "react";

function App() {
  const [count, setCount] = useState(0);

  // Directly update state
  const increase = () => setCount(count + 1);

  // Render UI
  return (
    <div className='App'>
      <span>Count: {count}</span>
      <button onClick={increase}>Add +1</button>
    </div>
  );
}

export default App;

进入全屏模式 退出全屏模式

输出:

这按预期工作。但是,直接更新状态是一种不好的做法,在处理多个用户使用的实时应用程序时可能会导致潜在的错误。为什么?因为与您可能想的相反,React 不会在单击按钮时立即更新状态,如示例演示所示。相反,React 会拍摄当前状态的快照,并安排此更新 (+1) 稍后进行以提高性能 - 这以毫秒为单位发生,因此人眼无法察觉。但是,虽然计划的更新仍处于待处理状态,但当前状态可能会因其他原因(例如多个用户的情况)而改变。计划的更新将无法知道这个新事件,因为它只记录了单击按钮时所拍摄的状态快照。

这可能会导致您的应用程序出现重大错误和奇怪的行为。让我们通过添加另一个在 2 秒延迟后异步更新计数状态的按钮来看看这一点。

import { useState } from "react";

function App() {
  const [count, setCount] = useState(0);

  // Directly update state
  const update = () => setCount(count + 1);

  // Directly update state after 2 sec
  const asyncUpdate = () => {
    setTimeout(() => {
      setCount(count + 1);
    }, 2000);
  };

  // Render UI
  return (
    <div className='App'>
      <span>Count: {count}</span>
      <button onClick={update}>Add +1</button>
      <button onClick={asyncUpdate}>Add +1 later</button>
    </div>
  );
}

进入全屏模式 退出全屏模式

注意输出中的错误:

注意到错误了吗?我们首先单击第一个“添加 +1”按钮两次(将状态更新为 1 + 1 u003d 2)。之后,我们单击“稍后添加 +1”——这将拍摄当前状态 (2) 的快照,并安排更新以在两秒后向该状态添加 1。但是,虽然这个计划的更新仍在过渡中,但我们继续点击“添加 +1”按钮三次,将当前状态更新为 5(即 2 + 1 + 1 + 1 u003d 5)。然而,异步调度更新尝试在两秒后使用它在内存中的快照 (2) 更新状态(即 2 + 1 u003d 3),但没有意识到当前状态已更新为 5。结果,状态更新为 3 而不是 6。

这种无意的错误经常困扰仅使用 setState(newValue) 函数直接更新状态的应用程序。更新 useState 的建议方法是通过函数更新向setState()传递一个回调函数,在这个回调函数中我们传递该实例的当前状态,例如setState(currentState => currentState + newValue)。这会将计划更新时间的当前状态传递给回调函数,从而可以在尝试更新之前了解当前状态。

因此,让我们修改示例演示以使用功能更新而不是直接更新。

import { useState } from "react";

function App() {
  const [count, setCount] = useState(0);

  // Directly update state
  const update = () => setCount(count + 1);

  // Directly update state after 3 sec
  const asyncUpdate = () => {
    setTimeout(() => {
      setCount((currentCount) => currentCount + 1);
    }, 2000);
  };

  // Render UI
  return (
    <div className='App'>
      <span>Count: {count}</span>
      <button onClick={update}>Add +1</button>
      <button onClick={asyncUpdate}>Add +1 later</button>
    </div>
  );
}

export default App;

进入全屏模式 退出全屏模式

输出:

通过函数更新,setState()函数知道状态已更新为 5,因此将状态更新为 6。

更新特定对象属性

另一个常见的错误是只修改对象或数组的属性而不是引用本身。

例如,我们用定义的“name”和“age”属性初始化一个用户对象。但是,我们的组件有一个按钮,它试图只更新用户名,如下所示。

import { useState, useEffect } from "react";

export default function App() {
  const [user, setUser] = useState({
    name: "John",
    age: 25,
  });

  // Update property of user state
  const changeName = () => {
    setUser((user) => (user.name = "Mark"));
  };

  // Render UI
  return (
    <div className='App'>
      <p>User: {user.name}</p>
      <p>Age: {user.age}</p>

      <button onClick={changeName}>Change name</button>
    </div>
  );
}

进入全屏模式 退出全屏模式

点击按钮前的初始状态:

单击按钮后的更新状态:

如您所见,用户不再是对象,而是被字符串“Mark”覆盖,而不是修改特定属性。为什么?因为 setState() 将返回或传递给它的任何值作为新状态分配。这个错误在 React 开发人员从基于类的组件迁移到函数式组件时很常见,因为他们习惯于在基于类的组件中使用this.state.user.property = newValue更新状态。

一种典型的老式方法是创建一个新的对象引用并将先前的用户对象分配给它,并直接修改用户名。

import { useState, useEffect } from "react";

export default function App() {
  const [user, setUser] = useState({
    name: "John",
    age: 25,
  });

  // Update property of user state
  const changeName = () => {
    setUser((user) => Object.assign({}, user, { name: "Mark" }));
  };

  // Render UI
  return (
    <div className='App'>
      <p>User: {user.name}</p>
      <p>Age: {user.age}</p>

      <button onClick={changeName}>Change name</button>
    </div>
  );
}

进入全屏模式 退出全屏模式

单击按钮后的更新状态:

请注意,仅修改了用户名,而其他属性保持不变。

然而,更新特定属性或对象或数组的理想和现代方式是使用 ES6 扩展运算符 (...)。当使用功能组件中的状态时,这是更新对象或数组的特定属性的理想方式。使用此扩展运算符,您可以轻松地将现有项目的属性解包到新项目中,同时修改或添加新属性到解包项目。

import { useState, useEffect } from "react";

export default function App() {
  const [user, setUser] = useState({
    name: "John",
    age: 25,
  });

  // Update property of user state using spread operator
  const changeName = () => {
    setUser((user) => ({ ...user, name: "Mark" }));
  };

  // Render UI
  return (
    <div className='App'>
      <p>User: {user.name}</p>
      <p>Age: {user.age}</p>

      <button onClick={changeName}>Change name</button>
    </div>
  );
}

进入全屏模式 退出全屏模式

结果将与最后一个状态相同。单击按钮后,名称属性将更新,而其余用户属性保持不变。

管理表单中的多个输入字段

管理表单中的多个受控输入通常是通过为每个输入字段手动创建多个useState()函数并将每个函数绑定到相应的输入字段来完成的。例如:

import { useState, useEffect } from "react";

export default function App() {
  const [firstName, setFirstName] = useState("");
  const [lastName, setLastName] = useState("");
  const [age, setAge] = useState("");
  const [userName, setUserName] = useState("");
  const [password, setPassword] = useState("");
  const [email, setEmail] = useState("");

  // Render UI
  return (
    <div className='App'>
      <form>
        <input type='text' placeholder='First Name' />
        <input type='text' placeholder='Last Name' />
        <input type='number' placeholder='Age' />
        <input type='text' placeholder='Username' />
        <input type='password' placeholder='Password' />
        <input type='email' placeholder='email' />
      </form>
    </div>
  );
}

进入全屏模式 退出全屏模式

此外,您必须为每个输入创建一个处理函数,以建立双向数据流,在输入输入值时更新每个状态。这可能是相当多余和耗时的,因为它涉及编写大量降低代码库可读性的代码。

但是,可以只使用一个 useState 挂钩来管理表单中的多个输入字段。这可以通过首先给每个输入字段一个唯一的名称然后创建一个useState()函数来实现,该函数使用与输入字段名称相同的属性进行初始化。

import { useState, useEffect } from "react";

export default function App() {
  const [user, setUser] = useState({
    firstName: "",
    lastName: "",
    age: "",
    username: "",
    password: "",
    email: "",
  });

  // Render UI
  return (
    <div className='App'>
      <form>
        <input type='text' name='firstName' placeholder='First Name' />
        <input type='text' name='lastName' placeholder='Last Name' />
        <input type='number' name='age' placeholder='Age' />
        <input type='text' name='username' placeholder='Username' />
        <input type='password' name='password' placeholder='Password' />
        <input type='email' name='email' placeholder='email' />
      </form>
    </div>
  );
}

进入全屏模式 退出全屏模式

之后,我们创建一个处理程序事件函数,该函数更新用户对象的特定属性,以在用户输入内容时反映表单中的更改。这可以使用扩展运算符来完成,并使用event.target.elementsName = event.target.value动态访问触发处理函数的特定输入元素的名称。

换句话说,我们检查通常传递给事件函数的事件对象的目标元素名称(与用户状态中的属性名称相同),并使用该目标元素中的关联值对其进行更新,如图所示以下:

import { useState, useEffect } from "react";

export default function App() {
  const [user, setUser] = useState({
    firstName: "",
    lastName: "",
    age: "",
    username: "",
    password: "",
    email: "",
  });

  // Update specific input field
  const handleChange = (e) => 
    setUser(prevState => ({...prevState, [e.target.name]: e.target.value}))

  // Render UI
  return (
    <div className='App'>
      <form>
        <input type='text' onChange={handleChange} name='firstName' placeholder='First Name' />
        <input type='text' onChange={handleChange} name='lastName' placeholder='Last Name' />
        <input type='number' onChange={handleChange} name='age' placeholder='Age' />
        <input type='text' onChange={handleChange} name='username' placeholder='Username' />
        <input type='password' onChange={handleChange} name='password' placeholder='Password' />
        <input type='email' onChange={handleChange} name='email' placeholder='email' />
      </form>
    </div>
  );
}

进入全屏模式 退出全屏模式

使用此实现,事件处理函数会为每个用户输入触发。在这个事件函数中,我们有一个setUser()状态函数,它接受用户的先前/当前状态并使用扩展运算符解包这个用户状态。然后我们检查事件对象中触发函数的任何目标元素名称(与状态中的属性名称相关)。一旦得到这个属性名,我们修改它以反映表单中的用户输入值。

结论

作为一个创建高度交互用户界面的 React 开发人员,你可能犯了上面提到的一些错误。希望这些有用的 useState 实践可以帮助您在构建基于 React 的应用程序时使用useState挂钩时避免其中的一些潜在错误。

作家:大卫赫伯特


无约束地构建基于 React 的 CRUD 应用程序

构建 CRUD 应用程序涉及许多重复性任务,会消耗您宝贵的开发时间。如果您从头开始,您还必须为应用程序的关键部分(如身份验证、授权、状态管理和网络)实施自定义解决方案。

查看精炼,如果您对下一个 CRUD 项目的具有健壮架构和行业最佳实践的无头框架感兴趣。

[

细化博客logo

](https://github.com/pankod/refine)

refine 是一个基于 React 的开源框架,用于构建 CRUD 应用程序没有约束。

它可以将您的开发时间加快 3X,而不会影响样式自定义项目工作流程的自由度。

refine 在设计上是无头的,它连接了 30+ 开箱即用的后端服务,包括自定义 REST 和 GraphQL API。

访问优化 GitHub 存储库以获取更多信息、演示、教程和示例项目。

Logo

React社区为您提供最前沿的新闻资讯和知识内容