2

I'm new to React and am working on a simple receipt scanning web app based on AWS (Amplify, AppSync, GraphQL, DynamoDB, S3). I'm using the useEffect hook to fetch data for the user currently logged in, via a GraphQL call, and am noticing duplicate runs of it. At first, there were three calls, after which I read about and disabled Strict Mode. But now, even with Strict Mode disabled, I am seeing two calls.

Debugging reveals that useEffect is called only once if I comment out setWeekly(getTotalFromItems(response)), but even as little as setWeekly() ends up creating duplicate calls.

I've perused this post as well as numerous others, but they all point to Strict Mode being the primary culprit, which is not the case here.

Network log attached below for reference.

Could someone help me understand what might be causing this double-call, and how to fix it?

Network log.

import WebFont from 'webfontloader';

import React, {
    useEffect,
    useState
} from 'react'

import {
    withAuthenticator,
    Text,
    View
} from '@aws-amplify/ui-react';

import {
    MainLayout
} from './ui-components';

import awsExports from "./aws-exports";
Amplify.configure(awsExports);

function App({signOut, user}) {
    const [weekly, setWeekly] = useState([]);
    
    useEffect(() => {

        WebFont.load({
            google: {
                families: ['DM Sans', 'Inter']
            }
        });

        async function fetchWeekly(queryTemplate, queryVars) {
            try {
                const weeklyData = await API.graphql(graphqlOperation(queryTemplate, queryVars))
                console.log(weeklyData)
                const response = weeklyData.data.getUser.receiptsByPurchaseDateU.items
                const total = getTotalFromItems(response) // sums 'total' in [{'date': '...', 'total': 9}, ...]
                setWeekly(total)
        } catch (err) {
                console.log('Error fetching data.');
                console.log(err)
        }
        }

        const queryVars = {
            username: user.attributes.email,
        }

        let d = new Date();
        d.setDate(d.getDate() - 7);
        d = d.toLocaleDateString('en-CA');
        let tmpl = generateSummaryTemplate(d) // returns a template string based on d
        fetchWeekly(tmpl, queryVars);
        console.log('Complete.')
    });

 return ( <View >
        <MainLayout/>
        </View >
        )
}

export default withAuthenticator(App);
Drew Reese
  • 165,259
  • 14
  • 153
  • 181
sonny
  • 313
  • 3
  • 11
  • You might just be missing a dependency array, if your intended result is for the hook to run only once on the initial render. E.g. `useEffect(() => {}, []/*<--- You are missing this? */);` – segFault Jan 15 '23 at 01:10

3 Answers3

4

The issue here is that the useEffect hook is missing a dependency array. The useEffect callback enqueues a weekly state update which triggers a component rerender and the useEffect hook is called again. This second time it again computes a value and enqueues a weekly state update. It's this second time that the state is enqueued with the same value as the current state value and React decides to bail on further rerenders. See Bailing out of State Updates.

If you update a State Hook to the same value as the current state, React will bail out without rendering the children or firing effects. (React uses the Object.is comparison algorithm.)

The solution is to add a dependency array with appropriate dependencies. Use an empty array if you want the effect to run once after the initial render when the component mounts. In this case it seems the passed user prop is the only external dependency I see at the moment. Add user to the dependency array. This is to indicate when the effect should run, i.e. after the initial mount/render and anytime user value changes. See Conditionally firing an effect.

Example:

useEffect(() => {
  ...

  async function fetchWeekly(queryTemplate, queryVars) {
    try {
      const weeklyData = await API.graphql(graphqlOperation(queryTemplate, queryVars));
      const response = weeklyData.data.getUser.receiptsByPurchaseDateU.items
      const total = getTotalFromItems(response) // sums 'total' in [{'date': '...', 'total': 9}, ...]
      setWeekly(total);
    } catch (err) {
      console.log('Error fetching data.');
      console.log(err);
    }
  }

  const queryVars = {
    username: user.attributes.email,
  };

  let d = new Date();
  d.setDate(d.getDate() - 7);
  d = d.toLocaleDateString('en-CA');
  let tmpl = generateSummaryTemplate(d); // returns a template string based on d
  fetchWeekly(tmpl, queryVars);
  console.log('Complete.');
}, [user]); // <-- user is external dependency
Drew Reese
  • 165,259
  • 14
  • 153
  • 181
  • updating the dependency may resolve the issue, however, the useState() is still unnecessary. – Shah Jan 15 '23 at 02:00
  • 1
    @Shah `fetchWeekly` is an `async` function and it's not clear where or how it would/could be used outside the `useEffect` hook to compute some local state. I don't think you can say with any certainty that just returning a value from `fetchWeekly` works since we don't know where it would/could be used and that value would need to be `awaited`. Does this make sense to you? – Drew Reese Jan 15 '23 at 02:15
  • Yes, I see what you’re saying. I should have been clearer. You may combine your useEffect and useState into either a useCallback or useMemo (one import instead of two). – Shah Jan 15 '23 at 04:13
  • I noticed your GraphQL layer may not handling errors properly. Maybe you didn’t include it intentionally to ask your question. But the gql client likely has its own hooks to handle errors. The whole try catch block May not be necessary – Shah Jan 15 '23 at 04:18
  • 1
    @Shah IDK, I didn't create or write the OPs `API` module. – Drew Reese Jan 15 '23 at 04:20
  • @Shah Normally, yes, the pattern of `useState` + `useEffect` generally equals `useMemo`, but this isn't necessarily the case when the value you are trying to memoize is returned from an `async` function, it depends on where & how the memoized value is used. – Drew Reese Jan 15 '23 at 04:25
  • @DrewReese That solved it! Thanks for this quick, detailed, and thorough answer. Much appreciated—and I hope this can help someone else in the future. – sonny Jan 15 '23 at 05:08
2

It’s because setWeekly() is called within your useEffect() and triggers another render. You may remove this useState() entirely and instead return the data you need in fetchWeekly().

Shah
  • 2,126
  • 1
  • 16
  • 21
2

First of add a dependency array as the second argument to the useEffect, after the callback function. Moreover, I may advice that you have service functions outside the useEffect body, don't overload it like that, is not appropirate.

Proau
  • 53
  • 3