0

I have a list consisting of custom components (MaterialBar) containing a couple of fields:

enter image description here

I do a bulk update/save, so when I hit the "Save materials"-button, all the new materials should be created (if the required fields have a value) and the current ones should be updated, if they have changed. I am just unsure how I get the state of the nested MaterialBars (isValid for the new items and isDirty for the existing ones). How I do it right now:

Parent/useFieldArray:

export interface ProjectMaterialFormValues {
    materialBars: IMaterialBar[];
}

function ProjectMaterials({
  project,
  attachMaterial,
  detachMaterial,
}: ProjectMaterialsProps) {
  const { control, handleSubmit, formState } =
    useForm<ProjectMaterialFormValues>({
      defaultValues: { materialBars: projectMaterialsForProject(project) },
      mode: "onChange",
    });

  const { fields, remove, append } = useFieldArray({
    control,
    name: "materialBars",
  });

  return (
    <>
      {fields.map(({ id }, index) => (
        <MaterialBar
          key={id}
          index={index}
          onDelete={(projectMaterialId) =>
            setMaterialToDelete(projectMaterialId)
          }
          control={control}
        />
      ))}

      <ButtonsContainer>
        <Button variant="contained" onClick={() => navigate(routes.INDEX)}>
          {texts.HOME_RETURN_HOME}
        </Button>
        <Button variant="contained" onClick={addMaterial}>
          {texts.BUTTON_ADD_NEW_MATERIALS}
        </Button>
        <Button
          variant="contained"
          disabled={isSaving}
          onClick={handleSubmit(saveAllMaterials)}
        >
          {texts.BUTTON_SAVE_MATERIALS}
        </Button>
      </ButtonsContainer>
    </>
  );
}

Each MaterialBar is then registered though a Controller:

MaterialBar:

interface MaterialBarProps {
  index: number;
  onDelete: (id: number) => void;
  control: Control<ProjectMaterialFormValues>;
}

function MaterialBar({ index, onDelete, control }: MaterialBarProps) {
  const { materials } = useAllMaterials();

  return (
    <Container>
      <FormField>
        <Controller
          control={control}
          name={`materialBars.${index}.materialId`}
          rules={{ required: true }}
          render={({ field: { onChange, value } }) => (
            <Dropdown
              label="Material"
              items={materials}
              selectedItem={value}
              onSelect={onChange}
            />
          )}
        />
      </FormField>
      // the other fields
      <FormField>
        <Controller
          control={control}
          name={`materialBars.${index}.amount`}
          rules={{ required: true }}
          render={({ field: { onChange, value } }) => (
            <TextField
              label="Amount"
              type="number"
              value={value}
              onChange={onChange}
            />
          )}
        />
      </FormField>
      <Button onClick={handleDelete}>
        <img src={remove} alt="delete" />
      </Button>
    </Container>
  );
}

Doing it like this, I cannot get the state of each component which means that I do all the vaidation manually right now:

const saveAllMaterials: SubmitHandler<ProjectMaterialFormValues> = ({
  materialBars,
}) => {
  materialBars.forEach((newMaterial, index) => {
    if (newMaterial.projectMaterialId) {
      if (formState.dirtyFields.materialBars) {
        const dirtyIndexes = Object.keys(
          dirtyFields.materialBars as unknown as string
        );
        if (dirtyIndexes.includes(index.toString())) {
          // The API does not support update. Instead the material must
          // be deleted and a new one is created with the updated values
          detachMaterial(fields[index].materialId!);
          attachMaterial(newMaterial);
        }
      }
    } else if (
      // Would be nice if I can do something like "newMaterial.isValid"
      !newMaterial.projectMaterialId &&
      newMaterial.materialId &&
      newMaterial.typeId &&
      newMaterial.amount
    ) {
      attachMaterial(newMaterial);
    }
  });
};

This seems really unnecessary. Is there a way where I can access the state of of the child when I iterate through all the children in the fieldArray, so I don't have to do the verbose calculations? I was thinking that perhaps I need to have another useForm in the MaterialBar, but how do I pass the state to the parent upon saving?

4
  • What are you using to validate your inputs, the ones not in the filed array? If they are all of the inputs the form have then are you in the mood for using yup validation to intelligently validate your dynamic inputs? If answer it's true then I would show how to validate dynamic inputs generated by the useArrayField of react-hook-form Commented Jun 6, 2022 at 22:42
  • Right now I use react-hook-form's own validation rules, but I already planned to use yup, so if you can make an example, that would be great. Commented Jun 9, 2022 at 9:56
  • Check the snippet, any doubt don't hesitate to ask Commented Jun 9, 2022 at 17:15
  • @OsmanysFuentes-Lombá I haven't forgotten you, I am just on a summer break from this project. I will test it as soon as I start working on it again! Commented Jul 5, 2022 at 10:39

1 Answer 1

3

import {Controller, useForm} from 'react-hook-form'
import {yupResolver} from "@hookform/resolvers/yup";
import * as yup from 'yup'

// validation rules
const validation = yup.object({
    //... other validations
    // assert your field is an array of object with the shape you want
    fieldArrayName: yup.lazy(() => yup.array().of(yup.object({
        prop1: yup.string().required(), // validate each object's entry
        prop2: yup.number().required()  // independently
    })))
})


const {control} = useForm(({
    // ...
    resolver: yupResolver(validation) // pass the validation rules to the form
}))

// render array field
{
    fields.map(
        ({id}, index) => (
            <div key={id}>
                <Controller
                    // the name of the input should be prefixed with the
                    // array field's name(.) the current index (.)
                    name={`fieldArrayName.${index}.prop1`}
                    control={control}
                    render={({field, fieldStatus: {invalid, error}}) => (
                        <>
                        <input  {...field}/>
                        {invalid && <p>{error.message}</p>}
                        </p>
                    )}/>
                <Controller
                    name={`fieldArrayName.${index}.prop2`}
                    control={control}
                    render={({field, fieldStatus: {invalid, error}}) => (
                        <>
                        <input  {...field}/>
                        {invalid && <p>{error.message}</p>}
                        </p>
                    )}/>
                ...
            </div>
        ))
}

Sign up to request clarification or add additional context in comments.

1 Comment

render={({field, fieldStatus: {invalid, error}}) => ( ... )} this line should be corrected as render={({field, fieldState: {invalid, error}}) => ( ... )} . It is fieldState not fieldStatus

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.