react-hook-form / devtools

📋 DevTools to help debug forms.
https://react-hook-form.com/dev-tools
MIT License
646 stars 47 forks source link

DevTools causes hydration errrors on page refresh in Next.js #187

Open raleigh9123 opened 1 year ago

raleigh9123 commented 1 year ago

I thought I had done something wonky in my app when these errors started appearing. I rolled back on some changes and learned that they appeared when I refresh the page that has the component imported.

I am working within a Blitz.js app which is built on Next.js (I am unsure, but am lead to believe that this is a problem within Next.js?)

The

component used by default in the Blitz.js installation is a little bit of a beast, in my opinion, and I added a couple more details to allow me to pass a custom prop to the child inputs.

My Form Component

// src / core / components / Form.tsx
// --> COMPONENT <-

// Node Modules Imports
import { useState, ReactNode, PropsWithoutRef, Children, cloneElement } from "react"
import { FormProvider, useForm, UseFormProps } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import { z } from "zod"
import { DevTool } from "@hookform/devtools"

// TS Declarations
export interface FormProps<S extends z.ZodType<any, any>>
  extends Omit<PropsWithoutRef<JSX.IntrinsicElements["form"]>, "onSubmit"> {
  /** All your form fields */
  children?: ReactNode
  /** Text to display in the submit button */
  schema?: S
  onSubmit: (values: z.infer<S>) => Promise<void | OnSubmitResult>
  initialValues?: UseFormProps<z.infer<S>>["defaultValues"]
  formtype?: string
}

interface OnSubmitResult {
  FORM_ERROR?: string
  [prop: string]: any
}

export const FORM_ERROR = "FORM_ERROR"

export function Form<S extends z.ZodType<any, any>>({
  children,
  schema,
  initialValues,
  onSubmit,
  formtype = "submit",
  ...props
}: FormProps<S>) {
  const ctx = useForm<z.infer<S>>({
    mode: "onChange",
    resolver: schema ? zodResolver(schema) : undefined,
    defaultValues: initialValues,
  })
  const [formError, setFormError] = useState<string | null>(null)

  let isProd = process.env.NODE_ENV === "production"

  return (
    <FormProvider {...ctx}>
      <form
        onSubmit={ctx.handleSubmit(async (values) => {
          const result = (await onSubmit(values)) || {}
          for (const [key, value] of Object.entries(result)) {
            if (key === FORM_ERROR) {
              setFormError(value)
            } else {
              ctx.setError(key as any, {
                type: "submit",
                message: value,
              })
            }
          }
        })}
        className="space-y-4"
        {...props}
      >
        {/* Form fields supplied as children are rendered here */}
        {Children.map(children, (child) =>
          cloneElement(child as React.ReactElement, { formtype })
        )}

        {formError && (
          <div role="alert" className="text-salmon-500 text-center">
            {formError}
          </div>
        )}

      </form>
      {!isProd && <DevTool control={ctx.control} /> }

    </FormProvider>
  )
}

export default Form

Again, the hydration errors ONLY occur when I refresh the page. They are listed in the following screenshot. I have not tried anything as a solution, but they do not appear when I remove the component.

Screenshot 2022-12-29 at 10 51 22 PM

The following is my package.json dependencies:

