admin管理员组

文章数量:1026989

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 utilizes useFormContext specifically for the register 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:

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

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 utilizes useFormContext specifically for the register 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).
Share Improve this question edited Nov 16, 2024 at 22:55 Drew Reese 205k18 gold badges246 silver badges274 bronze badges asked Nov 16, 2024 at 11:16 MustafaMustafa 9812 gold badges11 silver badges24 bronze badges
Add a comment  | 

2 Answers 2

Reset to default 2

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"],
});
  • When the 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.
  • When the 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:

  • Their state or props value updated
  • The parent component rerendered (e.g. itself and all its children)

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)

  • Your component function body (only top-level logic, so this doesn’t include code inside event handlers)
  • Functions that you pass to useState, set functions, useMemo, or useReducer
  • Some class component methods like 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.

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 utilizes useFormContext specifically for the register 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:

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

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 utilizes useFormContext specifically for the register 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).
Share Improve this question edited Nov 16, 2024 at 22:55 Drew Reese 205k18 gold badges246 silver badges274 bronze badges asked Nov 16, 2024 at 11:16 MustafaMustafa 9812 gold badges11 silver badges24 bronze badges
Add a comment  | 

2 Answers 2

Reset to default 2

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"],
});
  • When the 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.
  • When the 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:

  • Their state or props value updated
  • The parent component rerendered (e.g. itself and all its children)

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)

  • Your component function body (only top-level logic, so this doesn’t include code inside event handlers)
  • Functions that you pass to useState, set functions, useMemo, or useReducer
  • Some class component methods like 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.

本文标签: reactjsChild Component Rerenders Due to useFormContext in React Hook FormStack Overflow