How to modify the props of children passed from parent in React?

Viewed 29

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?

1 Answers

You can simplify the SafeForm component by using fieldset disabled attribute:

<form
  ...
>
  <fieldset disabled={requestInProgress}>{children}</fieldset>
</form>
Related