Why is useReducer hook not updating state? - reactjs

I am working on login/register components in React, and I'm using useContext and useReducer hooks to manage state. This is the first time I've tried it this way, and I'm not sure why the state is not changing. Below are the files for the login component. I've shown where I've console logged and what the results are.
This is the api:
export const login = ({ email, password }) => {
console.log(email, password);
// jennifer#jennifer.com 12345678
return fetch(`${DEV_AUTH_URL}/signin`, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
email,
password,
}),
})
.then((res) => {
return res.ok
? res.json()
: res.json().then(err => PromiseRejectionEvent.reject(err));
})
.then(data => data);
};
This is the state manager:
const AuthState = (props) => {
const initialState = {
token: null,
isAuth: false,
errorMsg: null,
user: {},
};
const [state, dispatch] = useReducer(AuthReducer, initialState);
const history = useHistory();
const handleLogin = (formData) => {
login(formData)
.then((res) => {
console.log(res);
// {token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjp7I…zk3fQ.Qx9zDeXBecToIEScCTDXzkBiTnATHab4cnyg0aSMdLE"}
res && res.token
? dispatch({ type: LOGIN_SUCCESS, payload: res })
: dispatch({ type: LOGIN_FAIL, payload: res });
})
.then(() => {
closeAllPopups();
console.log('jwt: ', localStorage.getItem('jwt'));
// {token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjp7I…zk3fQ.Qx9zDeXBecToIEScCTDXzkBiTnATHab4cnyg0aSMdLE"}
console.log('token: ', state.token);
// token: null
console.log('isAuth: ', state.isAuth);
// isAuth: false
history.push('/');
})
.catch((err) => dispatch({ type: LOGIN_FAIL, payload: err.toString() }));
};
This is what is in the reducer:
import {
LOGIN_SUCCESS,
LOGIN_FAIL,
} from '../types';
export default (state, action) => {
switch (action.type) {
case LOGIN_SUCCESS:
localStorage.setItem('jwt', action.payload.token);
return {
...state,
token: action.payload.token, // this is not changing state
isAuth: true, // this is also not changing state
};
case LOGIN_FAIL:
return {
...state,
token: null,
user: {},
isAuth: false,
errorMsg: action.payload,
};
default:
return state;
}
};

Issue
It's a bit unclear what you really want the logic flow to be, but React state updates are asynchronous and the state from the render cycle the handleLogin callback is invoked in is enclosed in callback scope for the life of that function. Just because React state updates are asynchronous doesn't mean they can be awaited on either.
Solution
From what I can tell you want to call login, and upon login success dispatch an action, close popups, log some values, and navigate home. Other than logging updated state this can all be completed in the first thenable.
const handleLogin = (formData) => {
login(formData)
.then((res) => {
console.log(res);
res?.token
? dispatch({ type: LOGIN_SUCCESS, payload: res })
: dispatch({ type: LOGIN_FAIL, payload: res });
closeAllPopups();
history.push('/');
})
.catch((err) => {
dispatch({ type: LOGIN_FAIL, payload: err.toString() });
});
};
Use an useEffect hook to log any state updates.
useEffect(() => {
console.log('jwt: ', localStorage.getItem('jwt'));
console.log('token: ', state.token);
console.log('isAuth: ', state.isAuth);
}, [state]);

Related

How to use Context with PassportJs Auth

I'm trying to let users log in and register using google Auth, I'm using Context to let the app knows if the user is signed in or not, Register and Login works fine, but i didn't know how to configure Google login.
Register.jsx (Register Page):
const [username,setUsername] = useState("");
const [email,setEmail] = useState("");
const [password,setPassword] = useState("");
const [repeatPassword, setRepeatPassword] = useState("");
const { dispatch, isFetching } = useContext(Context);
const handleSubmit = async (e) => {
e.preventDefault();
dispatch({ type: "REGISTER_START" });
setError(false);
try {
const res = await axios.post("/register", {
username,
email,
password,
repeatPassword,
});
dispatch({ type: "REGISTER_SUCCESS", payload: res.data });
} catch (err) {
dispatch({ type: "REGISTER_FAILURE" });
setError(true);
}
};
Login.jsx (Login Page):
const userRef = useRef();
const passwordRef = useRef();
const { dispatch, isFetching } = useContext(Context);
const [error, setError] = useState(false);
const handleSubmit = async (e) => {
e.preventDefault();
dispatch({ type: "LOGIN_START" });
try {
const res = await axios.post("/login", {
email: userRef.current.value,
password: passwordRef.current.value,
});
dispatch({ type: "LOGIN_SUCCESS", payload: res.data });
} catch (err) {
dispatch({ type: "LOGIN_FAILURE" });
setError(true)
}
};
Context Actions.js
export const LoginStart = (userCredentials) => ({
type: "LOGIN_START",
});
export const LoginSuccess = (user) => ({
type: "LOGIN_SUCCESS",
payload: user,
});
export const LoginFailure = () => ({
type: "LOGIN_FAILURE",
});
// THE SAME FOR REGISTER
Context Reducer.js
const Reducer = (state, action) => {
switch (action.type) {
case "LOGIN_START":
return {
user: null,
isFetching: true,
error: false,
};
case "LOGIN_SUCCESS":
return {
user: action.payload,
isFetching: false,
error: false,
};
case "LOGIN_FAILURE":
return {
user: null,
isFetching: false,
error: true,
};
// THE SAME FOR REGISTER
default:
return state;
}
};
export default Reducer;
I tried something like this but it didn't work
const handleGoogleLogin = async (e) => {
window.open("http://localhost:4000/auth/google/callback", "_self");
dispatch({ type: "LOGIN_START" });
try {
const res = await fetch("/login/success", {
method: "GET",
credentials: "include",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
"Access-Control-Allow-Credentials": true,
},
});
dispatch({ type: "LOGIN_SUCCESS", payload: res.user });
} catch (err) {
dispatch({ type: "LOGIN_FAILURE" });
setError(true)
console.log(err)
}
};
I don't if I have to make new actions just for social media authentication, or if this is the correct way but I'm missing something.
This is Auth.js route:
router.get("/login/success", (req, res) => {
if (req.user) {
res.status(200).json({
error: false,
message: "succesfull",
user: req.user,
});
} else {
res.status(403).json({ error: true, message: "Not Authorized" });
}
});

