reactjstypescriptformseditreact-hook-form

Edit form with custom component for react-hook-form: default value


It's been 3 months since I started learning ReactJS + TypeScript. My question is about using react-hook-form (v7) for editing a form. I want to use the custom component that I created, and I found how to do it by myself!

Here is a part of my form provider with react-hook-form

import { FormProvider, useForm } from 'react-hook-form';
import { useNavigate, useParams } from 'react-router-dom';
import InputText from 'components/commons/form/InputText';

import { supabase } from 'configs/supabase';
const EditEducation: React.FC = () => {
    const { educationId } = useParams();
    const [education, setEducation] = useState<education>();

    const getEducation = async (educationId: string | undefined) => {
        try {
          const { data, error } = await supabase
            .from('tables1')
            .select('data1, data2')
            .eq('id', educationId)
            .single();
    
          if (error) {
            seterror(error.message);
          }
    
          if (data) {
            return data;
          }
        } catch (error: any) {
          alert(error.message);
        }
      };

  useEffect(() => {
    getEducation(educationId).then((data) => {
      setEducation(data);
    });

    // eslint-disable-next-line
  }, [educationId]);

  const methods = useForm();

  const onSubmit = async (formData: any) => {
    const updateData = {
        data1 = formData.data1,
        data2 = formData.data2
    };

    try {
      setSaving(true);
      const { error } = await supabase.from('educations').update(updateData);
      if (error) {
        seterror(error.message);
      }
      if (!error) {
        navigate('/experiences/education');
      }
      setSaving(false);
    } catch (error: any) {
      seterror(error.message);
    }
  };

return (
    ...
          <FormProvider {...methods}>
            <form className="p-4" onSubmit={methods.handleSubmit(onSubmit)}>
                <InputText
                  id="data1"
                  label="Data1"
                  placeholder="Ex: data1"
                  defaultValue={education?.data1}
                  options={{ required: 'This field is required' }}
                />
                <Button type="submit">{saving ? 'Saving' : 'Save'}</Button>
            </form>
          </FormProvider>
    ...
)
};

Here is my custom component:

import React, { useEffect } from 'react';
import { useFormContext } from 'react-hook-form';

interface InputProps {
  id: string;
  label: string;
  placeholder?: string;
  defaultValue?: string;
}

const InputText: React.FC<InputProps> = ({
  id,
  label,
  placeholder,
  defaultValue,
  options,
  ...rest
}: InputProps) => {
  const {
    register,
    setValue,
    formState: { errors }
  } = useFormContext();

  useEffect(() => {
    if (defaultValue) setValue(id, defaultValue, { shouldDirty: true });
  }, [defaultValue, setValue, id]);

  return (
    <div className="">
      <label htmlFor={id} className="">
        {label}
      </label>
      <input
        type="text"
        placeholder={placeholder}
        className=""
        id={id}
        defaultValue={defaultValue}
        {...register(id, options)}
        {...rest}
      />
      {errors[id] && (
        <p className="">
          <span className="">*</span> {errors[id]?.message}
        </p>
      )}
    </div>
  );
};

export default InputText;

As you can see, I have used a formContext because I want to deconstruct my code into smaller components.

Now I'm having some doubts if my code is correct, especially when I use ut editing forms: if I set my default value via the defaultValue prop, I have to submit (error show) and then click inside the input to change the state to clean the error in the input component.

This is why I have added the useEffect hook to clean the input validation error, and it's working. What do you think about this? Is there a better way to manage it (I think, yup, it's a cleaner way to set the validation schema)?

Thanks in advance, and sorry for my rusty English. Great day to all, and I hope my code will help people.

I've used <FormProvider {...methods}> and it's working, but I do not know if it's a good way to do it.

Edits: In reality, I have to double submit to get my data, so I guess it's not the correct way. Any suggestions?

I have found a "solution": if I have a defaultValue in my props, I do in my component:

useEffect(() => {
    if (defaultValue) setValue(id, defaultValue, { shouldDirty: true });
}, [defaultValue, setValue, id]);

I do not think it is a better solution ...


Solution

  • I wrongly edited my previous answer, here is the original:

    You should provide default values to useForm, not to your component (so your InputText doesn't need to know about defaultValue or setValue, it will have the correct value thanks to the register method).

    To initialize the form, you can do

    useForm({ defaultValues: { data1: education?.data1 } });
    

    If the data you use to provide default values is loading after the form is initialized, you can use the reset method (see docs), which I personally put in a useEffect to watch for data update:

    const Component: React.FC = ({ defaultValues }) => {
      const {
        register,
        handleSubmit,
        reset,
      } = useForm({ defaultValues });
    
      useEffect(() => {
        reset(defaultValues);
      }, [defaultValues, reset]);
    
      return ...
    }
    

    On another note, you should define getEducation in the useEffect that calls it, instead of in the component, so that the method isn't declared every time your component is rendered. Snippet:

      useEffect(() => {
        const getEducation = () => {
          ...
        };
    
        getEducation();
      }, [educationId]);