React Context is creating an infinite loop when fetching user data - reactjs

I have a React Context, so I can save some data and reuse it where I want in my React-Project. Here is the code I'm working on:
import React, { useState, createContext } from "react"
import apiRequest from "../axios"
import { getCookie } from "../Utils"
import jwt_decode from "jwt-decode"
export const UserContext = React.createContext()
export const UserProvider = (props) => {
const [value, setValue] = useState({
loading: false,
isLoggedIn: false,
userId: "",
username: ""
})
const updateData = (toUpdate) => {
setValue({...value, ...toUpdate})
}
const fetchUser = async (userId, key) => {
await apiRequest("get", "/user/" + userId, null, () => {}, (data) => {
updateData({
loading: false,
isLoggedIn: true,
userId: userId,
username: data.user.username
})
}, (errMessage) => {
updateData({
loading: false
})
}, key)
}
// load user data if access token is set
const accessToken = getCookie("access")
if (accessToken && !value.loggedIn && !value.loading) {
updateData({ loading: true })
const { sub: userId } = jwt_decode(accessToken)
fetchUser(userId, accessToken) // if I comment this out, then no infinite loop
}
const methods = {
fetchUser,
updateData
}
return (
<UserContext.Provider value={[value, methods]}>
{props.children}
</UserContext.Provider>
)
}
I have commented the line, where it creates this loop. Can anyone tell me why it is behaving like that?

You need to do the fetch request in the useEffect so that it is fired only when the component is mounted or when the cookie value changes.
Try this;
import React, { useState, createContext, useEffect } from "react"
import apiRequest from "../axios"
import { getCookie } from "../Utils"
import jwt_decode from "jwt-decode"
export const UserContext = React.createContext()
export const UserProvider = (props) => {
const [value, setValue] = useState({
loading: false,
isLoggedIn: false,
userId: "",
username: ""
})
const updateData = (toUpdate) => {
setValue({...value, ...toUpdate})
}
const fetchUser = async (userId, key) => {
updateValue({ loading: true });
await apiRequest("get", "/user/" + userId, null, () => {}, (data) => {
updateData({
loading: false,
isLoggedIn: true,
userId: userId,
username: data.user.username
})
}, (errMessage) => {
updateData({loading: false})
}, key)
}
// load user data if access token is set
const accessToken = getCookie("access")
useEffect(() => {
if (accessToken && !value.loggedIn && !value.loading) {
const { sub: userId } = jwt_decode(accessToken)
fetchUser(userId, accessToken);
}
}, [accessToken, value.loggedId, value.loading]);
const methods = {
fetchUser,
updateData
}
return (
<UserContext.Provider value={[value, methods]}>
{props.children}
</UserContext.Provider>
)
}

Related

How can I await for an async action dispatch on the click of a button?

