reactjsnext.jsrerender

How to run code on (re)loading page but not when the component rerenders?


I have a React/Next application and I need to send an event to Google Tag Manager whenever a page with search results (re)loads:

But besides that, I have a requirement not to send an event to GTM in case that the component which is used in that page rerenders for any other reason (meaning if the inquiryKey and the results do not change, but it gets rerendered because of some event in it or in its parent or whatever else could be the reason).

With the code that I currently have, when I console.log(windows.dataLayer) in the browser console everything looks fine, but the test in which I'm using rerender is failing. Here's what I have so far:

Parent page:

export const ResultsContent: React.FC<ResultsContentProps> = ({
  hasResults,
  inquiryKey,
}) => {  
  return hasResults ? <ResultsComponent inquiryKey={inquiryKey} /> : <NoResults />;
};

Results Component:

export const ResultsComponent: React.FC<ResultsComponentProps> = ({
  inquiryKey,
}) => {
  // Get results from Redux, calculated based on the form inputs on the previous page
  const results = useSelector(getResults);
  const gtm = useGoogleTagManager();

  /* THIS IS THE PROBLEMATIC PART */
      useEffect(() => {
        gtm.push(createEvent(results)); // makes an event object in a format that we need
      }, []); // empty dependency array to prevent it from running multiple times

  return (/* Component body */);
}

For the dependency array I tried: [], [results], [...results], [inquiryKey, results] and [inquiryKey, ...results], but no success. I also tried useCallback instead of useEffect, but that breaks even more tests...

Here's the test which is failing:

it('should contain only one event when rerendered', () => {
    const inquiryKey = 'abc123';
    const { rerender } = render(<ResultsComponent inquiryKey={inquiryKey} />);
    rerender(<ResultsComponent inquiryKey={inquiryKey} />);
    expect(
      window.dataLayer.filter((el) => el.event === `event_name_goes_here`).length,
    ).toBe(1); // It fails, since it found 2 elements
  });

...and two more tests which are passing:

it('should contain only one event when rendered only once', () => {
    render(<ResultsComponent inquiryKey={inquiryKey} />);
    expect(
      window.dataLayer.filter((el) => el.event === `event_name_goes_here`).length,
    ).toBe(1);
  });

it('should contain three events when rendered three times', () => {
    render(<ResultsComponent inquiryKey={inquiryKey} />);
    render(<ResultsComponent inquiryKey={inquiryKey} />);
    render(<ResultsComponent inquiryKey={inquiryKey} />);
    expect(
      window.dataLayer.filter((el) => el.event === `event_name_goes_here`).length,
    ).toBe(3);
  });

I'm not sure if I wrote the failing test correctly... Does the rerender method really rerenders a component? What's the difference between this and calling render again?

Edit:

As @adsy proposed, I added a few lines to the ResultsComponent:

const counter = useRef<number>(0);
console.log('*** COUNTER ***', counter.current);
counter.current++;

When I run only the problematic test, here's the output:

console.log
*** COUNTER *** 0
at log (src/components/ResultsComponent/ResultsComponent.tsx:63:11)
console.log
*** COUNTER *** 1
at log (src/components/ResultsComponent/ResultsComponent.tsx:63:11)
console.log
*** COUNTER *** 2
at log (src/components/ResultsComponent/ResultsComponent.tsx:63:11)

Warning: Cannot update a component (`SomeBannerComponent`) while rendering a different component (`TestProviders`). To locate the bad setState() call inside `TestProviders`, follow the stack trace as described in https://reactjs.org/link/setstate-in-render
        at store (/some_path/TestProviders.tsx:67:3)
        at wrapper

      25 |   return render(<ResultsComponent inquiryKey={inquiryKey} />, {
      26 |     init(store) {
    > 27 |       store.dispatch(
         |             ^
      28 |         doSaveInquiryInputs(
      29 |           inquiryKey,
      30 |           {

... (full stack trace goes here) ...
 at rerender (node_modules/@testing-library/react/dist/pure.js:178:7)
      at Object.<anonymous> (src/components/ResultsComponent/ResultsComponent.test.tsx:249:5)

(line 249 is the line where I call the rerender method)

...and then below that two logs again:

console.log
*** COUNTER *** 0
at log (src/components/ResultsComponent/ResultsComponent.tsx:63:11)
console.log
*** COUNTER *** 1
at log (src/components/ResultsComponent/ResultsComponent.tsx:63:11)
console.log

Solution

  • Mystery solved - there was something wrong with our wrapper, so it was rerendering components too many times for some reason.