I have a reactJS webapp that uses redux saga & axios to send web requests for selecting products.
It uses takeEvery for the saga at the moment.
The problems:
As an example: user really quickly selects: product a, cancels it, selects it, cancels it, selects it and the final status is 'cancel' although the last action was selecting it.
How it should work:
Selecting products: If a user selects products very fast, all of them should correctly be added. Example: user adds a,b,c,d,e,f,g,h,i so all of these should be added.
Toggling: If a user toggles something repetitive & quickly, the final status has to match the latest action
As an example: User selects, cancels, selects, cancels => final state is cancel Moreover: User selects => cancels => selects => final state id select
What is the appropriate technical way to implement this ?
You might be unwittingly opening a can of worms here, depending on how much of a perfectionist you are and how reliable you need your app to be. And if you want optimistic updates, oh boy.
Things can get way more difficult, because HTTP requests are not necessarily guaranteed to arrive in order nor are the responses from the server.
Same goes for your database, which can run into concurrency issues and so you might want to start versioning your data.
There are some strategies you can use to reduce such problems.
Write your endpoints in non stateful way or minimize the amount of sent state. For example, instead of sending list of all selected items, just send the single item that should be toggled.
Take advantage of the throttle & debounce saga effects, they will help you with request delays, skips and distribution to minimize the chances of stuff going out of order.
Consider blocking user action until you get a response back where it makes sense
Be aware that canceling a saga (e.g. through the takeLatest
effect) doesn't cancel the ajax request itself (it will still arrive on your BE no matter what). It will only cancel the saga itself (in other words it will prevent your app from running the code after you would receive the data from BE)
Also be aware that using takeLatest
to cancel sagas is limited. E.g. if you have only single entity you might always want to cancel the previous saga, because data from the previous response are no longer relevant.
yield takeLatest(DO_ACTION, actionSaga);
But what if you have the same action on top of multiple entities and you want to cancel the previous action only if you work with the same entity but not when it is another entity? If the list of entities is static, you can use the function pattern instead:
yield takeLatest(action => action.type === DO_ACTION && action.id = 'foo', actionSaga);
yield takeLatest(action => action.type === DO_ACTION && action.id = 'bar', actionSaga);
yield takeLatest(action => action.type === DO_ACTION && action.id = 'baz', actionSaga);
Once the list gets too long or if you have dynamic ids/amount of entities you will need to write your own logic using infinite cycles, id:task map & cancel effect.
export function* dynamicIdWatcher() {
const taskMap = {}
while (true) {
let action = yield take(DO_ACTION)
if (taskMap[action.id]) yield cancel(taskMap[action.id])
const workerTask = yield fork(actionSaga, action)
taskMap[action.id] = workerTask
}
}
Sorry for not really answering your question, but I hope some of what I wrote can still be useful in figuring out your use case :)