Too many re-renders when setting state - useSWR

Viewed 5512

I am using useSWR to fetch data and then with the data, I want to get a total by using reduce. If I console.log the value out it works fine but as soon as I try to set the state with the value I get the 'Too may re-renders' message.

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;
4 Answers

You should use an useEffect that depends on data value so you can update it only if data changed between render, the accepted answer made use of the useSWR hook inside the useEffect and that's not correct

example:

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]);

}

EDIT:

You can also use the useMemo hook so you don't even need the useState anymore, useMemo will recalculate adults everytime the data dependencies change.

for more information : 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]);

}

You can achieve this by adding a third parameter (options) as follows:

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
    }

  );

It basically caches the data and prevents revalidation once there is a cache. Here is the link to the docs.

Alternatively, according to the docs, you can use useSWRImmutable, starting from version 1.0, to achieve the same result as it has those options set by default.

import useSWRImmutable from "swr/immutable"

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

I had the same problem and this was my first approach (of course with different data than your):

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);
   }

The problem is that as soon as there is a change of state, a re-render happens. In this case the useSWR returned data (probably) and changed the state of setAdults before there was the first render of the page. At this point adults is changed and so there's again the render before returning the page and so useSWR is triggered again and adults set again and the render called again and this will continue in an infinite loop. To solve this problem I decided to send the request only if takeData was true and takeData become true only after the first render of the page (because this is what is going to do useEffect in this case). But It didn't works and I dont't know why.

So I adopted the second solution:

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);
   }
  }, []);
  

Where here instead of making the request after the first render, the request is immediately done, but data is checked after the first render. The second solution works for me.

If someone knows why the firt solution doesn't work, please let me know ;)

The problem is that your setAdults(totalAdults); in your data check is causing an infinite loop. Hooks in React cause the component to re-render, so what is actually happening is that

data is true --> setAdults is triggered --> re-render --> data is true --> ...

Move your logic into an useEffect hook:

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);
}, []);

Also update your loading and error checks. Maybe create a new state variable for both.

Related