javascriptreact-final-formfinal-form

Can a field in react-final-form mark itself as invalid / prevent submission?


I have a custom file upload field that uploads files immediately once you select/drop them, and returns a UUID for later submission. So, basically what most webapps do nowadays (e.g. Facebook, Twitter, etc.) when you drop a file.

This is all easy enough to handle with final-form - my field simply calls final-form's onChange function once the upload finished to pass the UUID to final-form.

However, if a user submits the form while an upload is still running they will submit the form without a file UUID since as far as final-form is concerned, no file has been selected yet. Especially for bigger files this would be problematic as users might not realize they still have to wait (even with a loading indicator). Marking the field as required is not an option either, since not providing a file at all is valid (or the field may allow multiple files, or you are replacing a previously-uploaded file) - so the only case where the field is "invalid" is when a file is currently being uploaded.

Here is a codesandbox with a small dummy app that should provide a good starting point at any attempt to solve it: https://codesandbox.io/s/polished-fast-k80t7

The idea is that the field becomes invalid when clicking "Pretend to start uploading" and valid again after clicking "Pretend to finish uploading".

Please note that I'm looking for a clean way to do this while keeping things separated, i.e. I'd prefer to not add state for this to the component containing the Form - also because a validation functions need to be idempotent, so checking external state there would be pretty much broken (as my attempt of doing this shows).


Should the codesandbox links ever break, here's the relevant code from the first link (since the other one is just a broken attempt anyway):

import React, { useState } from "react";
import { render } from "react-dom";
import Styles from "./Styles";
import { Form, Field } from "react-final-form";

const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));

const onSubmit = async values => {
  await sleep(300);
  window.alert(JSON.stringify(values, 0, 2));
};

const MyFileUploader = ({ input: { value, onChange }, meta: { invalid } }) => {
  const [isUploading, setUploading] = useState(false);
  const handleStartClick = () => {
    setUploading(true);
  };
  const handleFinishClick = () => {
    setUploading(false);
    onChange("0xdeadbeef"); // let's pretend this is the file UUID ;)
  };
  const style = { color: invalid ? "#f00" : "#000" };
  if (value) {
    return <em style={style}>{value}</em>;
  } else if (isUploading) {
    return (
      <button type="button" onClick={handleFinishClick} style={style}>
        Pretend to finish uploading
      </button>
    );
  } else {
    return (
      <button type="button" onClick={handleStartClick} style={style}>
        Pretend to start uploading
      </button>
    );
  }
};

const App = () => (
  <Styles>
    <h1>React Final Form</h1>
    <Form
      onSubmit={onSubmit}
      initialValues={{ file: null }}
      render={({ handleSubmit, form, submitting, values }) => (
        <form onSubmit={handleSubmit}>
          <div>
            <label>File</label>
            <Field name="file" component={MyFileUploader} />
          </div>
          <div className="buttons">
            <button type="submit" disabled={submitting}>
              Submit
            </button>
            <button type="button" onClick={form.reset} disabled={submitting}>
              Reset
            </button>
          </div>
          <pre>{JSON.stringify(values, 0, 2)}</pre>
        </form>
      )}
    />
  </Styles>
);

render(<App />, document.getElementById("root"));

Solution

  • Interesting question.

    How about something like this? When the file is uploading, it renders a field that will always be invalid, thus blocking the submission.

    const SubmitBlocker = ({ children }) => (
      <Field name="uploading" validate={() => children}>
        {({ meta }) =>
          meta.touched && meta.error ? meta.error : null
        }
      </Field>
    );
    

    Edit beautiful-monad-84nu0