How to set timeout for yield call in React Redux-Saga - reactjs

In my rest API backend I do heavy processing and usually, it takes 1.5 minutes to produce a result, in that time I'm getting this error in my frontend react application.
Error: timeout of 60000ms exceeded
So, peer connection is lost.
How do I set request timeout in redux-saga

i was used race for such things. May be it will useful for you.
const {posts, timeout} = yield race({
posts: call(fetchApi, '/posts'),
timeout: delay(60 * 1000)
});
if (timeout) throw new Error('timeout of 60000ms exceeded')

import { eventChannel, END } from 'redux-saga'
function countdown(secs) {
return eventChannel(emitter => {
const iv = setInterval(() => {
secs -= 1
if (secs > 0) {
emitter(secs)
} else {
// this causes the channel to close
emitter(END)
}
}, 1000);
// The subscriber must return an unsubscribe function
return () => {
clearInterval(iv)
}
}
)
}
Hope this helps.

export function* create(action) {
try {
const { payload } = action;
const response = yield call(api.addPost, payload);
if (response.status === 200) {
console.log('pass 200 check');
yield put(appActions.setResourceResponse(response.data));
console.log(response.data);
payload.push('/add-news');
}
} catch (error) {
console.log(error);
yield put(
a.setResponse({
message: error.response.data,
status: error.response.status,
}),
);
}
}

Related

How To Know If Dispatch Is Success or No In Redux Saga

So i already create the createMonsterStart and it will hit my API, my API is returning response code, and I want to alert success when the response code is 00 otherwise it will alert failed, how can i achieve that? here is my code:
const onSubmitHandler = () => {
dispatch(createMonsterStart(monster))
if(dispatch success){
alert("success")
}else{
alert("error")
}
}
And here is the redux saga code:
export function* createMonsterAsync({ payload: { monster } }) {
try {
const user = yield select(getUser)
const a = yield call(createMonster, user.user.token, monster)
if (a.error) {
yield put(createMonsterFailure(a.error))
return false
}
const monsters = yield call(fetchMonsterAsync)
yield put(createMonsterSuccess(monsters))
} catch (error) {
yield put(createMonsterFailure(error))
}
}

Is there Promise.any like thing in redux saga?

I know about Redux Saga's all([...effects]) effect combinator that is very similar to Promise.all utility, but I've not found something similar to Promise.any behavior that will:
run all effects at the same time
fail if all effects fail (otherwise succeed)
if fail throw AggregateError of all errors
if succeed return nothing or just first result (from multiple results)
e.g.
export function* getHomeDataSaga() {
yield* any([
call(getTopUsersSaga, { payload: undefined }),
call(getFavoritesSaga, { payload: undefined }),
call(getTrendingTokensSaga, { payload: undefined }),
call(getTopCollectionsSaga, { payload: { itemsPerPage: 9, page: 1 } }),
]);
}
This would be very useful when you want to group multiple (decomposed) sagas in to a single saga, it won't fail-fast but finish all effects.
Answer
Based on Martin Kadlec answer ended up using:
export function* anyCombinator(effects: SagaGenerator<any, any>[]) {
const errors = yield* all(
effects.map((effect) =>
call(function* () {
try {
yield* effect;
return null;
} catch (error) {
return error;
}
}),
),
);
if (errors.every((error) => error !== null)) {
throw new AggregateError(errors);
}
}
There isn't an existing effect that would do that, but you can create your own utility that will do that for you. The any functionality is very similar to the all functionality in that in one case you will get all the results/errors and in the other you get the first one that succeeds/fails. So you can easily get the any functionality by flipping the all effect -> for each item you throw on success and return on error.
const sagaAny = (effects = []) => {
const taskRunner = function* (effect) {
let value;
try {
value = yield effect;
} catch (err) {
// On error, we want to just return it
// to map it later to AggregateError
return err;
}
// On success we want to cancel all the runners
// we do that by throwing here
throw value;
};
return call(function* () {
try {
const runners = effects.map((effect) => call(taskRunner, effect));
// If one of the runners throws on success the all effect will
// cancel all the other runners
const failedResults = yield all(runners);
throw new AggregateError(failedResults, "SAGA_ANY");
} catch (err) {
if (err instanceof AggregateError) throw err;
return err;
}
});
};
function* getHomeDataSaga() {
const result = yield sagaAny([
call(getTopUsersSaga, { payload: undefined }),
call(getFavoritesSaga, { payload: undefined }),
call(getTrendingTokensSaga, { payload: undefined }),
call(getTopCollectionsSaga, { payload: { itemsPerPage: 9, page: 1 } }),
]);
}
In case you would prefer not to cancel the other sagas once one succeeds, things get a bit trickier because in standard fork tree the main saga (e.g. getHomeDataSaga) would wait until all the forked sagas (task runners) are done before continuing. To get around that we can use the spawn effect, which will not block the main saga though it has some other implications (e.g. if you kill them main saga the spawned sagas will continue running).
Something like this should do the trick:
const sagaAny = (effects = []) => {
const taskRunner = function* (effect, resultsChannel) {
try {
value = yield effect;
yield put(resultsChannel, { type: "success", value });
} catch (err) {
yield put(resultsChannel, { type: "error", value: err });
}
};
return call(function* () {
const resultsChannel = yield call(channel);
yield all(
effects.map((effect) => spawn(taskRunner, effect, resultsChannel))
);
const errors = [];
while (errors.length < effects.length) {
const result = yield take(resultsChannel);
if (result.type === "success") {
yield put(resultsChannel, END);
return result.value;
}
if (result.type === "error") errors.push(result.value);
}
throw new AggregateError(errors, "SAGA_ANY");
});
};
I use custom channel here to send the results from the spawned runners to the utility saga so that I can react to each finished runner based on my needs.

