Логика в redux-saga
Ожидаемое поведение: идут запросы к апи с интервалом в 1.5 секунды пока не будет успешно выполненного запроса, или вызов экшена sendMessageCancel с нужным id.
Что имеется: после вызова экшена стейт меняется, но запросы продолжают идти с интервалом в 1.5 секунды
P.S. логи после выполнения race тоже не вызываются
Саги:
import {takeEvery, delay, call, put, race, take} from 'redux-saga/effects';
import axios, {AxiosResponse} from 'axios';
import {sendMessageStart, sendMessageSuccess, sendMessageProgress, sendMessageCancel, ISendMessage} from './slice';
import sendMessageAPI from '../../utils/api/sendMessageAPI';
import {ISendMessageResponse} from '../../interfaces/Responses/chat/ISendMessageResponse';
function* sendMessageSaga({payload}: ReturnType<typeof sendMessageStart>) {
const {token, cancel} = axios.CancelToken.source();
while (true) {
try {
let {action, api} = yield race({
action: take(action => {
return action.type == sendMessageCancel.type && action.payload == payload._id
}),
api: call(sendMessageAPI.send, payload, token, function* (progress: number) {
yield put(sendMessageProgress({message: payload._id, progress}));
})
});
const resp = api as AxiosResponse<ISendMessageResponse>;
console.log(resp);
console.log(action);
if (resp)
yield put(sendMessageSuccess(payload._id));
else {
console.log('Cancel');
return cancel();
}
break;
} catch (e) {
console.log(e);
yield delay(1500);
}
}
}
export default function* sendMessageWatchSaga() {
yield takeEvery(sendMessageStart.type, sendMessageSaga);
}
API
import axios, {CancelToken, CancelTokenSource} from 'axios';
import {ISendMessage} from '../../redux/sendMessage/slice';
import {ISendMessageResponse} from '../../interfaces/Responses/chat/ISendMessageResponse';
const client = axios.create({
baseURL: process.env.API_URL || 'http://localhost:5000'
});
const sendMessageAPI = {
async send(message: ISendMessage, cancelToken: CancelToken, callback: (progress: number) => void){
return client.post<ISendMessageResponse>('/messages', message, {
headers: {Authorization: `Bearer ${localStorage.getItem('token')}`},
cancelToken: cancelToken,
onUploadProgress: progressEvent => {
callback(progressEvent.loaded/progressEvent.total);
}
});
}
};
export default sendMessageAPI;
Slice
import {createSlice, PayloadAction} from '@reduxjs/toolkit';
import {RootState} from '../index';
import {IMessage} from '../../interfaces/IMessage';
//create initial state
export type ISendMessage = {file?: File} & IMessage
type ISendMessageItem = {
progress: number,
msg: ISendMessage
};
type ISendMessageState = Record<string, ISendMessageItem>;
const initialState: ISendMessageState = {};
//create slice
const sendMessageSlice = createSlice({
name: 'sendMessage',
initialState,
reducers: {
start(state, action: PayloadAction<ISendMessage>){
state[action.payload._id] = {
progress: 0,
msg: action.payload
};
},
success(state, action: PayloadAction<string>){
delete state[action.payload];
},
progress(state, action: PayloadAction<{message: string, progress: number}>){
state[action.payload.message].progress = action.payload.progress;
},
cancel(state, action: PayloadAction<string>){
delete state[action.payload];
}
}
});
//selectors
export const selectSendMessageState = (state: RootState) => state.sendMessage;
export const selectSendMessage = (id: string) => (state: RootState) => selectSendMessageState(state)[id];
export const selectSendMessagesForDialog = (id: string) =>
(state: RootState) => Object.values(selectSendMessageState(state)).filter(it => it.msg.dialog._id == id);
//exports
export const {
start: sendMessageStart, success: sendMessageSuccess,
progress: sendMessageProgress, cancel: sendMessageCancel
} = sendMessageSlice.actions;
export default sendMessageSlice.reducer;
Ответы (1 шт):
Автор решения: HTMLProgrammer
→ Ссылка
Проблема оказалась банальна, вызов апи и последующая задержка занимали поток саги, и она просто игнорировала вызов экшена sendMessageCancel, поэтому я использовал fork и всё заработало
import {takeEvery, delay, call, put, fork, take, cancel, cancelled, race, select} from 'redux-saga/effects';
import axios, {AxiosResponse, CancelToken} from 'axios';
import {ISendMessageResponse} from '../../interfaces/Responses/chat/ISendMessageResponse';
import {sendMessageStart, sendMessageSuccess, sendMessageProgress, sendMessageCancel, ISendMessage} from './slice';
import {messagesAdd} from '../messages';
import {dialogsAdd} from '../dialogs';
import {chatMessagesAdd} from '../chat/messages/slice';
import sendMessageAPI from '../../utils/api/sendMessageAPI';
import {selectChatDialogState} from '../chat/dialog/slice';
function* sendMessageApi(data: ISendMessage, token: CancelToken){
let i = 0;
while (true) {
try {
//make api call
const resp: AxiosResponse<ISendMessageResponse> = yield call(sendMessageAPI.send,
data, token, function* (progress: number) {
yield put(sendMessageProgress({message: data._id, progress}));
});
//get current dialog
const {dialog} = yield select(selectChatDialogState);
//update state
yield put(sendMessageSuccess(data._id));
yield put(messagesAdd({
...resp.data.newMessage,
author: resp.data.newMessage.author._id
} as any));
//update dialog last message
yield put(dialogsAdd({_id: data.dialog._id, lastMessage: resp.data.newMessage._id} as any));
if(dialog == data.dialog._id)
yield put(chatMessagesAdd({
message: resp.data.newMessage._id,
first: true
}));
//exit loop
break;
} catch (e) {
//exit on task cancel
if(yield cancelled())
return;
//increase interval
if(i < 60)
i++;
yield delay(1000 * i);
}
}
}
function* sendMessageSaga({payload}: ReturnType<typeof sendMessageStart>) {
const source = axios.CancelToken.source(),
//start send messages
task = yield fork(sendMessageApi, payload, source.token);
//start race between success sending and cancellation
const {api, cancelMessage} = yield race({
api: take(action => action.type == sendMessageSuccess.type && action.payload == payload._id),
cancelMessage: take(action => action.type == sendMessageCancel.type && action.payload == payload._id)
});
if(cancelMessage){
//stop api calls
yield cancel(task);
source.cancel();
}
}
export default function* sendMessageWatchSaga() {
yield takeEvery(sendMessageStart.type, sendMessageSaga);
}