My redux toolkit dispatch is firing twice, once immediately which for some reason doesn't properly update the state, then my entire app breaks because it cant't find the info that us supposed to have been provided, then it fires again and updates properly. So if i refresh the page then everything will load up because then it gets the information from my persistent storage in time.
This is the portion of the code where the dispatch is called:
const handleSignUp = (e) => {
e.preventDefault();
createUserWithEmailAndPassword(auth, email, password)
.then((cred) => {
updateProfile(cred.user, { displayName: name })
.then(() => {
dispatch(
login({
name: name,
uid: cred.user.uid,
jobDesc: jobDesc,
email: email,
})
);
})
})
.catch((err) => {
console.log(err);
})
.finally(() => {
setPassword("");
});
};
This is the userSlice where login is called from:
import { createSlice } from "#reduxjs/toolkit";
const initialState = {
user: null,
};
export const userSlice = createSlice({
name: "user",
initialState,
reducers: {
login: (state, action) => {
state.user = action.payload;
},
logout: (state) => {
state.user = null;
},
},
});
export const { login, logout } = userSlice.actions;
export default userSlice.reducer;
i have checked my redux dev tools so i know the dispatch calls twice and if first udpates partially and the completely.
this is text copied from my dev tools console that displays the state and it's changes:
name(pin):null
uid(pin):"CK3uLpCLD3hIOa0vXEzhA0oFcwr1"
email(pin):"jamesdev#gmail.com"
that is the first update and this is the second:
name(pin):"james"
uid(pin):"CK3uLpCLD3hIOa0vXEzhA0oFcwr1"
jobDesc(pin):"web dev"
email(pin):"jamesdev#gmail.com"
I've almost lost my mind trying to figure this out, please help.
Edit:
Sometimes it also throws this error at Login:
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
You are missing a return there at one place, which leads to the whole thing not waiting, but running in parallel:
.then((cred) => {
// vvv this was missing
return updateProfile(cred.user, { displayName: name })
.then(() => {
dispatch(
login({
name: name,
uid: cred.user.uid,
jobDesc: jobDesc,
email: email,
})
);
})
})
Generally I'd highly recommend writing the whole thing as async function with await instead as that makes it a whole lot more readable:
const handleSignUp = async (e) => {
e.preventDefault();
try {
const cred = await createUserWithEmailAndPassword(auth, email, password)
await updateProfile(cred.user, { displayName: name })
dispatch(
login({
name: name,
uid: cred.user.uid,
jobDesc: jobDesc,
email: email,
})
);
} catch (err) {
console.log(err);
} finally {
setPassword("");
};
};
Related
I have a react app where I use the useContext and useReducer hooks for the login and storage. While the login part works, what I want achieve is to redirect user to a specific page post successful login. I am using react-router#6 and tried to use useNavigate() to navigate user to particular route though it doesn't seem to work.
const AuthService = async (dispatch) => {
const MSAL_CONFIG = {} // populate MSAL config for Microsoft Graph API for AD auth
const msalInstance = new msal.PublicClientApplication(MSAL_CONFIG);
try {
const loginResponse = await msalInstance.loginPopup(scopes);
var username = loginResponse.account.username;
var userid = username.slice(0, username.indexOf("#"));
const loginData = {
auth_token: loginResponse.idToken,
user: {
name: loginResponse.account.name,
id: userid,
email: username,
},
};
const sessionData = {
user_id: userid,
id_token: loginResponse.idToken,
access_token: loginResponse.accessToken,
}
sessionStorage.setItem("currentUser", JSON.stringify(loginData));
dispatch({ type: "LOGIN_SUCCESS", payload: loginData });
return { loginData: loginData, error: null };
// dispatch({ type: 'LOGIN_SUCCESS', payload: loginData });
//sessionStorage.setItem('currentUser', JSON.stringify(data));
} catch (err) {
console.log("+++ Login error : ", err);
dispatch({ type: "LOGIN_ERROR", error: err });
return { loginData: null, error: err };
}
};
In my header.jsx, I have below code to handle the login button. It makes a call to the above AuthService. The code post AuthService() call, i.e. the if block, doesn't take effect, so user never gets redirected to the dashboard page.
const handleLogin = async () => {
await AuthService(dispatch)
console.log("userDetails.token : " + userDetails.token)
if (Boolean(userDetails.token)) {
navigate("/dashboard");
}
};
If I'm correct in understanding that this AuthService function eventually resolves and that the dispatched LOGIN_SUCCESS action updates the userDetails variable that is selected from the auth context state, then I think you have all that you need and are close to a working solution. The issue is that the userDetails value from the render cycle the handleLogin is called in is closed over in callback scope, it will never be a different value. If the userDetails.token value is falsey when handleLogin is called, it will remain falsey in the entire callback scope.
The AuthService function appears to return the same loginData object that is passed in the dispatched LOGIN_SUCCESS action to the store. handleLogin should await this value and conditionally navigate.
const AuthService = async (dispatch) => {
...
try {
const { account, idToken } = await msalInstance.loginPopup(scopes);
const { name, username } = account;
const userid = username.slice(0, username.indexOf("#"));
const loginData = {
auth_token: idToken,
user: {
name,
id: userid,
email: username,
},
};
...
sessionStorage.setItem("currentUser", JSON.stringify(loginData));
dispatch({ type: "LOGIN_SUCCESS", payload: loginData });
return { loginData, error: null }; // <-- return value
} catch (error) {
dispatch({ type: "LOGIN_ERROR", error });
return { loginData: null, error }; // <-- return value
}
};
const handleLogin = async () => {
const { loginData } = await AuthService(dispatch);
if (loginData && loginData.auth_token) { // or loginData?.auth_token
navigate("/dashboard", { replace: true });
}
};
I am trying to add authentication to a react website using firebase
I currently have this createasyncthunk
export const signin = createAsyncThunk<
// Return type of the payload creator
User | void,
// First argument to the payload creator
userData,
// Types for ThunkAPI
{
rejectValue: errorInterface;
}
>('authentication/signin', (user, thunkApi) => {
const { email, password } = user;
signInWithEmailAndPassword(auth, email, password)
.then((userCredential) => {
// Signed in
const user = userCredential.user;
return user;
})
.catch((error) => {
console.log(error.message, error.code)
const errorMessage: errorInterface = { message: error.code };
return thunkApi.rejectWithValue(errorMessage);
});
});
the user type is imported from fire base
the userData is the following interface to represent credentials used to log in
interface userData {
email: string;
password: string;
}
the error interface is the following
interface errorInterface {
message: string;
}
the authentication slice is this
export const authSlice = createSlice({
name: 'authentication',
initialState,
reducers: {},
extraReducers: (builder) => {
builder
.addCase(signin.pending, (state) => {
state.status = 'loading';
state.isAuth = false;
})
.addCase(signin.fulfilled, (state, action) => {
state.status = 'idle';
state.isAuth = true;
if (action.payload) {
state.user = action.payload;
}
})
.addCase(signin.rejected, (state, action) => {
state.status = 'failed';
state.isAuth = false;
if (action.payload) {
state.message = action.payload.message;
}
})
},
});
The problem is that signin.rejected is never trigered. Even when the catch block runs, rejectwithvalue is not updating the payload. I know the catch block is running because I can see the error in the console. Please help me out, thanks in advance
I have implemented user registration in my application using
firebase.auth().SignInWithEmailAndPassword (email, password)
And I need that when a user comes to the site, his profile will be synchronized as logged in, even the page will be reloaded or closed and opened again
class App extends Component {
constructor(props) {
super(props);
this.state = {
email: " ",
password: " ",
hasAccount: false,
}
}
componentDidMount() {
const db = firebase.database();
console.log(db)
}
handleChange = ({ target: { value, id } }) => {
this.setState({ [id] : value })
}
createAccount = () => {
const { email, password } = this.state;
firebase.auth().signInWithEmailAndPassword( email, password)
.then(Response => {
console.log(Response)
this.setState({ hasAccount: true })
})
.catch(error => console.log(error))
const auth = firebase.app().auth();
auth.setPersistence(firebase.auth.Auth.Persistence.LOCAL)
}
In many environments Firebase already persists the user's authentication credentials, and you don't need to explicitly enabled that with auth.setPersistence(firebase.auth.Auth.Persistence.LOCAL).
But to pick up the restored authentication state when the app is restarted/page reloaded, you need to use an onAuthStateChanged handler, as shown in the fist snippet in the documentation on getting the current user:
firebase.auth().onAuthStateChanged(function(user) {
if (user) {
// User is signed in.
this.setState({ hasAccount: true })
} else {
// No user is signed in.
}
});
You'll typically put this code in the constructor of your component, so that it starts monitoring authentication state right away.
componentDidMount() {
firebase.auth().onAuthStateChanged((user) => {
if (user) {
// User is signed in.
this.setState({ hasAccount: true })
}
});
I'm using useReducer to update the errorsState when user logged in and failed. I've read many solutions and it was said that dispatch is async and I know that so I put console.log inside the useEffect to see the errorsState change, but unfortunately it didn't changed. Here's my code
Login.jsx
export default function Login({ userProps }) {
//
// some variables and state
//
const { loading, user } = useLogin({ email: state.email }, state.submitted)
const [errors, dispatch] = useReducer(errorsReducer, errorsState)
useEffect(() => {
console.log("errors", errors) // it won't triggered because errors state didn't updating from UseLogin
}, [errors])
return content
}
Here is fetch function useLogin
AuthAction.js
export const useLogin = (data, submitted) => {
const [state, dispatch] = useReducer(userReducer, userState)
const [errors, errorsDispatch] = useReducer(errorsReducer, errorsState)
useEffect(() => {
if (!submitted) return
dispatch({
type: USER_ACTIONS.MAKE_REQUEST,
})
ticketApi.login(data).then(({ res, status }) => {
if (status !== "failed") {
// Save to local storage
const { token } = res
// set token to local storage
localStorage.setItem("jwtToken", token)
// Set token to Auth Header
setAuthToken(token)
// decode token to get user data with jwt-decode
const decoded = jwt_decode(token)
// set current user
return dispatch({
type: USER_ACTIONS.GET_USER,
payload: decoded,
})
}
dispatch({
type: USER_ACTIONS.END_REQUEST,
})
return errorsDispatch({
type: ERRORS_ACTIONS.GET_ERRORS,
payload: res.response.data,
})
})
}, [submitted])
return state
}
I've tried put console.log inside the ERRORS_ACTIONS.GET_ERRORS to see the response, and it was fine.
So where did i go wrong?
useReducer allows you to better manage complex states, it's not a state container, what you're doing there is to create 2 different states, one inside useLogin and the other in your Login component, return errors from your useLogin hook so the Login component can see it.
Login
export default function Login({ userProps }) {
//
// some variables and state
//
const { loading, user, errors } = useLogin({ email: state.email }, state.submitted)
useEffect(() => {
console.log("errors", errors)
}, [errors])
return content
}
useLogin
export const useLogin = (data, submitted) => {
const [state, dispatch] = useReducer(userReducer, userState)
const [errors, errorsDispatch] = useReducer(errorsReducer, errorsState)
useEffect(() => {
if (!submitted) return
dispatch({
type: USER_ACTIONS.MAKE_REQUEST,
})
ticketApi.login(data).then(({ res, status }) => {
if (status !== "failed") {
// Save to local storage
const { token } = res
// set token to local storage
localStorage.setItem("jwtToken", token)
// Set token to Auth Header
setAuthToken(token)
// decode token to get user data with jwt-decode
const decoded = jwt_decode(token)
// set current user
return dispatch({
type: USER_ACTIONS.GET_USER,
payload: decoded,
})
}
dispatch({
type: USER_ACTIONS.END_REQUEST,
})
return errorsDispatch({
type: ERRORS_ACTIONS.GET_ERRORS,
payload: res.response.data,
})
})
}, [submitted])
return { ...state, errors };
}
As you can see my user state is null at the beginning but when the user logs in, I need to user id for my action called setFavoriteCrypto
So I can retrieve the specific data that is related to the user connected but since vuex initialize the state every time there's a refresh of the page. the user becomes null
I know there's a package called persist state which is something I could work with.
But I would like to know if there's a different way of managing my user connected without depending on a package?
I am using node.js as my backend for this app.
import Api from '#/services/Api'
import axios from 'axios'
import router from '#/router'
const state = {
localStorageToken: localStorage.getItem('user-token') || null,
token: null,
user: null,
favoriteCrypto: []
}
const mutations = {
setToken(state, token) {
state.localStorageToken = token
state.token = token
},
setUser(state, user) {
state.user = user
},
setFavoritecrypto(state, crypto) {
state.favoriteCrypto = crypto
}
}
const actions = {
setToken({commit}, token) {
commit('setToken', token)
},
setUser({commit}, user) {
commit('setUser', user)
},
setFavoriteCrypto({commit}, token) {
if(state.user) {
return Api().get('getuserfavoritescoins', {
params: {
userId: state.user.id
}
})
.then(response => response.data.coins)
.then(cryptoFav => {
commit('setFavoritecrypto', cryptoFav)
})
}
else{
console.log("there's not user state")
}
},
loginUser({commit}, payload) {
return Api().post('login', payload)
.then(response => {
commit('setUser', response.data.user)
commit('setToken', response.data.token)
localStorage.setItem('user-token', response.data.token)
axios.defaults.headers.common['Authorization'] = response.data.token
router.push('/main').catch(e => {})
})
},
logoutUser({commit, dispatch}) {
localStorage.removeItem('user-token')
commit('setUser', null)
commit('setToken', null)
delete axios.defaults.headers.common['Authorization']
}
}
const getters = {
isAuthenticated: state => !!state.localStorageToken,
}
export default {
namespaced: true,
state,
getters,
actions,
mutations
}