| | |

React Hook Form 05 ๐Ÿ“‹ Handling Submission โ€” handleSubmit and the onSubmit callback

โฑ๏ธ Estimated read: 12 min | |

The handleSubmit function is the gatekeeper between the form’s internal validation and the application’s submission logic. It receives the onSubmit callback, runs the validation against every registered field, and invokes the callback only when the validation passes. The callback receives the typed form data, and the optional second callback receives the errors when the validation fails . The wrapper prevents the native form submission, so the page does not reload, and the SPA behavior is preserved without the developer writing the event.preventDefault() call.

The submission lifecycle is more than the isSubmitting flag. The formState tracks the isSubmitting, the isSubmitted, the isSubmitSuccessful, and the submitCount. The isSubmitting is true while the onSubmit callback’s promise is pending, the isSubmitted is true after the submission attempt completes regardless of the outcome, the isSubmitSuccessful is true only when the validation passed and the callback did not throw, and the submitCount increments on every attempt . The distinction between isSubmitted and isSubmitSuccessful is the mechanism that separates “the user tried” from “the form succeeded.”

Key point: handleSubmit(onValid, onInvalid?) validates the form and calls the onValid callback only when the validation passes. The onInvalid callback receives the FieldErrors object. The isSubmitting stays true until the promise returned by the onSubmit callback settles, so the async work must be awaited or returned. The handleSubmit does not swallow the errors thrown inside the callback, and the setError is the mechanism that maps the server-side errors to the fields.


Why the handleSubmit wrapper exists

The native submission problem. A native <form> submits to the action URL and reloads the page. The handleSubmit calls event.preventDefault() internally, and the page does not reload . The wrapper is the mechanism that keeps the SPA behavior, and the prevention is the reason the form does not navigate away.

The validation problem. The handleSubmit runs the validation before it calls the callback. The resolver or the register rules run, and the errors populate the formState.errors. The onValid callback is called only if the validation passes, and the onInvalid callback is called if it fails . The separation is the mechanism that keeps the invalid data out of the application.

The async state problem. The isSubmitting is true while the onValid callback’s promise is pending. The callback must return a promise for the state to be tracked, and the async function is the mechanism that returns the promise . A synchronous callback sets the isSubmitting to true and then immediately to false, and the spinner never appears.

The error problem. The handleSubmit does not swallow the errors that occur inside the onSubmit callback. If the callback throws or its promise rejects, the handleSubmit rethrows the error and sets isSubmitSuccessful to false . The callback must catch the error and call the setError to display the server-side errors, and the setError is the mechanism that maps the server errors to the form fields .

The double-submission problem. The handleSubmit does not block a second submission while the first one is still pending. The isSubmitting flag is the mechanism that disables the submit button, and the check at the start of the handler is the alternative . The double-submission guard is the developer’s responsibility.


a. The handleSubmit signature and the valid/invalid paths

The handleSubmit function accepts the onValid callback and the optional onInvalid callback. The onValid receives the typed data and the event, and the onInvalid receives the FieldErrors object and the event. The function returns a submit handler that the form’s onSubmit prop receives.

// The handleSubmit with the valid and invalid callbacks
import { useForm, type SubmitHandler, type SubmitErrorHandler } from "react-hook-form";

type LoginFormValues = {
  email: string;
  password: string;
};

function LoginForm() {
  const { register, handleSubmit, formState: { errors } } = useForm<LoginFormValues>({
    defaultValues: { email: "", password: "" },
  });

  // The onValid callback receives the typed data.
  const onSubmit: SubmitHandler<LoginFormValues> = (data) => {
    console.log("Valid:", data);
  };

  // The onInvalid callback receives the errors.
  const onError: SubmitErrorHandler<LoginFormValues> = (errors) => {
    console.log("Invalid:", errors);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit, onError)}>
      <input {...register("email")} />
      <input {...register("password")} type="password" />
      <button type="submit">Log in</button>
    </form>
  );
}

The SubmitHandler<FormValues> is the explicit type for the valid callback, and the SubmitErrorHandler<FormValues> is the type for the invalid callback. The inline arrow function is the equivalent, and the types document the intent. The onInvalid callback is optional, but the explicit handler is the mechanism for the focus management, the analytics, and the toast notification .

The handleSubmit also accepts the event, and the callback receives the event as the second argument. The event is the mechanism for the stopPropagation call when the form is nested inside another form, such as in a modal .

