Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bug: False-positive Warning: An update to Link inside a test was not wrapped in act(...) after upgrading React to 17.0.1? #20568

Closed
Vadorequest opened this issue Jan 10, 2021 · 8 comments
Labels
Resolution: Expected Behavior Status: Unconfirmed A potential issue that we haven't yet confirmed as a bug

Comments

@Vadorequest
Copy link

Vadorequest commented Jan 10, 2021

React version: 17.0.1
React-test-renderer: 17.0.1

Steps To Reproduce

  1. I upgraded React and react-test-renderer to 17.0.1 (I upgraded other packages as well, but believe it's related to those)
  2. Ran tests yarn test

Link to code example:

PR: UnlyEd/next-right-now#248

Reproducing:

  1. git clone https://github.com/UnlyEd/next-right-now.git next-right-now
  2. cd next-right-now && git checkout react-link-jest-act-repro
  3. cp .env.local.example .env.local - Uses the default ENV variables when running locally
  4. yarn
  5. yarn test (then type "a" to run them all)

The current behavior

Using the following code, all tests pass locally (but fail on Vercel) (and were passing prior upgrading react packages), but I get tons of warning no, such as Warning: An update to Link inside a test was not wrapped in act(...). and those warnings are new, and I couldn't get rid of them:

I18nLink.test.tsx:

import React from 'react';
import TestRenderer from 'react-test-renderer';
import { NavLink } from 'reactstrap';

import i18nContext from '../../stores/i18nContext';
import I18nLink from './I18nLink';

/**
 * @group unit
 * @group components
 */
describe('I18nLink', () => {
  const I18nLinkTest = (props) => {
    const { locale = 'en', href, text = 'Text', ...rest } = props;

    return (
      <i18nContext.Provider value={{ lang: null, locale: locale }}>
        <I18nLink
          href={href}
          {...rest}
        >
          {text}
        </I18nLink>
      </i18nContext.Provider>
    );
  };

  describe('should properly render', () => {
    test('when going to homepage, it should redirect to i18n homepage', () => {
      const renderer = TestRenderer.create(<I18nLinkTest href={'/'} />);
      const link: any = renderer.toJSON();

      expect(link.type).toEqual('a');
      expect(link.children).toEqual(['Text']);
      expect(link.props.href).toEqual('/en');
      expect(link).toMatchSnapshot();
    });
  });
});

I18nLink component:

import isArray from 'lodash.isarray';
import isEmpty from 'lodash.isempty';
import map from 'lodash.map';
import NextLink from 'next/link';
import React from 'react';

import useI18n, { I18n } from '../../hooks/useI18n';
import {
  I18nRoute,
  resolveI18nRoute,
} from '../../utils/app/router';

type ParamValueToForward = string | number | Array<string | number>;

type Props = {
  as?: string;
  children: React.ReactNode;
  className?: string;
  href: string;
  locale?: string; // The locale can be specified, but it'll fallback to the current locale if unspecified
  params?: { [key: string]: ParamValueToForward };
  passHref?: boolean;
  prefetch?: boolean;
  query?: { [key: string]: ParamValueToForward };
  replace?: boolean;
  scroll?: boolean;
  shallow?: boolean;
  wrapChildrenAsLink?: boolean; // Helper to avoid writing redundant code
};

/**
 * Wrapper around the native Next.js <Link> component. Handles localised links.
 *
 * Use the current active locale by default
 *
 * @example Recommended usage
 *  <I18nLink href={`/`}>Homepage</I18nLink>
 *  <I18nLink href={`/`} className="customClassName">Homepage</I18nLink>
 *
 * @example When specifying the locale to use (overrides default)
 *  <I18nLink href={`/`} locale={'fr-FR'}>Homepage in "fr-FR" locale</I18nLink>
 *
 * @example The recommended usage is equivalent to this (wrapChildrenAsLink is true, by default)
 *  <I18nLink href={`/`} wrapChildrenAsLink={false}><a>Homepage</a></I18nLink>
 *  <I18nLink href={`/`} wrapChildrenAsLink={false}><a className="customClassName">Homepage</a></I18nLink>
 *
 * @example When using children that use a <a> tag, you must set wrapChildrenAsLink to false to avoid using a link within a link
 *   <I18nLink
 *     href={`/`}
 *     wrapChildrenAsLink={false}
 *   >
 *      <NavLink>Homepage</NavLink>
 *   </I18nLink>
 *
 * @example When using route params (other than "locale"). Ex: /products/5
 *   <I18nLink
 *     href={'/products/[id]'}
 *     params={{
 *       id: 5,
 *     }}
 *   >
 *      Go to product 5
 *   </I18nLink>
 *
 * @example When using route query params. Ex: /products/5?userId=1
 *   <I18nLink
 *     href={'/products/[id]'}
 *     params={{
 *       id: 5,
 *     }}
 *     query={{
 *       userId: 1
 *     }}
 *   >
 *      Go to product 5 with userId 1
 *   </I18nLink>
 *
 * @param props
 */
const I18nLink: React.FunctionComponent<Props> = (props): JSX.Element => {
  const { locale: currentLocale }: I18n = useI18n();
  const {
    as,
    children,
    className,
    href,
    locale = currentLocale,
    params,
    passHref = true,
    query,
    wrapChildrenAsLink = true,
    ...rest // Should only contain valid next/Link props
  } = props;
  let {
    i18nHref, // eslint-disable-line prefer-const
    i18nAs,
  }: I18nRoute = resolveI18nRoute({ as, href, locale });

  if (!isEmpty(params)) {
    // If any params are provided, replace their name by the provided value
    map(params, (value: ParamValueToForward, key: string | number) => {
      if (isArray(value)) {
        value = value.join(',');
      }
      i18nAs = i18nAs.replace(`[${key}]`, value as string);
    });
  }

  if (!isEmpty(query)) {
    // If any query params are provided, append it to `as`, so it gets forwarded;
    const queryParamsString = Object.keys(query)
      .map((k) => {
        if (isArray(k)) {
          k = k.join(',');
        }
        return `${k}=${query[k]}`;
      })
      .join('&');
    i18nHref += `?${queryParamsString}`;
    i18nAs += `?${queryParamsString}`;
  }

  return (
    <NextLink
      href={i18nHref}
      as={i18nAs}
      {...rest}
      passHref={passHref}
    >
      {wrapChildrenAsLink ? (
        // eslint-disable-next-line jsx-a11y/anchor-is-valid
        <a className={className}>{children}</a>
      ) : (
        React.cloneElement(children as React.ReactElement)
      )}
    </NextLink>
  );
};

export default I18nLink;

The expected behavior

The current code shouldn't yield any warning. It's mistakenly reporting warnings thinking the tests update the react state, but we don't. We simply render a React component, without interacting with it, therefore the react state shouldn't change. I believe this is a false-positive warning.

@eps1lon
Copy link
Collaborator

eps1lon commented Jan 11, 2021

Thanks for the report.

It's mistakenly reporting warnings thinking the tests update the react state, but we don't.

It's not clear from the code you posted that the code isn't doing any state updates. Could you reduce this as much as possible so that we end up with a single test and every bit of component code visible in the repro? Otherwise it'll be very hard to identify how that false positive was produced.

@Vadorequest
Copy link
Author

Could you clarify what's a react state update in this context?

Every single test throw this warning, so any of them will be a good place to experiment. I can make a repro with the smallest possible code for easier understanding too, I'll do that now.

Vadorequest added a commit to UnlyEd/next-right-now that referenced this issue Jan 11, 2021
@Vadorequest
Copy link
Author

I've updated the description and added a proper reproduction branch/PR on my own repository.

@Elias-Graf
Copy link

@Vadorequest were you able to solve this? I'm facing this issue right now and my poor console buffer is not happy about it.

package.json

"dependencies": {
  "@material-ui/core": "^4.11.4",
  "@material-ui/lab": "^4.0.0-alpha.58",
  "@types/jsdom": "^16.2.12",
  "jsdom": "^16.6.0",
  "mysql": "^2.18.1",
  "next": "^11.0.1",
  "react": "17.0.2",
  "react-dom": "17.0.2",
  "reflect-metadata": "^0.1.13",
  "swr": "^0.5.6",
  "typeorm": "^0.2.34"
},
"devDependencies": {
  "@babel/core": "^7.14.6",
  "@babel/plugin-proposal-class-properties": "^7.14.5",
  "@babel/plugin-proposal-decorators": "^7.14.5",
  "@testing-library/react": "^11.2.7",
  "@types/chai": "^4.2.18",
  "@types/jest": "^26.0.23",
  "@types/mocha": "^8.2.2",
  "@types/node": "^15.12.2",
  "@types/react": "17.0.11",
  "@typescript-eslint/eslint-plugin": "^4.26.1",
  "@typescript-eslint/parser": "^4.26.1",
  "babel-plugin-transform-typescript-metadata": "^0.3.2",
  "chai": "^4.3.4",
  "eslint": "^7.28.0",
  "eslint-config-prettier": "^8.3.0",
  "eslint-plugin-prettier": "^3.4.0",
  "eslint-plugin-react": "^7.24.0",
  "jest": "^27.0.4",
  "mocha": "^9.0.0",
  "node-mocks-http": "^1.10.1",
  "prettier": "^2.3.1",
  "sqlite3": "^5.0.2",
  "ts-jest": "^27.0.3",
  "ts-node": "^10.0.0",
  "tsconfig-paths": "^3.9.0",
  "typescript": "^4.3.2"
}

@stale
Copy link

stale bot commented Jan 9, 2022

This issue has been automatically marked as stale. If this issue is still affecting you, please leave any comment (for example, "bump"), and we'll keep it open. We are sorry that we haven't been able to prioritize it yet. If you have any new additional information, please include it with your comment!

@stale stale bot added the Resolution: Stale Automatically closed due to inactivity label Jan 9, 2022
@jefer94
Copy link

jefer94 commented Mar 9, 2022

I have the same issue in a login form with react testing library, the test pass perfectly but I still getting this error, I will try with

await act(async () => {
    fireEvent.input(username, {
      target: {
        value: 'asdasd1',
      },
    })
  })
fireEvent.input(username, {
  target: {
    value: 'asdasd1',
  },
})

I need at least can hide this error

@stale stale bot removed the Resolution: Stale Automatically closed due to inactivity label Mar 9, 2022
@bvaughn bvaughn closed this as completed Mar 10, 2022
@bvaughn bvaughn reopened this Mar 10, 2022
@eps1lon
Copy link
Collaborator

eps1lon commented Mar 12, 2022

The update is caused by next/link. They check if the link is visible once rendered and schedule an update if the link is visible. The act warning comes from that update.

The fix is to unmount the component once your test is finished if you don't care about the state of the component when next considers it visible.
The problem is that this triggers another warning that is only fixed in next@^10.0.7 (vercel/next.js#22072) but you're using next@10.0.3.

In summary:

  1. Upgrade next to 10.0.7
  2. Apply
    diff --git a/src/components/i18n/I18nLink.test.tsx b/src/components/i18n/I18nLink.test.tsx
    index e613eafd..ff8088a5 100644
    --- a/src/components/i18n/I18nLink.test.tsx
    +++ b/src/components/i18n/I18nLink.test.tsx
    @@ -34,6 +34,8 @@ describe('I18nLink', () => {
          expect(link.children).toEqual(['Text']);
          expect(link.props.href).toEqual('/en');
          expect(link).toMatchSnapshot();
    +
    +      renderer.unmount()
        });
    
      //   test('when using custom CSS class', () => {

@jefer94
Copy link

jefer94 commented Mar 14, 2022

The problem was solved for me but I don't know how

@eps1lon eps1lon closed this as completed Mar 15, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Resolution: Expected Behavior Status: Unconfirmed A potential issue that we haven't yet confirmed as a bug
Projects
None yet
Development

No branches or pull requests

5 participants