How to fix the "Warning: useLayoutEffect does nothing on the server"?
Asked Answered
E

4

50

Heres the full error:

Warning: useLayoutEffect does nothing on the server, because its effect cannot be encoded into the server renderer's output format. This will lead to a mismatch between the initial, non-hydrated UI and the intended UI. To avoid this, useLayoutEffect should only be used in components that render exclusively on the client

      in ForwardRef(ButtonBase)
      in WithStyles(ForwardRef(ButtonBase))
      in ForwardRef(Button)
      in WithStyles(ForwardRef(Button))
      in form
      in div

I get it every time I run my test. Heres my test

/* eslint-disable quotes */
import React from "react"
import { shallow, configure } from "enzyme"
import LoginForm from "../src/components/LoginForm"
import Button from "@material-ui/core/Button"
import Adapter from "enzyme-adapter-react-16"
import { render, fireEvent, cleanup } from "@testing-library/react"

configure({ adapter: new Adapter() })

describe("<LoginForm />", () => {
  let wrapper
  let usernameInput
  let passwordInput
  let signInButton

  // Create initial props that get passed into the component
  const initialProps = {
    location: {
      state: {
        from: {
          pathname: "/",
        },
      },
    },
  }

  // Unit testing
  describe("Unit tests", () => {
    // what to do before each test
    beforeEach(() => {
      // Render the login form component, pass in props. (Shallow method renders the component without its children, good for unit tests.)
      wrapper = shallow(<LoginForm {...initialProps} />)
      usernameInput = wrapper.find("#username")
      passwordInput = wrapper.find("#password")
      signInButton = wrapper.find(Button)
    })

    // what to do after each test
    afterEach(() => {
      jest.clearAllMocks()
    })

    // UI Integrity test
    it("should match the snapshot", () => {
      // snapshots are text references of the html of the rendered component.
      expect(wrapper.html()).toMatchSnapshot()
    })

    it("should have a username inputs", () => {
      expect(usernameInput.length).toEqual(1)
    })

    it("should have the expected props on the username field", () => {
      expect(usernameInput.props()).toEqual({
        id: "username",
        name: "username",
        value: "",
        type: "username",
        onChange: expect.any(Function),
        required: true,
      })
    })

    it("should have a password field", () => {
      expect(passwordInput.length).toEqual(1)
    })

    it("should have the expected props on the password field", () => {
      expect(passwordInput.props()).toEqual({
        id: "password",
        name: "password",
        value: "",
        type: "password",
        onChange: expect.any(Function),
        required: true,
      })
    })

    it("should have a submit button", () => {
      expect(signInButton.length).toEqual(1)
    })

    it("should have the expected props on the button", () => {
      expect(signInButton.props()).toEqual({
        type: "button",
        variant: "contained",
        style: expect.objectContaining({
          marginTop: "10px",
        }),
        onClick: expect.any(Function),
        children: "Sign In",
      })
    })
  })

  // Integrations Testing
  describe("Integrations tests", () => {
    beforeEach(() => {
      // Render the login form component, pass in props. (render method renders the component with its children, good for integrations tests, uses react-test-library.)
      const { getByLabelText, getByText } = render(
        <LoginForm {...initialProps} />
      )
      usernameInput = getByLabelText(/Username/i)
      passwordInput = getByLabelText(/Password/i)
      signInButton = getByText("Sign In")
    })

    afterEach(cleanup)

    it("Username text change in onChange event", () => {
      expect(usernameInput.value).toBe("")

      fireEvent.change(usernameInput, { target: { value: "James" } })

      expect(usernameInput.value).toBe("James")
    })

    it("Password text change in onChange event", () => {
      expect(passwordInput.value).toBe("")

      fireEvent.change(passwordInput, { target: { value: "mypassword" } })

      expect(passwordInput.value).toBe("mypassword")
    })

    it("Test button submit", () => {
      const mockLogin = jest.fn()

      const button = shallow(<Button onClick={mockLogin} />)

      button.simulate("click")

      expect(mockLogin.mock.calls.length).toEqual(1)
    })
  })
})