I have this react component where in the user wants to send in a login request but whenever I dispatch the action, even before it is executed the further code in my component is executed.
I've tried making the login request function as async and even tried using await before dispatching the action but it's all been in vain.
Component file:
import React from 'react';
import BaseButton from '../BaseButton/BaseButton';
import { useState } from 'react';
import { userLogin } from '../../redux/auth/authActions';
import axios from 'axios';
import {connect} from 'react-redux'
function Login({ isLoggedIn, userLogin }) {
const [login, setLogin] = useState(true); //to see if the user wats to login or sign up
const [email, setEmail] = useState("");
const [name, setName] = useState("");
const [password, setPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const handleLogin = () => {
let userCredentials = {
email: email,
password: password
}
userLogin(userCredentials); // <------ i want to wait for this to execute before the below code is executed
if (isLoggedIn) {
console.log('im here');
} else {
console.log('wrong credentials');
}
}
const handleSignUp = async () => {
}
return login ? (
<>
{*/ ...some JSX for user input */}
<div className="flex justify-center">
<BaseButton variant={'solid'} onClick = {handleLogin}>Submit</BaseButton>
</div>
{*/ ...some more JSX for user input */}
<>
}
const mapStateToProps = (state) => {
return {
isLoggedIn: state.auth.isLoggedIn
}
}
const dispatchStateToProps = (dispatch) => {
return {
userLogin: (userCredentials) => dispatch(userLogin(userCredentials))
}
}
export default connect(mapStateToProps, dispatchStateToProps)(Login);
authActions:
import {
USER_LOGIN_REQUEST,
USER_LOGIN_SUCCESS,
USER_LOGIN_FAILURE,
} from './authTypes';
import axios from 'axios';
export const sendLoginRequest = () => {
return {
type: USER_LOGIN_REQUEST,
};
};
export const loginSucccess = () => {
return {
type: USER_LOGIN_SUCCESS,
};
};
export const loginFailure = (error) => {
return {
type: USER_LOGIN_FAILURE,
payload: error,
};
};
export const userLogin = (userCredentials) => {
return (dispatch) => {
try {
dispatch(sendLoginRequest());
axios
.post('http://localhost:3001/auth/login', userCredentials)
.then((data) => {
console.log(data.status);
dispatch(loginSucccess());
})
.catch(err => {
console.log("incorrect credentials");
dispatch(loginFailure('incorrect credentials'));
});
} catch(err) {
dispatch(loginFailure(err.message));
}
};
};
auth reducer file:
import {
USER_LOGIN_REQUEST,
USER_LOGIN_FAILURE,
USER_LOGIN_SUCCESS,
} from './authTypes';
const initialState = {
loading: false,
isLoggedIn: false,
error: ''
};
const authReducer = (state = initialState, action) => {
switch (action.type) {
case USER_LOGIN_REQUEST:
return {
...state,
loading: true
}
case USER_LOGIN_SUCCESS: return{
...state,
loading: false,
isLoggedIn: true,
}
case USER_LOGIN_FAILURE: return{
...state,
loading: false,
isLoggedIn: false,
error: action.payload
}
default: return state;
}
};
export default authReducer;
my store file:
import { createStore, applyMiddleware } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension';
import thunk from 'redux-thunk';
import rootReducer from '../rootReducer';
const store = createStore(rootReducer, composeWithDevTools(applyMiddleware(thunk)));
export default store;
root reducer:
import {combineReducers} from 'redux';
import authReducer from './auth/authReducer';
const rootReducer = combineReducers({
auth: authReducer
});
export default rootReducer;
The userLogin action isn't declared async nor does it return a Promise so this is why your handleLogin handler isn't able to wait for it to complete.
Convert userLogin to an async action function.
export const userLogin = (userCredentials) => async (dispatch) => {
try {
dispatch(sendLoginRequest());
const data = await axios.post('http://localhost:3001/auth/login', userCredentials);
console.log(data.status);
dispatch(loginSucccess());
return true; // <-- return resolved value
} catch(err) {
dispatch(loginFailure(err.message));
return false; // <-- return resolved value
}
};
Convert handleLogin to an async function so it can await the dispatched action to resolve. Note that handleLogin won't, and can't see any updated isLoggedIn state value from Redux while it has a current value closed over in scope from the time it was called.
const handleLogin = async () => {
const userCredentials = { email, password };
const authSuccess = await userLogin(userCredentials);
if (authSuccess) {
console.log('I'm here');
} else {
console.log('wrong credentials');
}
};
use async await or then catch in handleLogin function and also do not forgot to add return in userLogin and sub functions
`const handleLogin = async () => {
await userLogin(userCredentials);
if(isLoggedIn) {
console.log('here');
}
}`
or use then
`userLogin(userCredentials).then(() => { if(isLoggedIn){
console.log('here');
}});`

Next.Js: Cannot update a component (`AuthContextProvider`) while rendering a different component (`Login`)

I have a simple next.js app, that allows a user to login via a login-page. The login is done via a graphql-api. I'm using the react context-API and after the user has logged in succesfully I'm updating the context. Afterwards I would like to redirect the user to a dashboard-page. It actually works as intended, however always (and only) on the second login (= login, logout, login again) I get the following Error in my console:
I understand the error (or warning?), but I don't know why it occurs or what I'm doing wrong. Any suggestion to point me in the right direction is much appreciated.
Here's my code:
auth-context.ts
import { createContext, useEffect, useState } from 'react';
//type-definitions removed for better readability
const AuthContext = createContext<AuthContext>({
isAuthenticated: false,
isAdmin: false,
userId: '',
loginSuccessHandler: () => {},
logoutHandler: () => {},
});
const AuthContextProvider = ({ children }: AuthContextProviderProps) => {
const [authData, setAuthData] = useState<AuthData>({
isAuthenticated: false,
isAdmin: false,
userId: null,
});
useEffect(() => {
const storedIsAuthenticated = localStorage.getItem('isAuthenticated');
const storedUserId = localStorage.getItem('userId');
const storedRole = localStorage.getItem('role');
if (storedIsAuthenticated === '1') {
setAuthData({
isAuthenticated: true,
userId: storedUserId,
isAdmin: storedRole === 'ADMIN',
});
}
}, []);
const loginSuccessHandler = (isAuthenticated: boolean, userId: string, role: string) => {
localStorage.setItem('isAuthenticated', '1');
localStorage.setItem('userId', userId);
localStorage.setItem('role', role);
setAuthData({
isAuthenticated: isAuthenticated,
userId: userId,
isAdmin: role === 'ADMIN',
});
};
const logoutHandler = () => {
localStorage.removeItem('isAuthenticated');
localStorage.removeItem('userId');
localStorage.removeItem('role');
setAuthData({
isAuthenticated: false,
userId: null,
isAdmin: false,
});
};
return (
<AuthContext.Provider
value={{
isAuthenticated: authData.isAuthenticated,
isAdmin: authData.isAdmin,
userId: authData.userId,
loginSuccessHandler,
logoutHandler,
}}
>
{children}
</AuthContext.Provider>
);
};
export { AuthContext, AuthContextProvider };
pages/login.tsx
import { gql, useLazyQuery } from '#apollo/client';
import { useRouter } from 'next/router';
import { SyntheticEvent, useContext, useEffect, useRef } from 'react';
import TextInput from '../components/Input/TextInput';
import { AuthContext } from '../contexts/auth-context';
import type { NextPage } from 'next';
const Login: NextPage = () => {
const emailInputRef = useRef<HTMLInputElement>(null);
const passwordInputRef = useRef<HTMLInputElement>(null);
const authCtx = useContext(AuthContext);
const router = useRouter();
const LOGIN_QUERY = gql`
query LoginQuery($email: String!, $password: String!) {
login(email: $email, password: $password) {
userId
token
role
}
}
`;
const [login, { loading, error }] = useLazyQuery(LOGIN_QUERY);
const submitButtonHandler = (event: SyntheticEvent) => {
event.preventDefault();
const email = emailInputRef?.current?.value || null;
const password = passwordInputRef?.current?.value || null;
login({
variables: { email, password },
onCompleted(data) {
authCtx.loginSuccessHandler(true, data.login.userId, data.login.role);
},
onError(error) {
authCtx.logoutHandler();
},
});
};
/*
The following Effect leads to the warning/error, when I remove it, the error disappears.
However, that's not what I want, I want it to work like this
*/
useEffect(() => {
authCtx.isAuthenticated
? authCtx.isAdmin
? router.push('/admin')
: router.push('/my-account')
: router.push('/login');
}, [authCtx.isAuthenticated, authCtx.isAdmin]);
return (
<div>
<form>
<h1>Login</h1>
{loading && <p>Loading</p>}
{error && <p>Error: {error.message}</p>}
<TextInput type="email" id="email" ref={emailInputRef} />
<TextInput type="password" id="password" ref={passwordInputRef} />
<button onClick={submitButtonHandler} >
Submit
</button>
</form>
</div>
);
};
export default Login;
In _app.tsx I use my AuthContextProvider like this:
import '../styles/globals.scss';
import Layout from '../components/Layout/Layout';
import type { AppProps } from 'next/app';
import { AuthContextProvider } from '../contexts/auth-context';
import { ApolloProvider } from '#apollo/client';
import apolloClient from '../lib/apollo-client';
function MyApp({ Component, pageProps }: AppProps) {
return (
<ApolloProvider client={apolloClient}>
<AuthContextProvider>
<Layout>
<Component {...pageProps} />
</Layout>
</AuthContextProvider>
</ApolloProvider>
);
}
export default MyApp;
Thanks to #Anthony Ma's comment, I found that the onCompleted-Handler in login.tsx seemed to be the issue here. It updates the authContext while still being in the rendering-process of the login-component.
I changed my login.tsx file to add the data-response from my graphql-API to a state-object and then use an effect with that state-object as dependency (Find -> changed comments in the code below to see all changes).
Updated login.tsx:
import { gql, useLazyQuery } from '#apollo/client';
import { useRouter } from 'next/router';
/*
-> changed: add useState to the list of imports
*/
import { SyntheticEvent, useContext, useEffect, useRef, useState } from 'react';
import TextInput from '../components/Input/TextInput';
import { AuthContext } from '../contexts/auth-context';
import type { NextPage } from 'next';
//type definition "AuthData" goes here
const Login: NextPage = () => {
const emailInputRef = useRef<HTMLInputElement>(null);
const passwordInputRef = useRef<HTMLInputElement>(null);
const authCtx = useContext(AuthContext);
const router = useRouter();
/*
-> changed: Add authData State
*/
const [authData, setAuthData] = useState<AuthData>({
isAuthenticated: false,
userId: '',
role: '',
token: '',
});
const LOGIN_QUERY = gql`
query LoginQuery($email: String!, $password: String!) {
login(email: $email, password: $password) {
userId
token
role
}
}
`;
const [login, { loading, error }] = useLazyQuery(LOGIN_QUERY);
const submitButtonHandler = (event: SyntheticEvent) => {
event.preventDefault();
const email = emailInputRef?.current?.value || null;
const password = passwordInputRef?.current?.value || null;
login({
variables: { email, password },
/*
-> changed: Important: Set fetchPolicy to 'network-only' to prevent
caching of the response (otherwise the response wont change on
every login request and therefore the state wont change resulting
in the effect (see below) not being triggered.
*/
fetchPolicy: 'network-only',
onCompleted(data) {
/*
-> changed: add response data to the new state object "authData"
*/
setAuthData({
isAuthenticated: true,
userId: data.login.userId,
role: data.login.role,
token: data.login.token,
});
},
onError(error) {
authCtx.logoutHandler();
},
});
};
/*
-> changed: added this new effect with the authData state object as
dependency
*/
useEffect(() => {
authCtx.loginSuccessHandler({
isAuthenticated: authData.isAuthenticated,
userId: authData.userId,
role: authData.role,
token: authData.token,
});
}, [authData]);
useEffect(() => {
authCtx.isAuthenticated
? authCtx.isAdmin
? router.push('/admin')
: router.push('/my-account')
: router.push('/login');
}, [authCtx.isAuthenticated, authCtx.isAdmin]);
return (
<div>
<form>
<h1>Login</h1>
{loading && <p>Loading</p>}
{error && <p>Error: {error.message}</p>}
<TextInput type="email" id="email" ref={emailInputRef} />
<TextInput type="password" id="password" ref={passwordInputRef} />
<button onClick={submitButtonHandler} >
Submit
</button>
</form>
</div>
);
};
export default Login;

react toastify with redux from axios API

i am trying to send the error messages that sent from my server ( express ) to axios and the error message displays in toastify component but the error message doesn't show up here is the login axios function with the toastify how can i display toastify message inside my page from redux ?
here is my code :
// redux controller
const login = async (username, password) => {
await axios.post("/login",{username,password,},
{ withCredentials: true });};
// reducer page
export function generateError(prop) {
return function (dispatch) {
dispatch({
type: "USER_FAIL"
});
toast.error(prop);
};
}
export function generateSuccess(prop) {
return function (dispatch) {
dispatch({
type: "USER_SUCCESS"
});
toast.success(prop);
};
}
export const login = createAsyncThunk(
"/login",
async ({ username, password }) => {
try {
const data = await authService.login(username, password);
if (data) {
if (data.errors) {
const { username, password } = data.errors;
if (username) generateError(username)
else if (password) generateError(password);
} else {
generateSuccess(data.success);
}
}
return { user: data };
} catch (error) {
console.log(error);
}
}
);
// login page
const handleSubmit = (e) => {
e.preventDefault();
dispatch(login({ username, password }));
}
i am using react-tostify and #redux-toolkit but the message doesn't display inside my page
i fixed it and here is my code :
// auth.js ( redux page )
export const login = createAsyncThunk(
"/login",
async ({ username, password }) => {
try {
const {data} = await axios.post(
"/login",
{
username,
password,
},
{ withCredentials: true }
);
return { user: data };
} catch (error) {
console.log(error);
}
});
const initialState = user
? { isLoggedIn: true, user }
: { isLoggedIn: false, user: null };
const authSlice = createSlice({
name: "auth",
initialState,
extraReducers: {
[login.fulfilled]: (state, action) => {
state.isLoggedIn = true;
state.user = action.payload.user;
},
[login.rejected]: (state, action) => {
state.isLoggedIn = false;
state.user = null;
},
[logout.fulfilled]: (state, action) => {
state.isLoggedIn = false;
state.user = null;
},
}})
const { reducer } = authSlice; export default reducer;
Login Page :
const { isLoggedIn } = useSelector((state) => state.auth);
const dispatch = useDispatch();
const handleSubmit = (e) => {
e.preventDefault();
dispatch(login({ username, password })).then(data => {
console.log(data)
if (data.payload.user) {
if (data.payload.user.errors) {
const { username, password } = data.payload.user.errors;
if (username) generateError(username)
else if (password) generateError(password);
} else {
generateSuccess(data.success);
navigate("/dashboard");
}
}
})
}
i realized when i back the data it has an object name payload i used it to get the error messages from express and then i put the message in toastify function gettingError and here it is
const generateError = error => {
toast.error(error, {
position: "bottom-right",
})
}
Hai I'm also looking for the same problem while searching I found a solution at with this : react-toastify-with-redux
my Code : authAction.js
import 'react-toastify/dist/ReactToastify.min.css';
import { toast} from 'react-toastify';
export const registerUser = (userData) => dispatch =>{
axios.post('user/register',userData)
.then(res=>toast.success('Your Account Created Successfully ๐Ÿ‘'))
.then(res=> window.location = '/authentication/sign-in')
.catch(err=>dispatch(
{
type: GET_ERRORS,
payload: err.response.data
}
),toast.error("Error ๐Ÿ˜ฃ"))
// .catch((err)=> {return })
};
On your signUp page just add
<ToastContainer />
That's all ...
This answer is probably late. But I came across this problem and decided to do it my way. I know there is toast. promise to handle promises and I don't want to call dispatch.then every time. So I can up with passing dispatch to my action wrapper. Here is my code.
// utils.ts
type ArgumentTypes<F extends CallableFunction> = F extends (
...args: infer A
) => any
? A[0]
: never;
export const withToast = <T = AnyAction | typeof createAsyncThunk>(
action: T,
{ pending, error, success }: ToastPromiseParams<T>
) => {
return (
dispatch: ReturnType<typeof useAppDispatch>,
actionParams?: ArgumentTypes<T & CallableFunction> | void
) => {
const promise = dispatch(
(action as CallableFunction)(actionParams as any)
).unwrap();
toast.promise(promise, {
pending,
error,
success,
});
};
};
// actions.ts
export const login = createAsyncThunk(
"user/login",
async (payload: {
email: string;
password: string;
}): Promise<Partial<LoginAPIResponse>> => {
const { data } = await axios.post(`${API}/${LOGIN_EP}/`, payload);
return data;
}
);
export const loginWithToast = withToast(login, {
pending: "Logging in...",
error: {
render: (error: any) => {
return error?.password || error?.email
? "Invalid email or password"
: "Something went wrong";
},
},
success: "Logged in successfully",
});
// usage in component
const dispatch = useAppDispatch();
loginWithToast(dispatch, {
email: values.email.value,
password: values.password.value,
});
First createAsyncThunk:
import { coreAxios } from "utilities/axios"; // Own customized axios
import { createAsyncThunk } from "#reduxjs/toolkit";
const BASE_URL = process.env.REACT_APP_MAIN_URL
export const GetProducts = createAsyncThunk(
"inventory/GetProducts",
async () => {
const {data} = await coreAxios.get(`${BASE_URL}/api/product/list/`);
return data
}
);
Second createSlice:
import { createSlice } from "#reduxjs/toolkit";
import { GetProducts } from "services/inventory/product.service";
import { toast } from 'react-toastify';
export const productSlice = createSlice({
name: "products",
initialState: {
productsList: [],
productsLoading: false,
productsError: null,
},
extraReducers:
(builder) => {
builder.addCase(GetProducts.pending, (state) => {
toast.loading('Promise is pending...')
state.productsLoading = true
});
builder.addCase(GetProducts.fulfilled, (state, action) => {
toast.dismiss();
toast.success('Promise resolved ๐Ÿ‘Œ');
state.productsList = action.payload
state.productsLoading = false
state.productsError = null
});
builder.addCase(GetProducts.rejected, (state, action) => {
toast.dismiss();
toast.error('Promise rejected ๐Ÿคฏ ๐Ÿ˜ฃ')
state.productsLoading = false
state.productsError = action.error?.message
});
},
});
export default productSlice.reducer;
Third page:
import { ToastContainer } from 'react-toastify';
import { useSelector, useDispatch } from "react-redux";
import { GetProducts } from 'services/inventory/product.service';
const Product = () => {
const { productsList, productsLoading, productsError } = useSelector((state) => state.products);
const dispatch = useDispatch();
useEffect(() => {
dispatch(GetProducts());
}, []);
return (
<div className="grid crud-demo">
<h1>Hello Alim</h1>
<ToastContainer />
</div>
);
}

Why doesn't useEffect hook work on page refresh?

I'm working on a react project. I have my own API to fetch information. I'm using the useEffect hook to fetch profile information from API. My problem is when page mounts for the first time i can fetch the data with no problem but if i refresh the page it doesn't work at all. I know i have to give a second parameter to useEffect. I tried to put profile as the second argument even dispatched the getCurrentProfile function but when i do that it constantly fires off fetch request. I would be glad if anyone can help me with that. Thanks.
Here is my Profile component:
export const Profile = () => {
const dispatch = useDispatch();
useEffect(() => {
dispatch(getCurrentProfile());
}, [])
const profileReducer = useSelector((state) => state.profile);
const authReducer = useSelector((state) => state.auth);
const { profile, error, loading } = profileReducer;
const { user } = authReducer;
console.log("loading", loading)
console.log("profile", profile)
return loading && profile === null ? (
<div >
<Spinner />
</div>
) :
Here is my Profile action:
export const getCurrentProfile = () => async dispatch => {
try {
const res = await axios.get("/api/profile/me");
console.log(res);
dispatch({
type: "GET_PROFILE",
payload: res.data.data
})
} catch (err) {
dispatch({
type: "PROFILE_ERROR",
payload: { msg: err.response.statusText, status: err.response.status }
})
}
}
Here is my profile reducer:
export default (state = initialState, action) => {
const { type, payload } = action;
switch (type) {
case "GET_PROFILE":
return {
...state,
profile: payload,
loading: false
}
case "PROFILE_ERROR":
return {
...state,
error: payload,
profile: null
}
case "CLEAR_PROFILE":
return {
...state,
profile: null,
loading: false
}
default:
return state;
}
}
You might want to try adding conditional logic within the useEffect so you only trigger the dispatch if you don't already have a profile.
import "./styles.css";
import { useDispatch, useSelector } from "react-redux";
import { useEffect, useCallback } from "react";
import { getCurrentProfile } from "./action";
export const Profile = () => {
const dispatch = useDispatch();
const profileReducer = useSelector((state) => state.profile);
const authReducer = useSelector((state) => state.auth);
const { profile, error, loading } = profileReducer;
// read more about this here: https://stackoverflow.com/questions/58624200/react-hook-useeffect-has-a-missing-dependency-dispatch
const stableDispatch = useCallback(dispatch, []);
useEffect(() => {
if (!profile) {
stableDispatch(getCurrentProfile());
}
}, [profile, stableDispatch]);
const { user } = authReducer;
console.log("loading", loading);
console.log("profile", profile);
return loading && profile === null ? <div>Spinner</div> : "Actual Profile";
};
export default Profile;
Also, it doesn't seem like you're currently doing anything with the loading piece of stateโ€“at least from what you've shared here. You might want to dispatch an action indicating that you're loading before you start the fetch and then it will be set to false when you get the response.
Check out this codesandbox for reference: https://codesandbox.io/s/focused-kilby-gd2nr?file=/src/App.js
Reducers:
const initialState = {
profile: null,
loading: false
};
export const profile = (state = initialState, action) => {
const { type, payload } = action;
switch (type) {
case "LOADING_PROFILE":
return {
...state,
loading: true
};
case "GET_PROFILE":
return {
...state,
profile: payload,
loading: false
};
case "PROFILE_ERROR":
return {
...state,
error: payload,
profile: null
};
case "CLEAR_PROFILE":
return {
...state,
profile: null,
loading: false
};
default:
return state;
}
};
export const auth = (state = {}, action) => {
return state;
};
Action Creator:
import axios from "axios";
export const getCurrentProfile = () => async (dispatch) => {
try {
dispatch({ type: "LOADING_PROFILE" });
const res = await axios.get("https://jsonplaceholder.typicode.com/users/1");
console.log(res);
dispatch({
type: "GET_PROFILE",
payload: res.data.data
});
} catch (err) {
dispatch({
type: "PROFILE_ERROR",
payload: { msg: err.response.statusText, status: err.response.status }
});
}
};
index.js
import { StrictMode } from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import { createStore, combineReducers, applyMiddleware } from "redux";
import { profile, auth } from "./reducers";
import App from "./App";
import thunk from "redux-thunk";
const store = createStore(
combineReducers({
profile,
auth
}),
applyMiddleware(thunk)
);
const rootElement = document.getElementById("root");
ReactDOM.render(
<StrictMode>
<Provider store={store}>
<App />
</Provider>
</StrictMode>,
rootElement
);
Well i solved it by dispatching 'getCurrentProfile' not 'getCurrentProfile()' turns out using it like a function causes continuously firing off.
const profileReducer = useSelector((state) => state.profile);
const authReducer = useSelector((state) => state.auth);
const { profile, error, loading } = profileReducer;
const dispatch = useDispatch();
useEffect(() => {
if (!profile) {
console.log("It worked")
dispatch(getCurrentProfile());
}
}, [dispatch(getCurrentProfile)])

Hook won't get properly mocked Jest

For the last 2 hours, I've read unnumerous posts from StackOverflow, medium, and other independent blogs, and I haven't been able to crack or decipher how to properly mock a simple custom useAuth() hook.
I'm getting:
[TypeError: Cannot destructure property 'user' of '(0 , _auth.useAuth)(...)' as it is undefined.]
Here's my code:
The <Dashboard/> component which includes the useAuth() hook. (Code omitted due to brevity)
import { useAuth } from '../../../auth';
export const Dashboard: React.FC<RouteComponentProps> = (props) => {
const { user } = useAuth();
The dashboard.test.tesx file.
import { render, waitFor } from "../../../__tests_setup__/app-test-utils";
// Sets Up useAuth
import { mockPatientDetails } from "../../../setupTestPatient";
import { mockBaseAuth } from "../../../setupTestShared";
import { Dashboard } from "./index";
import { useAuth } from "../../../auth";
jest.mock("../../../auth");
describe("Tests the Patient's dashboard", () => {
beforeAll(() => {
(useAuth as any).mockReturnValue({
...mockBaseAuth,
user: {
...mockBaseAuth.user,
profile: mockPatientDetails.profile,
profile_id: mockPatientDetails.profile_id,
username: mockPatientDetails.username,
},
});
});
it("Will be able to assign a consultation now", async () => {
const renderer = await render(<Dashboard />);
waitFor(async () => {
await expect(renderer.getByText(`Hola ${mockPatientDetails.username}`));
});
expect(true).toBe(true);
});
});
Other variations tried:
import { render, waitFor } from "../../../__tests_setup__/app-test-utils";
// Sets Up useAuth
import { mockPatientDetails } from "../../../setupTestPatient";
import { mockBaseAuth } from "../../../setupTestShared";
import { Dashboard } from "./index";
import { useAuth } from "../../../auth";
// Variation 1
import * as auth from '../../../auth'
jest.spyOn(auth, "useAuth").mockImplementation(() => ({
...mockBaseAuth,
user: {
...mockBaseAuth.user,
profile: mockPatientDetails.profile,
profile_id: mockPatientDetails.profile_id,
username: mockPatientDetails.username,
},
}));
// Variation 2
jest.mock("../../../auth/index", () => ({
__esModule: true,
useAuth: jest.fn(() => ({
...mockBaseAuth,
user: {
...mockBaseAuth.user,
profile: mockPatientDetails.profile,
profile_id: mockPatientDetails.profile_id,
username: mockPatientDetails.username,
},
})),
}));
// Variation 3
There have been many other variations which I haven't included as I've completely forgotten about them.
Here's my folder structure, just in case.
P.S: Here are the variables shown above:
src/setupTestShared.ts
import { GENDER, InteractionMedias, UserProfile } from "./#dts";
import { useAuth } from "./auth";
const success = Promise.resolve({
type: "success" as const,
result: true,
});
export const mockBaseAuth: ReturnType<typeof useAuth> = {
login(u: string, p: string) {
return success;
},
authenticated: true,
logout() {},
register(p: UserProfile, pass: string) {
return success;
},
userExists(u: string) {
return Promise.resolve("USER_REGISTERED" as const);
},
user: {
date_of_birth: "1990-12-21",
email: "test#gmail.com",
emails: ["test#gmail.com"],
first_name: "Maria",
gender: GENDER.FEMALE,
id: 1,
identification_id: "402-2066666-1",
interaction_media_preferred: InteractionMedias.VIDEO,
last_name: "Anabelle",
loggedInDate: new Date(),
phones: ["809-544-5111"],
profile: "ANONYMOUS",
profile_id: 1,
username: "anonymous",
},
};
export const mockPatientDetails = {
username: "PAC123456",
profile: "patient" as const,
profile_id: 2,
};
What could it be?
It's working now!
This answer helped me:
https://stackoverflow.com/a/60282832/1057052
// https://stackoverflow.com/a/60282832/1057052
jest.mock("../../../auth", () => ({
// this isn't needed - __esModule: true,
useAuth: () => ({
...mockBaseAuth,
user: {
...mockBaseAuth.user,
profile: mockPatientDetails.profile,
profile_id: mockPatientDetails.profile_id,
username: mockPatientDetails.username,
},
}),
}));
The trick was not assigning the jest.fn() to the useAuth().

Resources