79196207

Date: 2024-11-16 22:59:58
Score: 0.5
Natty:
Report link

Could anyone explain why useFormContext is causing these re-renders and suggest a way to prevent them without removing useFormContext?

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"],
});

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:

InputX 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)

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.

Reasons:
  • Blacklisted phrase (1): appreciated
  • RegEx Blacklisted phrase (2.5): Could anyone explain
  • Long answer (-1):
  • Has code block (-0.5):
  • Contains question mark (0.5):
  • High reputation (-2):
Posted by: Drew Reese