I believe it has something to do with the material-ui component. I've looked into it, theres a similar question on here that says the issue has to do with a dependency that my project does not have. So I think it has to do with the material-ui component using useEffectLayout and the testing env doesnt like that for some reason. Im running my test with yarn and jest yarn test to run the test suite.

Emalee answered 23/9, 2019 at 22:40 Comment(6)
you may workaround by specifying env for tests gist.github.com/gaearon/… like /* @jest-environment node */Duenas
Is <LoginForm ... /> a HOC? I'm having the same issue.Chemulpo
Hey @skyboyer, thanks for that tip. I had actually tried doing just that before: adding "jest": { "testEnvironment": "node" } to my package.json file. But it gave a new issue when running yarn test: ReferenceError: window is not defined 24 | // }) 25 | > 26 | jest.spyOn(window.localStorage.__proto__, "setItem") | ^ 27 | window.localStorage.__proto__.setItem = jest.fn() 28 | at Object.window (src/setupTests.js:26:12) at Array.forEach (<anonymous>) So Im looking into that now...Emalee
ok, so I couldnt get rid of the above window not defined problem when adding the node jest-environement. But what did work for me was a comment in your like @skyboyer, that said adding import React from "react" React.useLayoutEffect = React.useEffect to my setup file would work. And it did. Its hacky, but whatever.Emalee
hi @Chemulpo it is not a HOCEmalee
jkeary I did not think about such a trick! if you compose official answer I'll definitely upvote it :)Duenas
E
58

adding

import React from "react" 
React.useLayoutEffect = React.useEffect 

to my frontend/src/setupTests.js test file is a way to suppress the jest warning. Ultimately, this looks to be an issue with Material-UI components having issues with Jest.

Emalee answered 30/9, 2019 at 18:44 Comment(3)
This is beautifulTrine
... is it really? It looks like useLayoutEffect overwrites useEffect.Wakeen
This literally changes the reference for useLayoutEffect to useEffect. Don't do this unless you think every place you used useLayoutEffect was a mistake but for some inane reason you don't feel like switching the references to useEffect (basically: do the right thing, not this).Overcrop
G
30

A bit cleaner solution might be to use jest mock like so:

jest.mock('react', () => ({
  ...jest.requireActual('react'),
  useLayoutEffect: jest.requireActual('react').useEffect,
}));
Goldsmith answered 8/7, 2020 at 9:28 Comment(4)
This should be the correct answer as it actually works in all cases and is the proper "mocking" way of doing things. Ignore the accepted answer above.Hyson
In Next JS, no need to import React explicitly, hence this one is more fashion.Tom
This one gives error - ./node_modules/@jest/core/build/collectHandles.js:10:0 Module not found: Can't resolve 'async_hooks' Does anybody know how to fix it? I tried adding async_hooks as a dependency, to no avail.Fixation
What file do we put this in? I'm guessing it has to be a global file that applies to all tests? Update: It can go in your setupTests.js file if you have one. See: create-react-app.dev/docs/running-tests/…Agnomen
C
11

you should check if the useLayoutEffect can be used this way:

import React, {useEffect, useLayoutEffect} from 'react';

const canUseDOM = typeof window !== 'undefined';
const useIsomorphicLayoutEffect = canUseDOM ? useLayoutEffect : useEffect;

then instead of the useLayoutEffect use a useIsomorphicLayoutEffect

Correggio answered 11/3, 2021 at 10:18 Comment(1)
This works outside of tests, so can be used for server-side rendering.Desrosiers
V
6

You can fix this by adding the following if statement to the beginning of your startup file (e.g. index.js):

if (typeof document === 'undefined') {
  React.useLayoutEffect = React.useEffect;
}

This will ensure that the SSR uses React.useEffect instead of React.useLayoutEffect because document is undefined on the server-side.

Vergos answered 2/12, 2021 at 1:34 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.