react-hook-form FormProvider performance with controlled components (or, how to memoize controlled components)

Viewed 388

I'm using FormProvider to deal with a dynamically created, "deeply nested" form. The FormProvider Performance documentation says, "...[FormProvider] also causes the component tree to trigger a re-render when React Hook Form triggers a state update", and recommends memoizing the components to address this. I'm using controlled components (MUI) and can't figure out how to do the memoizing. Here's the deeply-nested portion that returns the form input controls:

const FormContent = ( {content} ) => { 
  return content.map((item, i) => { 
    const name = item.component.props.name;

    return ( 
      <ConnectForm key={name + '_' + i}>
        {() => <Controller
          name={name}
          defaultValue=''
          render={({ field: { onChange, onBlur, value, name, ref },
                     fieldState: { error },
                     formState: { isDirty } }) => { 
              return React.cloneElement(item.component, { 
                name: name,
                value: value,
                onChange: onChange,
                inputRef: ref,
                error: isDirty && !!error,
                helperText: isDirty && error?.message,
                FormHelperTextProps: { error: true } 
              })
            } 
          } 
        />}
      </ConnectForm>
    );
  });
}

I tried to do

  render={ 
    memo(({ field: { onChange, onBlur, value, name, ref },
      return React.cloneElement(item.component, { 
        ...
      })
    },
    (prevProps, nextProps) =>
      prevProps.formState.isDirty === nextProps.formState.isDirty)
  } 

but Controller throws a TypeDef error that "props.render is not a function". If I do

{() => memo(<Controller
  ...
/>,
(prevProps, nextProps) =>
  prevProps.formState.isDirty === nextProps.formState.isDirty
)}

or

  memo(<ConnectForm key={name + '_' + i}>
    {() => <Controller
      ... 
    />}
  </ConnectForm>,
    (prevProps, nextProps) =>
      prevProps.formState.isDirty === nextProps.formState.isDirty
  )

It warns for memo that "The first argument must be a component. Instead received: object", and ConnectForm (or FormContent in the 2nd instance) throws an error, "Objects are not valid as a React child".

How do I go about implementing this "performance enhancement" with controlled component?

1 Answers

Hey not sure if you are still having this issue but I've built a new form library that doesn't come with any of these issues. You can have nested components without needing to do anything special.

Its' in beta and currently being worked on but it might solve the issues you are having.

You can check it out here if you feel so inclined: https://react-formz.zerry.dev/docs/intro/

import {
  Form,
  TextField,
  NumberField,
  DependentTextField,
} from "@zerry/react-formz";

const NestedInput = () => {
  return (
    <TextField
        required
        name="name"
        as={({ input }) => <input {...input} />}
    />
  )
}

const MyForm = () => {
  return (
    <Form initialValues={{ name: "", age: "" }}>
      <NestedInput />
      <NumberField
        required
        name="age"
        as={({ input }) => <input {...input} />}
      />
    </Form>
  );
};
Related