Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[1.x] Fix useForm re-renders by memoizing functions in React #1607

Merged
merged 4 commits into from
Sep 18, 2024

Conversation

Hasan-Mir
Copy link
Contributor

When using the useForm() in a React component that has some heavy pure components(components using React.memo) as children and passing some methods returned by useForm() like setData, clearErrors, etc to them, on every re-render, these methods returned by useForm() are getting new references so new props are passed to these React.memo()ed children and they will be re-rendered unnecessarily.
This pull request fixes this issue by using useCallback for those methods in the useForm hook.

@jameshulse
Copy link

jameshulse commented Aug 6, 2023

I just want to flag that this PR (mostly) fixed some issues that we were running into. I have ended up taking a copy of this useForm implementation and referencing it directly in our application. So just giving a +1 that this PR could be prioritised to bring in.

I also needed to add data as a dependency of the transformFunction callback to ensure that it worked correctly for how we are using the hook:

  const transformFunction = useCallback(
    (callback) => {
      transform = callback;
    },
    // This dependency was added
    [data]
  );

@Hasan-Mir
Copy link
Contributor Author

Hasan-Mir commented Aug 7, 2023

Hi @jameshulse, thank you for the hint, I've fixed it in the latest commit by using the useRef instead of declaring the transform as let transform = (data) => data. This way the transform function maintains the same reference between re-renders.

Furthermore, using useRef addresses another issue in the current useForm implementation. If we use a state inside of the callback that we pass to the transform function and we update that state without updating data (calling setData), the transform function will not utilize the updated state. Instead, it will use the stale version of it.

function GalleryForm(props) {
  const { data, submit, errors, setData, processing, transform } = useForm({
    picture: null,
    picurl: gallery?.type === 0 ? gallery?.url : "",
  });

  const [pictureSourceType, setPictureSourceType] = useState(
    data.picurl ? "pic_url" : "pic_upload"
  );

  transform((prevData) => ({
    ...prevData,
    picture: prevData.type === 0 || pictureSourceType === "pic_url" ? null : prevData.picture,
    picurl: prevData.type === 1 || pictureSourceType === "pic_upload" ? null : prevData.picurl,
  }));

  function handelSubmit(e) {
    e.preventDefault();
    submit("post", "gallery", {
      forceFormData: true,
    });
  }

  return (
    <form>
      {/* ... */}

      <TabButton
        onClick={() => {
          setPictureSourceType("pic_upload");
        }}
      >
        upload image
      </TabButton>
      <TabButton
        onClick={() => {
          setPictureSourceType("pic_url");
        }}
      >
        use image url
      </TabButton>

      {/* ... */}
    </form>
  );
}

Here when I update the pictureSourceType (for example from "pic_url" to "pic_upload") and then try to submit the form, the value of pictureSourceType remains as "pic_url" when the transform function executes!
This is due to the dependency list of the submit function, which is [data, setErrors]. As long as the data remains unchanged, the submit function maintains the same function reference from the last re-render where data was modified and received a new reference. So the value of transform function belongs to that render.
To address this issue without using useRef we need to call setData wherever we update that state that we use inside of the transform's callback (note that I don't want to set picture or picurl to null when I change the pictureSourceType tab). This approach ensures that the submit function employs the most recent value of the transform.

function GalleryForm(props) {
 // ...

 return (
   <form>
     {/* ... */}

     <TabButton
       onClick={() => {
         setPictureSourceType("pic_upload");
+        setData((prevData) => ({ ...prevData })); 
       }}
     >
       upload image
     </TabButton>
     <TabButton
       onClick={() => {
         setPictureSourceType("pic_url");
+        setData((prevData) => ({ ...prevData })); 
       }}
     >
       use image url
     </TabButton>

     {/* ... */}
   </form>
 );
}

@kendepelchin
Copy link

Anyone has an idea on what this PR is waiting? Or could I patch package this somehow?

Copy link
Contributor

@derrickreimer derrickreimer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These changes seem good & safe. It requires a bit of diligence to keep the dependency arrays up-to-date on the useCallback calls, but I think worthwhile for performance.

@Hasan-Mir
Copy link
Contributor Author

@reinink It has been 431 days since the last commit!😶

@pedroborges pedroborges changed the title Fix React's useForm hook that returns new references for functions like setData, clearErrors, etc on every re-render [1.x] Fix useForm re-renders by memoizing functions in React Sep 13, 2024
@pedroborges pedroborges added the react Related to the react adapter label Sep 13, 2024
Copy link
Collaborator

@pedroborges pedroborges left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Copy link
Contributor

@joetannenbaum joetannenbaum left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good, thank you! As @derrickreimer mentioned, we just have to take care to maintain the dependency arrays.

@pedroborges pedroborges merged commit 0a58bc4 into inertiajs:master Sep 18, 2024
@Hasan-Mir
Copy link
Contributor Author

@joetannenbaum
Adding Eslint with eslint-plugin-react-hooks simplifies maintaining the dependency arrays.

pedroborges added a commit to olragon/inertia that referenced this pull request Sep 24, 2024
* master: (95 commits)
  [1.x] Fix props reactivity (inertiajs#1969)
  [1.x] useForm wrongly overwrites default values ​​after successful submission (inertiajs#1935)
  Update changelog
  [1.x] Fix `resetScrollPositions` and `restoreScrollPositions` router methods (inertiajs#1980)
  [1.x] Fix [scroll-region] on html element with overflow-scroll (inertiajs#1782)
  [1.x] Fix useForm re-renders by memoizing functions in React (inertiajs#1607)
  [1.x] Fix "DataCloneError: <Object> could not be cloned" (inertiajs#1967)
  [1.x] preserveScroll should be true on initial page visit (inertiajs#1360)
  Fix type augmentation (inertiajs#1958)
  [1.x] Fix doubling hash in React StrictMode (inertiajs#1728)
  [1.x] Add SSR support for Svelte 5 (inertiajs#1970)
  [1.x] Fix <Render /> component to respect "preserveState" (inertiajs#1943)
  [1.x] Fix 'received an unexpected slot "default"' warning (inertiajs#1941)
  QA: Add @types/lodash to fix svelte-check
  QA: Update reactive if statement
  Review useForm types
  QA: Move the if server up
  QA: Revert tsconfig change
  QA: Remove plural
  QA: Remove unused props type + add extra types just in case
  ...

# Conflicts:
#	packages/react/src/index.ts
DennisNikolay added a commit to DennisNikolay/inertia-fix-useform-rerender that referenced this pull request Dec 19, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
react Related to the react adapter
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants