最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - dispatching two consecutive asyncthunk actions resulting in inconsistent behaviour - Stack Overflow

programmeradmin5浏览0评论

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
  • And what exactly is the inconsistent behavior? What are the observed results versus what you are expecting? Please edit the post to include these relevant details so readers don't have to click some 3rd-party link. Based on the code you shared all of dispatch(deleteSession(ObjIdsToDelete)) will complete before any of dispatch(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:42
  • Hi @DrewReese, thank you for your reply, I've updated the post, at the top, with the expected and observed behaviour. Please let me know if you have more questions. The funny thing is, If I use Postman to replicate this it works as expected. If I comment out await dispatch(saveSessions(sessions)).unwrap(); the deletion part works as expected. – bader322 Commented Mar 27 at 0:38
  • Assuming "Objects marked for deletion" means the array/list of ids to delete, then it seems the "Objects marked for deletion are not deleted from the database." is the problem... in the backend. Is the backend returning an OK response prior to the elements/objects actually being deleted from any backing resource? I don't think there's much you can do from the frontend here other than to maybe inject an artificial timeout between calls, which many would consider hacky. Better to make the backend delete endpoint wait to respond back to clients that resources have actually been deleted. – Drew Reese Commented Mar 27 at 1:16
Add a comment  | 

1 Answer 1

Reset to default 0

I 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 } from const 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();

发布评论

评论列表(0)

  1. 暂无评论