Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Error: cannot clone body after it is used #43

Closed
johnsonthedev opened this issue Jun 29, 2022 · 6 comments
Closed

Error: cannot clone body after it is used #43

johnsonthedev opened this issue Jun 29, 2022 · 6 comments

Comments

@johnsonthedev
Copy link

Remix throws an error Error: cannot clone body after it is used if I try to access the formData before calling the formAction method

const formData = await request.formData()

Here is a code to reproduce it:

import { ActionFunction } from "@remix-run/server-runtime"
import { InputError, makeDomainFunction } from "remix-domains"
import { Form, formAction } from "remix-forms"
import { z } from "zod"

const schema = z.object({
  email: z.string().nonempty().email(),
})

const takenEmails = ["foo@bar.com", "bar@foo.com"]

const mutation = makeDomainFunction(schema)(async (values) => {
  if (takenEmails.includes(values.email)) {
    throw new InputError("Email already taken", "email")
  }

  return values
})

export const action: ActionFunction = async ({ request }) => {
  const formData = await request.formData()

  return formAction({ request, schema, mutation })
}

export default () => <Form schema={schema} />
@LandSprutte
Copy link

Why do you want to clone the data? I've been running into the same problem a couple of times and manage to extract whatever logic I expected to have done in the action, to the mutation itself :)

@johnsonthedev
Copy link
Author

I want to use two forms on one page.

I tried to give each form a different action target <Form action='/api/change/email' but this caused other errors if the action is not on the same route.

I also tried to use the or function from zod but formAction from remix-forms can't work with it.

So my last idea was to call the mutation based on a hidden field. something like this:

import { ActionFunction, json } from "@remix-run/server-runtime"
import { InputError, makeDomainFunction } from "remix-domains"
import { Form, formAction } from "remix-forms"
import { z } from "zod"

const emailSchema = z.object({
  type: z.string().nonempty(),
  email: z.string().nonempty().email(),
})

const takenEmails = ["foo@bar.com", "bar@foo.com"]

const emailMutation = makeDomainFunction(emailSchema)(async (values) => {
  if (takenEmails.includes(values.email)) {
    throw new InputError("Email already taken", "email")
  }

  return values
})

const passwordSchema = z.object({
  type: z.string().nonempty(),
  password: z.string().nonempty(),
  confirmPassword: z.string().nonempty(),
})

const passwordMutation = makeDomainFunction(passwordSchema)(async (values) => {
  if (values.password !== values.confirmPassword) {
    throw new InputError("Password does not match", "confirmPassword")
  }

  return values
})

export const action: ActionFunction = async ({ request }) => {
  const formData = await request.formData()
  let type = formData.get("type")

  switch (type) {
    case "CHANGE_EMAIL":
      return formAction({
        request,
        schema: passwordSchema,
        mutation: passwordMutation,
      })
    case "CHANGE_PASSWORD":
      return formAction({
        request,
        schema: emailSchema,
        mutation: emailMutation,
      })
    default:
      return json("type missing", 400)
  }
}

export default () => (
  <div>
    <Form schema={emailSchema} hiddenFields={["type"]} values={{ type: "CHANGE_EMAIL" }} />
    <Form schema={passwordSchema} hiddenFields={["type"]} values={{ type: "CHANGE_PASSWORD" }} />
  </div>
)

@LandSprutte
Copy link

LandSprutte commented Jun 30, 2022

If you really want these in the route, I would suggest collecting the schema into one. This doesn't align with seperation of concerns so, but since you want to do two separate actions on the same page. I think this could be a solution. Let me know what you think :)

const schema = z.object({
  type: z.enum(["CHANGE_EMAIL", "CHANGE_PASSWORD"]),
  email: z.string().nonempty(),
  password: z.string().nonempty(),
  confirmPassword: z.string().nonempty(),
});

const mutation = makeDomainFunction(schema)(async (values) => {
  switch (values.type) {
    case "CHANGE_EMAIL":
    // do email stuff
    case "CHANGE_PASSWORD":
    // do password stuff
    default:
      return json("type missing", 400);
  }
});

export const action: ActionFunction = async ({ request }) =>
  formAction({
    mutation,
    schema,
    request,
  });

export default () => (
  <div>
    <Form schema={schema} values={{ type: "CHANGE_EMAIL" }} />
  </div>
);

@felipefreitag
Copy link
Contributor

Regarding the issue title, try with const formData = await request.clone().formData().

@felipefreitag
Copy link
Contributor

felipefreitag commented Jun 30, 2022

I tried your strategy of using the same action for both forms. I went past the cloning error, but there's something else happening because when I submit one form, the other one also gets client-side validation.
It can be something related to react-hook-form, we'll leave this issue open until we can figure it out.

In the meantime, the strategy of using different actions for each form is working and it's the one we use and recommend :) Do you want to dig into the errors you had using separate actions?

@diogob
Copy link
Contributor

diogob commented Jul 15, 2022

I'm closing this since it's a known behaviour of formData and there are plenty of workarounds (some already described in the thread)

@diogob diogob closed this as completed Jul 15, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants