最新消息:Welcome to the puzzle paradise for programmers! Here, a well-designed puzzle awaits you. From code logic puzzles to algorithmic challenges, each level is closely centered on the programmer's expertise and skills. Whether you're a novice programmer or an experienced tech guru, you'll find your own challenges on this site. In the process of solving puzzles, you can not only exercise your thinking skills, but also deepen your understanding and application of programming knowledge. Come to start this puzzle journey full of wisdom and challenges, with many programmers to compete with each other and show your programming wisdom! Translated with DeepL.com (free version)

reactjs - Child Component Re-renders Due to useFormContext in React Hook Form - Stack Overflow

matteradmin5PV0评论

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.

Post a comment

comment list (0)

  1. No comments so far