重构初级 React 代码 - 代码量减少 43%,数据结构更优

2025-05-25

重构初级 React 代码 - 代码量减少 43%,数据结构更优

作为一名(有志于成为)初级 React 开发者,很难判断自己的代码是否优秀以及如何改进。如果能得到经验丰富的开发者的评价就更好了。但分享代码需要勇气。而且在线社区里的评论通常很肤浅。

最近,一位开发者勇敢地申请了代码审查。值得称赞。这个功能很有意思,代码也还不错。但是,其中有一些在初级 React 开发者中很常见的错误。

所以,现在你有机会从这些错误中吸取教训。在这个页面上,你可以找到代码审查和逐步的重构之旅。

  • 使用更好的数据结构
  • 删除不必要的状态和
  • 一些其他重构和清理

我们提高了代码的性能,使其更易于阅读和维护,并将组件从最初的 177 行缩短到 102 行(这不是一个好的指标)。

如果您更喜欢视频,您可以在这里观看完整的审查和重构过程。

注意:本页上的代码与视频中使用 Set 而不是 Map 的代码相比略有改进。

目录

  1. 需要审查的组件
    1. 功能
    2. 原始代码
  2. 原始代码的问题
    1. 问题 1:次优数据结构
    2. 问题 2:没有从状态中导出变量
    3. 问题 3:直接访问 DOM
    4. 问题4:变量命名
  3. 重构
    1. 步骤 1:将数据结构从数组重构为对象
    2. 步骤 2:使用派生状态
    3. 步骤 3:删除 document.getElementById()
  4. 最终代码
  5. 总结

需要审查的组件

功能

该组件是一个包含一些稍微复杂功能的表格。以下是一段简短的视频:

该表格呈现了问题列表。问题的状态可以是“未解决”或“已解决”,状态会反映在最后一列。状态为“已解决”的问题无法选择。

当选中部分行(而非全部行)时,顶部的复选框会被部分选中。点击该复选框允许用户选择或取消选择所有状态为“未决”的问题。

原始代码

原始代码还不错。它能正常工作,并且完成了它的任务。在大多数情况下,它的可读性很高,尤其是在高层级。它与许多初级 React 开发人员编写的代码类似。同时,它也存在一些初级开发人员中常见的问题。我们稍后会讨论这些问题。

但首先,这是组件的代码。你也可以在 CodeSandbox 上找到一个可以运行的版本

import { useState } from "react";
import classes from "./Table.module.css";