Redux action not being fired inside a callback function

I'm using react with redux and saga as middleware. Below is a sample generator function that is being fired upon calling regarding action
function* createRoom({ payload }) {
try {
// block of code
}
} catch (error) {
handleError(error, (errorMessage: any) => {
console.log(errorMessage);
createRoomFailure(errorMessage);
});
}
}
handleError function
const handleError = (error, errorHandler) => {
if (error.response) {
const { data, config } = error.response;
console.log(
`${data.type} on method ${config.method} at ${config.baseURL}${config.url}`,
);
if (data.type === 'Network Error') {
errorHandler('Network Error');
} else if (data.status === 400) {
errorHandler('Bad Request');
} else if (data.status === 401) {
errorHandler(
'Unauthorized user. Please enter valid email and password.',
);
} else if (data.status === 403) {
errorHandler('Access Error');
} else if (data.status === 404) {
errorHandler('Method Not Found');
window.location.href = '/notFound';
} else if (data.status === 409) {
errorHandler('Duplicate Value');
} else {
errorHandler(data.type);
}
}
};
export default handleError;
but the problem is in the callback function, I can see the errorMessage in the console when I log it, but when I call the createRoomFailure action, it doesn't get fired.
Here is the createRoomFailure action
export const createRoomFailure = (errorMessage: any) => ({
type: RoomActionTypes.CREATE_ROOM_FAILURE,
payload: errorMessage,
});
can anyone tell me what's wrong here?
Action creators, such as createRoomFailure don't do anything by themselves outside of creating the action object. So if you just call the function of course nothing is going to happen.
What you need to do is to dispatch the action - that way redux can become aware of the returned object from the action creator and process it further.
You can dispatch actions in redux-saga using the put effect. But there is still the issue that you can not use effects outside of sagas. So you can't just use yield put(...) inside of your callback error handler.
In this case, where it seems your errorHandler is a synchronous function, I would suggest just rewriting it so that it returns the error message as string instead of using callback:
const handleError = (error) => {
if (error.response) {
const { data, config } = error.response;
return `${data.type} on method ${config.method} at ${config.baseURL}${config.url}`;
// ...
}
};
function* createRoom({ payload }) {
try {
// block of code
}
} catch (error) {
const errorMessage = yield call(handleError, error);
yield put(createRoomFailure(errorMessage));
}
}
In case your handleError will need to be asynchronous at some point, you can rewrite it to return a promise, which sagas can wait on.

Standard way of reconnecting to webSocket server in redux-Saga?

