javascriptreactjsreact-reduxform-datartk-query

formData in react/redux returns an undefined req.params.id in expressjs


I have a multipart form and I am using formData to send the data with a 'PUT' request. I am getting the error Cast to objectId failed for value 'undefined' (type string) at path '_id' for model 'Blogpost', and when I console.log req.params.id on the controller it returns undefined. I am using react-hook-form, redux-toolkit and multer-cloudinary for the image upload.

My frontend is:

const BlogpostEdit = () => {
  const { id } = useParams()

  const {
    data: blogpostData,
    isLoading,
    refetch,
    error
  } = useGetBlogpostDetailsQuery(id)

  const [updateBlogpost, { isLoading: loadingUpdate }] =
    useUpdateBlogpostMutation()

  useEffect(() => {
    if (blogpostData) {
      setValue('image', blogpostData.image)
      setValue('title', blogpostData.title)
      setValue('subtitle', blogpostData.subtitle)
      setValue('content', blogpostData.content)
      setValue('category', blogpostData.category)
    }
  }, [blogpostData])

  const {
    register,
    setValue,
    handleSubmit,
    formState: { errors }
  } = useForm()

  const onFormSubmit = async data => {
    if (data.image[0]) {
      const formData = new FormData()
      formData.append('_id', id)
      formData.append('image', data.image[0])
      data = { ...data, image: data.image[0].name }
      formData.append('image', data.image)
      formData.append('title', data.title)
      formData.append('subtitle', data.subtitle)
      formData.append('content', data.content)
      formData.append('category', data.category)
      try {
        const response = await updateBlogpost(formData).unwrap()
        toast.success('Blogpost has been updated')
        refetch()
      } catch (err) {
        toast.error(err?.data?.message || err.error)
      }
    } else {
      try {
        const response = await updateBlogpost({ ...data, _id: id }).unwrap()
        toast.success('Blogpost has been updated')
        refetch()
      } catch (err) {
        toast.error(err?.data?.message || err.error)
      }
    }
  }

  return (
    <FormContainer>
      {loadingUpdate && <Loader />}

      {isLoading ? (
        <Loader />
      ) : error ? (
        <p>{error.data.message}</p>
      ) : (
        <>
          <img src={blogpostData.image.url} style={{ width: '150px' }} />
          <form onSubmit={handleSubmit(onFormSubmit)}>
            <label htmlFor='image' name='image'>
              image
            </label>
            <input type='file' {...register('image')} />
            <p>{errors.image?.message}</p>
            <label htmlFor='title' name='title'>
              Title
            </label>
            <input type='text' {...register('title')} />
            <p>{errors.title?.message}</p>
            <label htmlFor='subtitle' name='subtitle'>
              Subtitle
            </label>
            <input type='text' {...register('subtitle')} />
            <p>{errors.subtitle?.message}</p>
            <label htmlFor='content' name='content'>
              Content
            </label>
            <textarea
              rows='10'
              cols='100'
              type='text'
              {...register('content')}
            />
            <p>{errors.content?.message}</p>
            <label htmlFor='category'>Choose a category:</label>
            <select name='category' {...register('category')}>
              <option value=''></option>
              <option value='game'>Game</option>
              <option value='tv'>TV</option>
              <option value='anime'>Anime</option>
              <option value='book'>Book</option>
            </select>
            <p>{errors.category?.message}</p>
            <button type='submit'>Submit</button>
          </form>
        </>
      )}
    </FormContainer>
  )
}

export default BlogpostEdit

The slice/mutation is:

updateBlogpost: builder.mutation({
      query: (data) => ({
        url: `${BLOGPOSTS_URL}/${data._id}`,
        method: 'PUT',
        body: data,
      }),
      invalidatesTags: ['Blogposts'],
    }),

The backend controller:

const updateBlogpost = asyncHandler(async (req, res) => {
  const { id } = req.params;
  const blogpost = await Blogpost.findByIdAndUpdate(id, { ...req.body });
  if (req.file) {
    blogpost.image.url = req.file.path;
    blogpost.image.filename = req.file.filename;
  }
  const updatedBlogpost = await blogpost.save();
  if (updatedBlogpost) {
    res.status(200).json(updatedBlogpost);
  } else {
    res.status(404);
    throw new Error('Resouce not found');
  }
});

and the route:

router
  .route('/:id')
  .put(registered, admin, upload.single('image'), updateBlogpost);

Submitting the form the data looks like this:

log from data

and the formData:

enter image description here

this is the error on the console:

enter image description here

and the error from the try/catch:

enter image description here

On the controller I get the req.body, the image successfully uploads but the req.params.id is undefined. If I do not add an image and just edit the title for example, everything works. These errors occur when I want to include an image. I use the exact same code on the 'POST' request and it works just fine. Now I know that I can upload the file first using a different route for the upload and a different route to update the model, but I just can not figure out why these errors are happening.


Solution

  • You are passing a FormData object to the mutation endpoint, which is not like a regular Javascript object that you can simply destructure properties from. You should use the accessor instead to access specific form data values.

    const formData = new FormData();
    formData.append("_id", "1234");
    
    console.log(formData._id); // undefined
    console.log(formData.get("_id")); // "1234"

    Update the endpoint to correctly access the id form data.

    updateBlogpost: builder.mutation({
      query: (data) => ({
        url: `${BLOGPOSTS_URL}/${data.get("_id")}`,
        method: 'PUT',
        body: data,
      }),
      invalidatesTags: ['Blogposts'],
    }),