Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React-hook-form useFieldArray for an array of strings instead of objects

I am using useFieldArray to fetch default values from my backend api. My categories is an array of strings. However, react-hook-form only supports array of objects. Here is my mongoose's schema

 type BookDocument = Document & {
  title: string
  description: string
  categories: string[]
  language: string
  publicationYear: number
}

const bookSchema = new Schema(
  {
    title: { type: String, required: true },
    description: { type: String, required: true },
    categories: [{ type: String, requried: true }],
    language: { type: String, required: true },
    publicationYear: { type: Number, required: true },
  },
  { timestamps: true }
)

Therefore, from the frontend I had to modify my form as follows:

type FormData = {
  title: string
  description: string
  categories: { category: string }[]
  language: string
  year: number
}

 const {
    handleSubmit,
    control,
    formState: { errors },
  } = useForm<FormData>({
    mode: 'onBlur',
    defaultValues: {
      title: book.title ?? '',
      description: book.description ?? '',
      categories:
        book.categories.map((elem) => {
          return { category: elem }
        }) ?? '',
      language: book.language ?? '',
      year: book.publicationYear ?? '',
    },
  })

The problem is when call the api request. The network payload will look like this and therefore can't be sent to the backend enter image description here

like image 745
Finlandary Avatar asked May 31 '26 10:05

Finlandary


1 Answers

There is a way. Check out the code I used to trick it to return flat array:

import {
  Box,
  Button,
  Grid,
  InputLabel,
  TextField,
  Typography,
} from '@mui/material';
import React from 'react';
import { Controller, useFieldArray, useFormContext } from 'react-hook-form';

import InputErrorMessages from '../../../shared/InputErrorMessages';

export default function Variables() {
  const {
    control,
    formState: {
      errors,
    },
  } = useFormContext();
  const { fields, append, remove } = useFieldArray({
    name: 'variables',
    control,
  });

  return(
    <Grid item xs={12}>
      <Typography variant="h6">Variables</Typography>
      <Box my={2}>
        <InputErrorMessages name="variables" errors={errors} />
      </Box>
      <Grid container spacing={2}>
        { fields.map(({ id }, index) => (
          <Grid item key={id} xs={12}>
            <Controller
              name={`variables.${index}`}
              control={control}
              render={({ field }) => (
                <>
                  <InputLabel id="size">{`CRV${index+1} =`}</InputLabel>
                  <TextField 
                    onChange={field.onChange}
                    onBlur={field.onBlur}
                    inputRef={field.ref}
                    name={`variables.${index}.value`}
                    value={field.value.value || field.value}
                    sx={{ width: '100%' }}
                  />
                </>)}
            />
          </Grid>
        ))}
        <Grid item>
          <Button onClick={() => append('value')}>Add Variable</Button>
        </Grid>
        <Grid item>
          { fields.length > 0 && (<Button onClick={() => remove(fields.length - 1)}>Delete Variable</Button>)}
        </Grid>
      </Grid>
    </Grid>
  )
}

I tricked it but not using the field item in the fields.map. Instead, I registered my own control using Controller because I want to control the name of the field and the value of it. Ie. I want to name the field variables.${index} instead of variables.${index}.value; the latter would be automatic if you used the field item returned by useFieldArray().

When you hit handleSubmit, you will see that the variable is a flat array.

However, there is a slight gotcha. If you load the form with fields from the backend, it will expect variable to be an array of objects with a value property; it won't understand a flat array of strings.

However, again, I was able to trick it because I control the value. Thus you see:

value={field.value.value || field.value}

Basically, if there is not value property on the object, then it must a string; so then just use the string.

Hope this helps!

like image 71
nukenin Avatar answered Jun 02 '26 02:06

nukenin