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]);
Related
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" });
}
});
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
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]);
I just created my sample Redux tool kit with js files. now I am trying to convert them to typescript. some of the error is now fixed. but I have no idea how to fix the two errors below.
Property 'name' does not exist on type 'void'. <<<< also for email, password... and token
A computed property name must be of type 'string', 'number', 'symbol', or 'any' <<<< for signupUser.fulfilled, signupUser.pending... so on
Please refer to my comments within the code below and please help me to how to solve the problems
import { createSlice, createAsyncThunk } from '#reduxjs/toolkit';
import axios from "axios";
export interface UserState {
username: string;
email: string;
isFetching: boolean;
isSuccess: boolean;
isError: boolean;
errorMessage: string;
}
interface check {
name: string;
email: string;
password: string;
}
const initialState: UserState = {
username: "",
email: "",
isFetching: false,
isSuccess: false,
isError: false,
errorMessage: "",
}
export const signupUser = createAsyncThunk(
'users/signupUser',
//
//
// async ({ name, email, password} ... <<< This is the problem 1.
//
async ({ name, email, password }, thunkAPI) => {
try {
const response = await fetch(
'https://mock-user-auth-server.herokuapp.com/api/v1/users',
{
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
name,
email,
password,
}),
},
);
const data = await response.json();
console.log('data', data);
if (response.status === 200) {
localStorage.setItem('token', data.token);
return { ...data, username: name, email };
}
return thunkAPI.rejectWithValue(data);
} catch (e) {
console.log('Error', e.response.data);
return thunkAPI.rejectWithValue(e.response.data);
}
},
);
export const loginUser = createAsyncThunk(
'users/login',
async ({ email, password }, thunkAPI) => {
try {
const response = await fetch(
'https://mock-user-auth-server.herokuapp.com/api/v1/auth',
{
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
email,
password,
}),
},
);
const data = await response.json();
console.log('response', data);
if (response.status === 200) {
localStorage.setItem('token', data.token);
return data;
}
return thunkAPI.rejectWithValue(data);
} catch (e) {
console.log('Error', e.response.data);
thunkAPI.rejectWithValue(e.response.data);
}
},
);
export const fetchUserBytoken = createAsyncThunk(
'users/fetchUserByToken',
async ({ token }, thunkAPI) => {
try {
const response = await fetch(
'https://mock-user-auth-server.herokuapp.com/api/v1/users',
{
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: token,
'Content-Type': 'application/json',
},
},
);
/* test case for Axios
const response = await axios.get(
'https://mock-user-auth-server.herokuapp.com/api/v1/users',
{
headers: {
Accept: 'application/json',
Authorization: token,
'Content-Type': 'application/json',
},
},
);
*/
const data = await response.json();
console.log('data', data, response.status);
if (response.status === 200) {
return { ...data };
}
return thunkAPI.rejectWithValue(data);
} catch (e) {
console.log('Error', e.response.data);
return thunkAPI.rejectWithValue(e.response.data);
}
},
);
export const userSlice = createSlice({
name: 'user',
initialState: {
username: '',
email: '',
isFetching: false,
isSuccess: false,
isError: false,
errorMessage: '',
},
reducers: {
clearState: state => {
state.isError = false;
state.isSuccess = false;
state.isFetching = false;
return state;
},
},
////
//// [signupUser.fulfilled] ... <<<< this is the problem 2.
////
////
extraReducers: {
[signupUser.fulfilled]: (state, { payload }) => {
console.log('payload', payload);
state.isFetching = false;
state.isSuccess = true;
state.email = payload.user.email;
state.username = payload.user.name;
},
[signupUser.pending]: state => {
state.isFetching = true;
},
[signupUser.rejected]: (state, { payload }) => {
state.isFetching = false;
state.isError = true;
state.errorMessage = payload.message;
},
[loginUser.fulfilled]: (state, { payload }) => {
state.email = payload.email;
state.username = payload.name;
state.isFetching = false;
state.isSuccess = true;
return state;
},
[loginUser.rejected]: (state, { payload }) => {
console.log('payload', payload);
state.isFetching = false;
state.isError = true;
state.errorMessage = payload.message;
},
[loginUser.pending]: state => {
state.isFetching = true;
},
[fetchUserBytoken.pending]: state => {
state.isFetching = true;
},
[fetchUserBytoken.fulfilled]: (state, { payload }) => {
state.isFetching = false;
state.isSuccess = true;
state.email = payload.email;
state.username = payload.name;
},
[fetchUserBytoken.rejected]: state => {
console.log('fetchUserBytoken');
state.isFetching = false;
state.isError = true;
},
},
});
export const { clearState } = userSlice.actions;
export const userSelector = (state: { user: UserState}) => state.user;
You'll need to define a type for the argument you put in there. If you only want to type the first argument and are fine with the defaults for thunkAPI, you can use
export const signupUser = createAsyncThunk(
'users/signupUser',
async ({ name, email, password }: ArgumentType, thunkAPI) => {
Otherwise, you'll have to declare generic arguments as described in https://redux-toolkit.js.org/usage/usage-with-typescript#createasyncthunk
const fetchUserById = createAsyncThunk<
// Return type of the payload creator
MyData,
// First argument to the payload creator
number,
{
dispatch: AppDispatch
state: State
extra: {
jwt: string
}
}
>('users/fetchById', async (userId, thunkApi) => {
For problem #2 please see that we discourage the object map notation for extraReducers as it is less typesafe in general and does not work in TypeScript. Please use the builder notation as described in https://redux-toolkit.js.org/api/createSlice#the-extrareducers-builder-callback-notation
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