"dependencies": {
    "@blitzjs/auth": "2.0.0-beta.19",
    "@blitzjs/next": "2.0.0-beta.19",
    "@blitzjs/rpc": "2.0.0-beta.19",
    "@headlessui/react": "1.7.7",
    "@heroicons/react": "2.0.13",
    "@hookform/error-message": "2.0.1",
    "@hookform/resolvers": "2.9.10",
    "@prisma/client": "4.8.0",
    "blitz": "2.0.0-beta.19",
    "framer-motion": "7.10.2",
    "next": "12.2.5",
    "node-device-detector": "2.0.9",
    "postmark": "3.0.14",
    "prisma": "4.8.0",
    "react": "18.2.0",
    "react-dom": "18.2.0",
    "react-hook-form": "7.40.0",
    "react-icons": "4.7.1",
    "zod": "3.20.2"
  },
  "devDependencies": {
    "@hookform/devtools": "4.3.0",
    "@next/bundle-analyzer": "12.0.8",
    "@tailwindcss/forms": "0.5.3",
    "@testing-library/jest-dom": "5.16.3",
    "@testing-library/react": "13.4.0",
    "@testing-library/react-hooks": "8.0.1",
    "@types/jest": "29.2.2",
    "@types/node": "18.11.9",
    "@types/preview-email": "2.0.1",
    "@types/react": "18.0.25",
    "@typescript-eslint/eslint-plugin": "5.30.5",
    "autoprefixer": "10.4.13",
    "eslint": "8.27.0",
    "eslint-config-next": "12.3.1",
    "eslint-config-prettier": "8.5.0",
    "husky": "8.0.2",
    "jest": "29.3.0",
    "jest-environment-jsdom": "29.3.0",
    "lint-staged": "13.0.3",
    "postcss": "8.4.19",
    "prettier": "^2.7.1",
    "prettier-plugin-prisma": "4.4.0",
    "pretty-quick": "3.1.3",
    "preview-email": "3.0.7",
    "tailwindcss": "3.2.4",
    "ts-jest": "28.0.7",
    "typescript": "^4.8.4"
  },
Nurou commented 1 year ago

@raleigh9123 I've the same issue in a next.js app caused by DevTools. I use the following dynamic import workaround for now:

const DevT = dynamic(
  () =>
    import('@hookform/devtools').then((module) => {
      return module.DevTool
    }),
  { ssr: false },
)
adamwdennis commented 1 year ago

@Nurou 's answer worked for me! Thank you!!

Here's a slightly updated, typescript-compatible version of it that worked for me (hope it can save a few minutes for others like me who enjoy having a painful eslint config 😄 ):

import dynamic from 'next/dynamic';
const DevT: React.ElementType = dynamic(
  () => import('@hookform/devtools').then((module) => module.DevTool),
  { ssr: false }
);

Then, (like @Nurou insinuates), I can use it like so:

...
return (
  <FormProvider {...methods}>
    <DevT control={methods.control} placement="top-left" />
    ...
  </FormProvider>
)
mohaimenkhalid commented 6 months ago

@adamwdennis cool.. working..

hvn47 commented 4 months ago

thank you so much

spacecat commented 4 months ago

React.ComponentType is the most appropriate type. It is more specific than React.ElementType and represents a React component class or a function component, which is what dynamic is expected to return.

Here is the updated code:

const DevTool: React.ComponentType = dynamic(
  () => import("@hookform/devtools").then((module) => module.DevTool),
  { ssr: false },
);

<DevTool control={control} />

Update: This will not work. VSCode will still complain about the control attribute:

Type '{ control: Control<Inputs, any>; }' is not assignable to type 'IntrinsicAttributes'.
  Property 'control' does not exist on type 'IntrinsicAttributes'.ts(2322)
(property) control: Control<Inputs, any>

So go with @adamwdennis' answer here: https://github.com/react-hook-form/devtools/issues/187#issuecomment-1369182795

const DevTool: React.ElementType = dynamic(
  () => import("@hookform/devtools").then((module) => module.DevTool),
  { ssr: false },
);

<DevTool control={control} />

Update 2:

Or you could do the following - seems to work too:

"use client";

import dynamic from "next/dynamic";
import { SubmitHandler, useForm, Control } from "react-hook-form";

type Inputs = {
  firstName: string;
};

interface DevToolProps {
  control: Control<Inputs>;
}

const DevTool: React.ComponentType<DevToolProps> = dynamic(
  () => import("@hookform/devtools").then((module) => module.DevTool),
  { ssr: false },
);

export default function Step01() {
  const { register, handleSubmit, control } = useForm<Inputs>({
    mode: "onChange",
  });
  const onSubmit: SubmitHandler<Inputs> = (data) => console.log(data);

  return (
    <>
      <form onSubmit={handleSubmit(onSubmit)}>
        <input
          className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
          defaultValue="Homer"
          {...register("firstName")}
        />
        <input
          className="rounded-md bg-indigo-600 px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
          type="submit"
        />
      </form>
      <DevTool control={control} />
    </>
  );
}