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