reactjsreact-hooksuse-effectjs-cookie

React app freezing due to useEffect hook in 16.9 while working fine in 16.8


The code runs fine on React 16.8, yet freezes on >=16.9. I looked at the changes for 16.9 but all it mentioned was an additional warning when there's an infinite loop, not an actual change in behaviour, so this shouldn't be happening, yet it is.

Setting useEffect's dependency to [] does break the loop but means the code needs to be modified and I'm not sure how.

The below code can also be found in a sandbox here. The version is set to 16.8.4 so it doesn't freeze.

index.jsx

//...
import { SessionContext, getSessionCookie, setSessionCookie } from "./session";
import "./styles.css";

const history = createBrowserHistory();

const LoginHandler = ({ history }) => {
  const [email, setEmail] = useState("");
  const [loading, setLoading] = useState(false);
  const handleSubmit = async e => {
    e.preventDefault();
    setLoading(true);
    // NOTE request to api login here instead of this fake promise
    await new Promise(r => setTimeout(r(), 1000));
    setSessionCookie({ email });
    history.push("/");
    setLoading(false);
  };

  if (loading) {
    return <h4>Logging in...</h4>;
  }

  return (
    //...
  );
};

const LogoutHandler = ({ history }: any) => {
  //...
};

const ProtectedHandler = ({ history }) => {
   //...
};

const Routes = () => {
  const [session, setSession] = useState(getSessionCookie());
  useEffect(
    () => {
      setSession(getSessionCookie());
    },
    [session]
  );

  return (
    <SessionContext.Provider value={session}>
      <Router history={history}>
        <div className="navbar">
          <h6 style={{ display: "inline" }}>Nav Bar</h6>
          <h6 style={{ display: "inline", marginLeft: "5rem" }}>
            {session.email || "No user is logged in"}
          </h6>
        </div>
        <Switch>
          <Route path="/login" component={LoginHandler} />
          <Route path="/logout" component={LogoutHandler} />
          <Route path="*" component={ProtectedHandler} />
        </Switch>
      </Router>
    </SessionContext.Provider>
  );
};

const App = () => (
  <div className="App">
    <Routes />
  </div>
);

const rootElement = document.getElementById("root");
render(<App />, rootElement);

session.ts

import React from "react";
import * as Cookies from "js-cookie";

export const setSessionCookie = (session: any): void => {
  Cookies.remove("session");
  Cookies.set("session", session, { expires: 14 });
};

export const getSessionCookie: any = () => {
  const sessionCookie = Cookies.get("session");

  if (sessionCookie === undefined) {
    return {};
  } else {
    return JSON.parse(sessionCookie);
  }
};

export const SessionContext = React.createContext(getSessionCookie());

Expected result: app functions in 16.9 just like in 16.8

Actual result: app freezes in 16.9 due to supposed infinite loop in useEffect

Error message: Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.


Solution

  • JSON.parse(sessionCookie) always returns a new object.

    setSession(getSessionCookie()) sets the session to that new object

    [session] says to run whenever a new session object is set in the state (which happens every render).

    So that's the infinite loop you're seeing. To stop it you can do something like:

    useEffect(
        () => {
          const newSessionCookie = getSessionCookie()
          if(newSessionCookie.uniqueId !== session.uniqueId) {
            setSession(getSessionCookie());
          }
        },
        [session]
      )
    

    You just need to be sure you have some unique identifier for the cookie.