Login authentication with axios and formik - reactjs

I'm new to using both axios and formik and likely making a simple mistake here. I'm attempting to check if the user's email and password exists and has been typed correctly. Upon authentication they should be logged in with useContext and then redirected to the homepage.
Currently, after submitting, the form simply remains greyed out without reaching the .then/.catch block. Is it a problem with the way I typed the auth parameter?
const LoginForm = () => {
const authenticate = useContext(AuthContext);
const [serverState, setServerState] = useState();
const handleServerResponse = (ok, msg) => {
setServerState({ok, msg});
};
const handleOnSubmit = (values, actions) => {
axios({
method: "POST",
url: "http://localhost:5000/api/users/login",
data: values,
auth: {
email,
password
}
})
.then(response => {
actions.setSubmitting(false);
actions.resetForm();
handleServerResponse(true, "Logged In!");
})
.catch(error => {
actions.setSubmitting(false);
handleServerResponse(false, error.response.data.error);
});
authenticate.login();
Router.push("/")
};
return (
<Formik
initialValues={{
email: "",
password: "",
}}
validationSchema={Yup.object().shape({
email: Yup.string(),
password: Yup.string(),
})}
onSubmit={handleOnSubmit}
>
{({ isSubmitting }) => (
<Form>
<Field
name="email"
type="email"
fullWidth
component={TextField}
variant="outlined"
label="Email"
/>
<Box pt={1}>
<Field
name="password"
type="password"
fullWidth
component={TextField}
variant="outlined"
label="Password"
/>
</Box>
<Box pt={2}>
<Button
type="submit"
variant="contained"
fullWidth
color="primary"
disabled={isSubmitting}
>
Submit
</Button>
{serverState && (
<Typography className={!serverState.ok ? "errorMsg" : ""}>
{serverState.msg}
</Typography>
)}
</Box>
</Form>
)}
</Formik>
);
};
The Node JS API on the backend:
const login = async (req, res, next) => {
const { email, password } = req.body;
let existingUser;
try {
existingUser = await User.findOne({ email: email });
} catch (err) {
const error = new HttpError("Login failed, please try again later.", 500);
return next(error);
}
if (!existingUser || existingUser.password !== password) {
const error = new HttpError("Invalid credentials, login failed.", 401);
return next(error);
}
res.json({ message: "Logged in!" });
};

The reason why the form is grayed out is because you use isSubmitting and when you submit the form, it is set to true, but inside handleOnSubmit, you have an synchronous function, so you only call actions.setSubmitting(false) once the formik already think you finished the onSubmit.
Another way of doing this is to make handleOnSubmit return a promise and once the promise is resolved, formik will set isSubmitting to false automatically.
This is explained in the docs
IMPORTANT: If onSubmit is async, then Formik will automatically set isSubmitting to false on your behalf once it has resolved. This means you do NOT need to call formikBag.setSubmitting(false) manually. However, if your onSubmit function is synchronous, then you need to call setSubmitting(false) on your own.
So what I recomendo you to do and solve your problem is make handleOnSubmit an async method and return the axios call (return a promise) or use await.
An example of that would be something like
const handleOnSubmit = (values, actions) => {
// returning a promise
return axios({
method: "POST",
url: "http://localhost:5000/api/users/login",
data: values,
auth: {
email,
password
}
})
.then(response => {
actions.setSubmitting(false);
actions.resetForm();
handleServerResponse(true, "Logged In!");
})
.catch(error => {
actions.setSubmitting(false);
handleServerResponse(false, error.response.data.error);
});
// this shouldn't be outside the .then/.catch
// if you are going to use .then/.catch, put the above line inside it
// authenticate.login();
// Router.push("/")
};
Or with async/await
// using async
const handleOnSubmit = async (values, actions) => {
// using await in the axios call
try {
const response = await axios({
method: "POST",
url: "http://localhost:5000/api/users/login",
data: values,
auth: {
email,
password
}
})
actions.setSubmitting(false);
actions.resetForm();
handleServerResponse(true, "Logged In!");
} catch(error) {
actions.setSubmitting(false);
handleServerResponse(false, error.response.data.error);
}
authenticate.login();
Router.push("/")
};

So what I recommend you to do and solve your problem is to make handle On Submit an async method and return the await.
const handleOnSubmit = async (values, actions) => {
try {
const response = await axios({
method: "POST",
url: "http://localhost:5000/api/users/login",
data: values,
auth: {
email,
password
}
})
actions.setSubmitting(false);
actions.resetForm();
handleServerResponse(true, "Logged In!");
} catch(error) {
actions.setSubmitting(false);
handleServerResponse(false, error.response.data.error);
}
authenticate.login();
Router.push("/")
};

