a2ron44
a2ron44

Reputation: 1771

React Hook Function in UseEffect with infinite loop

I am trying to call a hook in my App.js file using a hook. All the logic works, but I'm getting a warning error in console "React Hook useEffect has a missing dependency: 'initAuth'." I know there are a lot of issues on this ,but I'm not sure if this is related to the hook or the complexity I am doing at the high level of my app. The intent is to use the "initAuth" function to look at my local storage and get my user token, name, etc... I only want this on a hard page refresh, so it should only run once.

If I add initAuth (the function) or the authObject ( object), I get infinite loops.

function App() {

  const { initAuth, authObject } = useAuth();

  useEffect(() => {
    initAuth();
  }, []);
// this throws the warning.  I need to add dependency
}

Upvotes: 0

Views: 306

Answers (3)

a2ron44
a2ron44

Reputation: 1771

Thanks to Yanick's comment, this is how I initiated to provider to set my authorization. My login function uses an auth service for http call, but I use this context function to set the data properly.

import React, { useContext, useMemo, useState } from "react";
import http from "services/http";

const AuthContext = React.createContext({});

const AuthContextProvider = ({ children }) => {
  const [state, setState] = useState(() => {
    const authObject = JSON.parse(localStorage.getItem("authObject"));
    if (authObject) {
      //sets axios default auth header
      http.setJwt(authObject.token);
    }
    const isLoggedIn = !!authObject;
    return { isLoggedIn, authObject };
  });

  // avoid refresh if state does not change
  const contextValue = useMemo(
    () => ({
      ...state, // isLoggedIn, authObject
      login(auth) {
        localStorage.setItem("authObject", JSON.stringify(auth));
        http.setJwt(auth.token);
        setState({ authObject: auth, isLoggedIn: true });
        return true;
      },
      logout() {
        http.setJwt("");
        localStorage.removeItem("authObject");
        setState({ authObject: null, isLoggedIn: false });
      },
    }),
    [state]
  );

  return (
    <AuthContext.Provider value={contextValue}>{children}</AuthContext.Provider>
  );
};

const useAuthContext = () => useContext(AuthContext);

export { useAuthContext, AuthContextProvider };

And my App.js simply uses the ContextProvider, no need to run useEffect anymore on App.js.

      <AuthContextProvider>
        <ThemeProvider theme={darkState ? dark() : light()}>
          <CssBaseline>
            <BrowserRouter>
              
            //...app.js stuff    
               
            </BrowserRouter>
          </CssBaseline>
        </ThemeProvider>
      </AuthContextProvider>

In any component, I can now get access to isLoggedIn or authObject using a call like:

  const { isLoggedIn } = useAuthContext();

Upvotes: 0

Yanick Rochon
Yanick Rochon

Reputation: 53531

This is how I would implement this hook :

function App() {

  const { initialized, authObject, initAuth } = useAuth();

  useEffect(() => {
    if (!initialized) {
       initAuth();
    }
  }, [initialized, initAuth]);
  
  ...
}

Or, better yet :

function App() {

  const authObject = useAuth();   // let useAuth initialize itself

 
  ...
}

Typically, useAuth seems to be a multi-purpose hook, being used by various components, so it makes no sense to allow multiple components to call initAuth; the hook should only return the current state.

Preferably, you should implement that hook with a context

function App() {

  return (
    <AuthProvider>
      <AppContent />
    </AuthProvider>
  );
}

function AppContent() {
  const authObject = useAuth();

  ...

}

The contract, therefore, goes to the AuthProvider, and notifies every component using useAuth on state changes.


From OP's own answer, added some suggested improvements :

import React, { createContext, useContext, useState, useMemo } from "react";

const AuthContext = createContext({
  isLoggedIn:false /* :Boolean */,
  authObject:null  /* :Object */, 
  login: (
     username /* :String */,
     password /* :String */
  ) /* :Preomise<Boolean> */ => { 
     throw new Error('Provider missing');
  }
]);

const AuthContextProvider = ({ children }) => {
  // init state with function so we do not trigger a
  // refresh from useEffect. Use useEffect if the
  // initial state is asynchronous
  const [state, setState] = useState(() => {
    const authObject = localStorage.getItem("authObject");
    const isLoggedIn = !!authObject;
    
    return { isLoggedIn, authObject };
  });

  // avoid refresh if state does not change
  const contextValue = useMemo(() => ({
     ...state,   // isLoggedIn, authObject
     login: async (username, password) => {

        // implement auth protocol, here
        // do not expose setState directly in order to 
        // control what state is actually returned

        // setState({ isLoggedIn:..., authObject:... });

        // return true|false
     }
  }), [state]);

  return (
    <AuthContext.Provider value={ contextValue }>
      { children }
    </AuthContext.Provider>
  );
};

/**
 Usage: const { isLoggedIn, authObject, login } = useAuthContext();
 */
const useAuthContext = () => useContext(AuthContext);

export { useAuthContext, AuthContextProvider };

Upvotes: 0

Brian S
Brian S

Reputation: 5785

If you only want this effect to run once when the component first loads, then you can ignore the warning. You can disable the warning so it doesn't keep showing up in the console with the following:

useEffect(() => {
  initAuth();

// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

Upvotes: 2

Related Questions