Could anyone explain why
useFormContext
is causing these re-renders and suggest a way to prevent them without removinguseFormContext
?
The useFormContext
hook is not causing extra component rerenders. Note that your InputX
and InputY
components have nearly identical implementations*:
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>
);
}
* The difference being that InputX
has an additional someCalculator
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 the y
and z
form states, and not x
.
const [y, z] = useWatch({
control: methods.control,
name: ["y", "z"],
});
y
and z
form states are updated, this triggers MainForm
to rerender, which re-renders itself and its entire sub-ReactTree, e.g. its children. This means MainForm
, MemoInputX
, MemoInputY
, the "input Z" and all the rest of the returned JSX all rerender.x
form state is updated, only the locally subscribed InputX
(MemoInputX
) component is triggered to rerender.If you updated MainForm
to also subscribe to x
form state changes then you will see nearly identical rendering results and counts across all three X, Y, and Z inputs.
const [x, y, z] = useWatch({
control: methods.control,
name: ["x", "y", "z"],
});
I expected that
InputX
would only re-render when its specific data or relevant form state changes (like its own input data).
React components render for one of two reasons:
state
or props
value updatedInputX
rerenders because MainForm
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 the renderCount.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 a React.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
, or useReducer
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. 😎
useEffect(() => {
renderCount.current += 1;
console.log("Render count Input", renderCount.current);
});
Input components:
function InputX() {
const { register, control } = useFormContext();
const renderCount = useRef(0);
const x = useWatch({ name: "x", control });
useEffect(() => {
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 });
useEffect(() => {
renderCount.current += 1;
console.log("Render count InputY", renderCount.current);
});
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>
);
}
Any advice on optimizing this setup would be greatly appreciated!
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.