0

I need a help to solve this error:

"useDispatch is called in function that is neither a React function component nor a custom React Hook function".

Explanation:

store.js and userSlice.js hold the definition of my Redux related things (rtk).

Auth.js is meant to hold functions to authenticate/logout and keep redux "user" storage updated. By now I have just the google auth, that is authenticated when I call redirectToGoogleSSO.

The authentication part is working flawless and i'm retrieving the user info correctly, but I'm having a hard time making it update the user store. The dispatch(fetchAuthUser()) is where I get the error.

Sidebar.js is a navigation sidebar that will hold a menu to sign in/sign out and to access the profile.js (not implemented yet). If I bring all the code from Auth to inside my Sidebar component, the authentication work and the redux store is filled, but I would like to keep things in the Auth.js so I can use that in other components and not just in the Sidebar.


//store.js:

import { configureStore } from '@reduxjs/toolkit';
import userReducer from './userSlice';

export default configureStore({
    reducer: {
        user: userReducer
    }
});

//userSlice.js

import { createSlice } from '@reduxjs/toolkit';
import axios from "axios";

export const userSlice = createSlice({
  name: 'user',
  initialState: { 
    email: 'teste@123',
    name: 'teste name',
    picture: 'teste pic',
    isAuthenticated: false
  },
  reducers: {
    setUser (state, actions) {
      return {...state,     
          email: actions.payload.email,      
          name: actions.payload.name,
          picture: actions.payload.picture,
          isAuthenticated: true
         }
    },
    removeUser (state) {
      return {...state, email: '', name: '', picture: '', isAuthenticated: false}
    }
  }
});

export function fetchAuthUser() {  

  return async dispatch => {

    const response = await axios.get("/api/auth/user", {withCredentials: true}).catch((err) => {
      console.log("Not properly authenticated");
      dispatch(removeUser());
    });

    if (response && response.data) {
      console.log("User: ", response.data);
      dispatch(setUser(response.data));
    }
  }
};

export const { setUser, removeUser } = userSlice.actions;

export const selectUser = state => state.user;

export default userSlice.reducer;

//Auth.js

import React, {  useEffect } from 'react';
import { useDispatch } from 'react-redux';
import { fetchAuthUser } from '../../redux/userSlice';

export const AuthSuccess = () => {
    useEffect(() => {
        setTimeout(() => {
            window.close();
        },1000);
    });

    return <div>Thanks for loggin in!</div>
}

export const AuthFailure = () => {
    useEffect(() => {
        setTimeout(() => {
            window.close();
        },1000);
    });

    return <div>Failed to log in. Try again later.</div>
}

export const redirectToGoogleSSO = async() => { 
    const dispatch = useDispatch(); 
    let timer = null;
    const googleAuthURL = "http://localhost:5000/api/auth/google";
    const newWindow = window.open(
        googleAuthURL,
        "_blank",
        "toolbar=yes,scrollbars=yes,resizable=yes,top=200,left=500,width=400,height=600"
    );

    if (newWindow) {
        timer = setInterval(() => {
            if(newWindow.closed) {
                console.log("You're authenticated"); 
                dispatch(fetchAuthUser()); //<----- ERROR HERE ---->
                if (timer) clearInterval(timer);
            }
        }, 500);
    }
}

//Sidebar.js

import React from 'react';
import { Link } from 'react-router-dom';
import { redirectToGoogleSSO } from '../auth/Auth';
import { useSelector } from 'react-redux';

export const Sidebar = () => { 

    const handleSignIn = async() => { 
        redirectToGoogleSSO();
    };

    const {name,picture, isAuthenticated} = useSelector(state => state.user);  

    return (  
        <div id="sidenav" className="sidenav">
            <div className="nav-menu">
                <ul> 
                    {
                        isAuthenticated  
                        ? <li>
                            <img className="avatar" alt="" src={picture} height="40" width="40"></img>                        
                            <Link to="/" className="user">{name}</Link> 
                            <ul>
                                <li><Link to="/"><i className="pw-icon-export"/> logout</Link></li>
                            </ul>
                        </li>

                        : <li>
                            <Link to="/" className="login" onClick={handleSignIn}>                         
                                <i className="pw-icon-gplus"/>&nbsp;&nbsp;
                                Sign In / Sign Up 
                            </Link> 
                        </li> 
                    } 
                </ul>
            </div>
        </div> 
      )
}
Bruno Colli
  • 3
  • 1
  • 3
  • Just one question, did you test the fetchAuthUser function? it's working fine? because i think there's some mistakes here too – Yago Biermann Apr 12 '22 at 02:40

2 Answers2

2

You only can use the useDispatch hook from a react component or from a custom hook, in your case, you should use store.dispatch(), try to do the following:

import { configureStore } from '@reduxjs/toolkit';
import userReducer from './userSlice';

// following the docs, they assign configureStore to a const
const store = configureStore({
    reducer: {
        user: userReducer
    }
});
export default store;

Edit: i also noticed that you are trying to dispatch a function that is not an action, redux doesn't work like that, you should only dispatch the actions that you have defined in your reducer, otherwise your state will be inconsistent.

So first of all, move the fetchAuthUser to another file, like apiCalls.ts or anything else, it's just to avoid circular import from the store.js.

after this, call the store.dispatch on the fetchAuthUser:

// File with the fetch function
// Don't forget to change the path
import store from 'path/to/store.js'
export function fetchAuthUser() {

    const response = await axios.get("/api/auth/user", {withCredentials: true}).catch((err) => {
      console.log("Not properly authenticated");
      store.dispatch(removeUser());
    });

    if (response && response.data) {
      console.log("User: ", response.data);
      store.dispatch(setUser(response.data));
    }

};

In the Auth.js you don't have to call the dispatch, because you have already called it within your function.

export const redirectToGoogleSSO = async() => { 
    let timer = null;
    const googleAuthURL = "http://localhost:5000/api/auth/google";
    const newWindow = window.open(
        googleAuthURL,
        "_blank",
        "toolbar=yes,scrollbars=yes,resizable=yes,top=200,left=500,width=400,height=600"
    );

    if (newWindow) {
        timer = setInterval(() => {
            if(newWindow.closed) {
                console.log("You're authenticated");

                // Just call the fetchAuthUser, you are already dispatching the state inside this function
                await fetchAuthUser();
                if (timer) clearInterval(timer);
            }
        }, 500);
    }
}

So keep in mind that ever you need to use dispatch outside a react component or a custom hook, you must use the store.dispatch, otherwise it will not work, and don't forget to only dispatch actions to keep the state consistent. I suggest you to read the core concepts about redux, and also see this video to understand better how it works under the hoods. Hope i helped a bit!

Yago Biermann
  • 1,494
  • 1
  • 7
  • 20
0

Just as the error states, you are calling useDispatch in Auth.js-> redirectToGoogleSSO. This is neither a React Component nor a React Hook function. You need to call useDispatch in either of those. So you can:

  1. Handle the redux part of the user information and the Google SSO part in a component by calling both useDispatch and redirectToGoogleSSO in handleSignIn itself (this is probably easier to implement right now, you just need to move the dispatch code from redirectToGoogleSSO to handleSignIn), or

  2. turn redirectToGoogleSSO into a Hook you can call from within components.

cSharp
  • 2,884
  • 1
  • 6
  • 23