likes
comments
collection
share

React 的 useState,可能并没有想象中那么美好🧐🧐🧐

作者站长头像
站长
· 阅读数 14

在我们 React 开发的项目中,我们使用的最多的两个 hooks 无非就是 useState 和 useEffect 了,那么今天我们就来聊聊 useState 这个 hooks。

什么是 re-renders

在聊 useState 之前。我们先来了解一下什么是 re-renders。

在 React 中,re-render的意思是重新渲染,它指的是当组件的 UI 需要更新时,React 会重新计算组件的虚拟 DOM(Virtual DOM)并将其与之前的虚拟 DOM 进行比较,然后根据差异来更新实际的页面内容。

在实际开发中,当以下情况之一发生时,React 会触发重新渲染:

  • Props 更改:如果组件的 props 属性发生变化,React 会重新渲染组件以获取新的 props。
  • State 更改:如果组件的状态(state)使用 setState 方法来更新,React 会重新渲染以展示新的状态。
  • Context 更改:使用 React 的 Context API 时,如果与该 Context 相关的数据发生变化,会导致依赖该 Context 的组件重新渲染。
  • 父组件重新渲染:如果父组件重新渲染,通常会导致其子组件也重新渲染,除非使用了 React.memo 或 shouldComponentUpdate 等性能优化技巧。

使用 useState 的问题

正如我们已经知道的,只要组件内部状态变量的值发生了变化,react 就会重新渲染该组件以更新它的当前状态。尽管在小型应用程序中这不是一个大问题,但随着应用程序规模的增长,它可能会导致性能瓶颈。当涉及到表单时,每次输入(状态)发生变化时,React 都会尝试重新渲染以达到更新组件的效果。

接下来我们创建一个 React 组件,其中包含一个接受两个输入的表单:账号和密码,我们将使用状态来管理表单输入:

import React, { useRef, useState, useEffect } from "react";

const App = () => {
  const countRef = useRef(0);
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");

  useEffect(() => {
    countRef.current = countRef.current + 1;
  });

  function handleSubmit(e) {
    e.preventDefault();
    console.log({ email, password });
  }

  return (
    <div className="form-div">
      <form onSubmit={handleSubmit} autoComplete="off">
        <div className="input-field">
          <label htmlFor="email2">账号</label>
          <input
            id="email2"
            type="text"
            value={email}
            onChange={(e) => setEmail(e.target.value)}
            autoComplete="off"
          />
        </div>
        <div className="input-field">
          <label htmlFor="password2">密码</label>
          <input
            id="password2"
            type="password"
            value={password}
            onChange={(e) => setPassword(e.target.value)}
          />
        </div>
        <button type="submit">登录</button>
        <div>
          <p>
            组件被重新渲染了 <span>{countRef.current}</span>
          </p>
        </div>
      </form>
    </div>
  );
};

export default App;

接下来我们测试一下这个例子,如下图所示:

React 的 useState,可能并没有想象中那么美好🧐🧐🧐

正如你所看到的那样,组件被重新渲染了 16 次,并且随着输入字段数量的增加,计数将逐渐增加。在大多数情况下,表单值仅在表单提交期间使用。那么,是否需要为两个输入字段重新渲染组件大约 20 多次?答案显然是否定的!

此外,当输入字段的数量增加时,用于存储输入值的状态变量的数量也会增加,从而增加了代码库的复杂性。那么,有什么替代方法可以避免重新呈现,但又能实现表单的所有功能呢?

使用 FormData 来解决这个问题

因此,另一种方法是使用 JavaScript 的原生 FormData 接口。

FormData 接口提供了一种表示表单数据的键值对 key/value 的构造方式,并且可以轻松的将数据通过 XMLHttpRequest.send() 方法发送出去,本接口和此方法都相当简单直接。如果送出时的编码类型被设为 "multipart/form-data",它会使用和表单一样的格式。

下面是一些关于它的调用方式:

new FormData();
new FormData(form);
new FormData(form, submitter);

接下来我们使用这个方法对上面的例子进行更改,如下代码所示:

import React, { useRef, useEffect } from "react";

