React useState的错误用法避坑详解

   2023-02-07 学习力0
核心提示:目录引言冗余的 state简单示例问题分析解决方案重复的 state简单示例问题分析解决方案使用 useEffect 更新 state简单示例问题分析解决方案使用 useEffect 监听 state 变化简单示例问题分析解决方案矛盾的 state简单示例问题分析解决方案深度嵌套的 state简单

引言

本文源于翻译 Avoid These Common Pitfalls Of React useState 由公众号KooFE前端团队完成翻译

useState 是我们使用最频繁的 React hook,在代码中随处可见,但是也经常会出现一些错误的用法。

或许你已经经历过这些错误的用法,但是可能还没有意识到这是错误,比如写出了一些冗余的、重复的、矛盾的 state,让你不得不额外使用 useEffect 来处理它们。由于这些错误用法的存在,会让代码的可读性变差,提高了代码的维护成本。

了解这些易犯的错误,可以让我们获得如下收益:

  • 代码更容易阅读和维护
  • 减少代码出 Bug 的可能性
  • 降低代码的复杂程度

在本文中,将介绍一些关于 useState 的常见错误,以便在今后的工作中避免这些错误。

冗余的 state

对于初级开发者来说,定义和使用冗余的 state 是一个比较常见的错误。如果一个 state 依赖了另外一个 state,就是这种典型的错误用法。

简单示例

下面是一个简单的组件,允许用户编辑自己的姓名,其中第一个输入框是用户的姓氏,后一个输入框是用户的名字,然后将姓名组合在一起渲染在输入框的下面。

React useState的错误用法避坑详解

代码实现如下:

import { useState } from "react";
function RedundantState() {
  const [firstName, setFirstName] = useState(""); // 姓氏
  const [lastName, setLastName] = useState(""); // 名字
  const [fullName, setFullName] = useState(""); // 姓名
  const onChangeFirstName = (event) => {
    setFirstName(event.target.value);
    setFullName(`${event.target.value} ${lastName}`);
  };
  const onChangeLastName = (event) => {
    setLastName(event.target.value);
    setFullName(`${firstName} ${event.target.value}`);
  };
  return (
    <>
      <form>
        <input
          value={firstName}
          onChange={onChangeFirstName}
          placeholder="First Name"
        />
        <input
          value={lastName}
          onChange={onChangeLastName}
          placeholder="Last Name"
        />
      </form>
      <div>Full name: {fullName}</div>
    </>
  );
}

很明显,这段代码中的 fullName 是冗余的 state

问题分析

可能你会说,先后依次更新 firstName 和 fullName 会导致额外的渲染周期。

const onChangeFirstName = (event) => {
  setFirstName(event.target.value);
  setFullName(`${event.target.value} ${lastName}`);
};

但是,React state 的更新是批量更新,所以不会为每个 state 更新做单独的渲染。

React useState的错误用法避坑详解

因此,在大多数情况下,性能方面的差异不大。问题在于可维护性和引入错误的风险。让我们再次看一下示例代码:

const onChangeFirstName = (event) => {
  setFirstName(event.target.value);
  setFullName(`${event.target.value} ${lastName}`);
};
const onChangeLastName = (event) => {
  setLastName(event.target.value);
  setFullName(`${firstName} ${event.target.value}`);
};

每次更新firstName 或 lastName 时,我们都必须要更新 fullName。在更复杂的场景中,这很容易被遗漏。因此,这会导致代码更难重构,引入 bug 的可能性也会增加。

如前所述,在大多数情况下,我们不必担心性能。但是,如果被依赖的 state 是大型的数组或需要大量的计算,则可以使用 useMemo 来做优化处理。

解决方案

fullName 可以由 firstName 和 lastName 直接拼接而成。

export function RedundantState() {
  const [firstName, setFirstName] = useState("");
  const [lastName, setLastName] = useState("");
  const fullName = `${firstName} ${lastName}`;
  ...
  return (
    <>
      <form>
        ...
      </form>
      <div>Full name: {fullName}</div>
    </>
  );
}

重复的 state

在多个 state 中存在重复的数据,也是一个比较常见的错误。通常在做数据的转换、排序或过滤时会遇到这种情况。另一种常见情况是选择展示不同的数据,比如接下来介绍的例子。

简单示例