// The nested form with the stopPropagation
<form onSubmit={(event) => {
  event.stopPropagation();
  handleSubmit(onInnerSubmit)(event);
}}>
  {/* ... */}
</form>

The handleSubmit can also be called directly without the form event, which is the mechanism for the submit button outside the form or the keyboard shortcut. The handleSubmit(onSubmit)() without the event does not call preventDefault, and the direct call is the mechanism for the programmatic submission .


b. The async submission and the submit state

The async submission is the async callback that returns a promise. The isSubmitting is true while the promise is pending, and the isSubmitSuccessful is true after the promise resolves without the errors.

// The async submission
const onSubmit = async (data: LoginFormValues) => {
  // The isSubmitting stays true until this promise settles.
  try {
    const response = await fetch("/api/login", {
      method: "POST",
      body: JSON.stringify(data),
    });

    if (!response.ok) {
      const body = await response.json();
      // The server error is mapped to the form.
      setError("email", { type: "server", message: body.message });
      return;
    }

    // The success path.
    navigate("/dashboard");
  } catch (error) {
    // The network error is mapped to the root.
    setError("root.server", { type: "network", message: "Check your connection." });
  }
};

The isSubmitting is the mechanism that disables the button, and the isSubmitSuccessful is the mechanism that shows the success message. The submitCount is the number of the attempts, and the isSubmitted is the state that is true after the attempt regardless of the outcome .

// The submit state in the UI
function SubmitButton() {
  const {
    formState: { isSubmitting, isSubmitSuccessful, submitCount },
  } = useForm();

  return (
    <>
      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? "Logging in..." : "Log in"}
      </button>
      {isSubmitSuccessful && <p>Logged in successfully.</p>}
      {submitCount > 0 && !isSubmitSuccessful && <p>Attempt {submitCount}</p>}
    </>
  );
}

The setError is the mechanism that maps the server-side errors to the fields. The function accepts the field name and the error object, and the error object has the type, the message, and the optional ref . The root.server key is the mechanism for the errors that do not belong to any field, such as the network errors or the general API failures .

// The root error display
{errors.root?.server && (
  <p role="alert">{errors.root.server.message}</p>
)}

The reset after the successful submission is the mechanism that clears the form. The reset accepts the new default values or the undefined for the original defaults, and the keepValues option is the mechanism that keeps the current values while resetting the errors.


Complete Example Session

// ============================================
// PART 1: THE BASIC SUBMISSION
// ============================================

import { useForm } from "react-hook-form";

function LoginForm() {
  const { register, handleSubmit } = useForm({
    defaultValues: { email: "", password: "" },
  });

  return (
    <form onSubmit={handleSubmit((data) => console.log(data))}>
      <input {...register("email")} />
      <input {...register("password")} type="password" />
      <button type="submit">Log in</button>
    </form>
  );
}

// ============================================
// PART 2: THE INVALID CALLBACK
// ============================================

const onInvalid = (errors) => {
  console.log("Invalid:", errors);
};

<form onSubmit={handleSubmit(onSubmit, onInvalid)}>
  {/* ... */}
</form>

// ============================================
// PART 3: THE ASYNC SUBMISSION
// ============================================

const onSubmit = async (data) => {
  // The isSubmitting stays true until this promise settles.
  const response = await fetch("/api/login", {
    method: "POST",
    body: JSON.stringify(data),
  });
};

// ============================================
// PART 4: THE SUBMIT STATE
// ============================================

function SubmitButton() {
  const {
    formState: { isSubmitting, isSubmitSuccessful },
  } = useForm();

  return (
    <button disabled={isSubmitting}>
      {isSubmitting ? "Loading..." : "Submit"}
    </button>
  );
}

// ============================================
// PART 5: THE SERVER ERROR
// ============================================

const onSubmit = async (data) => {
  try {
    const response = await fetch("/api/login", {
      method: "POST",
      body: JSON.stringify(data),
    });

    if (!response.ok) {
      const body = await response.json();
      setError("email", { type: "server", message: body.message });
    }
  } catch (error) {
    setError("root.server", { type: "network", message: "Check your connection." });
  }
};

// ============================================
// PART 6: THE RESET AFTER THE SUCCESS
// ============================================

const onSubmit = async (data) => {
  await fetch("/api/login", { method: "POST", body: JSON.stringify(data) });
  reset(); // The form is reset to the defaultValues.
};

// ============================================
// PART 7: THE SUBMIT COUNT
// ============================================

function SubmitCount() {
  const { formState: { submitCount } } = useForm();
  return <p>The form has been submitted {submitCount} times.</p>;
}

