Search code examples
reactjstypescriptjestjsreact-router-domreact-testing-library

Protected route component and returning Navigate in tests causes infinite loop


Hey all I have a protected route component test that is causing an infinite loop, but only seems to happen in the test, not when running the app:

it("redirects to auth when user is not logged in", () => {
    const currentUser = null;
    render(
      <UserContext.Provider value={{ currentUser, setCurrentUser }}>
        <MemoryRouter>
          <ProtectedRoute user={currentUser}>
            <Profile />
          </ProtectedRoute>
        </MemoryRouter>
      </UserContext.Provider>
    );
    expect(screen.getByText(/sign in/i)).toBeInTheDocument();
  });

The loop occurs when the user in protected route is null. This is protected route:

const ProtectedRoute = ({ user, children }: {
  user: DocumentData | null | undefined;
  children: React.ReactElement;
}) => {
  if (!user) return <Navigate to="/auth" replace />;
  return children;
};

I think something in Auth causes the issue, when I replace the returned Navigate with just some text, no infinite loop. Here is Auth, could it be the useEffect causing the issue?

const Auth = () => {
  const { currentUser } = useContext(UserContext);
  const navigate = useNavigate();

  useEffect(() => {
    if (currentUser) navigate("/classes");
  }, [currentUser, navigate]);

  return (
    <AuthWrapper>
      <Routes>
        <Route path="/" element={<Navigate to="/auth/sign-up" />} />
      </Routes>
    </AuthWrapper>
  );
};

Solution

  • From what I can tell the render looping is caused in the unit test because no routes are being rendered, and so the ProtectedRoute component is unconditionally rendered and since currentUser is always null, i.e. falsey, the redirect to "/auth" is rendered and the loop repeats, ad nauseam.

    Similar to how the actual code is rendering routes to match and navigate to, so too should the unit tests.

    Here's an example, your actual routes are likely different. It renders initially on route "/profile" where the auth check should redirect to the other test route rendering the component that you are asserting has some text in the document.

    it("redirects to auth when user is not logged in", () => {
      const currentUser = null;
      const setCurrentUser = jest.fn(); // in case it's needed to be defined 🤷🏻‍♂️
    
      render(
        <UserContext.Provider value={{ currentUser, setCurrentUser }}>
          <MemoryRouter initialEntries={["/profile"]}>
            <Route
              path="/profile"
              element={(
                <ProtectedRoute user={currentUser}>
                  <Profile />
                </ProtectedRoute>
              )}
            />
            <Route path="/auth" element={<SignIn />} />
          </MemoryRouter>
        </UserContext.Provider>
      );
      expect(screen.getByText(/sign in/i)).toBeInTheDocument();
    });