I’m experiencing a challenge with React Hook Form where my child component InputX
re-renders every time there’s an update in the parent component or other parts of the application, despite no changes in InputX
itself. I’ve pinpointed that the re-renders are triggered by the use of useFormContext
to access the register
function within InputX
.
Here’s a brief outline of my setup:
- The
InputX
component utilizesuseFormContext
specifically for theregister
function. - The form is managed by a
FormProvider
in the parent component.
I’ve noticed that when I remove useFormContext
from InputX
, the unnecessary re-renders stop. This leads me to believe that something about how useFormContext
is interacting with register
or the context setup might be causing these updates.
import { memo, useRef, useMemo, useEffect } from "react";
import {
useFormContext,
useWatch,
useForm,
FormProvider,
} from "react-hook-form";
import {
MergeFormProvider,
useMergeForm,
useMergeFormUtils,
} from "./MergeFormProvider";
function InputX() {
const { register, control } = useFormContext();
const renderCount = useRef(0);
const x = useWatch({ name: "x", control });
renderCount.current += 1;
console.log("Render count InputX", renderCount.current);
const someCalculator = useMemo(() => x.repeat(3), [x]);
return (
<fieldset className="grid border p-4">
<legend>Input X Some calculator {someCalculator}</legend>
<div>Render count: {renderCount.current}</div>
<input {...register("x")} placeholder="Input X" />
</fieldset>
);
}
function InputY() {
const { register, control } = useFormContext();
const renderCount = useRef(0);
const y = useWatch({ name: "y", control });
renderCount.current += 1;
return (
<fieldset className="grid border p-4">
<legend>Input Y {y}</legend>
<div>Render count: {renderCount.current}</div>
<input {...register("y")} placeholder="Input Y" />
</fieldset>
);
}
function TodoByFormID() {
const { formID } = useMergeForm();
/**
* Handle component by form id
*/
return <div></div>;
}
const MemoInputX = memo(InputX);
const MemoInputY = memo(InputY);
function MainForm({ form }) {
const { setFieldOptions } = useMergeFormUtils();
const renderCount = useRef(0);
renderCount.current += 1;
const methods = useForm({
defaultValues: form,
});
const [y, z] = useWatch({
control: methods.control,
name: ["y", "z"],
});
const fieldOptions = useMemo<ISelect[]>(() => {
if (y.length) {
return Array.from({ length: y.length }, (_, index) => ({
label: index.toString(),
value: index + ". Item",
}));
}
return [];
}, [y]);
useEffect(() => {
setFieldOptions(fieldOptions);
}, [fieldOptions]);
return (
<FormProvider {...methods}>
<fieldset>
<legend>Main Form Y Value:</legend>
{y}
</fieldset>
<MemoInputX />
<MemoInputY />
<fieldset className="grid border p-4">
<legend>Input Z {z}</legend>
<div>Render count: {renderCount.current}</div>
<input {...methods.register("z")} placeholder="Input Z" />
</fieldset>
<TodoByFormID />
</FormProvider>
);
}
export default function App() {
const formID = 1;
const form = {
count: [],
x: "",
y: "",
z: "",
};
return (
<MergeFormProvider initialFormID={formID}>
<MainForm form={form} />
</MergeFormProvider>
);
}
For a clearer picture, I’ve set up a simplified version of the problem in this CodeSandbox: https://codesandbox.io/p/sandbox/39397x
Could anyone explain why useFormContext
is causing these re-renders and suggest a way to prevent them without removing useFormContext
? Any advice on optimizing this setup would be greatly appreciated!
I utilized useFormContext
in InputX
to simplify form handling and avoid prop drilling across multiple component layers in my larger project.
I expected that InputX
would only re-render when its specific data or relevant form state changes (like its own input data).
Note:
- The CodeSandbox link provided is a minimized version of my project. In the full project, I have components several layers deep (grand grand children).
2
Answers
It has been almost one week since I tried to understand why it re-renders. After several demonstrations and trials, I moved the FormProvider outside of MainForm.tsx and MergeFormProvider.tsx, which solved the issue. I realized that re-renders in MainForm.tsx were triggering the React Hook Form FormProvider, causing other child components to re-render as well. However, once I relocated the FormProvider, it stopped the unnecessary re-renders, including in the components where I use useFormContext.
This is fixed codebase: https://codesandbox.io/p/sandbox/react-hook-form-unnecessary-rerender-fixed-l679c7?workspaceId=afa5929a-cd92-42e4-8f73-bb3d8dbc7fe6
types.ts
RHFProvider.tsx
MergeFormProvider.tsx
App.tsx
The
useFormContext
hook is not causing extra component rerenders. Note that yourInputX
andInputY
components have nearly identical implementations*:* The difference being that
InputX
has an additionalsomeCalculator
value it is rendering.and yet it’s only when you edit inputs Y and Z that trigger X to render more often, but when you edit input X, only X re-renders.
This is caused by the parent
MainForm
component subscribing, i.e.useWatch
, to changes to they
andz
form states, and notx
.y
andz
form states are updated, this triggersMainForm
to rerender, which re-renders itself and its entire sub-ReactTree, e.g. its children. This meansMainForm
,MemoInputX
,MemoInputY
, the "input Z" and all the rest of the returned JSX all rerender.x
form state is updated, only the locally subscribedInputX
(MemoInputX
) component is triggered to rerender.If you updated
MainForm
to also subscribe tox
form state changes then you will see nearly identical rendering results and counts across all three X, Y, and Z inputs.React components render for one of two reasons:
state
orprops
value updatedInputX
rerenders becauseMainForm
rerenders.Now I suspect at this point you might be wondering why you also see so many "extra"
console.log("Render count InputX", renderCount.current);
logs. This is because in all the components you are not tracking accurate renders to the DOM, e.g. the "commit phase", all therenderCount.current += 1;
and console logs are unintentional side-effects directly in the function body of the components, and because you are rendering the app code within aReact.StrictMode
component, some functions and lifecycle methods are invoked twice (only in non-production builds) as a way to help detect issues in your code. (I’ve emphasized the relevant part below)useState
,set
functions,useMemo
, oruseReducer
constructor
,render
,shouldComponentUpdate
(see the whole list)You are over-counting the actual component renders to the DOM.
The fix for this is trivial: move these unintentional side-effects into a
useEffect
hook callback to be intentional side-effects. 😎Input components:
As laid out above, there’s really not any issue in your code as far as I can see. The only change to suggest was fixing the unintentional side-effects already explained above.