Related

Manual Cache Update | Dispatch function is not firing

I have the following API slice:
const mainApi = createApi({
reducerPath: 'mainApi',
baseQuery: fetchBaseQuery({
baseUrl: 'http://localhost:3001/api/v1',
}),
tagTypes: ['$USER'],
endpoints: (builder) => ({
login: builder.mutation({
query: ({ email, password }) => ({
url: '/auth/login',
method: 'POST',
body: {
email,
password,
},
credentials: 'include',
}),
transformResponse: (res) => res.data,
// invalidatesTags: ['$USER'],
async onQueryStarted(payload, { dispatch, queryFulfilled }) {
console.log('LOGIN QUERY STARTED');
try {
const { data: $USER } = await queryFulfilled;
dispatch(
mainApi.util.updateQueryData('getLoggedInUser', null, (draft) => {
console.log('METHOD DISPATCH RAN!!');
Object.assign(draft, $USER);
})
);
} catch (err) {}
},
}),
getLoggedInUser: builder.query({
query: () => ({
url: '/auth/logged-in-user',
credentials: 'include',
}),
transformResponse: (res) => res.data,
providesTags: ['$USER'],
}),
}),
});
and I have the following component which uses this API slice:
login.js
export default function Login() {
const navigate = useNavigate()
const [login, { isError, isSuccess, isLoading, error, data }] = useLoginMutation()
useEffect(() => {
switch (true) {
case isError:
console.log("ERROR!")
break
case isSuccess:
console.log('SUCCESSFULY LOGGED IN!, userdata:', data)
navigate('/', { replace: true })
}
}, [isError, isSuccess])
const formSubmit = (e) => {
e.preventDefault()
const formData = new FormData(e.target)
const email = formData.get('email')
const password = formData.get('password')
login({ email, password })
}
return (
<form onSubmit={formSubmit}>
<TextField name="email" type="email" required />
<TextField name="password" type="password" required />
<button type="subbmit">LOGIN</button>
</form>
)
}
when I input the email and the password on the front-end and click LOGIN I can successfully log in.
but it prints the following in the console:
LOGIN QUERY STARTED
SUCCESSFULY LOGGED IN!
but it does not print METHOD DISPATCH RAN!! which is causing other things in my app to fail, can you help me?

MISSING_REQUEST_URI firebase

I am using firebae auth to log in and sign up for my react app. I can get sign up working but I am having issues with log in. I followed the https://firebase.google.com/docs/reference/rest/auth#section-sign-in-email-password for the api link.
I followed the same code for log in than I did for sign up but I keep getting issues with it.
This is the error message i get when i try to log in.
Object {
"error": Object {
"code": 400,
"errors": Array [
Object {
"domain": "global",
"message": "MISSING_REQUEST_URI",
"reason": "invalid",
},
],
"message": "MISSING_REQUEST_URI",
},
}
This is my user action page for my app for log in.
export const login = (email, password) => {
return async dispatch => {
const response = await fetch('https://identitytoolkit.googleapis.com/v1/accounts:signInWithIdp?key=[API]', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ //javascript to json
//key value pairs of data you want to send to server
// ...
email: email,
password: password,
returnSecureToken: true
})
});
// console.log(await response.json());
const data = await response.json(); // json to javascript
console.log(data);
if (!response.ok) {
Alert.alert("There was a issue with logging in ")
} else {
await SecureStore.setItemAsync('email', data.email);
await SecureStore.setItemAsync('token', data.idToken);
dispatch({ type: SIGNUP, payload: { email: data.email, idToken: data.idToken } })
}
};
}
This is my user action page for my app for sign in.
export const signup = (email, password) => {
return async dispatch => {
const response = await fetch('https://identitytoolkit.googleapis.com/v1/accounts:signUp?key=[API]', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ //javascript to json
//key value pairs of data you want to send to server
// ...
email: email,
password: password,
returnSecureToken: true
})
});
// console.log(await response.json());
const data = await response.json(); // json to javascript
console.log(data);
if (!response.ok) {
Alert.alert("There was a issue with signing in ")
} else {
await SecureStore.setItemAsync('email', data.email);
await SecureStore.setItemAsync('token', data.idToken);
dispatch({ type: SIGNUP, payload: { email: data.email, idToken: data.idToken } })
}
};
};
Then this is my log in screen.
const LoginScreen = ({ navigation }) => {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const dispatch = useDispatch()
async function load() {
let emailFromSecureStore = await SecureStore.getItemAsync('email');
let tokenFromSecureStore = await SecureStore.getItemAsync('token');
if (emailFromSecureStore && tokenFromSecureStore) {
console.log("success", emailFromSecureStore);
// dispatch(restoreUser(emailFromSecureStore, tokenFromSecureStore));
} else {
console.log("failure");
}
}
useEffect(() => {
load(); // uncomment to read from secure store
}, [])
return (
<View style={styles.ViewStyle}>
<TextInput placeholder='Email'
onChangeText={setEmail}
value={email}
style={styles.loginText}/>
<TextInput placeholder='Password'
onChangeText={setPassword}
value={password}
style={styles.loginText}/>
<View style={styles.gap}>
</View>
<Pressable style={styles.buttonStyle} onPress={() => dispatch(login(email, password))}>
<Text style={styles.text}>Log in</Text>
</Pressable>
</View>
);
}

