As the title suggests, I have a form
component that has an onSubmit prop set to handle form submissions. Typically in my application, when a user submits a form, a long running process starts. During that time I want to display a spinner/disable the button to stop the user from submitting the form while the query loads.
However, when the submit button
type is submit
, any state setters that come from the useState
hook (or even a Redux dispatcher) don't seem to change the state.
I see that the component is re-rendered when the submit handler is invoked, even though I used event.preventDefault()
and event.stopPropagation()
. How can I prevent this re-render from occurring and messing with the state?
My code is below, and I have create a minimum reproduction in a Code Sandbox here. I utilized a setTimeout
function to mimic the API call that actually happens.
import React, {
ChangeEvent,
FormEvent,
FunctionComponent,
useRef,
useState
} from "react";
import { Button, Form, Modal, Spinner } from "react-bootstrap";
const SimpleForm: FunctionComponent = () => {
// form state
const [name, setName] = useState("TestName");
const [age, setAge] = useState("10");
// form state helpers
const [submitLoading, setSubmitLoading] = useState(false);
const formRef = useRef<HTMLFormElement | null>(null);
const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
event.stopPropagation();
const form = event.currentTarget;
if (form.checkValidity()) {
setSubmitLoading(true);
console.log("valid form");
setTimeout(() => console.log("blah"), 5000);
setSubmitLoading(false);
}
};
return (
<Modal centered size="lg" show={true}>
<Form ref={formRef} onSubmit={handleSubmit}>
<Modal.Header closeButton>
<Modal.Title>Simple Form</Modal.Title>
</Modal.Header>
<Modal.Body>
<Form.Group className="mb-3" controlId="instanceName">
<Form.Label as="h6">Name</Form.Label>
<Form.Control
required
type="text"
placeholder="Instance name here"
value={name}
spellCheck="false"
autoComplete="off"
onChange={(e) => {
setName(e.target.value);
}}
/>
</Form.Group>
<Form.Group className="mb-3" controlId="diskSize">
<Form.Label as="h6">Age</Form.Label>
<Form.Control
type="number"
min="10"
max="1000"
value={age}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
setAge(e.target.value)
}
/>
</Form.Group>
</Modal.Body>
<Modal.Footer>
<Button variant="secondary">Close</Button>
<Button
type="submit"
variant="success"
style={{
padding: submitLoading ? "0.375rem 3.875rem" : "0.375rem 0.8rem"
}}
>
{submitLoading ? (
<Spinner size="sm" animation="border" />
) : (
"Submit Form"
)}
</Button>
</Modal.Footer>
</Form>
</Modal>
);
};
I have tried replacing the inside of the function with a closure with and without the previous state (() => true
/ (prevState) => !prevState
). I tried replacing the useState
hook with a useRef
hook, and setting the ref manually inside the event handling function. I also moved the state into Redux, and that didn't seem to do anything either. I also tried adding an onClick
prop to the button
with a closure inside, and that seems to do nothing either.
Any help would be much appreciated!
CodePudding user response:
You're setting the loading state to false immediately after setting it to true, thus you don't see any changes. If you move the setSubmitLoading(false)
INSIDE the setTimeOut
, the changes will reflect for you, as it did for me on the sandbox.
const handleSubmit = () => {
console.log(`submitLoading in handler: ${submitLoading}`);
setSubmitLoading(true);
console.log("valid form");
setTimeout(() => setSubmitLoading(false), 5000);
};
CodePudding user response:
One problem would be that the setTimeout does nothing here, as it runs 5000ms later as the setSubmitLoading function, js basically "ignores" the setTimeout here unless you use await, which makes it wait for the function to complete (using await for the actual api request, it doesnt work on setTimeout)
which means React updates the state instantly not really changing anything
this suggests that maybe your onSubmit function doesnt use await correctly, or .then Promise callbacks
asyncFunction().then(() => setState(false))