5

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;
user8463989
  • 2,275
  • 4
  • 20
  • 48
  • I don't understand where you putted the code that starts with `if(data)`. Is in component's body or in a function/hook? – Giovanni Esposito Mar 15 '21 at 15:09
  • @GiovanniEsposito, I have updated my question. Does that help? also: https://swr.vercel.app/docs/data-fetching – user8463989 Mar 15 '21 at 15:11
  • 2
    You should never call a set method directly inside a render method. This will cause the component to rerender every time it renders, causing the too many re-renders error. Instead, use `useEffect` with apropriate arguments to call `setAdults` only when it has actually changed. – mousetail Mar 15 '21 at 15:16
  • @mousetail, thank you. I am trying to use useSWR instead of useEffect though.. – user8463989 Mar 15 '21 at 15:20
  • Can you post the full component? What do you render to the page? – Gh05d Mar 15 '21 at 15:26
  • 1
    @Gh05d, I will try to reduce the code of the component so I don't post unnecessary code. All that is being rendered to the page is a table with the data. I am looping over the data with .map but I want to show the total at the top hence my reduce function. – user8463989 Mar 15 '21 at 15:28
  • @Gh05d, okay. Updated question – user8463989 Mar 15 '21 at 15:31

4 Answers4

10

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

}
Nicolas Menettrier
  • 1,647
  • 1
  • 13
  • 28
9

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)
Jahchap
  • 101
  • 1
  • 4
0

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

Alessandro
  • 99
  • 2
  • 9
  • It doesn't work (first solution) because setAdults will trigger a rerender, it will go through your `if (data)` condition, since data is not undefined/null it will do setAdults again and retrigger a render that will go through ..... etc – Nicolas Menettrier Aug 23 '21 at 10:02
-1

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.

Gh05d
  • 7,923
  • 7
  • 33
  • 64
  • Thank you, much appreciated! – user8463989 Mar 15 '21 at 15:50
  • 3
    I am getting the violation rule message: React Hook "useSWR" cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook function. (react-hooks/rules-of-hooks)eslint in sandbox any alternative to this, why using one producing other issues just silly time waste isn't it ? I know here it sound too cocky, lot of waste times – Jatinder Jun 04 '21 at 18:58
  • 2
    It's not a correct solution, as said before useSWR cannot be called inside a useEffect hook + putting loading and error state in a new state is just wasting the potential of useSWR that provide everything for you to avoid unnecessary state and code. – Nicolas Menettrier Jul 19 '21 at 07:02