React: call parent hook which results in child not being rendered safely

I am trying to implement JWT based authentication in React. I followed this tutorial from digitalocean, but used axios (which is promise based) instead of the fetch API.
If authentication fails, the error message is displayed as required, and on a successful login, the correct page is being displayed. React throws the following error however:
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 the componentWillUnmount method.
I guess this is due to the fact that I call the loginpage parent's hook to set the JWT from within the .then() call from the axios promise, but the parent will stop rendering the loginpage and it's axios promise as soon as the jwt is set. How would I go about solving this neatly?
function App() {
const [token, setToken] = useState();
if(!token) {
return <EmployeeLogin setToken={setToken} />;
}
return (
<main>
<Switch>
<Route path="/someroute" component={someComponent} exact />
</Switch>
</main>
);
}
export default App;
// EmployeeLogin.jsx
const styles = theme => ({ ... });
class EmployeeLogin extends React.Component {
constructor(props) {
super(props);
const { setToken } = this.props;
this.state = {
email: '',
password: '',
error: null,
isLoading: false,
};
this.handleSubmitevents = this.handleSubmitevents.bind(this);
}
async handleSubmitevents(event) {
event.preventDefault();
const credentials = {
email: this.state.email,
password: this.state.password,
}
this.setState({
isLoading: true,
error: null,
});
axios.post('http://localhost:8080/account/employee/login', credentials, {
headers: {
"Content-Type": 'application/json', 'Accept': 'application/json'
}
})
.then(res => {
this.props.setToken(res.data.token); // Set the token to the parent (App.js)
})
.catch(error => {
this.setState({
error: error.response.data.message, // Show error message on failure
});
})
.then(() => {
this.setState({ isLoading: false }); // Always stop the loading indicator when done
});
}
componentWillUnmount() {
// How to terminate the promise neatly here?
}
render() {
const { classes } = this.props;
return (
<form className={classes.form} onSubmit={this.handleSubmitevents} noValidate>
<TextField
required
value={this.state.email}
onChange={(e) => { this.setState({ email: e.target.value }); }}
/>
<TextField
required
type="password"
value={this.state.password}
onChange={(e) => { this.setState({ password: e.target.value }); }}
/>
<Button type="submit">Sign In</Button>
</form>
);
}
}
EmployeeLogin.propTypes = {
classes: PropTypes.object.isRequired,
setToken: PropTypes.func.isRequired,
};
export default withStyles(styles)(EmployeeLogin);
Try to call setToken after setting isLoading to false so you can try something like below.
async handleSubmitevents(event) {
event.preventDefault();
const credentials = {
email: this.state.email,
password: this.state.password,
}
this.setState({
isLoading: true,
error: null,
});
let responseToken = ''; // set responseToken to blank before calling api
axios.post('http://localhost:8080/account/employee/login', credentials, {
headers: {
"Content-Type": 'application/json', 'Accept': 'application/json'
}
})
.then(res => {
responseToken = res.data.token; // set responseToken here
})
.catch(error => {
this.setState({
error: error.response.data.message, // Show error message on failure
});
})
.then(() => {
this.setState({ isLoading: false });
this.props.setToken(responseToken); // call props method from here
});
}

How do I integrate my Submit function in Formik

