Could anyone explain why
useFormContextis 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
InputXwould 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 useReducerconstructor, 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.