DevTools causes hydration errrors on page refresh in Next.js
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 <DevTool /> 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 <Form /> 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 <DevTool /> component.
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"
},
@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 },
)
@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>
)
@adamwdennis cool.. working..
thank you so much
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} />
</>
);
}