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.
Related
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
});
}
I am making a React application with the backend of the Django REST framework. Everything works fine except when retrieving the username. Retrieving the username is no problem but keeping it for a time is a problem. I'm going to explain using comments now in the code;
import React, { Component } from 'react';
import Nav from './Components/Navbar';
import LoginForm from './Components/LoginForm';
import SignupForm from './Components/SignupForm';
import Layout from './Containers/Layout';
import './App.css';
class App extends Component {
constructor(props) {
super(props);
this.state = {
displayed_form: '',
logged_in: localStorage.getItem('token') ? true : false,
username: '', // username state
error: null
};
}
componentDidMount() {
if (this.state.logged_in) {
fetch('http://localhost:8000/core/current_user/', { // fetch is used to get current user
headers: {
Authorization: `JWT ${localStorage.getItem('token')}`
}
})
.then(res => res.json())
.then(json => {
this.setState({ username: json.username }); // set the state
});
}
}
handle_login = (e, data) => {
e.preventDefault();
fetch('http://localhost:8000/token-auth/', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
})
.then(res => res.json())
.then(json => {
try {
localStorage.setItem('token', json.token);
this.setState({
logged_in: true,
displayed_form: '',
username: json.user.username,
error: '',
})
} catch (error) {
this.setState({error: "We Couldn't log you in, maybe theres a typo in the data you entered"})
this.handle_logout()
}
});
};
handle_signup = (e, data) => {
e.preventDefault();
fetch('http://localhost:8000/core/users/', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
})
.then(res => res.json())
.then(json => {
try {
localStorage.setItem('token', json.token);
this.setState({
logged_in: true,
displayed_form: '',
username: json.username,
})
if (this.state.username == 'A user with that username already exists.') {
this.setState({logged_in: false, username: '', error: 'Yikes, we could not create your account, you have used an existing username. Try again please...'})
this.handle_logout()
}
} catch (error) {
alert("Error")
}
});
};
handle_logout = () => {
localStorage.removeItem('token');
this.setState({ logged_in: false, username: '' });
};
display_form = form => {
this.setState({
displayed_form: form
});
};
render() {
let form;
switch (this.state.displayed_form) {
case 'login':
form = <LoginForm handle_login={this.handle_login} />;
break;
case 'signup':
form = <SignupForm handle_signup={this.handle_signup} />;
break;
default:
{
this.state.logged_in ?
form = null
:
form = <LoginForm handle_login={this.handle_login} />
}
}
return (
<div className="App">
<Nav
logged_in={this.state.logged_in}
display_form={this.display_form}
handle_logout={this.handle_logout}
/>
{form}
<h3>
{
this.state.logged_in && this.state.username !== undefined ?
<Layout username={this.state.username} />
: this.state.error ?
<p className="error">{this.state.error}</p>
: this.state.username == undefined ?
<div>
<p className="error" style={{maxWidth: 650, minHeight: 30, padding: 20}}>You exceeded the time being logged in, we take your account safety seriously. So please logout now and login again...</p>
{this.handle_logout}
</div>
:
<p></p>
}
</h3>
</div>
);
}
}
export default App;
If you read through everything, I have said that this.state.logged_in && this.state.username !== undefined ? show the layout component.
In that component, I have put an h1 with {this.state.username}.
The username gets shown but it becomes null/undefined after about 5 minutes. Even though, in the backend Django REST API, the current username can be seen.
I am very confused about why this is happening.
Update
I fixed this by using local storage to set a username and get the username. But, I want to know if this is safe.
I want to show a successful or failed alert after the update with fetch. I can show a message with the button, but no alert is shown in the fetch operation.
How to display alert in Fetch
show in react-alert fetch
I'm using the react-alert component.
https://www.npmjs.com/package/react-alert
async handleSubmitUpdate() {
await fetch(`${this.domain}/api/debt/update/`, {
method: "PUT",
headers: {
Authorization: `Bearer ${localStorage.getItem("token")}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
user: this.props..id,
})
})
.then(response => {
return response.json();
})
.then(json => {
this.componentDidMount();
return (<AlertMessageBox type={"success"} data={"Update succesfully"} />)
})
.catch(err => console.log(err));
}
AlertMessageBox.js
import React from 'react';
import { useAlert } from 'react-alert';
const AlertMessageBox = ({ type, data }) => {
const alert = useAlert();
console.log(data);
const showAlert = () => {
switch (type) {
case 'error':
alert.error(<div style={{color: 'red'}}> data </div>);
return;
case 'show':
alert.show(<div style={{color: 'white'}}> data </div>);
return;
case 'info':
alert.success(<div style={{color: 'green'}}> data </div>);
return;
default:
return;
}
};
return <div> { showAlert() } Deneme</div>;
};
export default AlertMessageBox;
You should have a state variable for this.
state = {
showSuccessAlert: false,
showFailAlert: false
}
Then you can set state variable on success / failure,
await fetch(`${this.domain}/api/debt/update/`, {
method: "PUT",
headers: {
Authorization: `Bearer ${localStorage.getItem("token")}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
user: this.props..id,
})
})
.then(response => {
return response.json();
})
.then(json => {
//this.componentDidMount(); //I don't think you can call this
//Change the state here which will show your Alert
this.setState({
showSuccessAlert: true
})
})
.catch(err => {
console.log(err);
//Change the state here which will show your Alert
this.setState({
showFailAlert: true
})
);
In your render method you should have your alert with condition,
render(){
return(
<div>
{ this.state.showSuccessAlert && <AlertMessageBox type={"success"} data={"Update succesfully"} /> }
{ this.state.showFailAlert && <AlertMessageBox type={"error"} data={"Data update failed"} /> }
....
</div>
)
}
i'm Trying to make react paypal button that changes the billing amount on props change.
I call the following component with props price and everytime the price change i would like to rerender the button to update the actual price.
const PaypalForm = props => {
let paypalRef = useRef();
useEffect(() => {
window.paypal
.Buttons({
createOrder: (data, actions) => {
return actions.order.create({
purchase_units: [
{
description: "test",
amount: {
currency_code: "USD",
value: props.price
}
}
]
});
},
onApprove: async (data, actions) => {
const order = await actions.order.capture();
console.log(order);
},
onError: err => {
setError(err);
console.error(err);
}
})
.render(paypalRef.current);
}, [props.price]);
return (
<Row className="justify-content-center">
{error && <div>Uh oh, an error occurred! {error.message}</div>}
<div ref={paypalRef} />
</Row>
);
};
Everything is working except that a new button is created and added in the bottom of old one at each props change. I would like my new button to replace the old one.
You can pass the amount to the forceRerender property of the button and the button will rerender each whenever the amount is updated.
You should really just use react-paypal-button-v2
It updates with props, works as a stateless function and works with SSR such as next.js.
It even allows bypassing actions.order.create() so that you can call your own API's.
import { PayPalButton } from "react-paypal-button-v2";
const PaypalButton = ({total, cart}) => {
return (
<PayPalButton
createOrder={(data, actions) => {
return fetch('/api/paypal/create-transaction', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
total: total,
cart: cart,
})
})
.then((response) => {return response.json()})
.then((data) => {return data.orderID})
.catch(error => console.log(error))
}}
onApprove={(data) => {
// Capture the funds from the transaction
return fetch('/api/paypal/capture-transaction', {
method: 'POST',
headers: {
'content-type': 'application/json'
},
body: JSON.stringify({ orderID: data.orderID })
})
.then((res) => { return res.json() })
.then((details) => {
if(details === 200){
console.log('success');
} else {
console.log('failure');
}
})
.catch(error => {
console.log(error)
})
}}
options={{
clientId: process.env.PAYPAL_CLIENT_ID
}}
/>
);
}
export default PaypalButton;
I have a Login component, which has form with inputs for user's data, and I have a method onFormSubmit with fetch. The problem is that I have no idea, how to pass token to another component ( it is there, I can console.log it ). The reason why I want to pass token to another component is that the other component is validating if user has logged in and by detecting token ( null = user didn't log in and redirect him to login page, otherwise go to protected pages )
My login.js component
class Login extends React.Component() {
constructor() {
super();
this.state = {
email: '',
password: '',
};
}
onInputChange = (e) => {
this.setState({
[e.target.id]: e.target.value
});
};
onFormSubmit = (e) => {
e.preventDefault();
fetch('http://localhost:3001/user/login', {
method: 'POST',
body: JSON.stringify({
email: this.state.email,
password: this.state.password
}),
headers: {
'Content-Type': 'application/json'
}
})
.then( res => {
if( res.status === 200){
this.props.history.push('/MyPlaces');
} else {
const error = new Error(res.error);
throw error;
}
})
.catch(err => {
console.error(err);
alert('Error login in please try again!');
});
};
render() {
return (
<div className="loginPanel">
<form onSubmit={this.onFormSubmit}>
<label>Email
<input type="text"
id="email"
value={this.state.email}
onChange={this.onInputChange}
/>
</label>
<label>Password
<input type="text"
id="password"
value={this.state.password}
onChange={this.onInputChange}/>
</label>
<input type="submit" value="Submit" />
</form>
</div>
);
};
}
export default Login;
My authentication component
import React, {Component} from 'react';
import { Redirect } from 'react-router-dom';
export default function withAuth(ComponentToProtect, props) {
return class extends Component {
constructor() {
super();
this.state = {
loading: true,
redirect: false
};
}
componentDidMount() {
fetch('')
.then(res => {
if( res.status === 200) {
this.setState({ loading: false});
} else {
const error = new Error(res.error);
throw error;
}
})
.catch(err => {
console.error(err);
this.setState({ loading: false, redirect: true });
})
}
render() {
const { loading, redirect } = this.state;
if( loading ){
return null;
}
if( redirect ){
return <Redirect to="/Login" />;
}
return (
<React.Fragment>
<ComponentToProtect {...this.props}/>
</React.Fragment>
);
}
}
}
I know that there is nothing in fetch in authentication component, I thought that I should've make another api request ( same as in login component, then after fetch just invoke
.then(res => res.json()).then(res => {
let token = res.token;
console.log("token: ", token);
});
but it just doesn't seem to be good idea I think. Could you please give me some guide how may I do that?
You can simply use localStorage.setItem('token-name', token); to store the token after fetch. Then you can retrieve it using localStorage.getItem('token-name') across all the component. If the getItem returns null then you can simply redirect to login.
For logout, you can simply update the token value to null.
Login Component
fetch('http://localhost:3001/user/login', {
method: 'POST',
body: JSON.stringify({
email: this.state.email,
password: this.state.password
}),
headers: {
'Content-Type': 'application/json'
}
})
.then(res => {
if( res.status === 200){
localStorage.setItem('token', res.token); //storing the token in localStorage.
this.props.history.push('/MyPlaces');
} else {
const error = new Error(res.error);
throw error;
}
})
.catch(err => {
console.error(err);
alert('Error login in please try again!');
});
Authentication Component
componentDidMount() {
let token = localStorage.getItem('token'); //retriving the token from localStorage
if(token === null){
this.setState({ loading: false, redirect: true });
}
}
Hope this will help you.