function Table({ issues }) {
  const [checkedState, setCheckedState] = useState(
    new Array(issues.length).fill({
      checked: false,
      backgroundColor: "#ffffff",
    })
  );
  const [selectDeselectAllIsChecked, setSelectDeselectAllIsChecked] =
    useState(false);
  const [numCheckboxesSelected, setNumCheckboxesSelected] = useState(0);

  const handleOnChange = (position) => {
    const updatedCheckedState = checkedState.map((element, index) => {
      if (position === index) {
        return {
          ...element,
          checked: !element.checked,
          backgroundColor: element.checked ? "#ffffff" : "#eeeeee",
        };
      }
      return element;
    });
    setCheckedState(updatedCheckedState);

    const totalSelected = updatedCheckedState
      .map((element) => element.checked)
      .reduce((sum, currentState, index) => {
        if (currentState) {
          return sum + issues[index].value;
        }
        return sum;
      }, 0);
    setNumCheckboxesSelected(totalSelected);

    handleIndeterminateCheckbox(totalSelected);
  };

  const handleIndeterminateCheckbox = (total) => {
    const indeterminateCheckbox = document.getElementById(
      "custom-checkbox-selectDeselectAll"
    );
    let count = 0;

    issues.forEach((element) => {
      if (element.status === "open") {
        count += 1;
      }
    });

    if (total === 0) {
      indeterminateCheckbox.indeterminate = false;
      setSelectDeselectAllIsChecked(false);
    }
    if (total > 0 && total < count) {
      indeterminateCheckbox.indeterminate = true;
      setSelectDeselectAllIsChecked(false);
    }
    if (total === count) {
      indeterminateCheckbox.indeterminate = false;
      setSelectDeselectAllIsChecked(true);
    }
  };

  const handleSelectDeselectAll = (event) => {
    let { checked } = event.target;

    const allTrueArray = [];
    issues.forEach((element) => {
      if (element.status === "open") {
        allTrueArray.push({ checked: true, backgroundColor: "#eeeeee" });
      } else {
        allTrueArray.push({ checked: false, backgroundColor: "#ffffff" });
      }
    });

    const allFalseArray = new Array(issues.length).fill({
      checked: false,
      backgroundColor: "#ffffff",
    });
    checked ? setCheckedState(allTrueArray) : setCheckedState(allFalseArray);

    const totalSelected = (checked ? allTrueArray : allFalseArray)
      .map((element) => element.checked)
      .reduce((sum, currentState, index) => {
        if (currentState && issues[index].status === "open") {
          return sum + issues[index].value;
        }
        return sum;
      }, 0);
    setNumCheckboxesSelected(totalSelected);
    setSelectDeselectAllIsChecked((prevState) => !prevState);
  };

  return (
    <table className={classes.table}>
      <thead>
        <tr>
          <th>
            <input
              className={classes.checkbox}
              type={"checkbox"}
              id={"custom-checkbox-selectDeselectAll"}
              name={"custom-checkbox-selectDeselectAll"}
              value={"custom-checkbox-selectDeselectAll"}
              checked={selectDeselectAllIsChecked}
              onChange={handleSelectDeselectAll}
            />
          </th>
          <th className={classes.numChecked}>
            {numCheckboxesSelected
              ? `Selected ${numCheckboxesSelected}`
              : "None selected"}
          </th>
        </tr>
        <tr>
          <th />
          <th>Name</th>
          <th>Message</th>
          <th>Status</th>
        </tr>
      </thead>

      <tbody>
        {issues.map(({ name, message, status }, index) => {
          let issueIsOpen = status === "open";
          let onClick = issueIsOpen ? () => handleOnChange(index) : null;
          let stylesTr = issueIsOpen
            ? classes.openIssue
            : classes.resolvedIssue;

          return (
            <tr
              className={stylesTr}
              style={checkedState[index]}
              key={index}
              onClick={onClick}
            >
              <td>
                {issueIsOpen ? (
                  <input
                    className={classes.checkbox}
                    type={"checkbox"}
                    id={`custom-checkbox-${index}`}
                    name={name}
                    value={name}
                    checked={checkedState[index].checked}
                    onChange={() => handleOnChange(index)}
                  />
                ) : (
                  <input
                    className={classes.checkbox}
                    type={"checkbox"}
                    disabled
                  />
                )}
              </td>
              <td>{name}</td>
              <td>{message}</td>
              <td>
                {issueIsOpen ? (
                  <span className={classes.greenCircle} />
                ) : (
                  <span className={classes.redCircle} />
                )}
              </td>
            </tr>
          );
        })}
      </tbody>
    </table>
  );
}

export default Table;
Enter fullscreen mode Exit fullscreen mode

表中呈现的问题数组如下所示。

[
  {
    "id": "c9613c41-32f0-435e-aef2-b17ce758431b",
    "name": "TypeError",
    "message": "Cannot read properties of undefined (reading 'length')",
    "status": "open",
    "numEvents": 105,
    "numUsers": 56,
    "value": 1
  },
  {
    "id": "1f62d084-cc32-4c7b-943d-417c5dac896e",
    "name": "TypeError",
    "message": "U is not a function",
    "status": "resolved",
    "numEvents": 45,
    "numUsers": 34,
    "value": 1
  },
  ...
]
Enter fullscreen mode Exit fullscreen mode

该组件较为复杂,共有 177 行代码 (LOC)。但正如前文所述,从总体上看,它的可读性还是不错的。以下是函数折叠后的效果。