const App = () => {
  const countRef = useRef(0);

  useEffect(() => {
    countRef.current = countRef.current + 1;
  });

  function handleSubmit(e) {
    e.preventDefault();
    const form = new FormData(e.currentTarget);

    const email = form.get("email");
    const password = form.get("password");

    console.log({ email, password });

    const body = {};
    for (const [key, value] of form.entries()) {
      body[key] = value;
    }

    console.log(body);
  }

  return (
    <div className="form-div">
      <form onSubmit={handleSubmit} autoComplete="off">
        <div className="input-field">
          <label htmlFor="email">账号</label>
          <input id="email" type="text" name="email" />
        </div>
        <div className="input-field">
          <label htmlFor="password">密码</label>
          <input id="password" type="password" name="password" />
        </div>
        <button type="submit">登录</button>
        <div>
          <p>
            组件被重新渲染了 <span>{countRef.current}</span>
          </p>
        </div>
      </form>
    </div>
  );
};

export default App;

在这个组件中,我们根本没有使用 useState 钩子。相反,我们将向输入标记添加 name 属性。一旦用户提交了表单,在 handlesSubmit 函数中,我们通过 e.handlesSubmit 提供表单对象来创建 FormData。然后,我们迭代 FormData.entries() 方法来获取表单键和值,以构建表单主体。

React 的 useState,可能并没有想象中那么美好🧐🧐🧐

如上图所示,我们对表单进行更改,但是并没有引起组件的重新渲染,你不觉得这种方式很牛逼吗哈哈哈哈哈。

FormData 它自动处理动态字段。也就是说,如果你的表单有动态生成的字段(根据用户输入添加/删除字段),用 useState 管理它们的状态需要额外的处理,而 FormData 会自动处理它。

通过这个例子来说明,每当 useState 发生变化时会引起组件的重新渲染,我们不要太过依赖 useState,可以采取其他的方案来更好的实现特定功能。

总结

从上面的例子我们可以知道,用 useState 维护表单的状态的话,可能需要多个 useState 维护。这个例子展示了一些可能的缺点:

  1. 性能问题:每次输入框的值变化时,都会触发组件的重新渲染,而且 handleChange 函数中的复杂处理可能会导致性能问题。由于 useState 不支持传递函数来更新状态,我们不能直接将复杂处理逻辑放到 setMessage 中,这可能使得性能优化变得更加复杂。
  2. 多个状态管理复杂性:随着组件的复杂度增加,可能需要管理更多的状态。当状态之间存在依赖关系时,使用多个 useState 可能会导致代码复杂化,可读性降低。
  3. 更新逻辑不优雅:handleChange 函数中的复杂处理逻辑可能会让更新 message 的代码变得不够优雅,而且可能需要在多个地方复用相似的逻辑,导致代码重复。

在项目中如果遇到了组件内部使用多个 useState 来管理状态,我们还可以通过以下方式来优化它们:

  1. 拆分组件:将复杂的组件拆分成更小的子组件,每个子组件只管理自己需要的状态。这样可以提高代码的可读性和维护性,同时减少状态冗余。
  2. 使用自定义 Hook:将一组相关的状态和逻辑封装到自定义的 Hook 中,然后在组件中使用该 Hook,可以使代码更加模块化和可复用。例如,可以创建一个名为 useCounter 的 Hook 来处理计数器相关的状态和逻辑。
  3. 使用对象或数组来管理相关状态:如果多个状态之间存在紧密的关联,可以考虑将它们合并到一个对象或数组中进行管理,而不是使用多个独立的 useState。这样可以减少状态变量的数量,简化代码。
  4. 使用函数式更新:useState 函数返回的更新函数可以接受一个回调函数作为参数,可以使用该回调函数来更新状态。使用函数式更新可以避免依赖于当前状态值的更新问题,特别是在进行批量更新时更加安全和可靠。
  5. 避免过度使用 useState:在组件中使用过多的 useState 可能会导致状态管理复杂化。应该审查每个状态是否真正需要独立的管理,是否可以通过计算、推导或从父组件传递来满足需求。
转载自:https://juejin.cn/post/7275595571733561381
评论
请登录