奇怪的无限循环与 useEffect 和 useCallback

Wierd infinite loop with useEffect and useCallback

那好吧。我有这段代码,只是为了练习,所以它可能有点乱(请忽略 cammelCase 约定或不良做法,除非它们是问题的原因,因为我已经注意到了),但是我要描述它的作用,我将展示代码。

我有这个组件,第一次渲染时没有数据,然后当用户单击一个按钮(在父组件中)时,API 它被调用(也在父组件中)并发送响应通过道具到这个有问题的组件。 在收到来自 API 的数据后 (props.rows),执行 useEffect 以格式化此数据并将其存储在状态 (Rows) 中。当 Rows 值更改时,应执行另一个 useEffect 并操作 Rows(不改变它)以创建另一种数据(TotalPages)。 问题是,当我调用 setTotalPages() 时,这会产生一个无限循环,并且出现错误 Uncaught Error: Too many re-renders。 React 限制渲染次数以防止无限循环。

我现在就展示代码

这是有问题的组件

import '../Css/RGrid.css';
//import {useLayoutEffect} from 'react';
import React, {useCallback, useState} from 'react';
import {useEffect} from 'react';
//import {useLayoutEffect} from 'react';

//import {useLayoutEffect} from 'react';
//import {Row} from 'react-bootstrap';
//import {act} from 'react-dom/cjs/react-dom-test-utils.production.min';

const RGridTest = props => {
  const [Rows, setRows] = useState([]);
  const [rowsPerPage, setRowsPerPage] = useState(10);
  const [TotalRows, setTotalRows] = useState(0);
  const [actualPageIndex, setActualPageIndex] = useState(0);
  const [TotalPages, setTotalPages] = useState(0);
  const [counter, setCounter] = useState(0);

  const ddlPages_OnChange = value => {
    try {
      setRowsPerPage(value);
    } catch (e) {
      console.log(e.message);
    }
  };

  const Export = value => {
    console.log(value);
  };

  const EnabledPaging = () => {
    let b = false;
    try {
      b = Rows.length > 0 && Rows.length < 9999 && rowsPerPage < 9999;
    } catch (e) {
      console.log(e);
    }
    return b;
  };

  const PrevPage = () => {
    try {
      if (actualPageIndex > 1) {
        setActualPageIndex(actualPageIndex - 1);
      }
    } catch (e) {
      console.log(e.message);
    }
  };

  const NextPage = () => {
    try {
      if (actualPageIndex < TotalPages) {
        setActualPageIndex(actualPageIndex + 1);
      }
    } catch (e) {
      console.log(e.message);
    }
  };

  const calcularPaginas = useCallback(() => {
    if (Rows.length === 0) {
      console.log('Sin filas');
      return false;
    }

    console.log('llegaron las filas');
    const cantidadFilas = Rows.length;
    let iTotal = 0;

    console.log(cantidadFilas);
    if (cantidadFilas % rowsPerPage === 0) {
      console.log('Division perfecta');
      iTotal = Math.ceil(cantidadFilas / rowsPerPage);
    }
    console.log('Division imperfecta');
    iTotal = Math.ceil(cantidadFilas / rowsPerPage);
    console.log(cantidadFilas, rowsPerPage);
    console.log(iTotal);
    setTotalPages(iTotal);
  }, [Rows]);

  const ChangeId = () => {
    let sText;
    let oComplete;
    try {
      if (props.rows.length === 0) {
        console.log('No change id');
        return;
      }
      sText = JSON.stringify(props.rows);
      sText = sText.replace('"' + props.ConfigurationId + '":', '"RowId":');
      oComplete = JSON.parse(sText);
      setRows(oComplete);
      setTotalRows(oComplete.length);

      console.log('ChangeId Rows');
      console.log(oComplete.length);
    } catch (e) {
      console.log(e.message);
    }
  };

  useEffect(() => {
    ChangeId();
  }, [props.rows]);

  useEffect(() => {
    calcularPaginas();
  }, [Rows, calcularPaginas]);

  return (
    <div>
      {props.isLoading ? (
        <h2>Loading ...</h2>
      ) : (
        <React.Fragment>
          <span>
            <select
              className="Select"
              name="ddlPages"
              id="ddlPages"
              key="ddlPages"
              onChange={e => ddlPages_OnChange(e.target.value)}
            >
              <option value="10"> 10 </option>
              <option value="25"> 25 </option>
              <option value="50"> 50 </option>
              <option value="100"> 100 </option>
              <option value="9999"> All </option>
            </select>
          </span>
          {props?.Export && (
            <span>
              <button value="csv" className="btn-2" onClick={e => Export(e.target.value)}>
                CSV
              </button>
              <button value="xls" className="btn-2" onClick={e => Export(e.target.value)}>
                Excel
              </button>
              <button value="pdf" className="btn-2" onClick={e => Export(e.target.value)}>
                PDF
              </button>
            </span>
          )}
          <table width="99%" border="0" align="center">
            <tr className="TrTittle">
              <td className="TdTittle" align="center">
                <a>{props.Tittle}</a>
              </td>
            </tr>
          </table>
          <table className="Table" key="tgrid" width="99%" align="center">
            <thead key="thead">
              <tr key="trHead">
                {props.columns.map((column, idx) => {
                  return (
                    <th className="TableCellBold" width={column.WidthColumn}>
                      {column.Titulo}
                    </th>
                  );
                })}
                {props.ShowDelete && (
                  <th width="1%" className="TableCellBold">
                    Action
                  </th>
                )}
              </tr>
            </thead>
            <tbody>
              {Rows.map((row, idx) => {
                if (idx <= rowsPerPage) {
                  return (
                    <tr key={'tr' + idx}>
                      {props.columns.map((column, colx) => {
                        return (
                          <td className="TableCell" width={column.WidthColumn}>
                            {column.Selector(row)}
                          </td>
                        );
                      })}
                      {props.ShowDelete && (
                        <td className="TableCellBold" align="center">
                          <a href="#" onClick={() => props.DeleteId(row.RowId)}>
                            <img
                              className="imgDelete"
                              title="Next"
                              border="0"
                              width="18px"
                              height="18px"
                            ></img>
                          </a>
                        </td>
                      )}
                    </tr>
                  );
                }
              })}
            </tbody>
            <tfoot>
              <tr key="trFoot">
                <td
                  key="tdFoot"
                  align="right"
                  colSpan={props.columns.length + 1}
                  className="TableCellBold"
                >
                  {EnabledPaging() && (
                    <div className="DivFooter">
                      <a href="#" onClick={PrevPage()}>
                        <img
                          className="imgPrev"
                          title="Next"
                          border="0"
                          width="18px"
                          height="18px"
                        ></img>
                      </a>

                      <a>
                        {' '}
                        Page {actualPageIndex + 1} / {TotalPages}{' '}
                      </a>

                      <a href="#" onClick={NextPage()}>
                        <img
                          className="imgNext"
                          title="Next"
                          border="0"
                          width="18px"
                          height="18px"
                        ></img>
                      </a>
                    </div>
                  )}
                </td>
              </tr>
            </tfoot>
          </table>
        </React.Fragment>
      )}
      <div>
        <button onClick={() => setCounter(prev => prev + 1)}>Mostrar totalRows</button>
      </div>
    </div>
  );
};

