设置状态时重新渲染太多 - useSWR

Too many re-renders when setting state - useSWR

我正在使用 useSWR 获取数据,然后使用数据,我想通过使用 reduce 来获得总计。如果我 console.log 值输出它工作正常但是一旦我尝试用值设置状态我得到 'Too may re-renders' 消息。

import Admin from "../../../components/admin/Admin";
import { useRouter } from "next/router";
import styles from "../../../styles/Dashboard.module.css";
import { getSession, useSession } from "next-auth/client";
import { useState } from "react";

/* BOOTSTRAP */
import Col from "react-bootstrap/Col";
import Container from "react-bootstrap/Container";
import Row from "react-bootstrap/Row";
import Spinner from "react-bootstrap/Spinner";
import Table from "react-bootstrap/Table";

import useSWR from "swr";
import axios from "axios";

const General = () => {
  const [session, loading] = useSession();
  const [adults, setAdults] = useState(null);
  const router = useRouter();
  const { id } = router.query;

  const fetcher = (url) =>
    axios
      .get(url, {
        headers: { Authorization: "Bearer " + session.user.servertoken },
      })
      .then((res) => res.data);

  const { data, error } = useSWR(
    `http://localhost:8000/api/admin/general/${id}`,
    fetcher
  );

   if (data) {
     const adults = data.map((a) => a.adults);
     const reducer = (accumlator, item) => {
       return accumlator + item;
     };
     const totalAdults = adults.reduce(reducer, 0);
     setAdults(totalAdults);
   }

  return (
    <Admin>
      <div className={styles.admin_banner}>
        <Container fluid>
          <Row>
            <Col>
              <h2>Bookings</h2>
              <h6>
                {adults}
              </h6>
            </Col>
          </Row>
        </Container>
      </div>
      <Container fluid>
        <Row>
          <Col>
            <div className={styles.admin_container}>
              {!error && !data && (
                <Spinner animation="border" role="status">
                  <span className="sr-only">Loading...</span>
                </Spinner>
              )}
              {!error && data && (
                <Table responsive="md">
                  <thead>
                    <tr>
                      <th>Name</th>
                    </tr>
                  </thead>
                  <tbody>
                    {data &&
                      !error &&
                      data.map((d) => (
                        <tr key={d._id}>
                          <td>
                            {d.firstName} {d.lastName}
                          </td>
                        </tr>
                      ))}
                  </tbody>
                </Table>
              )}
            </div>
          </Col>
        </Row>
      </Container>
    </Admin>
  );
};

export async function getServerSideProps(context) {
  const session = await getSession({
    req: context.req,
  });

  if (!session) {
    return {
      redirect: {
        destination: "/admin",
        permanent: false,
      },
    };
  } else {
    return { props: session };
  }
}

export default General;

问题是您的数据检查中的 setAdults(totalAdults); 导致了无限循环。 React 中的钩子导致组件重新渲染,所以实际发生的是

data 为真 --> setAdults 被触发 --> 重新渲染 --> data 为真 --> ...

将您的逻辑移动到 useEffect 挂钩中:

React.useEffect(() => {
  const fetcher = url =>
    axios
      .get(url, {
        headers: { Authorization: "Bearer " + session.user.servertoken },
      })
      .then(res => {
        const adults = data.map(a => a.adults);
        const reducer = (accumlator, item) => {
          return accumlator + item;
        };
        const totalAdults = adults.reduce(reducer, 0);
        setAdults(totalAdults);
      });

  useSWR(`http://localhost:8000/api/admin/general/${id}`, fetcher);
}, []);

同时更新您的加载和错误检查。也许为两者创建一个新的状态变量。

您应该使用依赖于数据值的 useEffect,这样您就可以仅在数据在渲染之间发生变化时更新它,接受的答案使用了 useEffect 中的 useSWR 挂钩,这是不正确的

示例:

const General = () => {
  const [session, loading] = useSession();
  const [adults, setAdults] = useState(null);
  const router = useRouter();
  const { id } = router.query;

  const fetcher = (url) =>
    axios
      .get(url, {
        headers: { Authorization: "Bearer " + session.user.servertoken },
      })
      .then((res) => res.data);

  const { data, error } = useSWR(
    `http://localhost:8000/api/admin/general/${id}`,
    fetcher
  );

  useEffect(() => {
   const adultsFetch = data.map((a) => a.adults);
   const reducer = (accumlator, item) => {
     return accumlator + item;
   };
   const totalAdults = adultsFetch.reduce(reducer, 0);
   setAdults(totalAdults);
  }, [data]);

}

