【问题标题】:How to update a nested react state如何更新嵌套的反应状态
【发布时间】:2022-06-13 11:53:56
【问题描述】:

我正在尝试更新包含嵌套值的反应状态。我想更新 3 级深度的数据。

这是保存数据的状态:

const [companies, setCompanies] = useState(companies)

这是第一家公司的数据(公司数组包含许多公司):

const companies = [
    {
      companyId: 100,
      transactions: [
        {
          id: "10421A",
          amount: "850",
        }
        {
          id: "1893B",
          amount: "357",
        }
    }
]

表格组件的代码如下:

function DataTable({ editCell, vendors, accounts }) {
    const columns = useMemo(() => table.columns, [table]);
    const data = useMemo(() => table.rows, [table]);
    const tableInstance = useTable({ columns, data, initialState: { pageIndex: 0 } }, useGlobalFilter, useSortBy, usePagination);
    const {
        getTableProps,
        getTableBodyProps,
        headerGroups,
        prepareRow,
        rows,
        page,
        state: { pageIndex, pageSize, globalFilter },
    } = tableInstance;

    return (
        <Table {...getTableProps()}>
            <MDBox component="thead">
                {headerGroups.map((headerGroup) => (
                    <TableRow {...headerGroup.getHeaderGroupProps()}>
                        {headerGroup.headers.map((column) => (
                            <DataTableHeadCell
                                {...column.getHeaderProps(isSorted && column.getSortByToggleProps())}
                                width={column.width ? column.width : "auto"}
                                align={column.align ? column.align : "left"}
                                sorted={setSortedValue(column)}
                            >
                                {column.render("Header")}
                            </DataTableHeadCell>
                        ))}
                    </TableRow>
                ))}
            </MDBox>
            <TableBody {...getTableBodyProps()}>
                {page.map((row, key) => {
                    prepareRow(row);
                    return (
                        <TableRow {...row.getRowProps()}>
                            {row.cells.map((cell) => {
                                cell.itemsSelected = itemsSelected;
                                cell.editCell = editCell;
                                cell.vendors = vendors;
                                cell.accounts = accounts;
                                return (
                                    <DataTableBodyCell
                                        noBorder={noEndBorder && rows.length - 1 === key}
                                        align={cell.column.align ? cell.column.align : "left"}
                                        {...cell.getCellProps()}
                                    >
                                        {cell.render("Cell")}
                                    </DataTableBodyCell>
                                );
                            })}
                        </TableRow>
                    );
                })}
            </TableBody>
        </Table>
    )
}

例如,我想更新 transactions 数组中第一个对象中的金额。我现在正在做的是更新整个 companies 数组,但是这样做会重新渲染整个表并产生问题。有没有办法我只能以仅重新呈现表中更新的字段而不重新呈现整个表的方式更新特定值?我见过其他答案,但他们假设所有值都是命名对象属性。

仅供参考,我没有使用任何状态管理,暂时不想使用。

【问题讨论】:

  • “这样做会重新渲染整个表格并产生问题”是什么意思?能详细点吗?
  • 看起来您正在使用库“react-tables”,但没有提及这一点。您是否能够在不使用该库的情况下提供问题的最小表示?您的示例表格组件假定您熟悉该库,并且还包含许多与逻辑相关的不必要代码,用于样式化和组成与您的问题无关的表格。有没有办法简化?
  • 您能解释一下“我看过其他答案,但他们认为所有值都被命名为对象属性”是什么意思吗?我按照您为公司数据提供的架构提供了对您问题的答案。

标签: javascript reactjs state


【解决方案1】:

你必须复制数据(至少是浅拷贝)来更新状态:

const nextCompanies = { ...companies };
nextCompanies.transactions[3].amount = 357;
setState(nextCompanies);

否则 react 不会看到对原始对象的更改。当然,您可以对子组件使用 memoization 来跳过无用的重新渲染。但我强烈建议仅在需要优化时才提供优化。你会让代码过于复杂而没有真正的利润。

【讨论】:

    【解决方案2】:

    当根据之前的状态更新状态时,您可能希望将回调传递给setCompanies()。例如:

    setCompanies((currCompanies) => {
      const nextCompanies = [...currCompanies];
      // modify nextCompanies
      return nextCompanies;
    })
    

    然后,为了让 React 仅重新渲染 DOM 中更改的元素,您应该确保在每个元素中设置 key 属性。这样,React 就会知道哪个元素发生了变化。

    // inside your component code
    return (
      <div>
        companies.map(company => (
            <Company key={company.id} data={company} /> 
        ))
      </div>
    )
    

    这能解决问题吗?如果没有,添加更多细节可能会有所帮助,以便我们能够完全理解。

    【讨论】:

      【解决方案3】:

      我现在正在做的是更新整个公司数组,但是做 这会重新渲染整个表格并产生问题。

      只要您的状态由一个代表所有公司的数组组成,并且您在同一个组件中呈现整个表格,就会出现这种情况。

      当状态或道具改变时,组件将 1) 重新渲染和 2) 使用新的状态或道具。新状态必须是您的公司列表的完整表示,以便可靠且准确地比较当前状态与先前状态。

      当你说它会产生问题究竟是什么类型的问题?

      您似乎在问两个问题。第一,仅修改状态子集(一笔交易的金额)时如何更新状态。第二,当渲染依赖于没有改变的状态或道具时,如何防止不必要的重新渲染。

      1。当我们只需要修改状态的一小部分时,有什么好的策略来更新状态?

      您需要在公司列表中修改一些特定于公司的数据。当我们需要遍历数组中的项并组成一个新数组时,我们可以使用map,根据原始数组控制我们为每个新项返回的内容。由于 map 返回一个新数组,我们可以直接映射状态,而不用担心改变状态。

      我们至少需要先了解几件事。我们正在更新什么交易,新的金额是多少?我们假设我们还希望公司 ID 能够识别执行交易的正确公司。我们将这些作为参数传递给最终会更新状态的函数。

      • 公司 ID
      • 交易的 ID
      • 新金额

      任何与公司 ID 不匹配的公司,我们只返回之前的值。

      当我们找到公司 ID 的匹配项时,我们希望修改其中一项交易,但返回所有其他先前值的副本。扩展运算符是一种方便的方法。下面的...company 将合并以前公司对象的副本以及我们的更新。

      Transactions 是另一个数组,因此我们可以使用与之前相同的 map() 策略。

      const handleChangeAmount = ({ companyId, transactionId, newAmount }) => {
          setCompanies(() => {
            return companies.map((company) => {
              return company.id === companyId
                ? {
                    ...company,
                    transactions: company.transactions.map((currTransaction) => {
                      return currTransaction.id === transactionId
                        ? {
                            id: currTransaction.id,
                            amount: newAmount
                          }
                        : currTransaction;
                    })
                  }
                : company;
            });
          });
        };
      

      2。如果 state 或 props 没有改变,我们如何告诉 React 跳过重新渲染?

      在之前的回答中提到,将键传递给元素将有助于 React 跟踪它们以确定发生了什么变化。

      • 这与 React 比较虚拟 DOM 中的变化有关,因此它知道是否重新绘制实际 DOM。但它仍然必须先渲染才能进行比较。所以这是渲染和绘画之间的重要区别。在 Chrome 中,您可以打开 Dev Tools > Rendering > Paint Flashing 以查看此操作。 React 仍然会渲染任何 JSX,而不管 key prop 是什么,但它会基于它进行比较以跳过重新绘制 DOM。

      如果我们的任务是为使用未更改状态的表格部分跳过渲染,我们需要一种在我们的组件中进行比较的方法。一个合理的方法是为每个公司渲染一个可重用的子组件&lt;Company /&gt;,只传递特定于该公司的道具。

      尽管我们的子公司只关心它的 props(而不是所有状态),但 React 仍然会在状态更新时渲染组件,因为 React 使用引用相等(是否引用内存中的同一对象)每当它收到新的 props 或 state,而不是它们持有的值。

      因此,我们需要一种方法来比较传入&lt;Company /&gt; 组件的道具值与以下条件:

      • 如果我们从 props 中获得新值,我们会重新渲染组件
      • 如果 props 的值没有改变,我们将使用上次渲染的先前存储的版本。

      我无法评论这些方法的内存/性能或其他含义,或者它是否对您的用例有意义,但 useMemo()useCallback() 似乎是完成您所要求的方法。

      来自 useCallback() 的文档

      这在将回调传递给依赖引用相等性以防止不必要的渲染的优化子组件时很有用

      来自 useMemo() 上的文档

      这种优化有助于避免在每次渲染时进行昂贵的计算。

      我在下面做了一个例子来更新交易金额。请记住,这仅用于说明目的,我不确定在您的用例中您在性能方面遇到了哪些实际问题或问题,以及 useMemo() 或 useCallback() 是否有意义。

      但是,由于您询问是否能够跳过重新渲染,这似乎是实现此目的的一种方法。

      • 在挂载时,子组件渲染两次,每个公司一次。
      • 该按钮将更新该公司第一笔交易的金额。
      • 当点击按钮时,只有一个&lt;Company /&gt;组件会渲染,而另一个会跳过渲染并使用记忆值。

      您可以检查控制台以查看此操作。扩展此场景,如果您有 100 家公司,更新一家公司的数量将导致 99 次跳过的重新渲染,而更新后的公司只有一个新组件渲染。

      https://codesandbox.io/s/use-memo-skip-child-update-amount-vvonum

      import { useState, useMemo } from "react";
      
      const companiesData = [
        {
          id: 1,
          transactions: [
            {
              id: "10421A",
              amount: "850"
            },
            {
              id: "1893B",
              amount: "357"
            }
          ]
        },
        {
          id: 2,
          transactions: [
            {
              id: "3532C",
              amount: "562"
            },
            {
              id: "2959D",
              amount: "347"
            }
          ]
        }
      ];
      
      const Company = ({ company, onChangeAmount }) => {
        const memoizedCompany = useMemo(() => {
          console.log(
            `AFTER MEMOIZED CHECK COMPANY ${company.id} CHILD COMPONENT RENDERED`
          );
          return (
            <div>
              <p>Company ID: {company.id}</p>
              {company.transactions.map((t, i) => {
                return (
                  <div key={i}>
                    <span>id: {t.id}</span>
                    <span>amount: {t.amount}</span>
                  </div>
                );
              })}
              <button onClick={onChangeAmount}> Change Amount </button>
            </div>
          );
        }, [company]);
      
        return <div>{memoizedCompany}</div>;
      };
      
      export default function App() {
        const [companies, setCompanies] = useState(companiesData);
      
        console.log("<App /> rendered");
      
        const handleChangeAmount = ({ companyId, transactionId, newAmount }) => {
          setCompanies(() => {
            return companies.map((company) => {
              return company.id === companyId
                ? {
                    ...company,
                    transactions: company.transactions.map((currTransaction) => {
                      return currTransaction.id === transactionId
                        ? {
                            id: currTransaction.id,
                            amount: newAmount
                          }
                        : currTransaction;
                    })
                  }
                : company;
            });
          });
        };
      
        return (
          <div className="App">
            {companies.map((company) => {
              return (
                <Company
                  key={company.id}
                  company={company}
                  onChangeAmount={() =>
                    handleChangeAmount({
                      companyId: company.id,
                      transactionId: company.transactions[0].id,
                      newAmount: Math.floor(Math.random() * 1000)
                    })
                  }
                />
              );
            })}
          </div>
        );
      }
      

      【讨论】:

        猜你喜欢
        • 2018-09-29
        • 2019-06-13
        • 1970-01-01
        • 2021-12-23
        • 2016-05-20
        • 1970-01-01
        • 2020-02-19
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多