I have the current code where with a button (OpenModal.jsx) the user open a modal where he can save data in a database, the problem that I have is that if the user is fast enough to click submit twice before the modal close he can save the same data twice (send a double submit).
What is the best way to prevent this?
OpenModal.jsx
const OpenModal = () => {
const [openModal, setOpenModal] = useState(false);
return (
<div className="container">
<button
className="openModalBtn"
onClick={() => {
setOpenModal(true);
}}
>
Set note
</button>
{openModal && <Modal closeModal={setOpenModal} />}
</div>
);
};
Modal.jsx
import { useState } from "react";
const Modal = ({ closeModal }) => {
const [data, setData] = useState({
note: "",
});
const submit = async (e) => {
e.preventDefault();
try {
const response = await axios.post(
`${process.env.REACT_APP_API_KEY}`,
{
note: data.note,
}
);
response.data.success ? closeModal(false) : null;
} catch (error) {
console.log(error);
}
};
const handle = (e) => {
const getData = { ...data };
getData[e.target.id] = e.target.value;
setData(getData);
};
return (
<div className="modal">
<div className="modal-content">
<form onSubmit={(e) => submit(e)}>
<div className="close-content">
<button
type="button"
className="btn-close"
onClick={() => {
closeModal(false);
}}
>
X
</button>
</div>
<div className="form-content">
<label>
Note:
<input
type="text"
required
onChange={(e) => handle(e)}
id="note"
/>
</label>
</div>
<div className="buttons-form">
<button
type="button"
className="btn-cancel"
onClick={() => {
closeModal(false);
}}
>
Cancel
</button>
<button className="btn-save" type="submit">
Save
</button>
</div>
</form>
</div>
</div>
);
};
Disable the button while the operation is processing. You can keep a disabled flag in state:
const [isDisabled, setIsDisabled] = useState(false);
And use it on the button:
<button className="btn-save" type="submit" disabled={isDisabled}>
Save
</button>
Then update that state as needed:
const submit = async (e) => {
setIsDisabled(true); // <--- here
e.preventDefault();
try {
const response = await axios.post(
`${process.env.REACT_APP_API_KEY}`,
{
note: data.note,
}
);
setIsDisabled(false); // <--- here
response.data.success ? closeModal(false) : null;
} catch (error) {
setIsDisabled(false); // <--- here
console.log(error);
}
};
For improved UX, you might even replace the button text with a spinner or some other indication that "something is processing" while it's disabled.
You could have an isLoading state that you set to true when the submit button is clicked and false when the request is completed. Then, you can either make the button disabled when that state is true or simply don't send the request in the submit function if the request is loading:
const [isLoading, setIsLoading] = useState(false);
const submit = async (e) => {
e.preventDefault();
if (isLoading)
return;
setIsLoading(true);
try {
const response = await axios.post(
`${process.env.REACT_APP_API_KEY}`, {
note: data.note,
}
);
response.data.success ? closeModal(false) : null;
} catch (error) {
console.log(error);
} finally {
setIsLoading(false);
}
};
You should introduce safety flag, to know when submitting started and when finished, and like that user will not be able to hit it twice because you can set constraints with flag. Do something like this:
const [data, setData] = useState({
note: "",
});
const [submitting, setSubmitting] = useState(false);
const submit = async (e) => {
e.preventDefault();
if(!submitting) {
setSubmitting(true);
try {
const response = await axios.post(
`${process.env.REACT_APP_API_KEY}`,
{
note: data.note,
}
);
response.data.success ? closeModal(false) : null;
} catch (error) {
console.log(error);
} finally {
setSubmitting(false);
}
}
};
Related
I am using React.js to create the front-end side of a web application that can control home appliances.
What I want to achieve is
I want to send the text entered in the modal to Backend.
At that time, I want to prevent the screen reload.
Issue is
When I put the text in the modal and press the Submit button, the page reload happens and I can't see what was sent in the console.
Please see the video below for details.
https://youtu.be/_ppCNBTBIvc
AddRoomModal.js
import Modal from 'react-bootstrap/Modal';
const cookies = new Cookies();
const AddRoomModal = (props) => {
const [room_name, setRoomName] = useState("");
const addRoom = (e) => {
setRoomName(e.target.value);
}
const clickSubmit = (e) => {
AddRoom(e.target.value);
}
const building_state = useSelector(state => state.building_state.building_state);
console.log(building_state);
const url_forgetroomname = {
"condo": "aaa.com",
"office": "bbb.com",
"house": "ccc.com"
}[building_state]
const AddRoom = async(data) => {
console.log("Body sent to server", {
home_rooms: room_name,
})
await axios.post(url_forgetroomname,
{
home_rooms: room_name,
},
{
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${cookies.get('accesstoken')}`
},
})
.then(result => {
alert('Succeded add room!');
console.log('Succeded add room!');
})
.catch(err => {
alert('Missed add room!');
console.log(err);
console.log('Missed add room!');
});
}
const getRoomName = async(data) => {
await axios.get(url_forgetroomname,
{
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${cookies.get('accesstoken')}`
},
})
.then(result => {
console.log(result.data)
setRoomName(result.data.home_rooms);
})
.catch(err => {
console.log(err);
});
}
return (
<>
<Modal show={props.show}>
<Modal.Body className="light_modal_body" >
<div className="light_modal_body">
<div className="range-container col-12">
</div>
<h3>Add Room</h3>
<form>
<input type="text" onChange={addRoom} value={room_name}/>
<div className="done_button">
<button className='btn btn-block btn-primary' type="submit" onClick={clickSubmit}>
OK
</button>
</div>
</form>
<div className="done_button">
<button onClick={props.handleHide} className='btn btn-block btn-danger'>Cancel</button>
</div>
</div>
</Modal.Body>
</Modal>
</>
);
}
export default AddRoomModal;
HeaderForSettingGetRoomName.js
import AddRoomModal from '../AddRoomModal';
const cookies = new Cookies();
const HeaderForSettingGetRoomName = (props) => {
const isLoggedInOn = useSelector(state => state.user.isLoggedIn);
// modal
const [show, setShow] = useState();
// Show Modal
const handleShow = () => {
setShow(true);
}
// Hide modal
const handleHide = () => {
setShow(false);
}
const building_state = useSelector(state => state.building_state.building_state);
console.log(building_state);
return (
<div>
<AddRoomModal
show={show} handleHide={handleHide}
/>
<div className="header">
<Link to={`/setting`} className=""> <img className="header_menu_back_leftside" src={ic_back} alt="" />
</Link>
<img className="header_logo" src={logo_image} />
<img className="ic_add_white" src={ic_add_white} onClick={handleShow}/>
</div>
</div>
);
}
export default HeaderForSettingGetRoomName;
You can disable the default form behaviour (submitting the form) by calling preventDefault on the submit event
const clickSubmit = (e) => {
e.preventDefault();
AddRoom(e.target.value);
}
I am using the Modal Component from React Bootstrap Everything works as expected, except from the fact that I cannot figure out how to auto close the Modal after a successful submit.
I know I am able to call the onHide fuction from the Modal component like this at button click:
<Button onClick={props.onHide}>Close</Button>
Is there a way to auto call this onHide function if, and only if there is a successful submit form the MailingListSendgrid component?
index.js
<ModalMailingList show={modalShow} onHide={() => setModalShow(false)} />
ModalMailingList.js
import Modal from "react-bootstrap/Modal";
import MailingListSendgrid from "#/components/MailingListSendgrid";
export default function ModalMailingList(props) {
return (
<Modal
{...props}
size="lg"
aria-labelledby="contained-modal-title-vcenter"
centered
className="special_modal" //Add class name here
>
<Modal.Header closeButton></Modal.Header>
<Modal.Body>
<MailingListSendgrid />
</Modal.Body>
</Modal>
);
}
MailingListSendgrid.js
.
.
.
.
const MailingListSendgrid = () => {
const [isError, setIsError] = useState(true);
const [shakeIt, setshakeIt] = useState(false);
const [mail, setMail] = useState("");
const [isLoading, setLoading] = useState(false);
const [message, setMessage] = useState(null);
const subscribe = () => {
const regEx = /[a-zA-Z0-9._%+-]+#[a-z0-9.-]+\.[a-z]{2,8}(.[a-z{2,8}])?/g;
setMail("");
if (!regEx.test(mail) && mail !== "") {
setIsError(true);
setshakeIt(true);
setMessage("Email is Not Valid");
setTimeout(() => {
setshakeIt(false);
}, 1000);
} else if (mail === "") {
setIsError(true);
setshakeIt(true);
setMessage("Email is Empty");
setTimeout(() => {
setshakeIt(false);
}, 1000);
} else {
setLoading(true);
axios
.put("api/MailingList", {
mail,
})
.then((result) => {
if (result.status === 200) {
setIsError(false);
setMessage(result.data.message);
setLoading(false);
}
})
.catch((err) => {
setIsError(true);
setMessage(err.data.message);
setLoading(false);
});
setMessage(null);
setshakeIt(false);
}
};
return (
.
.
.
<input
onChange={(e) => {
setMail(e.target.value);
}}
type="email"
className={`form-control required email w-auto text-center text-sm-start`}
placeholder={subscription.formPlaceholder}
value={mail}
autoComplete="email"
required
></input>
<button
type="submit"
name="subscribe"
onClick={subscribe}
className="input-group-text justify-content-center"
disabled={isLoading}
>
.
.
.
);
};
export default MailingListSendgrid;
I think that the best option here is to pass a function as a prop, thus still making the MailingListSendgrid reusable e.g.
<MailingListSendgrid onSubmit={()=> props.onHide()} />
And just use that in MailingListSendgrid if it was successfull.
Hi i am working on a React application where there are four options.when a user select an option corresponding input element will be added to the wrapper.In the following code add operation works fine but remove operation is not working properly ,it is not removing the corresponding element.Another problem the values on the inputs fields not present when the component re-renders.so experts guide me how i can acheive removing the corresponding row when the remove button is clicked and the input values should not be reset when the component re-renders.
But when I submit the input it will appear my data perfectly and when i restart the page and just click into edit and hit submit with the defaultValue it just clear all the data and send back to my backend with undefined value like this: [ undefined, undefined, undefined, undefined ]
Here is my full component:
const Agreement = (props) => {
const { agreement, editable, teamData, teamId, fetchTeamData } = props;
const [editing, setEditing] = useState(false);
const [title, setTitle] = useState("");
const [description, setDescription] = useState("");
const [showErrors, setShowErrors] = useState(false);
const [errorsArr, setErrorsArr] = useState();
const initialFormState = {
rule_0: teamData.rules.rule_0,
rule_1: teamData.rules.rule_1,
rule_2: teamData.rules.rule_2,
rule_3: teamData.rules.rule_3,
creator: teamData.User.public_user_id,
};
const [updateTeamData, setUpdateTeamData] = useState(initialFormState);
const [inputs, setInputs] = useState(teamData.rules);
const handleChange = (event) => {
const { name, value } = event.target;
// Update state
setUpdateTeamData((prevState) => ({
...prevState,
[name]: value,
}));
};
// Add more input
const addInputs = () => {
setInputs([...inputs, { name: `rule_${inputs.length + 1}` }]);
};
// handle click event of the Remove button
const removeInputs = (index) => {
const list = [...inputs];
list.splice(index, 1);
setInputs(list);
};
const clearInput = (dataName) => {
setUpdateTeamData((prevState) => {
delete prevState[dataName];
return {
...prevState,
};
});
};
const handleSubmit = async (event) => {
event.preventDefault();
setEditing(false);
// Send update request
const res = await axios.put(`/api/v1/teams/team/${teamId}`, updateTeamData);
// If no validation errors were found
// Validation errors don't throw errors, it returns an array to display.
if (res.data.validationErrors === undefined) {
// Clear any errors
setErrorsArr([]);
// Hide the errors component
setShowErrors(false);
// Call update profiles on parent
fetchTeamData();
} else {
// Set errors
setErrorsArr(res.data.validationErrors.errors);
// Show the errors component
setShowErrors(true);
}
};
const handleCancel = () => {
setEditing(false);
};
useEffect(() => {
if (agreement === "default") {
setTitle(defaultTitle);
setInputs(teamData.rules);
} else {
setTitle(agreement.title ?? "");
}
}, [agreement, teamData]);
console.log("teamData.rules", teamData.rules);
console.log("inputs", inputs);
return (
<div className="team-agreement-container">
{!editing && (
<>
<h4 className="team-agreement-rules-title">{title}</h4>
{editable && (
<div className="team-agreement-rules">
<EditOutlined
className="team-agreement-rules-edit-icon"
onClick={() => setEditing(true)}
/>
</div>
)}
<p className="team-agreement-rules-description">{description}</p>
{teamData.rules.map((rule, index) => (
<div className="team-agreement-rule-item" key={`rule-${index}`}>
{rule ? (
<div>
<h4 className="team-agreement-rule-item-title">
{`Rule #${index + 1}`}
</h4>
<p className="team-agreement-rule-item-description">
- {rule}
</p>
</div>
) : (
""
)}
</div>
))}
</>
)}
{/* Edit rules form */}
{editing && (
<div className="team-agreement-form">
{showErrors && <ModalErrorHandler errorsArr={errorsArr} />}
<h1>Rules</h1>
{inputs.map((data, idx) => {
return (
<div className="agreement-form-grid" key={`${data}-${idx}`}>
<button
type="button"
className="agreement-remove-button"
onClick={() => {
removeInputs(idx);
clearInput(`rule_${idx}`);
}}
>
<Remove />
</button>
<input
name={`rule_${idx}`}
onChange={handleChange}
value={teamData.rules[idx]}
/>
</div>
);
})}
{inputs.length < 4 && (
<div className="team-agreement-add-rule">
<button type="submit" onClick={addInputs}>
<Add />
</button>
</div>
)}
<div className="div-button">
<button className="save-button" onClick={handleSubmit}>
Save
</button>
<button className="cancel-button" onClick={handleCancel}>
Cancel
</button>
</div>
</div>
)}
</div>
);
};
export default Agreement;
How can I fix this error?
My thought is the problem is around [inputs, setInputs]
Try this
<input
//..
onChange={(event) => handleChange(event.target.value)}
//..
/>
then in your "handleChange" function
const handleChange = (event) => {
const { name, value } = event;
//....
};
I am trying to organize my code order to handle feed as feed.* based on my endpoint API, but however react doesn't allow me to directly send functions into component, but I want something similar to feed.results, feed. count
const [initialized, setIntialized] = useState(false);
const [feed, setFeed] = useState([]);
const browserFeed = async () => {
const response = await browse();
setFeed(response.results);
setIntialized(true);
};
useEffect(() => {
if (!initialized) {
browserFeed();
}
});
export const browse = () => {
return api.get('xxxxxxxx')
.then(function(response){
return response.data // returns .count , .next, .previous, and .results
})
.catch(function(error){
console.log(error);
});
}
<div className="searched-jobs">
<div className="searched-bar">
<div className="searched-show">Showing {feed.count}</div>
<div className="searched-sort">Sort by: <span className="post-time">Newest Post </span><span className="menu-icon">▼</span></div>
</div>
<div className="job-overview">
<div className="job-overview-cards">
<FeedsList feeds={feed} />
<div class="job-card-buttons">
<button class="search-buttons card-buttons-msg">Back</button>
<button class="search-buttons card-buttons">Next</button>
</div>
</div>
</div>
</div>
If it is pagination you are trying to handle here is one solution:
async function fetchFeed(page) {
return api.get(`https://example.com/feed?page=${page}`);
}
const MyComponent = () => {
const [currentPage, setCurrentPage] = useState(1);
const [feed, setFeed] = useState([]);
// Fetch on first render
useEffect(() => {
fetchFeed(1).then((data) => setFeed(data));
}, []);
// Update feed if the user changes the page
useEffect(() => {
fetchFeed(currentPage).then((data) => setFeed(data));
}, [currentPage]);
const isFirstPage = currentPage === 1;
return (
<>
<FeedsList feeds={feed} />
{isFirstPage && (
<button onClick={() => setCurrentPage(currentPage - 1)}>Back</button>
)}
<button Click={() => setCurrentPage(currentPage + 1)}>Next</button>
</>
);
};
Looking for a gentle push in the right direction. Working on a react project and using hooks. Yes, have read documents, but not fully understanding yes.
The ask is about a login routine. Login form works, but does not reflect failed login state until repeat submission; so I am getting previous state, not current.
Tried useEffect...no change. Code follows, and appreciated any constructive feedback:
From the Login form
import React, { useState, useEffect, useRef } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { Redirect } from 'react-router-dom'
import getAuthStatus from 'common/cyclone/auth/authenticated.status'
import {
authenticateByLogin,
authenticationSelector,
} from '../services/auth.service'
import Form from 'react-validation/build/form'
import Input from 'react-validation/build/input'
import CheckButton from 'react-validation/build/button'
const required = (value) => {
if (!value) {
return (
<div className="alert alert-danger" role="alert">
This field is required!
</div>
)
}
}
const Login = (props) => {
const form = useRef()
const checkBtn = useRef()
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const [loading, setLoading] = useState(false)
const [errorMessage, setErrorMessage] = useState(null)
const dispatch = useDispatch()
const { session, hasErrors } = useSelector(authenticationSelector)
useEffect(() => {}, [session, hasErrors])
const onChangeUsername = (e) => {
const username = e.target.value
setUsername(username)
}
const onChangePassword = (e) => {
const password = e.target.value
setPassword(password)
}
const handleLogin = (e) => {
e.preventDefault()
setLoading(true)
form.current.validateAll()
if (checkBtn.current.context._errors.length === 0) {
dispatch(authenticateByLogin(username, password))
.then(() => {
setLoading(false)
if (hasErrors) {
setErrorMessage(session.error.message)
} else {
//props.history.push('/profile')
// window.location.reload()
}
})
.catch(() => {
setLoading(false)
})
} else {
setLoading(false)
}
}
if (session.success) {
//console.log(session.success)
return <Redirect to="/profile" />
}
if (getAuthStatus()) {
return <Redirect to="/profile" />
}
return (
<div className="col-md-12">
<div className="card card-container">
<img
src="//ssl.gstatic.com/accounts/ui/avatar_2x.png"
alt="profile-img"
className="profile-img-card"
/>
<Form onSubmit={handleLogin} ref={form}>
<div className="form-group">
<label htmlFor="username">Username</label>
<Input
type="text"
className="form-control"
name="username"
value={username}
onChange={onChangeUsername}
validations={[required]}
/>
</div>
<div className="form-group">
<label htmlFor="password">Password</label>
<Input
type="password"
className="form-control"
name="password"
value={password}
onChange={onChangePassword}
validations={[required]}
/>
</div>
<div className="form-group">
<button className="btn btn-primary btn-block" disabled={loading}>
{loading && (
<span className="spinner-border spinner-border-sm"></span>
)}
<span>Login</span>
</button>
</div>
{hasErrors && (
<div className="form-group">
<div className="alert alert-danger" role="alert">
{errorMessage}
</div>
</div>
)}
<CheckButton style={{ display: 'none' }} ref={checkBtn} />
</Form>
</div>
</div>
)
}
export default Login
From the auth slice:
/** Third Party Libraries */
import { createSlice } from '#reduxjs/toolkit'
import qs from 'qs'
/**Axios Wrapper...nothing fancy here*/
import CycloneAPIInstance from 'common/cyclone/api/api.client'
import CycloneConfig from 'config/base'
/** Main API Server URL */
const API_URL = CycloneConfig.API_URL
const session = JSON.parse(localStorage.getItem('authentication'))
/** Define Initial State */
export const initialState = session
? {
hasErrors: false,
session: session,
}
: {
hasErrors: false,
session: [],
}
/** Define Slice */
const authenticationSlice = createSlice({
name: 'authentication',
initialState,
reducers: {
authenticateUser: (state) => {
state.hasErrors = false
},
authenticateUserSuccess: (state, { payload }) => {
state.hasErrors = false
state.session = payload
console.log(state.session)
},
authenticateUserFailure: (state, { payload }) => {
state.hasErrors = true
state.session = payload
},
deauthenticateUser: (state) => {
state.session = []
},
},
})
export const {
authenticateUser,
authenticateUserSuccess,
authenticateUserFailure,
deauthenticateUser,
} = authenticationSlice.actions
export const authenticationSelector = (state) => state.authentication
export default authenticationSlice.reducer
export function authenticateByLogin(user_name, user_password) {
let requestBody = {
user_name: user_name,
user_password: user_password,
}
let config = {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
}
return async (dispatch) => {
dispatch(authenticateUser())
try {
const response = await CycloneAPIInstance.post(
API_URL + 'auth/login',
qs.stringify(requestBody),
config
)
//console.log(response.data.content)
localStorage.setItem('session', JSON.stringify(response.data.content))
dispatch(authenticateUserSuccess(response.data.content))
} catch (error) {
//console.log(JSON.stringify(error.response.data))
dispatch(authenticateUserFailure(error.response.data))
}
}
}
export function deauthenticateByLogout() {
return async (dispatch) => {
dispatch(deauthenticateUser())
localStorage.removeItem('session')
}
}
Try to set the message when hasError change
useEffect(()=> {
if(hasErrors) {
setErrorMessage(session.error.message)
}
}, [hasErrors]);
This is quite some code so I just skipped through to fix the problem and not pick everything apart. My best guess is this part:
dispatch(authenticateByLogin(username, password))
.then(() => {
setLoading(false)
if (hasErrors) {
setErrorMessage(session.error.message)
} else {
//props.history.push('/profile')
// window.location.reload()
}
})
.catch(() => {
setLoading(false)
})
Here you execute the async authentication and then do thing based on "hasError". This "hasError" comes from a hook. We (or at least I) have no clear idea how this is managed. Thing is you cant be 100% sure that hasError is really trustworthy at the point you check it in the then-block. The hook might run just after the next render, which explains why you see the previous state, not the actual one.
Best guess would be to use the response from that async call, because there should be one => authenticate.then((response) => if(response.hasError) ...)
With this check you can set your own error state and your component should be up-to-date
Let me know if this fixes your error.