DRobinson
DRobinson

Reputation: 295

How do I stop useEffect from making my component re-render infinitely?

Edited again to explain that === compares the memory location of objects (Why are two identical objects not equal to each other?) which is why the useEffect() kept running again and again and again... I think I have it sorted now but will leave the post here without an answer for a while in case I'm talking absolute gibberish and someone would like to correct me :)

--- End of second edit ----

I have edited my Routes const to log the different sessions to the console. As you can see, they are equal but for some reason, the computer doesn't seem to think so. Does anyone have any idea why???

edited Routes (to show the values)
const Routes = () => {
  const [session, setSession] = useState(getSessionCookie());
  console.log('Route session before: ', session);


  useEffect(
    () => {
      //setSession(getSessionCookie());
      const stateCookie = session;
      const getCookie = getSessionCookie();
      console.log('stateCookie: ', stateCookie);
      console.log('getCookie: ', getCookie);
      console.log('Are equal? ', stateCookie === getCookie);
    },
    [session]
  );
  console.log('Route session after: ', session);

Console log

----- End of edit -------

I found a tutorial online to assist me in managing user sessions but I keep having a problem with the re-rendering of the component unless I take out the useEffect dependency. I've logged the values and types of the session variable but it doesn't change so I don't understand why the re-render keeps happening. Any help would be greatly appreciated.

index.js

import React, { useEffect, useState, useContext } from 'react';
import { render } from "react-dom";
import { Router, Switch, Route } from "react-router";
import { Link } from "react-router-dom";
import { createBrowserHistory } from "history";
import Cookies from "js-cookie";
import { SessionContext, getSessionCookie, setSessionCookie } from "./session";

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 (
    <div style={{ marginTop: "1rem" }}>
      <form onSubmit={handleSubmit}>
        <input
          type="email"
          placeholder="Enter email address"
          value={email}
          onChange={e => setEmail(e.target.value)}
        />
        <input type="submit" value="Login" />
      </form>
    </div>
  );
};

const ProtectedHandler = ({ history }) => {
  const session = useContext(SessionContext);
  if (session.email === undefined) {
    history.push("/login");
  }
  return (
    <div>
      <h6>Protected data for {session.email}</h6>
      <Link to="/logout">Logout here</Link>
    </div>
  );
};

const LogoutHandler = ({ history }) => {
  useEffect(
    () => {
      Cookies.remove("session");
      history.push("/login");
    },
    [history]
  );

  return <div>Logging out!</div>;
};

const Routes = () => {
  const [session, setSession] = useState(getSessionCookie());
  console.log('Routes session before: ', session);
  console.log('Routes session before typeof: ', typeof session);
  useEffect(
    () => {
      setSession(getSessionCookie());
    },
    [session] // <-------------- this is the dependency that seems to be causing the trouble
  );
  console.log('Routes session: ', session);
  console.log('Routes session typeof: ', typeof 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.js

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

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

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

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

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

Upvotes: 1

Views: 1906

Answers (2)

Duc Hong
Duc Hong

Reputation: 1179

this is a very common issue when working with useEffect, you should not have an object inside the dependency array, because Object just refers its reference, not actual value. And when session is created, it will have a new reference even when your prefer property value is the same => This is why it creates infinite loop.

If you put session as a dependency, you should have explicitly compare the property value, like session.value.

I'm not using context API much, but I guess there might be something wrong in your <Routes /> component, there might not need to update the session inside <Routes /> since it just plays as a provider role. Usually, this is the place you assign an initial context value.

Once user's logged in successfully, you can update session inside <LoginHandler />. The other child components which consume the context value just need to use useContext to get the latest session value.

So, basically your app might look like this:

// sessionContext.js
const SessionContext = React.createContext({
  session: {},
  setSession: () => {},
});

// components/routes.js
const Routes = () => {
  const [session, setSession] = useState({})
  const contextSession = {
    session,
    setSession
  }

  return (
    <SessionContext.Provider value={contextSession}>
      {children}
    </SessionContext.Provider>
  )
}

// components/childComponent.js
const ChildComponent = () => {
  const { session } = useContext(SessionContext)

  if (!session)
    return null;
  
  return <div>Logged-in</div>
}

For complicated state management, I suggest having a look at redux, you won't need to use context like the above example.

Upvotes: 1

wentjun
wentjun

Reputation: 42516

Given that you should only be logging the sessions when the session is updated/changed, you should do a comparison before updating the state, otherwise, it will cause an infinite loop as you are constantly updating the state.

useEffect(() => {
  // assuming that session is not an array or object
  if (getSessionCookie().email === session.email) {
    return;
  }
  setSession(getSessionCookie());
}, [session]);

Likewise, on your LogoutHandler component, you shouldn't update the history object while having it as part of the dependency array. In fact, there is no need to call history.push(), as you should already be in that route when the component is rendered. You should only remove the cookies once, thus you can call that when the component is mounted.

const LogoutHandler = ({ history }) => {
  useEffect(() => {
    Cookies.remove("session");
  }, []);

  return <div>Logging out!</div>;
};

Upvotes: 0

Related Questions