UseEffect 在 React 中运行无限渲染(超出最大更新深度)

UseEffect runs infinite renders in React (Maximum update depth exceeded)

我有一个购物车组件,里面有一组卡片。

每次用户从购物车中删除产品时,我都会将其从本地存储中删除, 并将其从 UI.

中删除
import React, { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import Layout from '../Layout';
import Card from '../Home/Card';
import { getCart } from './cartHelpers';

const Cart = () => {
  const [items, setItems] = useState([]);

  useEffect(() => {
    setItems(getCart());
  }, [items]);

  const showItems = () => {
    return (
      <div>
        <h2>You have {`${items.length}`} items in your cart</h2>
        <hr />
        {items.map((prod, index) => (
          <Card key={prod._id}
            ....... />
        ))}
      </div>
    );
  };

  const noItemsMsg = () => (
    <h2>
      Your cart is empty! <br /> <Link to='/shop'>Continue Shopping</Link>
    </h2>
  );



return (
    <Layout
      title='Shopping Cart'
      desc='Manage your cart items'
      className='container-fluid'
    >
      <div className='row'>
        <div className='col-6'>
          {items && items.length > 0 ? showItems(items) : noItemsMsg()}
        </div>

        <div className='col-6'>
          <p>
            Show Checkout Options / Shipping Address / Total / Update Quantity
          </p>
        </div>
      </div>
    </Layout>
  );
    };

export default Cart;

Card.js

import React, { useState } from 'react';
import moment from 'moment';
import { Link, Redirect } from 'react-router-dom';
import ShowImage from '../ShowImage';
import { addItemToCart, updateItem, removeItem } from '../Cart/cartHelpers';

const Card = ({product,
   .............
}) => {
  const [redirect, setRedirect] = useState(false);
  const [countHowManyCopies, setCountHowManyCopies] = useState(product.count);

  const showRemoveButton = () => {
    return (
      showRemoveProductBtn && (
        <button
          onClick={() => removeItem(product._id)}
          className='btn btn-outline-danger mt-2 mb-2'
        >
          Remove Product
        </button>
      )
    )
  };

  return (
    <div className={!showProductFullSize ? 'col-4 mb-3' : ''}>
      <div className='card'>
        <div className='card-header name'>{product.name}</div>
        <div className='card-body'>
  
  
          .............. // more code

          {showRemoveButton()}

          .............. // more code
        </div>
      </div>
    </div>
  );
};

export default Card;

每当用户点击删除项目按钮时,我得到:

 Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.

解决此问题的最佳方法是什么?

因为这段代码(数组中的items):

  useEffect(() => {
    setItems(getCart());
  }, [items]);

我猜你会在渲染组件时收到“超出最大更新深度”警告,无需等待单击“删除”按钮。 原因是:items改变时会调用setItems,然后setItems会改变items,所以会造成无限递归。

useEffect(() => {
    setItems(getCart());// setItems will change the items
  }, [items]);

嗯,我认为当前的方法不是干净的方法,因为很难通过这种方式控制组件 re-rendering。 我的建议是去掉 useEffect 的使用,然后添加 onRemoveItems 作为 CardComponent

的 props
import { addItemToCart, updateItem, removeItem } from '../Cart/cartHelpers';

const Cart = () => {
  const [items, setItems] = useState([]);
  //remove this
  //useEffect(() => {
  //  setItems(getCart());
  //}, [items]);
  const removeItem = (id) => {
    removeItem(id);
    setItems(getCart());
  }
  const showItems = () => {
    return (
      <div>
        <h2>You have {`${items.length}`} items in your cart</h2>
        <hr />
        {items.map((prod, index) => (
          <Card key={prod._id} onRemoveItem={removeItem}
            ....... />
        ))}
      </div>
    );
  };



const Card = ({product,
   .............
}) => {
  const [redirect, setRedirect] = useState(false);
  const [countHowManyCopies, setCountHowManyCopies] = useState(product.count);

  const showRemoveButton = () => {
    return (
      showRemoveProductBtn && (
        <button
          onClick={() => props.onRemoveItem(product._id)}
          className='btn btn-outline-danger mt-2 mb-2'
        >
          Remove Product
        </button>
      )
    )
  };

  return (
    <div className={!showProductFullSize ? 'col-4 mb-3' : ''}>
      <div className='card'>
        <div className='card-header name'>{product.name}</div>
        <div className='card-body'>
  
  
          .............. // more code

          {showRemoveButton()}

          .............. // more code
        </div>
      </div>
    </div>
  );
};

export default Card;

问题的根本原因在于使用“useEffect”来更新状态。

useEffect只能用于执行side-effects。

副作用是 activity 由于状态改变而触发的。

side-effects的一些例子是:

  • 在用户提供输入数据后提交表单。
  • 加载页面时从 API 中获取 JSON 数据。

更新useEffect中的状态会导致无限循环。


注意到的问题是在 useEffect 中更新状态的场景示例。

  • 每当调用 setItems 时,'items' 的状态就会得到 改变了。

  • 状态改变会触发useEffect,目的是运行 side-effects.

  • useEffect 再次调用 setItems,这导致了无限循环。

如何解决问题?

问题可以通过删除 useEffect 挂钩来解决,因为在给定的场景中没有必要。

在 Cart.js 文件中:从 useEffect 中删除项目依赖项。在此处导入 removeItem

import { removeItem } from '../Cart/cartHelpers';

const Cart = () => {
  const [items, setItems] = useState([]);

  useEffect(() => {
    setItems(getCart());
  }, []);

// Add this function and pass it to Card component
const handleRemove = (id) => {
    removeItem(id)
    setItems(getCart())
  }

  const showItems = () => {
    return (
      <div>
        <h2>You have {`${items.length}`} items in your cart</h2>
        <hr />
        {items.map((prod, index) => (
          <Card key={prod._id} handleRemove={handleRemove}
            ....... />
        ))}
      </div>
    );
  };

Card.js - 接收 handleRemove 作为 props

const Card = ({product, handleRemove
   .............
}) => {
  const [redirect, setRedirect] = useState(false);
  const [countHowManyCopies, setCountHowManyCopies] = useState(product.count);

  const showRemoveButton = () => {
    return (
      showRemoveProductBtn && (
        <button
          onClick={() => handleRemove(product._id)}
          className='btn btn-outline-danger mt-2 mb-2'
        >
          Remove Product
        </button>
      )
    )
  };...