function Table({ issues }) {
  // decides which checkboxes is selected
  const [checkedState, setCheckedState] = useState(...);
  // decides if the top checkbox is checked or not
  const [selectDeselectAllIsChecked, setSelectDeselectAllIsChecked] =
    useState(false);
  // represents the number of selected checkboxes
  const [numCheckboxesSelected, setNumCheckboxesSelected] = useState(0);

  // handles the click on a row / checkbox
  const handleOnChange = (position) => { ... };

  // responsible for setting the partially checked state of the top checkbox
  const handleIndeterminateCheckbox = (total) => { ... };

  // handles the click on the top checkbox
  const handleSelectDeselectAll = (event) => { ... };

  return (
    <table>
      ...
    </table>
  );
}
Enter fullscreen mode Exit fullscreen mode

还不错。但还有改进的空间。

原始代码的问题

问题 1:次优数据结构

让我们看一下组件的顶部。

function Table({ issues }) {
  const [checkedState, setCheckedState] = useState(
    new Array(issues.length).fill({ checked: false, backgroundColor: "#ffffff" })
  );

  ...
Enter fullscreen mode Exit fullscreen mode
  • checkedState变量是一个数组。
  • 它被初始化为具有与问题数组相同的长度。
  • 每个条目都是一个代表问题检查状态的对象。

第一个问题是对象数组总是难以改变。我们必须通过数组中的索引来访问正确的对象。所以我们需要找到并传递索引。

第二个问题是这种方法的扩展性不太好。例如,如果我们有 10k 个问题,那么我们就会得到一个 10k 长的checkedState数组,其中大部分都是false布尔值。

替代方案:我们可以使用不同的数据结构,例如将问题 ID 映射到其检查值的对象或Map 。

// alternative data structure (example when two checkboxes are checked)
{
  "issue-id-1": true,
  "issue-id-4": true,
}
Enter fullscreen mode Exit fullscreen mode

更好的是,JavaScript 有一个原生的Set,它类似于数组,但只能保存唯一值,并且针对以高性能方式访问它们进行了优化。

问题 2:没有从状态中导出变量

再次在组件的顶部,我们可以看到初级开发人员代码中的另一个非常常见的问题:可以从 props 或其他状态派生出的不必要的状态变量。

function Table({ issues }) {
  const [checkedState, setCheckedState] = useState(
    new Array(issues.length).fill({ checked: false, backgroundColor: "#ffffff" })
  );
  const [selectDeselectAllIsChecked, setSelectDeselectAllIsChecked] =
    useState(false);
  const [numCheckboxesSelected, setNumCheckboxesSelected] = useState(0);

  ...
Enter fullscreen mode Exit fullscreen mode

在这段代码中,我们有三个不必要的状态:

  • backgroundColor是一种可以从值中派生出来的样式checked#eee如果检查结果为真,则为#fff
  • selectDeselectAllIsChecked用于设置checked顶部复选框的 prop(可从checkedState和派生issues
  • numCheckboxesSelected是该复选框旁边的数字(可以从中得出checkedState

问题 3:直接访问 DOM

进一步阅读代码,我们可以找到处理indeterminate顶部复选框状态的函数:

function Table({ issues }) {
  ...

  const handleIndeterminateCheckbox = (total) => {
    const indeterminateCheckbox = document.getElementById(
      "custom-checkbox-selectDeselectAll"
    );
    let count = 0;

    issues.forEach((element) => {
      if (element.status === "open") {
        count += 1;
      }
    });

    if (total === 0) {
      indeterminateCheckbox.indeterminate = false;
      setSelectDeselectAllIsChecked(false);
    }
    if (total > 0 && total < count) {
      indeterminateCheckbox.indeterminate = true;
      setSelectDeselectAllIsChecked(false);
    }
    if (total === count) {
      indeterminateCheckbox.indeterminate = false;
      setSelectDeselectAllIsChecked(true);
    }
  };

  ...
Enter fullscreen mode Exit fullscreen mode

我们可以看到,顶部的复选框是通过 访问的document.getElementById(...)。这在 React 应用中相对不常见,而且通常没有必要。

indeterminateCheckbox.indeterminate请注意,必须直接在 DOM 元素上设置。我们不能将其设置indeterminate为复选框上的 prop。

问题4:变量命名

这段代码的作者在变量和函数名称的选择上花了不少功夫。值得称赞。

不幸的是,有些名称有点令人困惑。我们可以在组件顶部找到一些示例:

function Table({ issues }) {
  const [checkedState, setCheckedState] = useState(
    new Array(issues.length).fill({ checked: false, backgroundColor: "#ffffff" })
  );
  const [selectDeselectAllIsChecked, setSelectDeselectAllIsChecked] =
    useState(false);
  const [numCheckboxesSelected, setNumCheckboxesSelected] = useState(0);

  ...
Enter fullscreen mode Exit fullscreen mode

例如,checkedState是多余的,而且容易引起误解。我期望这里是一个布尔值,但这是一个对象数组。更好的名字可能是checkedItems

selectDeselectAllIsChecked有点容易理解,但读起来有点费劲。它告诉我们所有复选框是否都被选中了,但我得读上五遍。更好的名字可能是isEveryItemChecked……

在职 React - 电子邮件课程

重构

现在我们已经分析了原始代码的一些问题,让我们开始逐步重构它。

步骤 1:将数据结构从数组重构为集合

最有效的一步是使用Set而不是数组作为checkedState。我们也可以使用对象,但 Set 有一些优势,比如可以直接访问,这size一点我们会经常用到。与 Map 相比,Set 在这里更适合,因为我们只需要知道所有选定项的 ID,而不是键值对。

由于我们已经接触到此代码,因此我们还将backgroundColor从状态中删除并重命名状态变量。

//before
const [checkedState, setCheckedState] = useState(
  new Array(issues.length).fill({ checked: false, backgroundColor: "#ffffff" })
);

// after
const [checkedById, setCheckedById] = useState(new Set()); 
Enter fullscreen mode Exit fullscreen mode

提醒一下,状态的形状checkedById(表示为数组)现在看起来像这样:

// shape of checkedById (when two checkboxes are checked)
["issue-id-1", "issue-id-4"]
Enter fullscreen mode Exit fullscreen mode

这个改动意义重大,它会影响我们其余的代码。我们将逐一看看代码的每个部分是如何受到影响的。

让我们从连接到表中的复选框的处理函数开始。

以下是代码的前后情况:

// before
const handleOnChange = (position) => {
  const updatedCheckedState = checkedState.map((element, index) => {
    if (position === index) {
      return {
        ...element,
        checked: !element.checked,
        backgroundColor: element.checked ? "#ffffff" : "#eeeeee",
      };
    }
    return element;
  });
  setCheckedState(updatedCheckedState);

  const totalSelected = updatedCheckedState
    .map((element) => element.checked)
    .reduce((sum, currentState, index) => {
      if (currentState) {
        return sum + issues[index].value;
      }
      return sum;
    }, 0);
  setNumCheckboxesSelected(totalSelected);

  handleIndeterminateCheckbox(totalSelected);
};

// after
const handleOnChange = (id) => {
  const updatedCheckedById = new Set(checkedById);
  if (updatedCheckedById.has(id)) {
    updatedCheckedById.delete(id);
  } else {
    updatedCheckedById.add(id);
  }

  setCheckedById(updatedCheckedById);

  const totalSelected = updatedCheckedById.size;
  setNumCheckboxesSelected(totalSelected);
  handleIndeterminateCheckbox(totalSelected);
};
Enter fullscreen mode Exit fullscreen mode

我们清理了不少杂物。以下是一些简短的说明:

  1. 原始代码中有一行return sum + issues[index].value很有意思。它使用数据中的一个值来增加总和,并计算所选元素的数量。这看起来过于复杂了。
  2. 在重构的代码中,我们现在可以简单地使用它updatedCheckedById.size来获取选定元素的数量。
  3. 由于我们不希望false我们的状态中包含任何值,因此我们必须在通过取消选择复选框时删除相应的值updatedCheckedById.delete(id)

让我们继续讨论与顶部复选框相连的处理函数。

这是前后对比。

// before
const handleSelectDeselectAll = (event) => {
  let { checked } = event.target;

  const allTrueArray = [];
  issues.forEach((element) => {
    if (element.status === "open") {
      allTrueArray.push({ checked: true, backgroundColor: "#eeeeee" });
    } else {
      allTrueArray.push({ checked: false, backgroundColor: "#ffffff" });
    }
  });

  const allFalseArray = new Array(issues.length).fill({
    checked: false,
    backgroundColor: "#ffffff",
  });
  checked ? setCheckedState(allTrueArray) : setCheckedState(allFalseArray);

  const totalSelected = (checked ? allTrueArray : allFalseArray)
    .map((element) => element.checked)
    .reduce((sum, currentState, index) => {
      if (currentState && issues[index].status === "open") {
        return sum + issues[index].value;
      }
      return sum;
    }, 0);
  setNumCheckboxesSelected(totalSelected);
  setSelectDeselectAllIsChecked((prevState) => !prevState);
};

// after
const handleSelectDeselectAll = (event) => {
    if (event.target.checked) {
      const openIssues = issues.filter(({ status }) => status === "open");
      const allChecked = new Set(openIssues.map(({ id }) => id));
      setCheckedById(allChecked);
      setNumCheckboxesSelected(allChecked.size);
    } else {
      setCheckedById(new Set());
      setNumCheckboxesSelected(0);
    }

    setSelectDeselectAllIsChecked((prevState) => !prevState);
  };
Enter fullscreen mode Exit fullscreen mode

再次简单说明一下:

  • allTrueArray原始实现中,使用和有点繁琐allFalseArray。我们也可以重构它,但由于我们现在使用对象,所以我们可以直接删除这段代码。
  • 因为我们在新的实现中使用了 Set,所以创建新allChecked状态非常简单。我们筛选所有未解决的问题,然后根据 ID 数组构造一个新的 Set

我们需要调整的最后一段代码是从组件返回的 JSX。特别是元素内部的代码<tbody>

// before
<tbody>
  {issues.map(({ name, message, status }, index) => {
    let issueIsOpen = status === "open";
    // we have to use the id of the issue instead of the index here
    let onClick = issueIsOpen ? () => handleOnChange(index) : null;
    let stylesTr = issueIsOpen
      ? classes.openIssue
      : classes.resolvedIssue;

    return (
      <tr
        className={stylesTr}
        // the backgroundColor isn't part of checkedState anymore
        // so we have to adjust this
        style={checkedState[index]}
        key={index}
        onClick={onClick}
      >
        <td>
          {issueIsOpen ? (
            <input
              className={classes.checkbox}
              type={"checkbox"}
              id={`custom-checkbox-${index}`}
              name={name}
              value={name}
              // we have to use the id instead of the index here
              checked={checkedState[index].checked}
              onChange={() => handleOnChange(index)}
            />
          ) : (
            <input
              className={classes.checkbox}
              type={"checkbox"}
              disabled
            />
          )}
        </td>
        ...
      </tr>
    );
  })}
</tbody>

// after
<tbody>
  {issues.map(({ id, name, message, status }, index) => {
    let issueIsOpen = status === "open";
    let onClick = issueIsOpen ? () => handleOnChange(id) : null;
    let stylesTr = issueIsOpen
      ? classes.openIssue
      : classes.resolvedIssue;

    return (
      <tr
        key={id}
        className={stylesTr}
        style={{ backgroundColor: checkedById.has(id) ? "#eee" : "#fff" }}
        onClick={onClick}
      >
        <td>
          {issueIsOpen ? (
            <input
              className={classes.checkbox}
              type={"checkbox"}
              id={`custom-checkbox-${index}`}
              name={name}
              value={name}
              checked={checkedById.has(id)}
              onChange={() => handleOnChange(id)}
            />
          ) : (
            <input
              className={classes.checkbox}
              type={"checkbox"}
              disabled
            />
          )}
        </td>
        ...
      </tr>
    );
  })}
</tbody>
Enter fullscreen mode Exit fullscreen mode

再次简单说明一下:

  • index我们必须更改此代码中的大部分内容。
  • 现在,我们不再使用元素key={index}上的写操作,而是使用as 键。这在当前版本中不会改变任何内容,因为问题的顺序不会改变。但这是最佳实践,将来我们可能会在表中引入某种排序功能。<tr>id
  • 以前,行的背景颜色是通过以下代码设置的:style={checkedState[index]}。这需要一点时间来理解。对于任何给定的行,这都转换为例如style={{ checked: true, backgroundColor: "#fff" }}
  • 新版本在这方面更加明确:style={{ backgroundColor: checkedById[id] ? "#eee" : "#fff" }}

您可以在 GitHub 上找到所有代码更改,并在 CodeSandbox 上找到此步骤的功能版本(请注意,您需要进行更改App.jsx才能在浏览器中看到此版本的组件呈现)。

步骤 2:使用派生状态

我们上面讨论的第二个问题是不必要的状态变量selectDeselectAllIsCheckednumCheckboxesSelected。如上所述,这些可以很容易地从checkedById状态 和issues数据数组中得出。

// before
function Table({ issues }) {
  const [checkedById, setCheckedById] = useState(new Set());
  const [selectDeselectAllIsChecked, setSelectDeselectAllIsChecked] =
    useState(false);
  const [numCheckboxesSelected, setNumCheckboxesSelected] = useState(0);

  ...

// after
function Table({ issues }) {
  const [checkedById, setCheckedById] = useState(new Set());
  const openIssues = issues.filter(({ status }) => status === "open");
  const numOpenIssues = openIssues.length;
  const numCheckedIssues = checkedById.size;

  ...
Enter fullscreen mode Exit fullscreen mode

这里我们不一定需要这个openIssues变量。但这样我们就可以在重构后的handleSelectDeselectAll函数中重用它(这里没有显示)。

除了这些更改之外,我们还必须从其余代码中删除对状态设置器的调用。这些更改有两个优点:

  1. 我们去掉了几行代码。
  2. 更重要的是,我们不再需要管理额外的状态。这很棒,因为我们很容易忘记更新状态,从而引入错误。

最后一步,我们可以记忆计算numOpenIssues。当前版本不需要此功能,但一旦我们处理更大的数据集,此功能可能就有必要了。

const openIssues = useMemo(
  () => issues.filter(({ status }) => status === "open"),
  [issues]
);
Enter fullscreen mode Exit fullscreen mode

您可以在 GitHub 上查看代码更改的完整列表,并在 CodeSandbox 上查看此步骤的运行版本

步骤 3:删除 document.getElementById()

我们重构之旅的最后一步重要步骤是以 React 方式访问顶部复选框,而不是使用document.getElementById()

React 的方式是什么?我们使用ref

// before
function Table({ issues }) {
  ...

  const handleIndeterminateCheckbox = (total) => {
    const indeterminateCheckbox = document.getElementById(
      "custom-checkbox-selectDeselectAll"
    );
    let count = 0;

    issues.forEach((element) => {
      if (element.status === "open") {
        count += 1;
      }
    });

    if (total === 0) {
      indeterminateCheckbox.indeterminate = false;
    }
    if (total > 0 && total < count) {
      indeterminateCheckbox.indeterminate = true;
    }
    if (total === count) {
      indeterminateCheckbox.indeterminate = false;
    }
  };

  ...

// after
function Table({ issues }) {
  ...

  const topCheckbox = useRef();
  const handleIndeterminateCheckbox = (checkedById) => {
    const numSelected = checkedById.size;
    if (numSelected === 0) {
      topCheckbox.current.indeterminate = false;
    } else if (numSelected === numOpenIssues) {
      topCheckbox.current.indeterminate = false;
    } else {
      topCheckbox.current.indeterminate = true;
    }
  };

  ...

  return (
    <table className={classes.table}>
      <thead>
        <tr>
          <th>
            <input
              ref={topCheckbox}
              ...
Enter fullscreen mode Exit fullscreen mode

一些简短的说明:

  • 最重要的是我们document.getElementById()用替换了 。这也允许我们从复选框中useRef()删除。id
  • 原始代码中这行代码issues.forEach((element) => {负责计算未解决的问题数量。这种方法有点繁琐,可以很容易地用 替换。但是我们已经在组件顶部issues.filter(...).length定义了变量。所以不再需要这行代码了。numOpenIssues
  • 我们用了if ... else if ... else三个if语句。我们还调整了条件,使唯一true值放在底部。我认为这让我们的大脑更容易处理。

实际上,我们甚至不需要该if ... else if ... else语句。我们可以简单地用一行代码替换它:

topCheckbox.current.indeterminate =
      numSelected > 0 && numSelected < numOpenIssues;
Enter fullscreen mode Exit fullscreen mode

使用此行,保留该功能也没有多大意义handleIndeterminateCheckbox

您可以在 GitHub 上找到所有代码更改,并在 CodeSandbox 上找到此步骤的功能版本

在职 React - 电子邮件课程

最终代码

经过一些额外的清理(这里是更改)后,我们能够将代码从最初的 177 LOC 减少到 102 LOC。

import { useMemo, useState, useRef } from "react";
import classes from "./Table.module.css";

function Table({ issues }) {
  const topCheckbox = useRef();
  const [checkedById, setCheckedById] = useState(new Set());

  const openIssues = useMemo(
    () => issues.filter(({ status }) => status === "open"),
    [issues]
  );
  const numOpenIssues = openIssues.length;
  const numCheckedIssues = checkedById.size;

  const handleOnChange = (id) => {
    const updatedCheckedById = new Set(checkedById);
    if (updatedCheckedById.has(id)) {
      updatedCheckedById.delete(id);
    } else {
      updatedCheckedById.add(id);
    }
    setCheckedById(updatedCheckedById);

    const updatedNumChecked = updatedCheckedById.size;
    topCheckbox.current.indeterminate =
      updatedNumChecked > 0 && updatedNumChecked < numOpenIssues;
  };

  const handleSelectDeselectAll = (event) => {
    if (event.target.checked) {
      const allChecked = new Set(openIssues.map(({ id }) => id));
      setCheckedById(allChecked);
    } else {
      setCheckedById(new Set());
    }
  };

  return (
    <table className={classes.table}>
      <thead>
        <tr>
          <th>
            <input
              type="checkbox"
              ref={topCheckbox}
              className={classes.checkbox}
              checked={numOpenIssues === numCheckedIssues}
              onChange={handleSelectDeselectAll}
            />
          </th>
          <th className={classes.numChecked}>
            {numCheckedIssues
              ? `Selected ${numCheckedIssues}`
              : "None selected"}
          </th>
        </tr>
        <tr>
          <th />
          <th>Name</th>
          <th>Message</th>
          <th>Status</th>
        </tr>
      </thead>

      <tbody>
        {issues.map(({ id, name, message, status }) => {
          const isIssueOpen = status === "open";
          return (
            <tr
              key={id}
              className={
                isIssueOpen ? classes.openIssue : classes.resolvedIssue
              }
              style={{ backgroundColor: checkedById.has(id) ? "#eee" : "#fff" }}
            >
              <td>
                <input
                  type="checkbox"
                  className={classes.checkbox}
                  checked={checkedById.has(id)}
                  disabled={!isIssueOpen}
                  onChange={() => handleOnChange(id)}
                />
              </td>
              <td>{name}</td>
              <td>{message}</td>
              <td>
                <span
                  className={
                    isIssueOpen ? classes.greenCircle : classes.redCircle
                  }
                />
              </td>
            </tr>
          );
        })}
      </tbody>
    </table>
  );
}

export default Table;
Enter fullscreen mode Exit fullscreen mode

您可以在 GitHub 上找到所有代码更改,并在 CodeSandbox 上找到最终组件的版本

从我的角度来看,这不仅更简洁,而且更具可读性。

总结

  1. 注意可能从其他状态或数据中得出的不必要的状态。
  2. 使用对象、映射或集合通常比数组更容易、更高效。
  3. React 访问 DOM 元素的方式是useRef()而不是document.getElementById()

在职 React - 电子邮件课程

文章来源:https://dev.to/jkettmann/refactoring-a-juniors-react-code-43-less-code-with-a-better-data-struct-eam
PREV
如何使用 Node.js、React.js、Nodemailer 和 OAuth2 实现电子邮件功能
NEXT
不要在作品集网站上浪费时间“我申请了 100 多次 React 工作,但都被拒绝了!我做错了什么?”