Redux saga won't trigger two async calls simultaneously - redux

I am trying to use redux saga to make the async calls simultaneously as I load the page... but only the loadPositions() is being called. anyone have an idea why? I think it has to do with a race condition. Please correct me.
const fetchPositions = () => {
return fetch(POSITIONS_API_ENDPOINT).then(function (response) {
return response.json().then(function (results) {
return results.map(function (p) {
return {
position: p.position,
platformId: p.platform_id
}
})
})
})
};
const fetchBanners = () => {
return fetch(BANNER_API_ENDPOINT).then(function (response) {
return response.json().then(function (results) {
return results.map(function (p) {
console.log(p)
return {
banner_id: p.banner_id,
type: p.image.type,
width: p.image.width
}
})
})
})
};
export function* loadBanners() {
try {
const banners = yield call(fetchBanners);
yield put({type: "BANNERS_LOADED", banners})
} catch (error) {
yield put({type: "BANNERS_LOAD_FAILURE", error: error})
}
}
export function* loadPositions() {
try {
const positions = yield call(fetchPositions);
yield put({type: "POSITIONS_LOADED", positions})
} catch (error) {
yield put({type: "POSITION_LOAD_FAILURE", error: error})
}
}
export function* rootSaga() {
yield [
loadBanners(),
loadPositions()
]
}

Try this:
Start the initial parallel loading by firing the ON_LOAD_START action
Make all your requests in parallel using the yield [] syntax and fire the appropriate actions with result data.
Root saga, compose all of your sagas:
export default function* rootSaga() {
yield fork(watchOnLoad);
}
Watcher saga, waits for action ON_LOAD_START before kicking of the worker saga:
function* watchOnLoad() {
// takeLatest will not start onLoad until an action with type
// ON_LOAD_START has been fired.
yield* takeLatest("ON_LOAD_START", onLoad);
}
Worker saga, actually makes all the requests in parallel and fires the success or error actions with relevant result data:
export function* onLoad() {
try {
const [banners, positions] = yield [
call(fetchBanners),
call(fetchPositions)
]
yield put({type: "ON_LOAD_SUCCESS", banners, positions})
} catch (error) {
yield put({type: "ON_LOAD_ERROR", error})
}
}

Related

Basic redux-saga, getting back undefined data