编辑:

您还可以使用 useMemo 挂钩,这样您甚至不再需要 useState,每次数据依赖关系发生变化时,useMemo 都会重新计算成人。

更多信息:https://reactjs.org/docs/hooks-reference.html#usememo

const General = () => {
  const [session, loading] = useSession();
  const router = useRouter();
  const { id } = router.query;

  const fetcher = (url) =>
    axios
      .get(url, {
        headers: { Authorization: "Bearer " + session.user.servertoken },
      })
      .then((res) => res.data);

  const { data, error } = useSWR(
    `http://localhost:8000/api/admin/general/${id}`,
    fetcher
  );

  const adults = useMemo(() => {
    if (!data) return null;
    const adultsFetch = data.map((a) => a.adults);
    const reducer = (accumlator, item) => {
      return accumlator + item;
    };
    const totalAdults = adultsFetch.reduce(reducer, 0);
    return totalAdults;
  }, [data]);

}

我遇到了同样的问题,这是我的第一种方法(当然数据与你的不同):

const General = () => {
  const [session, loading] = useSession();
  const [adults, setAdults] = useState(null);
  const router = useRouter();
  const { id } = router.query;
  const [takeData, setTakeData] = useState(false)

  const fetcher = (url) =>
    axios
      .get(url, {
        headers: { Authorization: "Bearer " + session.user.servertoken },
      })
      .then((res) => res.data);

  const { data, error } = useSWR(
    takeData ? `http://localhost:8000/api/admin/general/${id}` : null,
    fetcher
  );

  useEffect(() => {
     setTakeData(true)
  }, [])

   if (data) {
     const adults = data.map((a) => a.adults);
     const reducer = (accumlator, item) => {
       return accumlator + item;
     };
     const totalAdults = adults.reduce(reducer, 0);
     setAdults(totalAdults);
   }

问题是一旦状态发生变化,就会发生重新渲染。 在这种情况下,useSWR 返回数据(可能)并在第一次呈现页面之前更改 setAdults 的状态。此时 adults 已更改,因此在返回页面之前再次进行渲染,因此再次触发 useSWR 并再次设置 adults 并再次调用渲染,这将继续无限循环。 为了解决这个问题,我决定仅当 takeData 为真时才发送请求,并且只有在第一次呈现页面后 takeData 才变为真(因为在这种情况下,这就是要做的 useEffect )。但是它没有用,我不知道为什么。

所以我采用了第二种方案:

const General = () => {
  const [session, loading] = useSession();
  const [adults, setAdults] = useState(null);
  const router = useRouter();
  const { id } = router.query;
  const [takeData, setTakeData] = useState(false)

  const fetcher = (url) =>
    axios
      .get(url, {
        headers: { Authorization: "Bearer " + session.user.servertoken },
      })
      .then((res) => res.data);

  const { data, error } = useSWR(
    takeData ? `http://localhost:8000/api/admin/general/${id}` : null,
    fetcher
  );

  
 useEffect(() => {
   if (data) {
     const adults = data.map((a) => a.adults);
     const reducer = (accumlator, item) => {
       return accumlator + item;
     };
     const totalAdults = adults.reduce(reducer, 0);
     setAdults(totalAdults);
   }
  }, []);
  

此处不是在第一次渲染后发出请求,而是立即完成请求,但在第一次渲染后检查数据。 第二种解决方案对我有用。

如果有人知道为什么第一个解决方案不起作用,请告诉我 ;)

您可以通过添加第三个参数(选项)来实现,如下所示:

const { data, error } = useSWR(
    `http://localhost:8000/api/admin/general/${id}`,
    fetcher,
    {
        revalidateOnFocus: false,
        revalidateIfStale: false,
        // revalidateOnReconnect: false, // personally, I didn't need this one
    }

  );

它基本上是缓存数据,一旦有缓存就防止重新生效。 Here is the link to the docs.

或者,根据文档,您可以使用 useSWRImmutable,从 1.0 版开始,获得与默认设置相同的结果。

import useSWRImmutable from "swr/immutable"

const { data, error } = useSWRImmutable(`http://localhost:8000/api/admin/general/${id}`, fetcher)