how to catch login error authorization 401 in React, Reducer

I'm trying to handle the login of a react app using reducer.
Here is the reducer fuction =>
export const AuthReducer = (initialState, action) => {
switch (action.type) {
case "REQ_LOGIN":
return {
...initialState,
loading: true
};
case "REQ_REGISTER":
return {
...initialState,
loading: true
};
case "LOGIN_OK":
return {
id: action.payload.user,
token: action.payload.token,
rol: action.payload.rol,
loading: false
};
case "LOGIN_FAIL":
return {
...initialState,
loading: false,
errorMessage: action.error
};
case "LOGOUT":
return {
id: undefined,
token: undefined,
loading: false
};
default:
throw new Error(`Unhandled action type: ${action.type}`);
}
};
and this is the fuction I user to dispatch the login =>
const requestOptions = (sendData) => {
return {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(sendData)
}
};
const fetchUserToDatabase = async (urlPetition, reqData) => {
try {
const res = await fetch(`${BASE_URL}/${urlPetition}`, requestOptions(reqData));
const data = await res.json();
return data;
} catch (error) {
console.error(error)
}
};
export const loginUser = async (dispatch, loginData) => {
localStorage.removeItem('currentUser');
try {
dispatch({ type: "REQ_LOGIN" });
const data = await fetchUserToDatabase("users/login", loginData)
if (data.data) {
dispatch({ type: "LOGIN_OK", payload: data.data });
localStorage.setItem('currentUser', JSON.stringify(data.data));
}
} catch (error) {
dispatch({ type: 'LOGIN_FAIL', error: error });
}
};
Then whenever I need to login I call loginUser(dispatch, loginForm), for example.
The problem is that the 401 unauthorized error is not being handled by the try, catch but is being sent in the response as res.status. so I literally don't know how to catch it outside the reducer and the actions function.
Error auth 401

GraphQL + Redux + JWT User Authentication