So I'm currently learning Redux-Saga and need a little help.
I've received the action and the watcherSaga has caught it and sent it to the workerSaga which runs a function with axios.get to receive data. In the function, I can actually console.log the data and return it, however when it gets back to the saga, the data is undefined. Here are some screenshots, please let me know if you need any other information.
You need to return await axios.get(API_URL).
E.g.
rootSaga.js:
import { call, put, takeEvery } from 'redux-saga/effects';
import { getBlogsSaga } from './getBlogSaga';
const BLOGS = {
LOAD: 'BLOGS_LOAD',
};
function setBlogs(payload) {
return {
type: 'SET_BLOGS',
payload,
};
}
function* displayBlogs() {
const data = yield call(getBlogsSaga);
console.log(data);
yield put(setBlogs(data));
}
function* rootSaga() {
yield takeEvery(BLOGS.LOAD, displayBlogs);
}
export { rootSaga, displayBlogs };
getBlogSaga.ts:
const getBlogsSaga = async () => {
return await Promise.resolve().then(() => {
return [1, 2, 3];
});
};
export { getBlogsSaga };
rootSaga.test.ts:
import { displayBlogs } from './rootSaga';
import { runSaga } from 'redux-saga';
describe('63000691', () => {
it('should pass', async () => {
const dispatched: any[] = [];
await runSaga(
{
dispatch: (action) => dispatched.push(action),
getState: () => ({}),
},
displayBlogs,
).toPromise();
});
});
test result:
PASS src/stackoverflow/63000691/rootSaga.test.ts
63000691
✓ should pass (16 ms)
console.log
[ 1, 2, 3 ]
at src/stackoverflow/63000691/rootSaga.ts:17:11
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 2.235 s, estimated 3 s
Your arrow function uses curly braces { so there is no implicit return. Either explicitly return axios.get (and incidentally since you are returning a promise there is no need to use async/await) or change to parens to take advantage of the explicit return.
const getBlogsSaga = async () => {
return await axios.get(..
}
or
const getBlogsSaga = async () => (
await axios.get(...
)

React-Saga - how to make nested generators work

I upgraded from old redux saga to the latest version and the following stopped working.
function* loadAlbumPhoto({ entity }, entityId) {
try {
const { accessToken: at } = yield select(state => state.user.info);
let {
data: { data: albums }
} = yield call(API.loadAlbumByEntityId, { entityName: entity, entityId, type: PHOTO });
if (!albums.length) {
const options = {
entityId,
entityName: entity,
title: PHOTO,
type: PHOTO
};
yield call(API.createAlbum, options);
({ data: { data: albums } } = yield call(API.loadAlbumByEntityId, { entityName: entity, entityId, type: PHOTO }));
}
const [album] = albums;
const { data: { data: photos } } = yield call(API.loadPhotosByAlbumId, album.id);
return yield photos.map(function* (photo) {
const src = yield getPhotoUrl(photo.uploadData.path, at);
return {
src,
uploadId: photo.uploadId,
photoId: photo.id
};
});
} catch (err) {
console.log(err);
return [];
}
}
function* getPhotoUrl(path, at) {
try {
const userPhoto = yield API.userPhoto(path, at);
return userPhoto;
} catch (err) {
/* eslint-disable no-console */
console.log(err);
/* eslint-enable no-console */
}
return "";
}
As you can see i am trying to return array from loadAlbumPhoto but my problem is that i need to call getPhotoUrl function which is also a generator function.
The problem is that the result of loadAlbumPhoto is Array of generators and not Array of values. It happened since my upgrade to the last version of redux and redux saga.
Already tried to use yield* but not working or i don't know how to use it.
yield*
I would do a bit of refactoring of the anonymous generator and then convert your yield to use all: https://redux-saga.js.org/docs/api/#alleffects---parallel-effects
function* getPhotoDetails(photo) {
const src = yield getPhotoUrl(photo.uploadData.path, at);
return {
src,
uploadId: photo.uploadId,
photoId: photo.id
};
}
function* loadAlbumPhoto({ entity }, entityId) {
// similar up to yield photos.map...
return yield all(photos.map(photo => call(getPhotoDetails, photo)));
// similar after
}

Throtting same saga/epic action with takeLatest on different actions

I'm new on redux-saga/observable stuff. But I couldn't handle my scenario which looks so fit on these. So; I want to call API if any changes happen on the form. But I don't want to call API a lot because of the performance reasons.
Basically, when I trigger SLIDERCHANGED, TEXTBOXCHANGED, CHECKBOXCHECKED, I want to call also getData function. But I need to put a delay to check other actions. For example; If SLIDERCHANGED is triggered, It should wait for 1sn and if TEXTBOXCHANGED is triggered at that time, It will be canceled and wait for 1sn more to call the getData function. So that's why I tried to implement Redux-saga or redux-observable.
I have actions types;
const SLIDERCHANGED = 'APP/sliderchanged';
const TEXTBOXCHANGED = 'APP/textboxchanged';
const CHECKBOXCHECKED = 'APP/checkboxchecked';
const LOADING = 'APP/loading';
const LOADDATA = 'APP/loaddata';
const ERRORLOADDATA = 'APP/errorloaddata';
I have also actions;
export function updateSliderValue(val) {
return { type: SLIDERCHANGED, val };
}
export function updateTextboxValue(val) {
return { type: TEXTBOXCHANGED, val };
}
export function updateCheckboxValue(val) {
return { type: CHECKBOXCHECKED, val };
}
export function loading() {
return { type: LOADING };
}
export function loadData(data) {
return { type: LOADDATA, data };
}
export function errorLoadData(err) {
return { type: ERRORLOADDATA, err };
}
export function getData(vehicleInfo) { // redux-thunk ASYNC function
return (dispatch, getState) => {
dispatch(loading());
return dispatch(apiCall('/getAllData', {}))
.then(payload => {
dispatch(loaddata(payload));
})
.catch(error => {
dispatch(errorLoadData(error));
});
};
}
With redux-saga, I did this but it doesn't work. It calls a getData function on each change with 1sn delay.
import { put, throttle } from 'redux-saga/effects';
import {
SLIDERCHANGED,
TEXTBOXCHANGED,
CHECKBOXCHECKED
} from './constants';
import { getData } from './actions';
function* onFormUpdate() {
yield put(getData());
}
export function* watchFormChange() {
yield throttle(
10000,
[SLIDERCHANGED, TEXTBOXCHANGED, CHECKBOXCHECKED],
onFormUpdate
);
}
With redux-observable, Also somehow I get the same error.
import { ofType } from 'redux-observable';
import { delay, map, debounceTime } from 'rxjs/operators';
import {
SLIDERCHANGED,
TEXTBOXCHANGED,
CHECKBOXCHECKED
} from './constants';
import { getData } from './actions';
export const onFormUpdate = action => {
return action.pipe(
ofType(SLIDERCHANGED, TEXTBOXCHANGED, CHECKBOXCHECKED),
debounceTime(1000),
map(() => getData())
);
};
Does anyone have any idea or opinion to make this happen?
Without having tested it, I think you can write a solution like this:
import { delay } from 'redux-saga'
import { put } from 'redux-saga/effects'
function* onFormUpdate() {
yield delay(1000)
yield put(getData())
}
export function* watchFormChange() {
yield takeLatest(
[SLIDERCHANGED, TEXTBOXCHANGED, CHECKBOXCHECKED],
onFormUpdate,
)
}
The idea here is that takeLatest will cancel onFormUpdate as soon as another one of the three actions is dispatched. So while onFormUpdate is in delay and then canceled, the next step, which is put, will not be called anymore.

redux not picking up an object dispatched via actions

I created a rootSaga in sagas.js as
function* fetchStuff(action) {
try {
yield put({type: 'INCREMENT'})
yield call(delay, 1000)
yield put({type: 'DECREMENT'})
const highlights = yield call(API.getStuff, action.data.myObject);
} catch (e) {
yield put({type: 'FETCH_STUFF_FAILED', message: e});
}
}
export default function* rootSaga() {
yield takeEvery('INIT_LOAD', fetchStuff);
}
I am calling the INIT_LOAD after thirdParty.method:
class myClass extends Component {
constructor(props) {
super(props);
}
componentDidMount() {
this.load();
}
load = () => {
this.init = () => {
this.myObject = thirdParty.method(event => {
const action = {
type: 'INIT_LOAD',
payload: {
myObject: this.myObject
}
};
store.dispatch(action);
});
};
this.init();
};
render() {
return (
<div id="render-here" />
);
}
Passing the this.myObject in the action that is dispatched does not trigger the saga. If I change the action payload to a string, like the following, the saga is triggered.
const action = {
type: 'INIT_LOAD',
payload: {
myObject: 'this.myObject'
}
};
Why am I unable to pass this.myObject but a string is ok?
UPDATE: It is not a saga issue. I replicated the same issue with just plain redux. The rootReducer as
export default function rootReducer(state = initialState, action) {
switch (action.type) {
case 'INIT_LOAD':
return Object.assign({}, state, { myObject: action.payload.myObject });
default:
return state;
}
}
As I mentioned in the comment below, assigning it to an object Obj does not change the issue
let Obj = {};
...
load = () => {
this.init = () => {
Obj.myObject = thirdParty.method(event => {
const action = {
type: 'INIT_LOAD',
payload: {
myObj: Obj
}
};
store.dispatch(action);
});
};
this.init();
};
UPDATE2
I cleaned the code up & simply dispatched an action in the component that triggers the saga. Inside the saga is where I do the init(). I ran into another issue where the object that I was trying to save in the redux store has active socket sessions (which were given me cross-domain issues). Although I didn't solve my original problem, not storing a socket object made my problem go away.

Redux-saga dispatch more than one action with reusable generator

I am using a reusable generator function to make the call to REQUEST/SUCCESS/FAILURE actions. I then have another generator to call that function but I would like to get some kind of feedback and raise another action. Not sure how to explain it, here is an example of what I want to do:
/* in actions/index.js */
export const login = {
request: () => action(constants.LOGIN.REQUEST),
success: (response) => {
try {
jwtDecode(response.auth_token);
} catch (e) {
return action(constants.LOGIN.FAILURE,
{ payload: { error: {
status: 403,
statusText: 'Invalid token',
} } });
}
return action(constants.LOGIN.SUCCESS, { payload: { response } });
},
failure: error => action(constants.LOGIN.FAILURE, { payload: { error } }),
};
/* sagas/index.js */
function* postEntity(entity, apiFn, body) {
yield put(entity.request());
const { response, error } = yield apply(null, apiFn, body);
if (response) {
yield put(entity.success(response));
} else {
yield put(entity.failure(error));
}
}
function* postLogin(action) {
yield postEntity(login, api.login, [action.payload.email, action.payload.password]);
// How can I get some kind of feedback (succeed or not) from postEntity here and do a put(something_else) if succeeded?
}
export default function* rootSaga() {
yield takeLatest(constants.LOGIN_USER, postLogin);
}
Any feedback is really appreciated.
Thanks!
Have postEntity return a value like response or true/false. Then in postLogin check for that value.
const result = yield postEntity(login, api.login, [action.payload.email, action.payload.password]);
then check the value of result and fire success/failure events accordingly like you did in postEntity.
if(result) {yield put(successCreator())} else { ...}

Resources