这个组件用于显示项目列表,用户可以单击相应的按钮来打开 modal 弹窗。

React useState的错误用法避坑详解

在下面的代码中就存在这种错误用法。

import { useState } from "react";
// const items = [
//   {
//     id: "item-1",
//     text: "Item 1",
//   },
//   ...
// ]
function DuplicateState({ items }) {
  const [selectedItem, setSelectedItem] = useState();
  const onClickItem = (item) => {
    setSelectedItem(item);
  };
  return (
    <>
      {selectedItem && <Modal item={selectedItem} />}
      <ul>
        {items.map((row) => (
          <li key={row.id}>
            {row.text}
            <button onClick={() => onClickItem(row)}>Open</button>
          </li>
        ))}
      </ul>
    </>
  );
}

这段代码中的问题是,将 item 原封不动地拷贝到了 state 中。

问题分析

在上面的代码中,这种重复的数据违反了单一数据源原则。事实上,一旦用户选择了任何一项,我们就会出现两个数据源:selectedItem 状态和 items 数组中的数据。

假如,用户能够在 modal 弹窗中编辑这些数据。可能会是这样的:

  • 用户在 modal 弹窗中更改数据并提交
  • 将向服务器发送请求并更新数据库中的 item
  • 前端更新 item 数据(通过服务器的响应或重新请求 items 列表)。
  • 前端使用新的 items 数组重新渲染。
  • 现在的问题是:DuplicateState 组件内部发生了什么?

这就是问题所在。selectedItem 状态仍将包含旧数据。它将不同步。你可以想象,在更复杂的情况下,这可能会成为一个令人讨厌的 bug。

当然,我们可以写代码来实现 selectedItem 状态同步。但我们不得不使用 useEffect 来监听 items 数组中的变化。

解决方案

一个更简单的解决方案是只跟踪选定的 id。正如你所看到的,该解决方案 “冗余的 state” 部分中的解决方案非常相似:我们只需从 id 中计算出 selectedItem 变量。

// const items = [
//   {
//     id: "item-1",
//     text: "Item 1",
//   },
//   ...
// ]
function DuplicateState({ items }) {
  const [selectedItemId, setSelectedItemId] = useState();
  const selectedItem = items.find(({ id }) => id === selectedItemId);
  const onClickItem = (itemId) => {
    setSelectedItemId(itemId);
  };
  return (
    <>
      {selectedItem && <Modal item={selectedItem} />}
      <ul>
        {items.map((row) => (
          <li key={row.id}>
            {row.text}
            <button onClick={() => onClickItem(row.id)}>Open</button>
          </li>
        ))}
      </ul>
    </>
  );
}

使用 useEffect 更新 state

另一个常见问题是使用 useEffect 来监听变量的变化。

简单示例

我们继续使用上一节的示例:

React useState的错误用法避坑详解

在组件中,当 items 发生变化后,使用 useEffect 同步给 selectedItem。

import { useEffect, useState } from "react";
// const items = [
//   {
//     id: "item-1",
//     text: "Item 1",
//   },
//   ...
// ]
function DuplicateState({ items }) {
  const [selectedItem, setSelectedItem] = useState();
  useEffect(() => {
    if (selectedItem) {
      setSelectedItem(items.find(({ id }) => id === selectedItem.id));
    }
  }, [items]);
  const onClickItem = (item) => {
    setSelectedItem(item);
  };
  return (
    <>
      {selectedItem && <Modal item={selectedItem} />}
      <ul>
        {items.map((row) => (
          <li key={row.id}>
            {row.text}
            <button onClick={() => onClickItem(row)}>Open</button>
          </li>
        ))}
      </ul>
    </>
  );
}

这段代码能够正常工作,并同步保持 selectedItem 状态。是不是觉得它的实现方式有点 hack?

问题分析

这种方法存在多个问题:

  • useEffect 不容易阅读和理解。因此,使用 useEffect 的次数越少越好。
  • 在 useEffect 中更新 state 会导致额外的渲染。虽然不会引起性能方面的大问题,但也需要考虑。
  • 在代码中,我们在 selectedItem 状态和 items 属性之间引入了某种隐藏的关系。在阅读或更改代码时,这很容易错过。
  • 在正确的时间触发 useEffect 中的代码可能很困难。在这种模式中,我们经常要额外引入其他解决方法,例如避免在第一次渲染时运行代码。下面是一个示例:
function DuplicateState({ items }) {
  const [selectedItem, setSelectedItem] = useState();
  const firstRender = useRef(true);
  useEffect(() => {
    if (firstRender.current) {
      firstRender.current = false;
      return;
    }
    setSelectedItem(items.find(({ id }) => id === selectedItem.id));
  }, [items]);
  ...

如果你想使用 useEffect 或在另一个开发人员的代码中看到它,问问自己是否真的需要它。也许可以通过前面介绍的方法来避免这种情况。

解决方案

您可能已经猜到了:上一节的解决方案也帮助我们删除 useEffect。如果我们只存储所选项目的 ID 而不是整个对象,那么就没有什么可同步的。

import { useState } from "react";
// const items = [
//   {
//     id: "item-1",
//     text: "Item 1",
//   },
//   ...
// ]
function DuplicateState({ items }) {
  const [selectedItemId, setSelectedItemId] = useState();
  const selectedItem = items.find(({ id }) => id === selectedItemId);
  const onClickItem = (id) => {
    setSelectedItem(id);
  };
  return (
    <>
      {selectedItem && <Modal item={selectedItem} />}
      <ul>
        {items.map((row) => (
          <li key={row.id}>
            {row.text}
            <button onClick={() => onClickItem(row.id)}>Open</button>
          </li>
        ))}
      </ul>
    </>
  );
}

使用 useEffect 监听 state 变化

与上一节相关的另外一个常见问题是使用 useEffect 对状态的变化做出反应。但解决方案略有不同。

简单示例

这是一个显示产品的组件。用户可以通过单击按钮显示或隐藏产品详细信息。无论何时显示或隐藏产品信息,我们都会触发一个动作(在本例中,会触发一个埋点数据上报)。

import { useEffect, useState } from "react";
function ProductView({ name, details }) {
  const [isDetailsVisible, setIsDetailsVisible] = useState(false);
  useEffect(() => {
    trackEvent({ event: "Toggle Product Details", value: isDetailsVisible });
  }, [isDetailsVisible]);
  const toggleDetails = () => {
    setIsDetailsVisible(!isDetailsVisible);
  };
  return (
    <div>
      {name}
      <button onClick={toggleDetails}>Show details</button>
      {isDetailsVisible && <ProductDetails {...details} />}
    </div>
  );
}

代码中的 useEffect 会侦听 isDetailsVisible 是否变化,并相应地触发埋点事件。

问题分析

代码中的问题如下:

  • useEffect通常不容易理解。
  • 它可能会导致不必要的渲染周期(如果在效果内部更新了状态)。
  • 很容易引入与渲染生命周期相关的错误。事实上,这段代码在初始渲染期间运行trackEvent,这会导致一个 bug。
  • 它将影响与实际原因分开。在这段代码中,我们看到 trackEvent 正在运行,是因为 isDetailsVisible 发生了更改。但真正的原因是用户按下了 “显示详细信息” 按钮。

解决方案

在许多情况下,可以删除用于监听 state 变化的 useEffect。通常,我们可以将这些功能放在更新 state 的代码旁边。在这里,我们可以将 trackEvent(...) 移动到 toggleDetails 函数中。

function ProductView({ name, details }) {
  const [isDetailsVisible, setIsDetailsVisible] = useState(false);
  const toggleDetails = () => {
    setIsDetailsVisible(!isDetailsVisible);
    trackEvent({ event: "Toggle Product Details", value: !isDetailsVisible });
  };
  return (
    <div>
      {name}
      <button onClick={toggleDetails}>Show details</button>
      {isDetailsVisible && <ProductDetails {...details} />}
    </div>
  );
}

矛盾的 state

当您使用相互依赖的多个 state 时,这些状态可能存在多种组合,稍有不慎就会设置出错误的 state,让这些 state 呈现出相互矛盾的渲染结果。因此,我们需要更直观的方式来组织和管理这些状态组合。

简单示例

下面是一个很基本的数据请求的示例,组件可以处于不同的状态:要么正在加载数据,要么发生错误,要么已成功获取数据。

export function ContradictingState() {
  const [data, setData] = useState(null);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState(null);
  useEffect(() => {
    setIsLoading(true);
    setError(null);
    fetchData()
      .then((data) => {
        setData(data);
        setIsLoading(false);
      })
      .catch((error) => {
        setIsLoading(false);
        setData(null);
        setError(error);
      });
  }, []);
  ...

问题分析

这种方法的问题是,如果我们不小心,我们可能会产生有矛盾的 state。例如,在上面的示例中,当发生错误时,我们可能忘记将 isLoading 设置为 false。

对于哪些 state 是允许组合的,也是很难理解的。在上面的例子中,理论上我们可以有 8 种不同的 state 组合。但你不能很直观的看到哪些状态组合是真正存在的。

解决方案

多个状态之间相互依赖,更推荐用 useReducer 来替代 useState。

const initialState = {
  data: [],
  error: null,
  isLoading: false
};
function reducer(state, action) {
  switch (action.type) {
    case "FETCH":
      return {
        ...state,
        error: null,
        isLoading: true
      };
    case "SUCCESS":
      return {
        ...state,
        error: null,
        isLoading: false,
        data: action.data
      };
    case "ERROR":
      return {
        ...state,
        isLoading: false,
        error: action.error
      };
    default:
      throw new Error(`action "${action.type}" not implemented`);
  }
}
export function NonContradictingState() {
  const [state, dispatch] = useReducer(reducer, initialState);
  useEffect(() => {
    dispatch({ type: "FETCH" });
    fetchData()
      .then((data) => {
        dispatch({ type: "SUCCESS", data });
      })
      .catch((error) => {
        dispatch({ type: "ERROR", error });
      });
  }, []);
  ...

这样一来,就可以大大减少了我们的理解成本。我们可以很直观地看到我们有 3 个动作和 4 个可能的组件状态(“FETCH”、“SUCCESS”、“ERROR”和初始状态)。

深度嵌套的 state

我们这里提到的最后一个常见问题是(深度)嵌套对象的 state。如果我们只是渲染数据,这可能不存在什么问题。但是,一旦开始更新嵌套数据项,就会遇到一些麻烦。

简单示例

这里我们有一个组件,用于渲染深度嵌套的注释。JSX 在这里并不重要,所以省略了,我们假设 updateComment 是绑定到按钮上的回调函数。

function NestedComments() {
  const [comments, setComments] = useState([
    {
      id: "1",
      text: "Comment 1",
      children: [
        {
          id: "11",
          text: "Comment 1 1"
        },
        {
          id: "12",
          text: "Comment 1 2"
        }
      ]
    },
    {
      id: "2",
      text: "Comment 2"
    },
    {
      id: "3",
      text: "Comment 3",
      children: [
        {
          id: "31",
          text: "Comment 3 1",
          children: [
            {
              id: "311",
              text: "Comment 3 1 1"
            }
          ]
        }
      ]
    }
  ]);
  const updateComment = (id, text) => {
    // this gets complicated
  };
  ...

问题分析

这种嵌套 state 的问题是,我们必须以不可变的方式更新它,否则组件不会重新渲染。上面示例中的深度嵌套注释,我们以硬编码的方式来实现:

const updateComment = (id, text) => {
  setComments([
    ...comments.slice(0, 2),
    {
      ...comments[2],
      children: [
        {
          ...comments[2].children[0],
          children: [
            {
              ...comments[2].children[0].children[0],
              text: "New comment 311"
            }
          ]
        }
      ]
    }
  ]);
};

这种实现方式非常复杂。

解决方案

与深度嵌套的 state 不同,使用扁平的数据结构要容易得多。我们可以为每一个数据项增加 ID 字段,通过 ID 之间相互引用来描述嵌套关系。代码看起来像这样:

function FlatCommentsRoot() {
  const [comments, setComments] = useState([
    {
      id: "1",
      text: "Comment 1",
      children: ["11", "12"],
    },
    {
      id: "11",
      text: "Comment 1 1"
    },
    {
      id: "12",
      text: "Comment 1 2"
    },
    {
      id: "2",
      text: "Comment 2",
    },
    {
      id: "3",
      text: "Comment 3",
      children: ["31"],
    },
    {
      id: "31",
      text: "Comment 3 1",
      children: ["311"]
    },
    {
      id: "311",
      text: "Comment 3 1 1"
    }
  ]);
  const updateComment = (id, text) => {
    const updatedComments = comments.map((comment) => {
      if (comment.id !== id) {
        return comment;
      }
      return {
        ...comment,
        text
      };
    });
    setComments(updatedComments);
  };
  ...

现在,通过它的 ID 找到正确的数据项,并在数组中替换它就容易多了。

以上就是React useState的错误用法避坑详解的详细内容,更多关于React useState错误避坑的资料请关注其它相关文章!

原文地址:https://juejin.cn/post/7179613872855187493
 
反对 0举报 0 评论 0
 

免责声明:本文仅代表作者个人观点,与乐学笔记(本网)无关。其原创性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容、文字的真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
    本网站有部分内容均转载自其它媒体,转载目的在于传递更多信息,并不代表本网赞同其观点和对其真实性负责,若因作品内容、知识产权、版权和其他问题,请及时提供相关证明等材料并与我们留言联系,本网站将在规定时间内给予删除等相关处理.

  • React实现基于Antd密码强度校验组件示例详解
    React实现基于Antd密码强度校验组件示例详解
    目录引言效果预览组件思想组件开发引言最近在开发 Nest 和 Umi 技术栈的个人项目,在用户管理模块需要用到一个密码强度校验组件,在网上寻找一方资料,没有找到自己想要的,特此自己造轮子!效果预览组件思想既然是密码强度校验,那么强度就必须有个梯度,这
    03-16
  • 03 React快速入门(三)——实现从一个输入框中添加完数据后此输入框内容清除的功能
    03 React快速入门(三)——实现从一个输入框中
    功能描述:      我们在一个输入框输入内容,然后点击添加按钮,此输入框的内容就会添加到页面上,但是此输入框中还存在上次输入的内容,我们想在每次输入添加完成之后,此输入框中的内容就会清除,如图:   实现思路:      我们可以先在输入框上定
    03-08
  • react编译器jsxTransformer,babel
    1.JSX是什么JSX其实是JavaScript的扩展,React为了代码的可读性更方便地创建虚拟DOM等原因,加入了一些类似XML的语法的扩展。2.编译器——jsxTransformerJSX代码并不能直接运行,需要将它编译成正常的JavaScript表达式才能运行,jsxTransformer.js就是这一编
    03-08
  • G2( bizCharts ) React 绘制混合图例
    G2( bizCharts ) React 绘制混合图例
     G2( bizCharts ) React 绘制混合图例,// data-set 可以按需引入,除此之外不要引入别的包import React from 'react';import { Chart, Axis, Tooltip, Geom, Legend, Label } from 'bizcharts';import DataSet from '@antv/data-set';// 下面的代码会被作为
    03-08
  • React-多页面应用 react怎么写页面
    React-多页面应用 react怎么写页面
    1初始化项目npm init create-react-app my-app2.修改indeximport React from 'react';import ReactDOM from 'react-dom';import './index.css';import App from './App';ReactDOM.render(App /, document.getElementById('root')
    03-08
  • react-native start 启动错误解决方法
    ERRORError watching file for changes: EMFILE{"code":"EMFILE","errno":"EMFILE","syscall":"Error watching file for changes:","filename":null}Error: Error watching file for changes: EMFILE
    03-08
  • React兄弟组件通信(发布者-订阅者模式)
    // eventProxy.js'use strict';const eventProxy = {onObj: {},oneObj: {},on: function(key, fn) {if(this.onObj[key] === undefined) {this.onObj[key] = [];}this.onObj[key].push(fn);},one: function(key, fn) {if(this.oneObj[key] === undefined) {thi
    03-08
  • React笔记_(7)_react路由 react路由配置
    路由路由(routing)是指分组从源到目的地时,决定端到端路径的网络范围的进程。路由器当然是作为一个转发设备出现的,主要是转发数据包来实现网络互联。那么react的路由到底指的是什么呢?举个栗子~~~在网页中点击后,从A页面跳到B页面,跳转过程中url发生变
    03-08
  • react-native关闭所有黄色警告 react native st
     将以下这两句话加在index.js(入口文件)中,放在AppRegistry.registerComponent('App', () = App)之前即可1 console.ignoredYellowBox = ['Warning: BackAndroid is deprecated. Please use BackHandler instead.','source.uri should not be an empty str
    03-08
  • react中style的写法 react styles
    div style={{width: 20px; height=30px}}style的写法/div 
    03-08
点击排行