0

I'm using a DynamicForm component with a ref in a React functional component. I want to update the form once my Firebase collection is loaded. Here’s the relevant part of my component:

    const dynamicFormRef = useRef(null);
    const { firebaseCollection } = useGetFirebaseCollection(firebaseCollectionName().trucks);
    
    useEffect(() => {
        if (firebaseCollection && dynamicFormRef.current) {
            dynamicFormRef.current.setFormData((prevFormData) => ({
                ...prevFormData,
                truckId: {
                    ...prevFormData.truckId,
                    options: firebaseCollection.map((truck) => truck.nrAuto),
                },
            }));
        }
    }, [firebaseCollection, dynamicFormRef.current]);
    
    
    return (
    
       <DynamicForm
          ref={dynamicFormRef}
          fields={truckDocumentDatabaseSchema().fields}
          mode="add"
          onSubmit={submitData}
       />
   )

It runs well, but ESLint complains:

React Hook useEffect has an unnecessary dependency: 'dynamicFormRef.current'. Either exclude it or remove the dependency array. Mutable values like 'dynamicFormRef.current' aren't valid dependencies because mutating them doesn't re-render the component  react-hooks/exhaustive-deps

So, I try to remove it from the useEffect's dependency array, he useEffect does not run when I expect it to.

I suspect that firebaseCollection is loaded before DynamicForm is rendered, so when the useEffect first runs, dynamicFormRef.current is null. But I’m not entirely sure.

Note: DynamicForm exposes setFormData via useImperativeHandle:

const DynamicForm = forwardRef(
    (
        { fields, onSubmit, mode, ...otherProps },
        ref
    ) => {
    const [formData, setFormData] = useState(fields);
    const [isEditable, setIsEditable] = useState(mode === "add");

    // form logic ...

    useImperativeHandle(ref, () => ({
        submitForm: () => handleFormSubmit(),
        isLoading: () => loading,
        isEditable: isEditable,
        setFormData: setFormData,
    }));

    return (
        <FullBox>
            <Box component="form" onSubmit={handleFormSubmit}>
                {Object.entries(formData).map(([fieldName, fieldData]) => (
                    <FieldRenderer
                        key={fieldName}
                        fieldName={fieldName}
                        fieldData={fieldData}
                        formData={formData}
                        isEditable={isEditable}
                        handleChange={handleChange}
                    />
                ))}
            </Box>
        </FullBox>
    );
});
export default DynamicForm;

How do I solve this? If I want it to work, the linter complains; if I want the linter to be happy, the useEffect doesn't do its job. I thought I understood (at least in principle) what refs are, but it seems there are some gaps in my understanding.

1
  • "useEffect does not run when I expect it to" - when? One of the problems is that useImperativeHandle doesn't have dependency list, it's a new value on each render. This will cause additional effect runs. "dynamicFormRef.current is null" - no, a child mounts before a parent Commented Sep 12 at 6:47

1 Answer 1

2

The React Hooks ESLint plugin is trying to protect you from subtle bugs. It knows:

  • dynamicFormRef (the ref object) is stable across renders.

  • dynamicFormRef.current (its .current property) is mutable and doesn’t trigger re-renders when it changes.

Putting dynamicFormRef.current in the dependency array doesn’t make sense, because React will never re-run your effect when .current changes. That’s why you see:

“Mutable values like ‘dynamicFormRef.current’ aren’t valid dependencies…”

As mentioned in @Ori Drori's answer, some other state change is causing your component to re-render, which in turn causes your useEffect to check its dependencies for changes and re-runs the effect because dynamicFormRef.current has a different value compared to the last render.


Right now your parent is reaching imperatively into the child through a ref to set state. That’s exactly what forwardRef/useImperativeHandle is for, but it’s not how React normally wants you to pass data down.

In your case you don’t really need a ref. You already know what the data is in the parent (firebaseCollection), and you’re rendering the child. So just pass the data as a prop and let the child update its own state from that prop.

Instead of creating the form and then imperatively calling setFormData, you prepare the fields before you render DynamicForm:

const { firebaseCollection } = useGetFirebaseCollection(firebaseCollectionName().trucks);

// take your schema
const baseFields = truckDocumentDatabaseSchema().fields;

// merge in the options before rendering
const fieldsWithTruckOptions = {
  ...baseFields,
  truckId: {
    ...baseFields.truckId,
    options: firebaseCollection?.map(truck => truck.nrAuto) ?? [],
  },
};

return (
  <DynamicForm
    fields={fieldsWithTruckOptions}
    mode="add"
    onSubmit={submitData}
  />
);

Your DynamicForm becomes a pure component. It just renders the fields prop it’s given. You don’t need forwardRef or an imperative setFormData:

const DynamicForm = ({ fields, onSubmit, mode, ...otherProps }) => {
  const [formData, setFormData] = useState(fields);
  const isEditable = mode === "add";

  // update local state when parent `fields` change
  useEffect(() => {
    setFormData(fields);
  }, [fields]);

  return (
    <Box component="form" onSubmit={handleFormSubmit} {...otherProps}>
      {Object.entries(formData).map(([fieldName, fieldData]) => (
        <FieldRenderer
          key={fieldName}
          fieldName={fieldName}
          fieldData={fieldData}
          formData={formData}
          isEditable={isEditable}
          handleChange={handleChange}
        />
      ))}
    </Box>
  );
};
Sign up to request clarification or add additional context in comments.

2 Comments

your answer works only in the very specific case you've described. But consider this: sometimes I want to update the ONLY CERTAIN FIELDS inside the form programatically (for example, after reading data from a PDF). I cannot rely on the field prop to update the data inside the form as it will overwrite existing data. This is where the imperativeHandle becomes useful.
You can expose formData through forwardRef to access the data on the form, which you can use to update fields from the parent. However, I don't recommend exposing setFormData this way, as updating the form's state through the ref will make the logic of your component less maintainable and harder to follow.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.