I want to implement a reusable "safe form" component. That is, whenever the form is submitted, I would like to disable all <input> elements (that is, add a disabled attribute to them) on it until the submit handler returns. This is so that the user won't try to double submit a form or think that they can modify a form while the submission is in progress.
To do so, I came up with the following:
SafeForm.jsx
import { cloneElement, useState } from "react";
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function disableInputComponents(elements) {
console.log('elements are', elements);
return elements.map(element => {
if (element.type === undefined) return element;
const props = { ...element.props };
const children = element.props.children;
if (element.type === 'input') props.disabled = true;
return cloneElement(
element,
props,
Array.isArray(children) ? [...disableInputComponents(children)] : children,
);
});
}
export default function SafeForm({ onSubmit, children }) {
const [requestInProgress, setRequestInProgress] = useState(false);
return (
<form onSubmit={async event => {
event.preventDefault();
setRequestInProgress(true);
// To simulate network delay in order to observe if the input elements are disabled or not.
await sleep(2000);
await onSubmit(event);
setRequestInProgress(false);
}}>
{requestInProgress ? disableInputComponents(children) : children}
</form>
);
}
Login.jsx
import { useState } from "react";
import SafeForm from "./SafeForm";
export default function Login({ onSuccess }) {
const [token, setToken] = useState('');
return (
<SafeForm
onSubmit={async () => {
const response = await fetch(
`http://localhost:3000/session`,
{ headers: { "Authorization": `Bearer ${token}` } }
);
if (response.ok) {
onSuccess({ ...(await response.json()), token });
}
}}
>
<h3>Login</h3>
<div className="mb-3">
<label className="form-label" htmlFor="token">Token</label>
<input className="form-control" id="token" value={token}
required
onChange={({ target: { value } }) => { setToken(value); }}
/>
</div>
<input className="btn btn-primary" type='submit' value='Login' />
</SafeForm>
);
}
This seems to work, albeit with warnings about "Each child in a list should have a unique "key" prop.". However, this feels hacky and I have a feeling that there might be a more idiomatic/elegant/appropritate solution.
What would be the right way of accomplishing this?