// ============================================
// PART 8: THE NESTED FORM
// ============================================

<form onSubmit={(event) => {
  event.stopPropagation();
  handleSubmit(onInnerSubmit)(event);
}}>
  {/* ... */}
</form>

// ============================================
// PART 9: THE DIRECT CALL
// ============================================

<button type="button" onClick={handleSubmit(onSubmit)}>
  Save
</button>

// ============================================
// PART 10: THE CHECKLIST
// ============================================

// [ ] The handleSubmit wraps the submit callback.
// [ ] The onValid callback has the typed data.
// [ ] The onInvalid callback receives the errors.
// [ ] The onSubmit callback is async and returns the promise.
// [ ] The isSubmitting disables the button.
// [ ] The setError maps the server-side errors.
// [ ] The reset clears the form after the success.

The ten parts covered the basic submission, the invalid callback, the async submission, the submit state, the server error, the reset, the submit count, the nested form, the direct call, and the checklist. The through-line is that the handleSubmit is the validation boundary, and the submit state is the async lifecycle.


Quick Reference

The handleSubmit signature

ParameterTypePurpose
onValidSubmitHandler<FormValues>The callback for the valid data
onInvalidSubmitErrorHandler<FormValues>The callback for the errors

The submit state

PropertyTypePurpose
isSubmittingbooleanTrue while the async callback is pending
isSubmittedbooleanTrue after the submission attempt, regardless of the outcome
isSubmitSuccessfulbooleanTrue after the successful submission
submitCountnumberThe number of the attempts

The error mapping

APIPurpose
setError(name, error)Map the server error to the field
setError("root.server", error)Map the general error to the root
clearErrors(name)Remove the field error
reset(values)Reset the form and the errors

The server error object

PropertyTypePurpose
typestringThe error category
messagestringThe error message
refHTMLElementThe optional element reference

Best Practices

โœ… Do This:

// Use the async callback
const onSubmit = async (data) => { await fetch(...); };           // โœ…

// Use the onInvalid callback for the failure path
handleSubmit(onSubmit, onInvalid)                                  // โœ…

// Map the server errors with setError
setError("email", { message: body.message });                      // โœ…

// Use the isSubmitting to disable the button
<button disabled={isSubmitting}>Submit</button>                     // โœ…

โŒ Don’t Do This:

// Use the synchronous callback with the spinner
const onSubmit = (data) => { fetch(...); }; // the isSubmitting is not tracked // โŒ

// Throw the server error without the setError
if (!response.ok) throw new Error("..."); // the error is not displayed       // โŒ

// Forget the reset after the success
await fetch(...); // the form keeps the values                               // โŒ

Common Pitfalls

PitfallWhy It HappensFix
The spinner never appearsThe callback is synchronousUse the async callback
The server error is not displayedThe setError is not calledMap the error with setError
The form is not reset after the successThe reset is missingCall the reset after the success
The isSubmitSuccessful is always falseThe callback throwsCatch the error and set the field error
The native page reload happensThe handleSubmit is not usedWrap the callback with handleSubmit

Real-World Examples

1. The basic submission

handleSubmit((data) => console.log(data))

2. The invalid callback

handleSubmit(onSubmit, onInvalid)

3. The async submission

const onSubmit = async (data) => { await fetch(...); };

4. The isSubmitting

const { formState: { isSubmitting } } = useForm();

5. The isSubmitSuccessful

const { formState: { isSubmitSuccessful } } = useForm();

6. The submit count

const { formState: { submitCount } } = useForm();

7. The setError

setError("email", { message: body.message });

8. The root error

setError("root.server", { message: "Network error" });

9. The reset

reset();

10. The direct call

onClick={handleSubmit(onSubmit)}

Visual

The handleSubmit Flow

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  THE handleSubmit FLOW                                       โ”‚
โ”‚                                                              โ”‚
โ”‚  The form is submitted                                       โ”‚
โ”‚         โ”‚                                                    โ”‚
โ”‚         โ–ผ                                                    โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                                    โ”‚
โ”‚  โ”‚ The validation runs โ”‚  The resolver or the register rulesโ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                                    โ”‚
โ”‚             โ”‚                                                โ”‚
โ”‚      โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”                                         โ”‚
โ”‚      โ–ผ             โ–ผ                                         โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                                    โ”‚
โ”‚  โ”‚ Valid  โ”‚   โ”‚Invalid โ”‚                                    โ”‚
โ”‚  โ”‚        โ”‚   โ”‚        โ”‚                                    โ”‚
โ”‚  โ”‚onValid โ”‚   โ”‚onInvalidโ”‚                                   โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                                    โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

