'Can't delete a row from MUI DataGrid in MERN CRUD app using Redux/RTK?

For the life of me, I can't figure out why I can't get the delete button to work on this Material UI (MUI V5) Data Grid table. I'm a little new to coding, especially with MERN and Redux, so my brain is fried at the moment, and I've tried a million things all weekend, including Google, Stack Overflow, and taking a long break. The code below is how I think it should work, but when the button is clicked, nothing happens on the front end. A delete request is allegedly successfully sent to the server (or fulfilled), but of course does not actually delete the JSON record from the db, and I get this response message back in the console using redux dev tools:

"Cast to ObjectId failed for value "undefined" (type string) at path "_id" for model "Purchase""

Creating or adding items works fine on the front end in a separate form on a separate route. Delete worked fine when I tested it in Postman before building out the front end. The answer is probably very simple, I just can't figure it out due to inexperience. Is it a simple JS issue with a object variable that I'm not seeing (seems like it from the error message) or maybe a state management/redux issue? Could it have something to do with MongoDB's default "_id" property interacting with MUI? Or something else entirely? Here is the data grid component:

import React, { useState, useEffect } from 'react'
import { DataGrid, GridToolbar } from '@mui/x-data-grid'
import {getPurchases} from '../features/purchases/purchaseSlice'
import {useSelector, useDispatch} from 'react-redux'
import { Button } from '@mui/material'
import {deletePurchase} from '../features/purchases/purchaseSlice'

const TableForm = () => {

  const [pageSize, setPageSize] = useState(10);
  
  const dispatch = useDispatch()
  const {purchase, purchases, isError, message} = useSelector((state) => state.purchases)

  useEffect(() => {

    if (isError) {
      console.log(message);
    }

    dispatch(getPurchases());

  }, [isError, dispatch, message]);

  const renderDetailsButton = () => {
    return (
        <strong>
            <Button
                className="delete"
                variant="contained"
                color="primary"
                size="small"
                style={{ margin: 0}}
                onClick={() => {dispatch(deletePurchase(purchases.id))}}
            >
                 X           
            </Button>
        </strong>
    )
}

    
  const columns = [
    {field: '_id', headerName: 'ID', sortable: 'false', filterable: 'false', disableColumnMenu: 'true', visibility: 'false'},
    {field: 'title', headerName: 'Title', editable: true},
    {field: 'producer', headerName: 'Producer'},
    {field: 'director', headerName: 'Director'},
    {field: 'platform', headerName: 'Platform'},
    {field: 'year', headerName: 'Year of Release', maxwidth: 75},
    {field: 'price', headerName: 'Price', maxwidth: 75},
    {field: 'length', maxwidth: 75},
    {field: 'requesterName', headerName: 'Requester Name'},
    {field: 'requesterEmail', headerName: 'Requester Email'},
    {field: 'requesterDepartment', headerName: 'Requester Department'},
    {field: 'notes', headerName: 'Notes/Comments'},
    {field: 'createdAt', headerName: 'Created On', type: 'date' },
    {
        field: 'col5',
        headerName: 'Edit',
        width: 150,
        renderCell: renderDetailsButton,
        disableClickEventBubbling: true,
    },
    /*{
        field: 'col6',
        headerName: 'Delete',
        width: 150,
        renderCell: renderDetailsButton,
        disableClickEventBubbling: true,
    },*/
  ];


  return (
    <div style={{ height: 700, width: '100%' }}>
      <DataGrid experimentalFeatures={{ newEditingApi: true }} getRowId={row => row._id}
        sx={{
          boxShadow: 1,
          border: 1,
          borderColor: 'lightgrey',
          '& .MuiDataGrid-cell:hover': {
            color: 'primary.main',
          },
        }}
        initialState={{
          columns:{
            columnVisibilityModel: {_id: false}
          },
        pagination: {
          pageSize: 10,
          },
        }}
        rows={purchases}
        columns= {columns}
        components={{Toolbar: GridToolbar}}
        pageSize={pageSize}
        onPageSizeChange={(newPageSize) => setPageSize(newPageSize)}
        rowsPerPageOptions={[10, 25, 50, 100]}
      />
    </div>
  )
}

export default TableForm

The Slice:

import {createSlice, createAsyncThunk} from '@reduxjs/toolkit'
import purchaseService from './purchaseService'



const initialState = {
    purchases: [],
    isError: false,
    isSuccess: false,
    isLoading: false,
    message: ''
}

//Create new Purchase
export const createPurchase = createAsyncThunk(
    'purchases/create', 
    async (purchaseData, thunkAPI) => {
    try {
        const token = thunkAPI.getState().auth.user.token
        return await purchaseService.createPurchase(purchaseData, token)
    } catch (error) {
        const message =
            (error.response && 
            error.response.data &&
            error.response.data.message) ||
            error.message ||
            error.toString()
        return thunkAPI.rejectWithValue(message)
    }
})

