Explanation
I am trying to use AsyncStorage in order to save a Token during Login. The token is recieved from my backend as a response after the user presses the Login button. After a successful login the screen goes to the ProfileScreen where I try to retrieve the saved token.
Problem
When I try to retrieve the item in the ProfileScreen and console log it I seem to get a Promise object filled with other objects and inside there I can see my value. How do I recieve the value ? (or should I say how do I fulfill the promise :) )
Code
Utilities/AsyncStorage.js(Here I have my helper functions for store and retrieve item)
const keys = {
jwtKey: 'jwtKey'
}
const storeItem = async (key, item) => {
try {
var jsonOfItem = await AsyncStorage.setItem(key, JSON.stringify(item));
console.log('Item Stored !');
return jsonOfItem;
} catch (error) {
console.log(error.message);
}
};
const retrieveItem = async key => {
try {
const retrievedItem = await AsyncStorage.getItem(key);
const item = JSON.parse(retrievedItem);
console.log('Item Retrieved !');
return item;
} catch (error) {
console.log(error.message);
}
return;
};
LoginScreen.js(Here, after login button is pressed I recieve a response from my backend with Token)
const LoginScreen = ({componentId}) => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const loginPressed = async () => {
await axios
.post('localhost', {
email,
password,
})
.then(function(res) {
console.log(res);
storeItem(keys.jwtKey, res.data.token);
push(componentID, views.profileScreen());
})
.catch(function(error) {
console.log(error);
});
};
ProfileScreen.js(On this screen I try to retrieve the token because I will be using it)
const ProfileScreen = ({componentID}) => {
let testingAsync = retrieveItem(keys.jwtKey);
console.log(testingAsync);
The console log gives me promise object filled with other objects.
Promise{_40:0, _65:0 , _55:null, _72:null}
And inside the _55 I can find the value of the token.
Thank you for your comments! I solved it by using .then() in my ProfileScreen as #Bergi mentioned in his comment. Then after I recieved the second comment I made an async&await function in my ProfileScreen inside a useEffect to prevent it from repeating itself which solved the issue for me!
Related
fetchedAuthor is a author object. isFollow is his follower count. when someone clicks on folow isFollow changes. when isFollow changes i want to rerun useEffect. when the author changes, the fetchedAuthor changes but i dont want this useEffect to reRun as this is strictly for follower handling not author handling but at the same time when the author changes i want this useEfffect to know that author has changed so the next time when isFollow changes the useEffect doesnt fetch with the previous fetchedAuthor but the latest value of fetchedAuthor.
useEffect(() => {
setCurrentAuthor(fetchedAuthor) ;
},[fetchedAuthor]) ;
useEffect(async () => {
try {
const response = await fetch(`URL/${currentAuthor}/${isFollow}`);
const data = await response.json();
console.log(data);
} catch (error) {
console.error(error);
}},[isFollow]) ;
would this help me to get the appropriate response ?
Use a ref to store the current fetchedAuthor. Update the ref whenever fetchedAuthor changes. Use the ref's value when calling the api:
const authorRef = useRef(fetchedAuthor);
useEffect(() => {
authorRef.current = fetchedAuthor;
}, [fetchedAuthor]);
useEffect(async() => {
try {
const response = await fetch(`URL/${authorRef.current}/${isFollow}`);
const data = await response.json();
console.log(data);
} catch (error) {
console.error(error);
}
}, [isFollow]);
I don't know why the below code works properly without any error and response is also recorded once the loginHandler() is fired but once I add the dispatch function inside the try block after the response has got, the catch block with the check whether a response has been got or not executes!
function Login() {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [msg, setMsg] = useState("");
const dispatch = useDispatch();
const LoginHandler = async (e) => {
e.preventDefault();
try {
const response = await axios.post(
LOGIN_URL,
JSON.stringify({ username, password }),
{
headers: { "Content-Type": "application/json" },
withCredentials: false,
}
);
console.log(JSON.stringify(response?.data?.access));
dispatch(
login({
username: username,
accessToken: response?.data?.access,
})
);
const userName = useSelector((state) => state.user.value.userName);
const accessToken = useSelector(
(state) => state.user.value.userAccessToken
);
console.log("USER NAME STORED IN STORE = " + userName);
console.log("USER ACESS TOKEN STORED IN STORE = " + accessToken);
setMsg("Login Successful!");
} catch (err) {
if (!err?.response) {
console.log("NO SERVER RESPONSE");
setMsg("NO SERVER RESPONSE!");
} else {
console.log("SOMETHING WRONG!");
setMsg("SOMETHING WENT WRONG!");
}
}
};
return (
<LoginForm />
);
}
export default Login;
The Error I get is,
react-dom.development.js:16227 Uncaught (in promise) Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.
at Object.throwInvalidHookError (react-dom.development.js:16227:9)
at useContext (react.development.js:1618:21)
at useReduxContext (useReduxContext.js:21:24)
at useSelector2 (useSelector.js:40:9)
at LoginHandler (Login.jsx:49:22)
I have also tried to put the dispatch function inside another function and then call it, but the problem persists!
The issue it seems is that the code is attempting to use the useSelector hook in a nested function, which breaks the Rules of Hooks.
Only Call Hooks at the Top Level
Don’t call Hooks inside loops, conditions, or nested functions.
Instead, always use Hooks at the top level of your React function,
before any early returns. By following this rule, you ensure that
Hooks are called in the same order each time a component renders.
That’s what allows React to correctly preserve the state of Hooks
between multiple useState and useEffect calls.
Move the useSelector calls out of the callback, or just reference the response values directly.
Use the useEffect hook if you care to log the updated state values.
Example:
function Login() {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [msg, setMsg] = useState("");
const dispatch = useDispatch();
const userName = useSelector((state) => state.user.value.userName);
const accessToken = useSelector(
(state) => state.user.value.userAccessToken
);
useEffect(() => {
console.log("USER NAME STORED IN STORE = " + userName);
console.log("USER ACCESS TOKEN STORED IN STORE = " + accessToken);
}, [userName, accessToken]);
const LoginHandler = async (e) => {
e.preventDefault();
try {
const response = await axios.post(
LOGIN_URL,
JSON.stringify({ username, password }),
{
headers: { "Content-Type": "application/json" },
withCredentials: false,
}
);
console.log(JSON.stringify(response?.data?.access));
dispatch(
login({
username: username,
accessToken: response?.data?.access,
})
);
console.log("USER NAME USED = " + username);
console.log("USER ACCESS TOKEN RETURNED = " + response?.data?.access);
setMsg("Login Successful!");
} catch (err) {
if (!err?.response) {
console.log("NO SERVER RESPONSE");
setMsg("NO SERVER RESPONSE!");
} else {
console.log("SOMETHING WRONG!");
setMsg("SOMETHING WENT WRONG!");
}
}
};
return (
<LoginForm />
);
}
export default Login;
Hopefully a simply one.
I make an API call in my component which brings down some account information such as AccountUid, Category etc, i use state to set these.
useEffect(() => {
fetch(feed_url, {
headers: {
//Headers for avoiding CORS Error and Auth Token in a secure payload
"Access-Control-Allow-Origin": "*",
Authorization: process.env.REACT_APP_AUTH_TOKEN,
},
})
//Return JSON if the Response is recieved
.then((response) => {
if (response.ok) {
return response.json();
}
throw response;
})
//Set the Account Name state to the JSON data recieved
.then((accountDetails) => {
setAccountDetails(accountDetails);
console.log(accountDetails.accounts[0].accountUid);
console.log(accountDetails.accounts[0].defaultCategory);
})
//Log and Error Message if there is an issue in the Request
.catch((error) => {
console.error("Error fetching Transaction data: ", error);
});
}, [feed_url]);
This Works perfectly well and it Logs the correct values in my .then when testing it.
The issue however is that i want to pass these down as props. But i get an error that they are being returned as null (My default state).. i presume as they're jumping ahead.
<div className="App">
<GetAccountName
accountUID={accountDetails.accounts[0].accountUID}
defCategory={accountDetails.accounts[0].defaultCategory}
/>
</div>
How do i pass the the 2 details im logging as props?? I've tried setting default state to "" instead of null and just get that it is undefined.
If you dont want to use conditional render in your child component, so you should try optional chaining
<GetAccountName
accountUID={accountDetails?.accounts?.[0]?.accountUID}
defCategory={accountDetails?.accounts?.[0]?.defaultCategory}
/>
Since fetching is asyncronous, the most common way is to show some loading indicator (like a spinner) & once the data come in, show the component instead.
If you don't need an indicator, you might just return null.
The general idea is to manipulate some intermediary states (e.g. data, isError) based on the promise state.
Check out react-query library example or a lighter abstraction like useFetch hook to see how they manage it.
Here's a sample implementation of useFetch taken from this article:
const useFetch = (url, options) => {
const [response, setResponse] = React.useState(null);
const [error, setError] = React.useState(null);
const [abort, setAbort] = React.useState(() => {});
React.useEffect(() => {
const fetchData = async () => {
try {
const abortController = new AbortController();
const signal = abortController.signal;
setAbort(abortController.abort);
const res = await fetch(url, {...options, signal});
const json = await res.json();
setResponse(json);
} catch (error) {
setError(error);
}
};
fetchData();
return () => {
abort();
}
}, []);
return { response, error, abort };
};
I'm using React Native/Firebase/Redux to build a simple login system. I am trying to work out how to capture errors that happen as a result of failed login attempts.
Here's my authscreen.js:
const [alertShowing, setAlertShowing] = useState(false);
const [alertMessage, setAlertMessage] = useState('');
...
function handleLogin() {
const response = dispatch(login(email, password));
console.log(response);
}
actions.js:
export const login = (email, password) => {
return async (dispatch) => {
try {
const response = await Firebase.auth().signInWithEmailAndPassword(email, password);
dispatch(getUser(response.user.uid));
} catch (e) {
return e;
}
};
};
My console.log(response) above correctly shows me the error message, but this obviously isn't very useful to users. And please note too that I can log in properly when using correct credentials.
What I really want to do in my handleLogin() is check if the response is an error, and if so, setlAlertShowing(true) and setAlertMessage to what I've gotten back from the useDispatch hook so that I may display it nicely to the user.
How should I go about this? TIA.
Firebase errors messages are designed for developers and not standard users friendly. The solution is to identify authentication error code and map with user-friendly messages.
list of error code https://firebase.google.com/docs/auth/admin/errors
You can use function like this to map authError to meaningful error messages.
function mapAuthCodeToMessage(authCode) {
switch (authCode) {
case "auth/invalid-password":
return "Password provided is not corrected";
case "auth/invalid-email":
return "Email provided is invalid";
// Many more authCode mapping here...
default:
return "";
}
}
and use it later
export const login = (email, password) => {
return async (dispatch) => {
try {
const response = await Firebase.auth().signInWithEmailAndPassword(email, password);
dispatch(getUser(response.user.uid));
} catch (e) {
dispatch({type:"authError",message:mapAuthCodeToMessage(e.code)}));
}
};
};
Working on a login form / logout button with React/Redux front end and my own nodejs/express api. Having an issue with the login form. Most of the time it works just fine, but I'm getting erros on a regular basis. First error is forbidden, which tells me that the user is not quite authenticated before send the userDetails request.
Then there's another bug where Redux doesn't change the role of the user, which I need to dynamically render the nav. I'm thinking converting handleLogin to async/await will be the solution, but I believe I'm not doing it right.
import React from 'react';
import { login, userDetails } from '../axios/homeApi';
import { useForm } from 'react-hook-form';
import { useDispatch } from 'react-redux';
import { setLogin, setRole } from '../redux/actions';
const LoginForm = () => {
const { handleSubmit, register, errors } = useForm();
const dispatch = useDispatch();
const handleLogin = values => {
login(values.email, values.password)
.then(res => {
const token = res.data.token;
window.localStorage.setItem('auth', token);
dispatch(setLogin({ loggedIn: true }));
userDetails()
.then(res => {
const role = res.data.data.role;
dispatch (setRole({ role }));
})
})
}
return (
<div>
<form action="" onSubmit={handleSubmit(handleLogin)} className="footer-form">
<input
type="email"
placeholder="Enter Email Here"
name="email"
ref={register({ required: "Required Field" })}
/>
<input
type="password"
placeholder="Enter Password Here"
name="password"
ref={register({
required: "Required Field",
minLength: { value: 6, message: "Minimum Length: 6 Characters" }
})}
/>
{errors.password && errors.password.message}
{errors.email && errors.email.message}
<input type="submit" value="Login" />
</form>
</div>
)
}
export default LoginForm;
Here's my best attempt at converting handleLogin to async/await. I'm trying to understand how I'm supposed to pull data from these calls.
const handleLogin = async values => {
try {
const {data: {token}} = await login(values.email, values.password)
window.localStorage.setItem('auth', token);
console.log(token);
const user = await userDetails();
await dispatch(setLogin({ loggedIn: true}))
await dispatch(setRole(user.data.data.role))
} catch(err) {
console.log(err)
}
}
Any help/guidance on this would be greatly appreciated.
You have to think when you use await, the variable value is the same that returned into res without await.
So if you have:
login(values.email, values.password)
.then(res => {
})
This is like:
var login = await login(values.email, values.password);
So using this logic, this:
login(values.email, values.password)
.then(res => {
const token = res.data.token;
// whatever
userDetails()
.then(res => {
const role = res.data.data.role;
// whatever
})
})
Turn into:
var login = await login(values.email, values.password)
const token = login.data.token;
// do whatever
var userDetails = await userDetails()
const role = userDetails.data.data.role;
// whatever
Check how works this example. The code is "the same". One using .then and the other using await.
runThenFunction();
runAwaitFunction();
function runThenFunction(){
console.log("Enter then function")
this.returnPromise(2000).then(res => {
console.log(res);
this.returnPromise(1000).then(res => {
console.log(res);
});
});
//You can log here and it will be displayed before the promise has been resolved
console.log("Exit then function")
}
async function runAwaitFunction(){
console.log("Enter await function")
var firstStop = await returnPromise(1000);
console.log(firstStop)
var secondStop = await returnPromise(4000);
console.log(secondStop)
// Using await the code "stops" until promise is resolved
console.log("Exit await function")
}
function returnPromise(time){
return new Promise(resolve => setTimeout(() => resolve("hello: "+time+"ms later."), time));
}
Looks like you may have already have an answer, but it looks to me like it may be because you are not waiting on your dispatch of setLogin to complete. I don't know how your setLogin method is setup, but it would have to be a thunk. I found the following post that explains it well.
How to return a promise from an action using thunk and useDispatch (react-redux hooks)?
Consider any object with a then property, which is a function that accepts a callback as its 1st parameter, for example:
let obj = {
then: callback => callback('hello')
};
await converts any such object into the value then provides to the callback. Therefore:
(await obj) === 'hello'
In your example there are two instances where you require the value returned to a then callback:
login(...).then(res => { /* got res */ });
and
userDetails().then(res => { /* got res */ });
Think of await simply as a way of getting the value returned to an object's then callback! In this case the objects are the result of login(...) and userDetails(), and you can convert to:
let res = await login(...);
and
let res = await userDetails();
You can see this also saves a bunch of indentation, one of many reasons people enjoy using async / await!
These conversions from the then-callback-value to await, when inserted into your code, look like:
const handleLogin = async values => {
let loginRes = await login(values.email, values.password);
let token = loginRes.data.token;
window.localStorage.setItem('auth', token);
dispatch(setLogin({ loggedIn: true }));
let userDetailsRes = await userDetails();
let role = userDetailsRes.data.data.role;
dispatch(setRole({ role }));
};
Note that the function must be marked async, and that I've renamed res to a more specific name, since both responses now exist in the exact same scope, and need to be differentiated from each other.
Overall whenever you use then to get ahold of some value in a callback, you are able to convert to the more elegant await equivalent.