The Async Submit State

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  THE ASYNC LIFECYCLE                                         โ”‚
โ”‚                                                              โ”‚
โ”‚  The submit callback is called                               โ”‚
โ”‚         โ”‚                                                    โ”‚
โ”‚         โ–ผ                                                    โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                                    โ”‚
โ”‚  โ”‚ isSubmitting: true  โ”‚  The button is disabled            โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                                    โ”‚
โ”‚             โ”‚                                                โ”‚
โ”‚             โ–ผ                                                    โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                                    โ”‚
โ”‚  โ”‚ The promise settles โ”‚                                    โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                                    โ”‚
โ”‚             โ”‚                                                โ”‚
โ”‚      โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”                                         โ”‚
โ”‚      โ–ผ             โ–ผ                                         โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                                    โ”‚
โ”‚  โ”‚Success โ”‚   โ”‚Failure โ”‚                                    โ”‚
โ”‚  โ”‚        โ”‚   โ”‚        โ”‚                                    โ”‚
โ”‚  โ”‚isSubmitโ”‚   โ”‚setErrorโ”‚                                    โ”‚
โ”‚  โ”‚Success โ”‚   โ”‚        โ”‚                                    โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                                    โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

The Server Error Mapping

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  THE SERVER ERROR MAPS TO THE FIELD                          โ”‚
โ”‚                                                              โ”‚
โ”‚  The server response                                         โ”‚
โ”‚  { "message": "The email is taken." }                        โ”‚
โ”‚         โ”‚                                                    โ”‚
โ”‚         โ–ผ                                                    โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                                    โ”‚
โ”‚  โ”‚ setError("email",   โ”‚  The error is stored in the        โ”‚
โ”‚  โ”‚   { message })      โ”‚  formState.errors.email            โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                                    โ”‚
โ”‚             โ”‚                                                โ”‚
โ”‚             โ–ผ                                                    โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                                    โ”‚
โ”‚  โ”‚ The error is        โ”‚  The UI renders it with the        โ”‚
โ”‚  โ”‚ displayed           โ”‚  other field errors                โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                                    โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Summary

ItemValue
handleSubmitThe validation boundary, prevents the native submission
onValidThe callback for the valid data
onInvalidThe callback for the errors
isSubmittingTrue while the async callback is pending
isSubmittedTrue after the attempt, regardless of the outcome
isSubmitSuccessfulTrue after the successful submission
setErrorMaps the server error to the field
resetResets the form and the errors

Key takeaways:

  • The handleSubmit prevents the native submission and validates before it calls the callback. The onValid callback receives the typed data, and the onInvalid callback receives the errors. The separation is the mechanism that keeps the invalid data out of the application .
  • The async callback is the mechanism for the submit state. The isSubmitting stays true until the promise settles, and the isSubmitSuccessful is true after the successful submission. A synchronous callback does not track the state .
  • The setError maps the server errors to the fields. The callback catches the server error and calls the setError with the field name and the error object. The root.server key is the mechanism for the general errors .
  • The reset clears the form after the success. The function restores the default values and clears the errors. The keepValues option keeps the current values while resetting the errors.
  • The isSubmitted and the isSubmitSuccessful are different states. The isSubmitted is true after the attempt, and the isSubmitSuccessful is true only when the validation passed and the callback did not throw .

Remember: The handleSubmit is the validation boundary. It prevents the native submission, runs the validation, and calls the onValid callback only if the validation passes. The onInvalid callback is the failure path. The async callback is the mechanism for the isSubmitting state, and the setError is the mechanism for the server errors. The reset is the cleanup after the success.

Was this helpful?


Stop using slow, ad-bloated tool sites! ๐Ÿคฎ

๐Ÿ”Ž Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
โœ… Finance (Mortgage, Interest, Inflation)
โœ… Tech (Base64, JSON, Dev Suite, IP)
โœ… Health (BMI, BMR, TDEE)
โœ… Productivity (Timer, Workspace, QR)

โšก๏ธ Fast & Private
๐Ÿ”’ No data leaves your device
๐Ÿ’Ž 100% Free

๐Ÿ”— Use it now: https://tools.kandz.me
๐Ÿ”– Bookmark itโ€”youโ€™ll need it later!