I'm trying to integrate my submit function that sends the data gotten from the form to my django API.
I have done the form validation with formik and I have tried calling the function to post the data.
How my onSubmit function for formik looks:
const Registration = () => (
<Formik
initialValues={{
username: "",
email: "",
password: "",
re_password: "",
}}
onSubmit={(values, { setSubmitting }) => {
handleRegistration(values, this.props.history);
setSubmitting(false);
}}
>
And the function for submitting data to the server:
const handleRegistration = (e, values) => {
e.preventDefault();
fetch("http://127.0.0.1:8000/auth/users/", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(values),
})
.then((resp) => resp.json())
.then((res) => {
console.log("registration res", res);
window.location.href = "/login";
})
.catch((error) => {
console.log("registration error", error);
});
};
This is the way I'm submitting with formik:
const onSubmit = (values, { resetForm, setSubmitting }) => {
const data = new FormData();
setSubmitting(true);
data.append('form', JSON.stringify({ values.firstValue, values.secondValue }));
// call post('/yourEndpoint', data)
};
Any particular reason you're using setSubmitting(false) ? I also see you have e.preventDefault(); and I'm not sure if you need that.

showing success and error messages in react/redux app

I'm trying to add toast notifications to my app, one plugin I've been trying to use is react-toastify.
The issue I'm having is probably more a general react/redux issue more than with a plugin such as react-toastify.
I'm using a reducer to set the redux state for errors and success messages, from what I understand with the current code, each error or success message is persistent in the store until another action is called to clear them.
The issue I can't figure out is how do I trigger a toast only once. Eg. I enter the wrong credentials, it creates an error toast, but whenever the state changes and reloads (typing anything into the email or password fields) it creates another toast.
How do I get it to only show once?
userActions.js
function handleErrors(res) {
if (res.ok) {
return res.json();
} else {
return res.json().then(err => {throw err;});
}
}
export const login = (user) => dispatch => {
fetch(`${url}/login`,
{
credentials: 'include',
method: 'post',
body: user,
headers: new Headers({
'Content-Type': 'application/json',
'Accept': 'application/json'
})
})
.then(handleErrors)
.then(res =>
dispatch({
type: LOGIN,
payload: res
})
)
.catch(error =>
dispatch({
type: ERROR,
payload: error
})
)
}
userReducer.js
const initialState = {
errors: '',
success: ''
};
export default function(state = initialState, action) {
switch (action.type) {
case LOGIN:
return {
...state,
errors: '',
success: action.payload.message
};
case ERROR:
return {
...state,
success: '',
errors: action.payload.message
}
default:
return state;
}
}
app.js
app.post('/login', function(req, res) {
... return res.status(500).send({ message: 'Wrong credentials' });
... return res.status(200).send({ message: 'good!' });
});
login.js
class Login extends React.Component {
constructor() {
super();
this.state = {
email: "",
password: ""
}
}
handleChange = event => {
this.setState({
[event.target.id]: event.target.value
});
}
render() {
const { errors, login, success } = this.props;
if (success !== '') toast.success(success, {
position: toast.POSITION.TOP_CENTER
});
if (errors !== '') toast.error(errors, {
position: toast.POSITION.TOP_CENTER
});
return (
<div>
<input type="text" id="email" placeholder="Email Address" onChange={this.handleChange} />
<input type="password" id="password" placeholder="Password" onChange={this.handleChange} />
<button onClick={() => login(JSON.stringify({email: this.state.email, password: this.state.password}))}>Log In</button>
<ToastContainer />
</div>
)
}
}
const mapStateToProps = state => ({
errors: state.store.errors,
success: state.store.success
});
export default connect(mapStateToProps, {login})(Login);
You're calling toast.success or toast.error inside render which makes a new toast pop up every time you re-render the component.
The solution is simple. Move your toast calls outside render, where they will only be called once.
One way to achieve this is to return a value from your userAction.
export const login = (user) => dispatch => {
return new Promise((resolve, reject) => {
fetch(`${url}/login`,
{
credentials: 'include',
method: 'post',
body: user,
headers: new Headers({
'Content-Type': 'application/json',
'Accept': 'application/json'
})
})
.then(handleErrors)
.then(res => {
dispatch({
type: LOGIN,
payload: res
})
resolve(res)
}
)
.catch(error => {
dispatch({
type: ERROR,
payload: error
})
reject(error)
}
)
}
}
Then use that value to toast in login.js.
class Login ... {
...
loginUser = () => {
this.props.login(JSON.stringify({email: this.state.email, password: this.state.password}))
.then(res => {
toast.success(res.message, { position: toast.POSITION.TOP_CENTER })
}
).catch(error => {
toast.error(error.message, { position: toast.POSITION.TOP_CENTER })
}
)
}
...
render() {
return (
...
<button onClick={this.loginUser}>Log In</button>
...
)
}
}
There are other ways to achieve the same functionality and depending on the structure of your project, you may want to toast in a more generalized way.

Resources