//Get purchases
export const getPurchases = createAsyncThunk('purchases/getAll', async(_, thunkAPI) => {
    try {
        const token = thunkAPI.getState().auth.user.token
        return await purchaseService.getPurchases(token)
    } catch (error) {
        const message =
            (error.response && 
            error.response.data &&
            error.response.data.message) ||
            error.message ||
            error.toString()
        return thunkAPI.rejectWithValue(message)
    }
})

//Delete Purchase
export const deletePurchase = createAsyncThunk(
    'purchases/delete', 
    async (id, thunkAPI) => {
    try {
        const token = thunkAPI.getState().auth.user.token
        return await purchaseService.deletePurchase(id, token)
    } catch (error) {
        const message =
            (error.response && 
            error.response.data &&
            error.response.data.message) ||
            error.message ||
            error.toString()
        return thunkAPI.rejectWithValue(message)
    }
})

export const purchaseSlice = createSlice({
    name: 'purchase',
    initialState,
    reducers: {
        reset: (state) => initialState,
    },
    extraReducers: (builder) => {
        builder
            .addCase(createPurchase.pending, (state) => {
                state.isLoading = true
            })
            .addCase(createPurchase.fulfilled, (state, action) => {
                state.isLoading = false 
                state.isSuccess = true
                state.purchases.push(action.payload)
            })
            .addCase(createPurchase.rejected, (state, action) => {
                state.isLoading = false 
                state.isError = true
                state.message = action.payload
            })
            .addCase(getPurchases.pending, (state) => {
                state.isLoading = true
            })
            .addCase(getPurchases.fulfilled, (state, action) => {
                state.isLoading = false 
                state.isSuccess = true
                state.purchases = action.payload
            })
            .addCase(getPurchases.rejected, (state, action) => {
                state.isLoading = false 
                state.isError = true
                state.message = action.payload
            })
            .addCase(deletePurchase.pending, (state) => {
                state.isLoading = true
            })
            .addCase(deletePurchase.fulfilled, (state, action) => {
                state.isLoading = false 
                state.isSuccess = true
                state.purchases = state.purchases.filter((purchases) => purchases._id !== action.payload.id)})
            .addCase(deletePurchase.rejected, (state, action) => {
                state.isLoading = false 
                state.isError = true
                state.message = action.payload
            })
    }
})

export const {reset} = purchaseSlice.actions
export default purchaseSlice.reducer

Slice Service:


const API_URL = '/api/purchases/';

//Create new Purchase
const createPurchase = async (purchaseData, token) => {
    const config = {
        headers: {
            Authorization: `Bearer ${token}`
        }
    }

    const response = await axios.post(API_URL, purchaseData, config)

    return response.data
}


//Get purchases
const getPurchases = async (token) => {
    const config = {
        headers: {
            Authorization: `Bearer ${token}`
        }
    }

    const response = await axios.get(API_URL, config)

    return response.data
}

//Delete Purchase
const deletePurchase = async (purchaseId, token) => {
    const config = {
        headers: {
            Authorization: `Bearer ${token}`
        }
    }

    const response = await axios.delete(API_URL + purchaseId, config)

    return response.data
}

const purchaseService = {
    createPurchase,
    getPurchases,
    deletePurchase,
}

export default purchaseService

And the page/route the data grid component is on, just in case

import {useEffect} from 'react'
import {useNavigate} from 'react-router-dom'
import {useSelector} from 'react-redux'
import TableForm from '../components/tableForm'
import '../index.css'
import Spinner from '../components/Spinner'


function Table() {
  const {user} = useSelector((state) => state.auth)
  let navigate = useNavigate();
  const {isLoading, isError, message} = useSelector((state) => state.purchases)

  useEffect(() => {

    if (isError) {
      console.log(message);
    }

    if (!user) {
      navigate('/login')
    }
  
  }, [user, navigate, isError, message] )

  return (
    <>
      <div className='heading'>
        <h1>License Table</h1>
      </div>
    
      <TableForm/>
    </>
  )
}

export default Table


Solution 1:[1]

So, figured out the solution for anyone else who might stumble upon this question. RenderDetailsButton should be passed id as below.

const renderDetailsButton = (id) => {
    return (
      <strong>
        <Button
          className="delete"
          variant="contained"
          color="primary"
          size="small"
          style={{ margin: 0 }}
          onClick={() => { dispatch(deletePurchase(id))}}
        >
          X
        </Button>
      </strong>
    )
  }

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1