2

It's been 3 months I learn ReactJS + TypeScript. My question is about to use react-hook-form (v7) for editing a form. I want to use my custom component that I created and 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 had use a formContext because I want to deconstruct my code into smaller components.

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

This is why I have add 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 hope my code will help people.

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

Edit : In reality, I have to double submit to get my data so I guess it's not the correct way, any sugestions ?

Edit2 : 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 the better solution ...

2 Answers 2

2

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]);
Sign up to request clarification or add additional context in comments.

Comments

1

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

editForm.tsx
    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>
    )
    ...
myCustomComponent.tsx

    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 had use a formContext because I want to deconstruct my code into smaller components. Now I'm having some doubts if I correctly code, specialy when I use ut editing forms : if set my default value via "defaultValue" prop, I have to submit (error show) then clique inside the input to change the state in order to clean the error in the input component.

This is why I have add 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 hope my code will help people.

Edit1 : In reality, I have to double submit to get my data so I guess it's not the correct way, any sugestions ?

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

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

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

Edit3 : Thanks @Jérémy Rippert this is my working solution :

editForm.tsx
    ...
    const methods = useForm();
    const { reset } = methods;

    useEffect(() => {
        reset(education);
    }, [reset, education]);

    return (
    <FormProvider {...methods}>
        <form className="p-4" onSubmit={methods.handleSubmit(onSubmit)}>
            <InputText
            id="degree"
            label="Degree"
            placeholder="Ex: Master 2 - Design & Marketing"
            options={{ required: 'This field is required' }}
            />
        </form>
    </FormProvider>
    )
    ...
myCustomComponent.tsx
    ...
    const InputText: React.FC<InputTextProps> = ({
      id,
      label,
      placeholder,
      options
    }: InputTextProps) => {
        const {
          register,
          formState: { isDirty, isValid, touchedFields, dirtyFields, errors }
        } = useFormContext();

        return (
        <input
            type="text"
            placeholder={placeholder}
            className={`block w-full rounded-lg border ${
            errors[id] ? 'border-red-600' : 'border-gray-600'
            } bg-gray-700 p-2.5 text-sm text-white placeholder-gray-400 focus:outline-none`}
            id={id}
            {...register(id, options)}
        />
        )
    ...

Thanks again @Jérémy Rippert

5 Comments

Salut Jérémy (je continu quand même en anglais), I had put my fetchData into the useEffect, it's okay now (can't edit my post yet). I try to pass defaultValues to the useForm and even if I pass all data, on my input I have nothing unless I write into quote " " a default value : code defaultValue:{data1: education.data1 code -> not working, code defaultValue:{data1: "example" code -> working. I don't have to pass it to my component, that's true, juste have to find why it does not want my object.
@alexcool68 glad you could work out a solution! However I think you edited my comment instead of your post (it's the first time I answer a post so I wasn't sure whether you edited my answer, sorry)
You are right, I edited your comment instead of valide your answer and it was an misstake. I don't found how to restore it so I guess it was your call and only you can re-edit your comment so you did good ! Merci encore pour ta réponse en tout cas, si je peux faire quelque chose pour te donner les points ou changer un truc dis le moi, je connais pas trop Stackoverflow encore ...
We are both learning how SO works ahah it's cool. Content d'avoir pu aider ! Franchement super que tu aies réussi à mettre en place react-hook-form alors que tu as commencé le react / TS il y à peine 3 mois, gg! Je crois que ma réponse originale m'a donné des points aussi grâce à ton upvote, merci :)
Pas de soucis, j'ai mis ta réponse en "réponse" donc tu as full crédit ! Oui j'apprends vite et c'est pas trop compliqué pour le moment.

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.