export default RGridTest;


这是父项,如果按下了调用 API 的按钮


import './Css/App.css';
import 'bootstrap/dist/css/bootstrap.css';
import GrillaCompleta from './Components/GrillaCompleta';
import {BrowserRouter as Router, Switch, Route, Link} from 'react-router-dom';
import RGrid from './Components/RGrid';
import RGridTest from './Components/RGridTest';
import {ListAll} from './Components/Helpers';
import React, {useState} from 'react';
import TestSocialReducer from './Components/TestSocialReducer';

function App() {
  const [Dogs, setDogs] = useState([]);
  const [isCargando, setIsCargando] = useState(false);

  function FindDogs() {
    setIsCargando(true);
    ListAll().then(lDog => {
      setDogs(lDog);
      setIsCargando(false);
    });
  }

  /*
  useEffect(() => {

  }, []);
  */

  function Topics() {
    return (
      <div>
        <h2>Topic</h2>
      </div>
    );
  }

  const GrillaConfiguracion = [
    {
      Titulo: 'Nombre',
      Selector: fila => fila.name,
      WidthColumn: '30%',
      Ordenable: true,
    },
    {
      Titulo: 'Tamaño',
      Selector: fila => fila.breed_group,
      WidthColumn: '30%',
    },
  ];

  return (
    <Router>
      <div>
        <ul>
          <li key="home">
            <Link to="/">Home</Link>
          </li>
          <li key="GrillaCompleta">
            <Link to="/GrillaCompletaPrueba"> Grilla Completa Dog Test </Link>
          </li>
          <li key="Topic">
            <Link to="/topics">Topics</Link>
          </li>
          <li key="RGrilla">
            <Link to="/RGrilla">RGrilla</Link>
          </li>
          <li key="GrillaTest">
            <Link to="/RGrillaTest">RGrillaTest</Link>
          </li>
          <li key="Reducer Test">
            <Link to="/TestSocialReducer">TestSocialReducer</Link>
          </li>
        </ul>

        <Switch>
          <Route path="/rGrilla">
            <button key="btnFind" id="btnFind" onClick={FindDogs}>
              Ver Perros
            </button>
            <br></br>
            <RGrid
              key="RGrid1"
              Tittle="Grilla Dogs"
              rows={Dogs}
              columns={GrillaConfiguracion}
              ShowDelete
              Export
              DeleteId={id => console.log(id)}
              isLoading={isCargando}
            />
          </Route>
          <Route path="/rGrillaTest">
            <button key="btnFindTest" id="btnFindTest" onClick={FindDogs}>
              Ver Perros Test
            </button>
            <br></br>
            <RGridTest
              key="RGridTest"
              Tittle="Grilla Dogs Test"
              rows={Dogs}
              columns={GrillaConfiguracion}
              ShowDelete="true"
              Export="true"
              DeleteId={id => console.log(id)}
              isLoading={isCargando}
              ConfigurationId="id"
            />
          </Route>

          <Route path="/GrillaCompletaPrueba">
            <GrillaCompleta></GrillaCompleta>
          </Route>
          <Route path="/topics">
            <Topics />
          </Route>
          <Route path="/TestSocialReducer">
            <TestSocialReducer></TestSocialReducer>
          </Route>
          <Route path="/">
            <h2>Home</h2>
          </Route>
        </Switch>
      </div>
    </Router>
  );
}

export default App;



这里有问题的组件叫做


          <Route path="/rGrillaTest">
            <button key="btnFindTest" id="btnFindTest" onClick={FindDogs}>
              Ver Perros Test
            </button>
            <br></br>
            <RGridTest
              key="RGridTest"
              Tittle="Grilla Dogs Test"
              rows={Dogs}
              columns={GrillaConfiguracion}
              ShowDelete="true"
              Export="true"
              DeleteId={id => console.log(id)}
              isLoading={isCargando}
              ConfigurationId="id"
            />
          </Route>

就是这样,仅此而已,没有redux,没有外部包,什么都没有。

我希望我已经清楚了,感谢您接下来的回答

嗯,这个错误在 **s 中非常痛苦,但它非常简单,在 RgridTest 的 JSX 中你可以看到有一个按钮可以调用回调

<a href="#" onClick={NextPage()}>