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.
Related
user.service.ts
async findWithMail(email:string):Promise<any> {
return this.userRepository.findOne({email});
}
auth.service.ts
async signup(email:string,password:string,name?:string,surname?:string,phone:string){
if(email) {
const users = await this.userService.findWithMail(email);
if(users) {
throw new BadRequestException('email in use');
}
}
if(!password) return {error:"password must be!"};
const salt = randomBytes(8).toString('hex');
const hash = (await scrypt(password,salt,32)) as Buffer;
const result = salt + '.' +hash.toString('hex');
password = result;
const user = await
this.userService.create(email,password,name,surname,phone);
return user;
}
auth.service.spec.ts
let service:AuthService;
let fakeUsersService: Partial<UserService>;
describe('Auth Service',()=>{
beforeEach(async() => {
fakeUsersService = {
findWithMail:() => Promise.resolve([]),
create:(email:string,password:string) => Promise.resolve({email,password} as User),
}
const module = await Test.createTestingModule({
providers:[AuthService,{
provide:UserService,
useValue:fakeUsersService
}],
}).compile();
service = module.get(AuthService);
});
it('can create an instance of auth service',async()=> {
expect(service).toBeDefined();
})
it('throws an error if user signs up with email that is in use', async () => {
await service.signup('asdf#asdf.com', 'asdf')
});
})
When ı try to run my test its give me error even this email is not in database its give error: BadRequestException: email in use. I couldnt figure out how to solve problem
You can use isExists method instead of findOne.
Also you can add extra check for your findWithMail method. Check the length of db request result. Like if (dbReqResult.length === 0) return false; else true
please put your attention on your mocked user service, especially on findWithEmail function, this part
beforeEach(async() => {
fakeUsersService = {
findWithMail:() => Promise.resolve([]),
create:(email:string,password:string) =>
Promise.resolve({email,password} as User),
}
...
try to resolve the promise to be null not [] (empty array) or change your if(users) on your auth.service to be if(users.length > 0), why? it because empty array means to be thruthy value so when run through this process on your auth.service
if(email) {
const users = await this.userService.findWithMail(email);
// on this part below
if(users) {
throw new BadRequestException('email in use');
}
}
the 'users' executed to be truthy value so it will invoke the error. I hope my explanation will help you, thank you
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, and just wanted some advice on error handling.
I have my fetch request in an async function, this function is in another folder and is being imported in my App.js file. Im doing this because I want to try out testing with mock service worker, and have read its easier with the requests in a separate file.
From looking at my code below, is this best practice for error handling? Is there a better way thats more concise?
Here is my fetch async function, at the moment i've purposely gave the wrong env variable name so it will give me a 401 unauthorised error.
require('dotenv').config()
export const collect = async () => {
const key = process.env.REACT_APP_API_KE
try{
const res = await fetch(`http://api.openweathermap.org/data/2.5/weather?q=london&appid=${key}`)
if(res.status !== 200){
throw new Error(res.status)
}
const data = await res.json()
return data
} catch (error){
let err = {
error: true,
status: error.message,
}
return err
}
}
This is being called in my App.js file (not rendering much at the moment)
import { useState } from 'react'
import { collect } from './utilities/collect'
require('dotenv').config()
function App() {
const [data, setData] = useState("")
const [error, setError] = useState({ error: false, status: "" })
const handleFetch = async () => {
let newData = await collect()
if(newData.error){
setError({ error: newData.error, status: newData.status })
}else {
setData(newData)
}
}
return (
<div className="App">
<h1>weather</h1>
<button onClick={handleFetch}>fetch</button>
</div>
);
}
export default App;
Any help or advise would be great.
When writing an abstraction around Promises or async and await one should make sure it is used appropriately, that is a good Promse must allow it consumer to use it then and catch method or should allow it consumer use try and catch to consume it and provide appropriate information on Errors
From Your code, The abstraction doesnt gives back an appropriate response and doesnt follow the standard behavior of a promise it always resolve and never reject and though the code works its implementation of the collect is different from a standard Promise and wont be nice for a standard code base, for example a good abstraction will provide error information returned from the third party api
Appropriate way to amend code
The third party api returns this response
{
"cod":401,
"message": "Invalid API key. Please see http://openweathermap.org/faq#error401 for more info."}
This should be your implementation
// Your fetch functon abstraction modified
require('dotenv').config()
const collect = async () => {
const key = process.env.REACT_APP_API_KE;
const res = await fetch(
`http://api.openweathermap.org/data/2.5/weather?q=london&appid=${key}`,
);
if (res.status !== 200) {
const error = await res.json();
throw {message: error.message,status:error.cod};
}
const data = await res.json();
return data;
};
Your app component should now be like this
import { useState } from 'react'
import { collect } from './utilities/collect'
require('dotenv').config()
function App() {
const [data, setData] = useState("")
const [error, setError] = useState({ error: false, status: "" })
const handleFetch = async () => {
try {
let newData = await collect()
setData(newData)
} catch(e){
setError({ error: e.message, status: e.status })
}
}
return (
<div className="App">
<h1>weather</h1>
<button onClick={handleFetch}>fetch</button>
</div>
);
}
export default App;
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!
I'm trying to apply the ajax method posted here: https://github.com/redux-observable/redux-observable/blob/master/docs/basics/Epics.md
import { ajax } from 'rxjs/ajax';
// action creators
const fetchUser = username => ({ type: FETCH_USER, payload: username });
const fetchUserFulfilled = payload => ({ type: FETCH_USER_FULFILLED, payload });
// epic
const fetchUserEpic = action$ => action$.pipe(
ofType(FETCH_USER),
mergeMap(action =>
ajax.getJSON(`https://api.github.com/users/${action.payload}`).pipe(
map(response => fetchUserFulfilled(response))
)
)
);
// later...
dispatch(fetchUser('torvalds'));
When trying this method, I get the message:
TypeError: Object(...)(...).pipe is not a function
So pipe doesn't appear to exist. (It concerns the second pipe after the ajax call).
How do I fix this?
I installed the following dependencies:
"rxjs": "^6.5.2",
"rxjs-compat": "^6.5.2",
Edit:
I changed my code to ajax.get and the calling code:
export const fetchTrendingEpic = action$ => action$.pipe(
ofType(FETCH_TRENDING_REQUEST),
mergeMap(async(action) => {
const res = await fetchPostStats(action.payload);
console.log(res);
res.pipe(
map(response => {
console.log('response', response);
setTrendingPlaces({trendingPlaces: response});
})
)
})
);
The res was properly printed (showing an observable), but now I get an error:
TypeError: Cannot read property 'type' of undefined
This is how I create my store in dev:
const createEnhancer = (epicMiddleware) => {
const middleware = [ epicMiddleware, createLogger() ];
let enhancer;
if (getEnvironment() === 'development') {
enhancer = composeWithDevTools(
applyMiddleware(...middleware),
// other store enhancers if any
);
};
export default (initialState) => {
const epicMiddleware = createEpicMiddleware();
const enhancer = createEnhancer(epicMiddleware);
const store = createStore(rootReducer, initialState, enhancer);
epicMiddleware.run(rootEpic);
return store;
}
Edit note:
This is code executed in NodeJS (SSR).
I'm struggling with this, don't really understand how this can be so hard to get working without error.
Can't quite see how the example code will ever work when ajax.getJSON returns a promise, not an Observable...
My service that called the ajax request was marked with async because the previous implementation used Axios before the refactoring.
This was the reason why my console logged a promise as return value of the function.
Removing async returns the Observable as expected.
As #Dez mentioned in the comments, there is also need to add return values.
And last but not least, rxjs ajax does not work in a NodeJS environment.
Therefore, based on this thread:
https://github.com/ReactiveX/rxjs/issues/2099
I have found that someone created the following npm package that I will try out shortly: https://github.com/mcmunder/universal-rxjs-ajax
Axios works fine with rxjs in nodejs, just doing something like this...
const { from } = require('rxjs');
const { map } = require('rxjs/operators');
const axios = require('axios');
const responsePromise = axios.get('https://jsonplaceholder.typicode.com/todos/1');
const response$ = from(responsePromise);
response$
.pipe(
map(response => ({ type: 'RESPONSE_RECEIVED', payload: response.data}))
)
.subscribe(console.log);