I am working on trying to set up JWT authentication using qraphql and redux.
Currently the register function is working and posts the new user to the database, but I cant seem to get the loadUser function to work.
authAction.js
const API_URL = 'https://my-server.com/api';
export const register = ({name, email, password}) => dispatch => {
axios.post(
API_URL, {
query: `mutation {
userSignup( name: "${name}", email: "${email}", password: "${password}"){
name,
email,
password
}
}
`,
})
.then(res =>
dispatch({
type: REGISTER_SUCCESS,
payload: res.data.data
}))
.catch(err => {
dispatch({
type: REGISTER_FAIL
})
})
}
export const loadUser = (email, password) => dispatch => {
dispatch({type: USER_LOADING})
axios.post(
API_URL, {
query: `query {
userLogin(email: "${email}", password: "${password}"){
email,
password,
token
}
}
`,
})
.then(res => dispatch({
type: USER_LOADED,
type: LOGIN_SUCCESS,
payload: res.data.data
}))
.catch(err => {
dispatch(retrunErrors(err.response.data, err.response.status));
dispatch({
type: AUTH_ERROR
});
});
}
Both of these functions should return the token, and the reducer should set the token to localStorage.
authReducer.js
...
case LOGIN_SUCCESS:
case REGISTER_SUCCESS:
localStorage.setItem('token', action.payload.token)
console.log(action.payload)
return {
...state,
...action.payload,
isAuthenticated: true,
isLoading: false,
}
...
I did have the login working using hooks (did not get to configuring the signUp), but it was all in app.js and it really needed to be broken down. I was told that i needed to move everything to redux, so here i am. I have dug through tons of documentation, but cant find the solution.
FWIW, here is the old app.js (its long)
const API_URL = 'https://my-server.com/api';
function App() {
const initialLoginState = {
isLoading: false,
userName: null,
userToken: null,
};
const setUser = (token, user) => {
if (token) {
axios.defaults.headers.common.Authorization = `Bearer ${token}`;
} else {
delete axios.defaults.headers.common.Authorization;
}
return {type: 'LOGIN', id: user.email, token: token};
};
// Set a user after login or using local (AsyncStorage) token
const setUserLocally = async (key, value) => {
// Set token & user object
// const items = [
// ['userToken', token],
// ['user', JSON.stringify(user)],
// ];
await localStorage.setItem(key, value);
};
const unsetUserLocally = () => {
// Remove token
localStorage.removeItem('userToken');
localStorage.removeItem('user');
return {type: 'LOGOUT'};
};
const loginReducer = (prevState, action) => {
switch (action.type) {
default:
return {
...prevState,
userToken: action.token,
isloading: false,
}
case 'RETRIEVE_TOKEN':
return {
...prevState,
userToken: action.token,
isloading: false,
};
case 'LOGIN':
return {
...prevState,
userName: action.id,
userToken: action.token,
isloading: false,
};
case 'LOGOUT':
return {
...prevState,
userName: null,
userToken: null,
isloading: false,
};
case 'REGISTER':
return {
...prevState,
userName: action.id,
userToken: action.token,
isloading: false,
};
}
};
const [loginState, dispatch] = useReducer(loginReducer, initialLoginState);
const auth = {
signIn: async (userName, password) => {
try {
const response = await axios
.post(
API_URL,
query({
operation: 'userLogin',
variables: {
email: userName,
password: password,
},
fields: ['user {name, email, role}', 'token'],
})
);
let message = 'Please try again.';
if (response.data.errors && response.data.errors.length > 0) {
message = response.data.errors[0].message;
} else if (response.data.data.userLogin.token !== '') {
const token = response.data.data.userLogin.token;
const user = response.data.data.userLogin.user;
setUserLocally('userToken', token).then(() => {
return setUserLocally('user', JSON.stringify(user));
});
dispatch(setUser(token, user));
}
} catch (error) {
console.log(error);
}
},
signOut: () => {
dispatch(unsetUserLocally());
},
signUp: () => {
// setUserToken('sdf');
// setIsLoading(false);
},
getCurrentUser: () => {
//return 'test'; //JSON.parse(AsyncStorage.getItem('userToken'));
let userArr = {};
const value = async () => {
await localStorage.multiGet(['user', 'userToken']).then(
(response) => {
response.forEach((item) => {
userArr[item[0]] = item[1];
});
return userArr;
},
);
};
return value;
},
};
console.log(loginState.userToken)
useEffect(() => {
let users = async () => {
let userToken;
try {
userToken = await localStorage.getItem('userToken');
} catch (e) {
console.log(e);
}
dispatch({type: 'RETRIEVE_TOKEN', token: userToken});
};
users();
}, []);
// if (loginState.isLoading === true) {
// return <Loading />;
// }
return (
<AuthContext.Provider value={auth}>
<Router>
<AuthSwitcher user={loginState.userToken}/>
</Router>
</AuthContext.Provider>
)
}
export default App

useSelector doesn't load the state in time

