I have a modal created with Tailwind/DaisyUI in my React project. We are using Redux Toolkit for state management. I am trying to use this modal to display a form that allows the user to update tickets. I actually copied most of the form from my CreateTicket.jsx page. When I click the submit button, Redux does updateTicket>Pending and then goes to updateTicket>Rejected and I cannot get it to go to updateTicket>Fulfilled. I'm not even sure what to investigate at this point because there doesn't seem to be any error in the backend or frontend console, or anything like that.
I have confirmed that the updateTicket functionality in the TicketService.js works fine (this is where you actually hit the API endpoint from). Beyond that, I recently reintroduced the 'isLoading' piece of state into the TicketSlice.js and then added explicit cases for pending and rejected - before that there was no isLoading state and the only declared case was updateTicket.fulfille. Unfortunately, this didn't change anything. For now, I'm leaving it there - but will likely remove it if it really isn't necessary.
Relevant code from the Ticket.jsx page where the modal is
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useSelector, useDispatch } from 'react-redux';
import BackButton from '../components/BackButton';
import {
getTicket,
closeTicket,
updateTicket,
} from '../features/tickets/ticketSlice';
import { useParams, useNavigate } from 'react-router-dom';
import Spinner from '../components/Spinner';
function Ticket() {
const { user } = useSelector((state) => state.auth);
const { ticket, isLoading } = useSelector((state) => state.tickets);
const [firstName, setFirstName] = useState(user.firstName);
const [lastName, setLastName] = useState(user.lastName);
const [email, setEmail] = useState(user.email);
const [subject, setSubject] = useState('');
const [priority, setPriority] = useState('low');
const [description, setDescription] = useState('');
const dispatch = useDispatch();
const navigate = useNavigate();
const { ticketId } = useParams();
useEffect(() => {
dispatch(getTicket(ticketId)).unwrap().catch(toast.error);
}, [dispatch, ticketId]);
const onSubmit = (e) => {
e.preventDefault();
dispatch(updateTicket({ subject, priority, description }))
.unwrap()
.then(() => {
navigate('/tickets');
toast.success('Ticket updated successfully');
})
.catch(toast.error);
};
if (!ticket || isLoading) {
return <Spinner />;
}
return (
<>
<BackButton />
<label htmlFor='edit-ticket-modal' className='btn btn-outline'>
Open Modal
</label>
<input type='checkbox' id='edit-ticket-modal' className='modal-toggle' />
<label htmlFor='edit-ticket-modal' className='modal cursor-pointer'>
<label className='modal-box relative' htmlFor=''>
<label className='label'>
<span className='font-bold text-xl label-text'>
Customer First Name
</span>
</label>
<input
type='text'
className='input input-md w-full'
value={firstName}
disabled
/>
<label className='label'>
<span className='font-bold text-xl label-text'>
Customer Last Name
</span>
</label>
<input
type='text'
className='input input-bordered input-md w-full'
value={lastName}
disabled
/>
<label className='label'>
<span className='font-bold text-xl label-text'>Customer Email</span>
</label>
<input
type='text'
className='input input-bordered input-md w-full'
value={email}
disabled
/>
<form onSubmit={onSubmit} className='flex flex-col'>
<label className='label'>
<span className='font-bold text-xl label-text'>Subject</span>
</label>
<input
type='text'
className='input input-bordered input-md w-full'
value={subject}
placeholder='Subject'
onChange={(e) => setSubject(e.target.value)}
/>
<label className='label'>
<span className='font-bold text-xl label-text'>Priority</span>
</label>
<select
name='priority'
className='select select-bordered w-full'
value={priority}
onChange={(e) => setPriority(e.target.value)}
>
<option value='low'>Low</option>
<option value='medium'>Medium</option>
<option value='high'>High</option>
</select>
<label className='label'>
<span className='font-bold text-xl label-text'>Description</span>
</label>
<textarea
name='description'
placeholder='Description'
className='textarea textarea-bordered h-24'
value={description}
onChange={(e) => setDescription(e.target.value)}
/>
<div>
<button className='btn btn-outline w-48 mt-5'>Submit</button>
</div>
</form>
</label>
</label>
</>
);
}
export default Ticket;
Relevant code from TicketSlice.js (Edited to add console.error(error))
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import { extractErrorMessage } from '../../utils';
import ticketService from './ticketService';
const initialState = {
tickets: [],
ticket: {},
isLoading: false,
};
// Update user ticket
export const updateTicket = createAsyncThunk(
'tickets/updateTicket',
async (ticketId, ticketData, thunkAPI) => {
try {
const token = thunkAPI.getState().auth.user.token;
return await ticketService.updateTicket(ticketId, ticketData, token);
} catch (error) {
console.error(error);
return thunkAPI.rejectWithValue(extractErrorMessage(error));
}
}
);
export const ticketSlice = createSlice({
name: 'ticket',
initialState,
reducers: {},
extraReducers: (builder) => {
builder
.addCase(getTickets.pending, (state) => {
state.ticket = null;
})
.addCase(getTickets.fulfilled, (state, action) => {
state.tickets = action.payload;
})
.addCase(getTicket.fulfilled, (state, action) => {
state.ticket = action.payload;
})
.addCase(closeTicket.fulfilled, (state, action) => {
state.ticket = action.payload;
state.tickets = state.tickets.map((ticket) => {
// NOTE: we could remove the 'return' and the curly braces
// that wrap the return statement or we can do it this way
return ticket._id === action.payload._id ? action.payload : ticket;
});
})
.addCase(updateTicket.pending, (state) => {
state.isLoading = true;
})
.addCase(updateTicket.rejected, (state) => {
state.isLoading = false;
})
.addCase(updateTicket.fulfilled, (state, action) => {
state.isLoading = false;
state.ticket = action.payload;
state.tickets = state.tickets.map((ticket) => {
// NOTE: we could remove the 'return' and the curly braces that wrap the return statement or we can do it this way
return ticket._id === action.payload._id ? action.payload : ticket;
});
});
},
});
After some more troubleshooting and w/ the suggestions from a reply here, I've got some error info to add:
This is the error within the payload of tickets/updateTicket/rejected:
"TypeError: Cannot read properties of undefined (reading 'rejectWithValue') at http://localhost:3000/static/js/bundle.js:2648:21 at http://localhost:3000/static/js/bundle.js:6427:86 at step (http://localhost:3000/static/js/bundle.js:5122:17) at Object.next (http://localhost:3000/static/js/bundle.js:5071:14) at http://localhost:3000/static/js/bundle.js:5184:61 at new Promise (<anonymous>) at __async (http://localhost:3000/static/js/bundle.js:5166:10) at http://localhost:3000/static/js/bundle.js:6389:18 at http://localhost:3000/static/js/bundle.js:6465:10 at http://localhost:3000/static/js/bundle.js:77375:18"
Doesn't seem entirely useful, but I also got an error by adding console.error(error) into my trycatch which is similarly hard to understand:
TypeError: Cannot read properties of undefined (reading 'getState')
at ticketSlice.js:55:1
at createAsyncThunk.ts:634:1
at step (RefreshUtils.js:271:1)
at Object.next (RefreshUtils.js:271:1)
at RefreshUtils.js:271:1
at new Promise (<anonymous>)
at __async (RefreshUtils.js:271:1)
at createAsyncThunk.ts:599:1
at createAsyncThunk.ts:684:1
at index.js:16:1
The issue was that I had added an extra parameter into the arrow function parameter of the 'createAsyncThunk' thinking that was how one would get and use both the ticketId and ticketData. The fix was to only pass the ticketData, and then extract the ticketId from the ticketData because that arrow function only takes 2 parameters in total per the documentation and those parameters are 'arg' and thunkAPI. You cannot have three. The new code that works is:
// Update user ticket
export const updateTicket = createAsyncThunk(
'tickets/updateTicket',
async (ticketData, thunkAPI) => {
try {
const token = thunkAPI.getState().auth.user.token;
return await ticketService.updateTicket(ticketData, token);
} catch (error) {
return thunkAPI.rejectWithValue(extractErrorMessage(error));
}
}
);