Toaster causing errors in React when spying on ‘useState’

I have a notifications component that has some defaults for a toaster. When running tests against it using a jest mock implementation on useState, I’m receiving some strange errors.

Notifications.tsx

import { Toaster } from "react-hot-toast";

export const Notifications = () => {
    return (
      <Toaster
        position="top-center"
        containerStyle={{ top: 30 }}
        gutter={10}
        toastOptions={{
          style: {
            minWidth: "1000px",
          },
          success: {
            style: {
              backgroundColor: "#e0fce0",
            },
          },
          error: {
            style: {
              backgroundColor: "#fce0e0",
            },
          },
          duration: Infinity,
        }}
      />
    );
  };

Page

Using the component (complexity reduced).

import { useContext, useEffect, useState } from "react";
import { Notifications } from "../components/Notifications";

export default function Page() {
  const [selected, useSelected] = useState("default");

  return (
    <>
      <Notifications />
      <div>
          Same content
      </div>
    </>
  );
}

Page.test.tsx

import { render, waitFor } from "@testing-library/react";
import Page from "../Page";
import React from "react";

jest.mock("../../components/Notifications/index", () => () => (
  <div>Toaster</div>
));

describe("Page", () => {
  let stateSpy: any;

  beforeEach(() => {
    stateSpy = jest.spyOn(React, "useState");
  });

  afterEach(() => {
    stateSpy.mockReset();
  });

  it("should load the page with defaults", async () => {
    render(
        <Page />
    );

    await waitFor(() => {
      // Run some tests
    });
  });

  it("should load the page with a second value", async () => {
    //@ts-ignore
    stateSpy.mockImplementationOnce(() => ["second value", jest.fn()]);
    
    render(
        <Page />
    );

    await waitFor(() => {
      // Run some tests
    });
  });

  it("should load the page with a third value", async () => {
    //@ts-ignore
    stateSpy.mockImplementationOnce(() => ["third value", jest.fn()]);
    
    render(
        <Page />
    );

    await waitFor(() => {
      // Run some tests
    });
  });

  it("should load the page with a fourth value", async () => {
    //@ts-ignore
    stateSpy.mockImplementationOnce(() => ["fourth value", jest.fn()]);

    render(
        <Page />
    );

    await waitFor(() => {
      // Run some tests
    });
  });
});

On the first test (using the default value for state), I receive the following error on the render:

Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: undefined. You likely forgot to export your component from the file it's defined in, or you might have mixed up default and named imports.

On the latter three tests (setting a value for state), I receive the following error on the line const [selected, useSelected] = useState("default"); (in the Page.tsx):

TypeError: undefined is not iterable (cannot read property Symbol(Symbol.iterator))

If I change mockImplementationOnce to mockImplementation on the latter three tests, then all four tests generate the first error (Element type is invalid...).

If I remove the reference to the <Notifications /> component (or just comment out the <Toaster /> component in it), then the tests run fine. You’ll notice that I’m trying to mock the notifications component in the test. But, even mocking it doesn’t seem to make a difference. I still get the errors regardless.

Just FYI (not sure if it makes a difference), but <Toaster /> is a functional component (React.FC), not a JSX element.

  • I don’t think I’ve ever seen a react test that spies on useState. That seems like a bad idea. If you want to change the state in a test, then you should interact with the component in the way that changes that state.

    – 

  • @AlexWayne there is a child component that updates state (lifting state). Then other child components of the page take advantage of the changed state. Additionally, what’s rendered is changed based on state. Given that this is a unit test, I need to eliminate dependencies on child components.

    – 

  • Well the “complexity reduced” example has been reduced to not actually use any of the values returned by useState so it’s hard to know how to recommend an alternative. If the state is completely internal, then you don’t need to mock that. If its manipulatable from the outside, then that interface is what what you should be testing.

    – 




  • Got it working. Added an answer to the fix. Thanks for your help, nonetheless.

    – 

Fixed

I thought I was mocking the Notification component, however, the paths in the Page.tsx and Page.test.tsx for the component were different (e.g., "./components/Notifications" vs. "../../components/Notifications/index"). When I removed the trailing /index in the test. Everything worked as expected.

Leave a Comment