I'm working on a project of mine using React and Redux.
I'm trying to retrieve the token field from my auth state from my store (which contains an authentication key that I intend to send over to my server side) using useSelectorbut the thing is, that it doesn't load in time. How do I fix this?
I'll add below parts of my code that are associated with this issue:
LikeButton.js: (here is the part where I'm trying to retrieve token)
...
const LikeButton = ({ postId }) => {
const classes = useStyles();
const [isLiked, setIsLiked] = useState(false);
const isMount = useIsMount();
const dispatch = useDispatch();
const { token } = useSelector((state) => state.auth);
const { likedPostsIds } = useSelector((state) => state.likes);
useEffect(() => {
if (token) dispatch(fetchLikedPosts(token));
}, [token]);
...
likeActions.js:
...
export const fetchLikedPosts = ({ token }) => (dispatch) => {
fetch("http://localhost:8080/graphql", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: fetchLikedPostsQuery(),
})
.then((res) => res.json())
...
authActions.js
...
export const signIn = ({ password, email }) => (dispatch) => {
dispatch({
type: IS_AUTH_LOADING,
});
fetch("http://localhost:8080/graphql", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: signInQuery(password, email),
})
.then((res) => res.json())
.then((resData) => {
if (resData.errors) {
dispatch(showNotification("User authentication has failed!", ERROR));
dispatch({
type: AUTH_ERROR,
});
} else {
setLocalStorageAuth(resData.data.signIn.token, expiryDate);
dispatch({
type: SIGN_IN,
payload: resData.data.signIn,
});
}
})
.catch((err) => {
dispatch(
showNotification(
"User authentication has failed. Please try again later.",
ERROR
)
);
dispatch({
type: AUTH_ERROR,
});
emptyLocalStorage();
});
};
...
auth.js:
...
const initialState = {
token: localStorage.getItem("token"),
isAuth: localStorage.getItem("isAuth"),
expiryDate: localStorage.getItem("expiryDate"),
isLoading: false,
};
export default function authReducer(state = initialState, action) {
switch (action.type) {
case REGISTER_LOADING:
case IS_AUTH_LOADING:
return {
...state,
isLoading: true,
};
case SIGN_IN:
return {
token: action.payload.token,
isAuth: true,
isLoading: false,
};
case AUTH_ERROR:
case LOGIN_FAIL:
case REGISTER_FAIL:
case LOGOUT_SUCCESS:
return {
token: null,
isAuth: false,
isLoading: false,
};
case CREATE_USER:
return {
...state,
isLoading: false,
};
default:
return state;
}
}
...
The fetchLikedPosts function expects an object with a token key as an argument. the effect currently passes just the token as an argument. Try
useEffect(() => {
// note the change from token to {token}
if (token) dispatch(fetchLikedPosts({token}));
}, [token]);

Current user data after login in React-Redux

I'm working on an auth system in React-Redux.After successfull login, I want to keep the current data.I can't do it right tho.
ACTIONS CODE:
// ** LOGIN USER **
export const login = ({ email, password }) => (dispatch) => {
const body = JSON.stringify({ email, password });
axios
.post('/login', body, getHeaders())
.then(console.log(body))
.then((res) => {
dispatch({
type: LOGIN_SUCCESS,
payload: res.data,
});
history.push('/');
})
.then(() => {
dispatch({
type: USER_LOADED,
});
})
.catch((err) => {
console.log(err);
dispatch(returnErrors(err.message, err.id, 'LOGIN_FAIL'));
dispatch({
type: LOGIN_FAIL,
});
});
};
How can I store the user data in USER_LOADED type?It works only before the first reload.
REDUCERS CODE:
const initialState = {
token: localStorage.getItem('token'),
isAuthenticated: null,
isLoading: false,
user: null,
profileInitialized: false,
profileData: null,
};
export default function (state = initialState, action) {
switch (action.type) {
// USER AUTHENTICATION
case USER_LOADING:
return {
...state,
isLoading: true,
};
case USER_LOADED:
return {
...state,
...action.payload,
isAuthenticated: true,
isLoading: false,
};
case LOGIN_SUCCESS:
case REGISTER_SUCCESS:
localStorage.setItem('token', action.payload.token);
return {
...state,
...action.payload,
isAuthenticated: true,
isLoading: false,
};
case AUTH_ERROR:
case LOGIN_FAIL:
case LOGOUT_SUCCESS:
case REGISTER_FAIL:
localStorage.removeItem('token');
+++
I should add that JWT stays after reload so user is connected but with no username shown for example.
Right after the login redirect
After Reload
I know the code
.then(() => {
dispatch({
type: USER_LOADED,
});
})
is wrong but I don't know how to make it work
I assume the user data is from POST /login. If this is the case, I think you need to remove .then(console.log(body)) first, since it doesn't return the response to the next .then function. Also, you need to return the user data down the promise chain. I hope that helps.
axios
.post('/login', body, getHeaders())
.then((res) => {
dispatch({
type: LOGIN_SUCCESS,
payload: res.data,
});
history.push('/');
return res.data; // pass the user data down to the promise chain
}).then((user) => {
dispatch({
type: USER_LOADED,
payload: user
});
})
EDIT
Based on the comment in the original question. The question is about persisting data in browser and pull it back when USER_LOADED is triggered.
I think you can use the local storage to store the user information just like how you do it with token for the initialState. So it will always try to use saved user info.
After the POST call, you can add the user info from the payload into the local storage. So it will still be available after page refresh.
axios
.post('/login', body, getHeaders())
.then((res) => {
dispatch({
type: LOGIN_SUCCESS,
payload: res.data,
});
// I assume user data is part of the `POST /login` payload
localStorage.setItem('user', res.data.user)
history.push('/');
return res.data; // pass the user data down to the promise chain
})
I hope this answers your question.

Resources