I'm building a nextjs app with redux and I'm at the point where I'm dispatching two consecutive asyncThunk actions to the backend api, one action sends a DELETE request and the other sends a POST request.
Expected Behaviour:
- Objects marked for deletion should be deleted from the database and removed from state.
- Receive a 200 success code from backend once those jobs are done.
Observed/Actual Behaviour:
- Objects marked for deletion are not deleted from the database.
- Objects are removed from state for a split second then returned back to state while still maintaining their mark for deletion. i.e
markedForRemoval = true
- Receive a 200 success code from backend even though the jobs are not done.
Here's a video link to what is happening
- I mark the data that I want to delete, then I click on Save Changes
- The expected behaviour is that it should delete anything marked with delete and saves any new ones I've created.
Observation: If I comment out either one of these, the uncommented action runs properly. It has to do with them being consecutive perhaps??
await dispatch(deleteSession(ObjIdsToDelete)).unwrap();
await dispatch(saveSessions(sessions)).unwrap();
Here's my relevant slice code: first I got this debugging utility from claude.ai
to try to figure out if there is a race condition but there doesn't seem to be
---- You can skip this if you want to ---
// Debugging utility for tracking async operations
class AsyncTracker {
private static operations: Record<
string,
{
startTime: number;
endTime?: number;
status: 'pending' | 'completed' | 'failed';
}
> = {};
static start(operationId: string) {
this.operations[operationId] = {
startTime: Date.now(),
status: 'pending',
};
console.log(
`Operation ${operationId} started at ${new Date().toISOString()}`
);
}
static end(operationId: string, status: 'completed' | 'failed') {
if (this.operations[operationId]) {
this.operations[operationId].endTime = Date.now();
this.operations[operationId].status = status;
console.log(
`Operation ${operationId} ${status} at ${new Date().toISOString()}`
);
console.log('Operation details:', this.operations[operationId]);
}
}
static analyzeRaceConditions() {
const operations = Object.entries(this.operations);
const overlappingOperations = operations.filter(([_, op1]) =>
operations.some(
([_, op2]) =>
op1 !== op2 &&
op1.startTime < (op2.endTime || Infinity) &&
(op1.endTime || Infinity) > op2.startTime
)
);
if (overlappingOperations.length > 0) {
console.warn(
'Potential race conditions detected:',
overlappingOperations
);
}
}
}
Here's my relevant code in my redux slice, First: the action with POST
req
export const saveSessions = createAsyncThunk(
'sessions/saveSessions',
async (sessions: session[]) => {
const operationId = `operation-${Date.now()}`;
try {
// Start tracking the operation
AsyncTracker.start(operationId);
const res = await axios.post(
'http://localhost:3000/api/sessions',
sessions
);
// Mark operation as completed
AsyncTracker.end(operationId, 'completed');
return res.data;
} catch (err) {
// Mark operation as failed
AsyncTracker.end(operationId, 'failed');
if (err instanceof Error) {
return console.error({
error: err.message,
});
} else {
console.error('An unknown error occurred:', err);
}
}
}
);
Next is the action with DELETE
req
export const deleteSession = createAsyncThunk(
'sessions/deleteSession',
async (_ids: string[], { rejectWithValue }) => {
const operationId = `operation-${Date.now()}`;
try {
// Start tracking the operation
AsyncTracker.start(operationId);
// Use Promise.all to resolve all delete requests
const results = await Promise.all(
_ids.map(async (_id) => {
const req = await axios.delete('http://localhost:3000/api/sessions', {
params: {
_id: _id,
},
});
return req.data;
})
);
// Mark operation as completed
AsyncTracker.end(operationId, 'completed');
return results;
} catch (err) {
AsyncTracker.end(operationId, 'failed');
// Use rejectWithValue for proper error handling
return rejectWithValue(
err instanceof Error
? { error: err.message }
: { error: 'An unknown error occurred' }
);
}
}
);
My extraReducers
object
extraReducers: (builder) => {
builder.addCase(deleteSession.pending, () => {});
builder.addCase(deleteSession.fulfilled, (state, { payload }) => {
state = state.filter((session) => {
return !payload.some((p) => p._id === session._id);
});
console.log('deleteSession.fulfilled', state);
return state;
});
builder.addCase(deleteSession.rejected, (state, { payload }) => {
console.log('deleteSession.rejected', payload);
});
builder.addCase(saveSessions.pending, () => {});
builder.addCase(saveSessions.fulfilled, (state, { payload }) => {
state = payload.all_data;
return state;
});
},
And this is how I'm calling them in my component
const handleSave = async () => {
await dispatch(deleteSession(ObjIdsToDelete)).unwrap();
await dispatch(saveSessions(sessions)).unwrap();
};
I'm building a nextjs app with redux and I'm at the point where I'm dispatching two consecutive asyncThunk actions to the backend api, one action sends a DELETE request and the other sends a POST request.
Expected Behaviour:
- Objects marked for deletion should be deleted from the database and removed from state.
- Receive a 200 success code from backend once those jobs are done.
Observed/Actual Behaviour:
- Objects marked for deletion are not deleted from the database.
- Objects are removed from state for a split second then returned back to state while still maintaining their mark for deletion. i.e
markedForRemoval = true
- Receive a 200 success code from backend even though the jobs are not done.
Here's a video link to what is happening
https://drive.google/file/d/1H-9YKeT6mxYoTtisdffV-28Ucjzufpcf/view?usp=sharing
- I mark the data that I want to delete, then I click on Save Changes
- The expected behaviour is that it should delete anything marked with delete and saves any new ones I've created.
Observation: If I comment out either one of these, the uncommented action runs properly. It has to do with them being consecutive perhaps??
await dispatch(deleteSession(ObjIdsToDelete)).unwrap();
await dispatch(saveSessions(sessions)).unwrap();
Here's my relevant slice code: first I got this debugging utility from claude.ai
to try to figure out if there is a race condition but there doesn't seem to be
---- You can skip this if you want to ---
// Debugging utility for tracking async operations
class AsyncTracker {
private static operations: Record<
string,
{
startTime: number;
endTime?: number;
status: 'pending' | 'completed' | 'failed';
}
> = {};
static start(operationId: string) {
this.operations[operationId] = {
startTime: Date.now(),
status: 'pending',
};
console.log(
`Operation ${operationId} started at ${new Date().toISOString()}`
);
}
static end(operationId: string, status: 'completed' | 'failed') {
if (this.operations[operationId]) {
this.operations[operationId].endTime = Date.now();
this.operations[operationId].status = status;
console.log(
`Operation ${operationId} ${status} at ${new Date().toISOString()}`
);
console.log('Operation details:', this.operations[operationId]);
}
}
static analyzeRaceConditions() {
const operations = Object.entries(this.operations);
const overlappingOperations = operations.filter(([_, op1]) =>
operations.some(
([_, op2]) =>
op1 !== op2 &&
op1.startTime < (op2.endTime || Infinity) &&
(op1.endTime || Infinity) > op2.startTime
)
);
if (overlappingOperations.length > 0) {
console.warn(
'Potential race conditions detected:',
overlappingOperations
);
}
}
}
Here's my relevant code in my redux slice, First: the action with POST
req
export const saveSessions = createAsyncThunk(
'sessions/saveSessions',
async (sessions: session[]) => {
const operationId = `operation-${Date.now()}`;
try {
// Start tracking the operation
AsyncTracker.start(operationId);
const res = await axios.post(
'http://localhost:3000/api/sessions',
sessions
);
// Mark operation as completed
AsyncTracker.end(operationId, 'completed');
return res.data;
} catch (err) {
// Mark operation as failed
AsyncTracker.end(operationId, 'failed');
if (err instanceof Error) {
return console.error({
error: err.message,
});
} else {
console.error('An unknown error occurred:', err);
}
}
}
);
Next is the action with DELETE
req
export const deleteSession = createAsyncThunk(
'sessions/deleteSession',
async (_ids: string[], { rejectWithValue }) => {
const operationId = `operation-${Date.now()}`;
try {
// Start tracking the operation
AsyncTracker.start(operationId);
// Use Promise.all to resolve all delete requests
const results = await Promise.all(
_ids.map(async (_id) => {
const req = await axios.delete('http://localhost:3000/api/sessions', {
params: {
_id: _id,
},
});
return req.data;
})
);
// Mark operation as completed
AsyncTracker.end(operationId, 'completed');
return results;
} catch (err) {
AsyncTracker.end(operationId, 'failed');
// Use rejectWithValue for proper error handling
return rejectWithValue(
err instanceof Error
? { error: err.message }
: { error: 'An unknown error occurred' }
);
}
}
);
My extraReducers
object
extraReducers: (builder) => {
builder.addCase(deleteSession.pending, () => {});
builder.addCase(deleteSession.fulfilled, (state, { payload }) => {
state = state.filter((session) => {
return !payload.some((p) => p._id === session._id);
});
console.log('deleteSession.fulfilled', state);
return state;
});
builder.addCase(deleteSession.rejected, (state, { payload }) => {
console.log('deleteSession.rejected', payload);
});
builder.addCase(saveSessions.pending, () => {});
builder.addCase(saveSessions.fulfilled, (state, { payload }) => {
state = payload.all_data;
return state;
});
},
And this is how I'm calling them in my component
const handleSave = async () => {
await dispatch(deleteSession(ObjIdsToDelete)).unwrap();
await dispatch(saveSessions(sessions)).unwrap();
};
Share
Improve this question
edited Mar 27 at 0:40
bader322
asked Mar 26 at 20:14
bader322bader322
153 bronze badges
3
|
1 Answer
Reset to default 0I found out what's happening, It is like Drew Reese said, the issue is on backend.
Here's what's happening:
The DELETE route does delete the object from the db and it returns 200 ok with
{ deletedCount: 1, acknowledged: true }
fromconst deletionResult = await Session.deleteMany({ _id: { $in: _id }, });
Then the POST route runs. Inside this I'm passing all objects from state and then querying db to filter out existing objects like this
// Find existing sessions to avoid duplicates
const existingIds = new Set(
(
await Session.find({
_id: { $in: data.map((datum) => datum._id) },
}).distinct('_id')
).map((id) => id.toString())
);
// Prepare new sessions for bulk insert
const newSessions = data.filter((datum) => !existingIds.has(datum._id));
Now, newSessions
has an array of objects that were said were deleted in DELETE route and the most interesting thing I see is that this newSessions
array has an exact replica of the deleted objects that shouldn't be the same like _id
, created_at
, updated_at
That's what made me think that this was not a backend issue since I saw no change in these values as I expect _id
, created_at
, updated_at
to be different everytime an object gets created.
I solved this issue by simply changing the order of dispatching the actions, instead of
await dispatch(deleteSession(ObjIdsToDelete)).unwrap();
await dispatch(saveSessions(sessions)).unwrap();
now it is this:
await dispatch(saveSessions(sessions)).unwrap();
await dispatch(deleteSession(ObjIdsToDelete)).unwrap();
dispatch(deleteSession(ObjIdsToDelete))
will complete before any ofdispatch(saveSessions(sessions))
is started, so if any weirdness is observed I doubt it is the frontend code. But again.... we've no idea what you are saying is the issue. – Drew Reese Commented Mar 26 at 21:42await dispatch(saveSessions(sessions)).unwrap();
the deletion part works as expected. – bader322 Commented Mar 27 at 0:38