I'm trying to connect from my react App to websocket server using redux-saga and want to capture the connection loss (server error, reboot) so that to reconnect say in intervals of 4 seconds until the connection is again back. The problem is on reconnecting to webSocket the redux store does not get updated anymore.
I tried using eventChannel of redux-saga as per following code. Unfortunately there was not or at least I couldn't find any documentation answering ws reconnect in redux-saga.
import {eventChannel} from 'redux-saga';
import {all, takeEvery, put, call, take, fork} from 'redux-saga/effects'
import {INITIALIZE_WS_CHANNEL} from "../../constants/ActionTypes"
import {updateMarketData} from "../actions"
function createEventChannel() {
return eventChannel(emit => {
//Subscribe to websocket
const ws = new WebSocket('ws://localhost:9000/rates');
ws.onopen = () => {
console.log("Opening Websocket");
};
ws.onerror = error => {
console.log("ERROR: ", error);
};
ws.onmessage = e => {
return emit({data: JSON.parse(e.data)})
};
ws.onclose = e => {
if (e.code === 1005) {
console.log("WebSocket: closed");
} else {
console.log('Socket is closed Unexpectedly. Reconnect will be attempted in 4 second.', e.reason);
setTimeout(() => {
createEventChannel();
}, 4000);
}
};
return () => {
console.log("Closing Websocket");
ws.close();
};
});
}
function * initializeWebSocketsChannel() {
const channel = yield call(createEventChannel);
while (true) {
const {data} = yield take(channel);
yield put(updateMarketData(data));
}
}
export function * initWebSocket() {
yield takeEvery(INITIALIZE_WS_CHANNEL, initializeWebSocketsChannel);
}
export default function* rootSaga() {
yield all ([
fork(initWebSocket)
]);
}
UPDATE
To complete the accepted answer by #azundo for someone looking for a complete example of websocket & redux-saga I'm adding following code:
function * initializeWebSocketsChannel() {
console.log("going to connect to WS")
const channel = yield call(createEventChannel);
while (true) {
const {data} = yield take(channel);
yield put(updateMarketData(data));
}
}
export function * startStopChannel() {
while (true) {
yield take(START_CHANNEL);
yield race({
task: call(initializeWebSocketsChannel),
cancel: take(STOP_CHANNEL),
});
//if cancel wins the race we can close socket
ws.close();
}
}
export default function* rootSaga() {
yield all ([
startStopChannel()
]);
}
The START_CHANNEL and STOP_CHANNEL actions can be called in componentDidMount and componentWillUnmount of react component life cycle, respectively.
The reason this isn't working is because your recursive call to createEventChannel is not being yielded to the saga middleware redux-saga has no way of knowing of the subsequent event channel creations. You'll want your recursive function to be defined within the event channel instead, see code below, so there is only one eventChannel that is always hooked into the store.
Also note the addition of emitting END on an expected socket close so that you don't leave the eventChannel open forever if you don't reconnect.
import {eventChannel, END} from 'redux-saga';
let ws; //define it here so it's available in return function
function createEventChannel() {
return eventChannel(emit => {
function createWs() {
//Subscribe to websocket
ws = new WebSocket('ws://localhost:9000/rates');
ws.onopen = () => {
console.log("Opening Websocket");
};
ws.onerror = error => {
console.log("ERROR: ", error);
};
ws.onmessage = e => {
return emit({data: JSON.parse(e.data)})
};
ws.onclose = e => {
if (e.code === 1005) {
console.log("WebSocket: closed");
// you probably want to end the channel in this case
emit(END);
} else {
console.log('Socket is closed Unexpectedly. Reconnect will be attempted in 4 second.', e.reason);
setTimeout(() => {
createWs();
}, 4000);
}
};
}
createWs();
return () => {
console.log("Closing Websocket");
ws.close();
};
});
}

retry functionality in redux-saga

in my application I have the following code
componentWillUpdate(nextProps) {
if(nextProps.posts.request.status === 'failed') {
let timer = null;
timer = setTimeout(() => {
if(this.props.posts.request.timeOut == 1) {
clearTimeout(timer);
this.props.fetchData({
page: this.props.posts.request.page
});
} else {
this.props.decreaseTimeOut();
}
}, 1000);
}
}
What it does is that, when an API request encountered an error maybe because there is no internet connection (like how facebook's chat works), or there was an error in the back-end, it would retry after five seconds, but the setTimeout needs to be set every one second to update a part of the store, i.e., the line this.props.decreaseTimeOut();, but if the counter has run out, so five seconds have passed, the if block would run and re-dispatch the fetchData action.
It works well and I have no problem with it, at least in terms of functionality, but in terms of code design, I know that it's a side-effect and it should not be handled in my react-component, and since I'm using redux-saga (but I'm new to redux-saga, I just learned it today), I want to transform that functionality into a saga, I don't quite have an idea as to how to do that yet, and here is my fetchData saga by the way.
import {
take,
call,
put
} from 'redux-saga/effects';
import axios from 'axios';
export default function* fetchData() {
while(true) {
try {
let action = yield take('FETCH_DATA_START');
let response = yield call(axios.get, '/posts/' + action.payload.page);
yield put({ type: 'FETCH_DATA_SUCCESS', items: [...response.data.items] });
} catch(err) {
yield put({ type: 'FETCH_DATA_FAILED', timeOut: 5 });
}
}
}
The less intrusive thing for your code is using the delay promise from redux-saga:
catch(err) {
yield put({ type: 'FETCH_DATA_FAILED'});
for (let i = 0; i < 5; i++) {
yield call(delay, 1000);
yield put(/*Action for the timeout/*);
}
}
But I'd refactor your code in this way:
function* fetchData(action) {
try {
let response = yield call(axios.get, '/posts/' + action.payload.page);
yield put({ type: 'FETCH_DATA_SUCCESS', items:[...response.data.items] });
} catch(err) {
yield put({ type: 'FETCH_DATA_FAILED'});
yield put({ type: 'SET_TIMEOUT_SAGA', time: 5 });
}
}
}
function *setTimeoutsaga(action) {
yield put({type: 'SET_STATE_TIMEOUT', time: action.time}); // Action that update your state
yield call(delay, 1000);
// Here you use a selector which take the value if is disconnected:
// https://redux-saga.js.org/docs/api/#selectselector-args
const isStillDisconnected = select()
if (isStillDisconnected) {
yield put({type: 'SET_TIMEOUT_SAGA', time: action.time - 1});
}
function *fetchDataWatchers() {
yield takeEvery('FETCH_DATA_START', fetchData);
yield takeEvery('SET_TIMEOUT_SAGA', setTimeoutSaga);
// You can insert here as many watcher you want
}
export default [fetchDataWatchers]; // You will use run saga for registering this collection of watchers

Resources