How can I handle multiple dependent requests in a saga? - javascript

I'm trying to make two request, one to save an image an other to save a product with the url from obtained from the first requests
This is what I want to do
first: save product image (I'm using axios for requests)
second: get the url from 'productImage' and then include it in the params to save
this is my code
function* createProduct(action) {
const { endpoint, params } = action.payload;
try {
const productImage = yield call(new api().createImage, { endpoint, params });
// I need to wait the url of the image and include it on the params for the second request before is executed
// E.g. params.image = productImage.url
const product = yield call(new api().createProduct, { endpoint, params });
yield put({
type: CREATE_PRODUCT_SUCCEED,
payload: {
product
}
});
} catch (e) {
yield put({
type: CREATE_PRODUCT_FAILED,
payload: {
...e
}
});
}
}
export default function* createProductWatcher() {
yield takeEvery(CREATE_PRODUCT_EXECUTION, createProduct);
}

The best pattern here is to split your saga (createProduct) into two separate sagas:
createImage - will handle the image creation for the product
createProduct - will handle the product creation with the given image
// Creates the product image
function* createImage(action) {
const { endpoint, params } = action.payload;
try {
const image = yield call(new api().createImage, { endpoint, params });
yield put({
type: CREATE_IMAGE_SUCCEED,
// Here you pass the modified payload to createProduct saga
payload: {
endpoint,
params: { image: image.url }
}
});
} catch(e) {
yield put({
type: CREATE_IMAGE_FAILED,
payload: {
...e
}
});
}
}
//Creates the product with the image
function* createProduct(action) {
const { endpoint, params } = action.payload;
try {
const product = yield call(new api().createImage, { endpoint, params });
yield put({
type: CREATE_PRODUCT_SUCCEED,
payload: {
product
}
});
} catch(e) {
yield put({
type: CREATE_PRODUCT_FAILED,
payload: {
...e
}
});
}
}
Then use the builtin yield* operator to compose multiple Sagas in a sequential way.
// Another saga for chaining the results
function* invokeSagasInOrder(sagas) {
try {
const image = yield* createImage();
yield* createProduct(image);
} catch(e) {
console.log(e);
}
}
Welcome to stackoverflow!

Related

How can i use yield in redux-saga?

After receiving the result value of the refresh function, axiosInstace is executed before saving the accesstoken to AsyncStorage, so the updated token cannot be retrieved in axios.js through AsyncStorage.getItem. i want to save accesstoken first in refresh and get acecesstoken in axios.js and send to axiosInstace
How can I solve this problem?
this is my code
(saga.js)
function getPostAPI(data) {
return axiosInstace.post('/kakao/getpost', data);
}
function* getPost(action) {
try {
const result = yield call(getPostAPI, action.data);
yield put({
type: GETPOST_SUCCESS,
data: result.data,
});
} catch (err) {
if (err.response.data === 'jwtEx') {
yield put({
type: REFRESH_REQUEST,
// data: action.data,
});
yield put({
type: GETPOST_REQUEST,
data: action.data,
});
} else {
yield put({
type: GETPOST_FAILURE,
error: err.response.data,
});
}
}
}
function refreshAPI() {
return axiosInstace.post('/kakao/refresh');
}
function* refresh() {
try {
const result = yield call(refreshAPI);
yield AsyncStorage.setItem(
'accesstoken',
`${result.data.accessToken}`,
() => {
// console.log('accesstoken 재발급 저장 완료');
},
);
yield put({
type: REFRESH_SUCCESS,
data: result.data,
});
} catch (err) {
yield put({
type: REFRESH_FAILURE,
error: err.response.data,
});
}
}
(axios.js)
AxiosInstance.interceptors.request.use(async (cfg) => {
const acecesstoken = await AsyncStorage.getItem('accesstoken');
const refreshtoken = await AsyncStorage.getItem('refreshtoken');
if (acecesstoken) {
cfg.headers.Authorization = `Bearer ${acecesstoken} ${refreshtoken}`;
}
return cfg;
});
export default AxiosInstance;
A simple solution would be to call your refresh() generator directly:
function* getPost(action) {
try {
const result = yield call(getPostAPI, action.data);
yield put({
type: GETPOST_SUCCESS,
data: result.data,
});
} catch (err) {
if (err.response.data === 'jwtEx') {
yield call(refresh);
// you could also redispatch the original action
yield put(action);
} else {
yield put({
type: GETPOST_FAILURE,
error: err.response.data,
});
}
}
}
Alternatively your can start a race between REFRESH_SUCCESS and REFRESH_FAILURE:
const { success, failure } = yield race({
success: take('REFRESH_SUCCESS'),
failure: take('REFRESH_FAILURE'),
});
if(success) {
// continue
} else {
// handle refresh failure
}

how can i use AsyncStorage data Everywhere?

I am using the saga library. And tokens are stored in AsyncStorage.
What I want is to freely use the token obtained from AsyncStorage in the loadUserPosts function or in loadPosts.
In this case, where should async be added and how do I fix the code?
this is my code
const token = await AsyncStorage.getItem('tokenstore');
function* loadUserPosts(action) {
try {
console.log(token)
yield put({
type: LOAD_USER_POSTS_SUCCESS,
data: result.data,
});
} catch (err) {
}
}
function* loadPosts(action) {
try {
console.log(token)
yield put({
type: LOAD_POSTS_SUCCESS,
data: result.data,
});
} catch (err) {
}
function* watchLoadPost() {
yield takeLatest(LOAD_POST_REQUEST, loadPosts);
}
function* watchLoadUserPosts() {
yield throttle(5000, LOAD_USER_POSTS_REQUEST, loadUserPosts);
}
export default function* postSaga() {
yield all([
fork(watchLoadPosts),
fork(watchLoadUserPosts),
]);
}
You can try and yield you async result. You might not even need async becuase generator function will yield until it gets a result.
function* loadUserPosts(action) {
try {
const token = yield AsyncStorage.getItem('tokenstore');
console.log(token)
yield put(LOAD_USER_POSTS_SUCCESS(token));
} catch (err) {
}

How to retrieve the value of a yield function call?

I am working on a project which began last year, and the developers are not with me. They wrote this code :
import { put, takeLatest, all, call } from 'redux-saga/effects';
import { getUserByUsernameService } from '../../services/userServices';
import 'regenerator-runtime/runtime';
function* fetchUser() {
const response = yield call(getUserByUsernameService);
yield put({ type: 'FETCHED_USER', payload: response.data.user });
}
function* actionWatcher() {
yield takeLatest('FETCHING_USER', fetchUser);
}
export default function* rootSaga() {
yield all([actionWatcher()]);
}
Code of getUserByUsernameService :
import {
makeGetRequest,
makePostRequest,
makePutRequest,
makeDeleteRequest,
} from '../utils/reqUtils';
export const getUserByUsernameService = (params) => {
let headers = {
"Access-Control-Allow-Origin": "*"
};
makeGetRequest('/user', params, headers);
}
Code of makeGetRequest :
import axios from 'axios';
export const makeGetRequest = (endpoint, params = {}, headers) => {
const options = {
method: 'GET',
headers: { ...headers },
params: params,
url: endpoint,
};
return axiosInstance(options)
.then((resp) => resp.data)
.catch((e) => {
console.log(e);
throw e;
});
};
At runtime I get Cannot read property 'data' of undefined corresponding to the code
yield put({ type: 'FETCHED_USER', payload: response.data.user });
So what is wrong ?
The generator yield returns an object that you can iterate using next method.
I think you should use response.next().valute.data.user.
I think also as you consume the generator in fetchUser you should not yield the result of the call API method.
function* fetchUser() {
const response = call(getUserByUsernameService);
yield put({ type: 'FETCHED_USER', payload: response.next().value.data.user });
}
This is a simple typo — you forgot to return something from your getUserByUsernameService function! You are calling makeGetRequest but not returning the response. You want
return makeGetRequest('/user', params, headers);

componentwillmount() Uncaught Error: Actions must be plain objects. Use custom middleware for async actions

I'm implementing get all image by type with redux-saga. I have 2 types, let's say, type kristik and type motif.
When I'm implementing type kristik, it got successful response, but when it comes to type motif, the response is error.
here my code that has the error in console
componentWillMount() => {
const { dispatch } = this.props;
dispatch(getAllMotif());
}
I got error in dispatch(getAllMotif()); in commponentWillMount()
Here my getAllMotif() code
getAllMotif(token) {
const path = `motif`;
const method = 'get';
return request.process(method, path, null, token);
},
Here my sagas getAllMotif code
export function* getAllMotif() {
try {
let { detail } = yield select(state => state.user);
const result = yield call(API.getAllMotif, detail.api_token);
yield put({
type: types.GET_ALL_MOTIF_SUCCESS,
payload: result,
});
} catch (err) {
yield put(handleError(err));
yield put({
type: types.GET_ALL_MOTIF_FAILURE,
payload: err,
});
}
}
here my reducer
case types.GET_ALL_MOTIF_SUCCESS:
return {
...state,
motif: [
...action.payload.data.data
]
};
here my request code
internals.process = (method, path, payload, token, contentType=internals.contentType) => {
const request = {
url: `${API_URL}/${path}`,
method: method,
headers: {
'Content-Type': contentType,
'Accept': 'application/json',
},
};
if (token) {
request.params = {
token: token,
};
}
if (payload) {
request.data = payload;
}
return axios.request(request)
.then(res => {
if (![200, 201].includes(res.status)) {
throw new Error(res.status);
}
return res.data;
})
.catch((error) => {
console.error(method, path, error);
return Promise.reject({
message: error.response.data.error,
code: error.response.status
});
});
};
I don't know why in this type get error, because in type kristik also have very similar code.
You didn't dispatch an action that wasn't a plain object, your function getAllMotif not return a plain object. That lead to the error here.
You should dispatch an normal action
getAllMotifAction(token) {
const path = `motif`;
const method = 'get';
return { type: 'GET_ALL_MOTIF', data: { path, method } };
},
Then in in saga, you catch this action and handle it with your saga function
takeLatest('GET_ALL_MOTIF', getAllMotif);

trying to access generator function features inside facebook graph api callback

This code is inside a redux saga file.
I'm trying to send the response back to the saga function but I can't.
new GraphRequestManager().addRequest(infoRequest).start();
infoRequest = new GraphRequest(
'/me', {
httpMethod: 'GET',
version: 'v2.5',
parameters: {
'fields': {
'string': 'email,first_name,last_name,id'
}
}
}, startFacebookRegister);
the facebookRegister function
function * startFacebookRegister(err, res) {
const { id, email, first_name, last_name } = res;
yield put(initFacebookLogin.success(res));
}
If we look at GraphRequestManager.start.
const that = this;
const callback = (error, result, response) => {
if (response) {
that.requestCallbacks.forEach((innerCallback, index, array) => {
if (innerCallback) {
// here it is called as a normal callback, not a generator
innerCallback(response[index][0], response[index][1]);
}
});
}
if (that.batchCallback) {
that.batchCallback(error, result);
}
};
NativeGraphRequestManager.start(this.requestBatch, timeout || 0, callback); } ```
The callback you pass is invoked as a normal Function, as opposed to a GeneratorFunction.
And since they differ in how they are executed, that's why nothing happens, since your GeneratorFunction, when invoked, is just instantiated and sits there until getting garbage collected.
So the preferred option to solve this, which is aligned with the redux-saga flow is using eventChannel, which keeps the flow inside redux-saga and is actually advised for similar cases.
The other one, not so preferable, would be to just call raw store.dispatch, it obviously breaks through the abstraction levels and flow of redux-saga, but still gets the job done, by invoking some other saga action which can handle calling your startFacebookRegister with the response.
EventChannel
We need to wrap the whole GraphRequestManager... block into an eventChannel, so we can redirect the response or error back into saga flow from the vendor control.
Can be something like this:
import {
cancelled,
call,
take,
put
} from "redux-saga/effects";
import {
eventChannel,
END
} from 'redux-saga';
function* startFacebookRegister(err, res) {
const {
id,
email,
first_name,
last_name
} = res;
yield put(initFacebookLogin.success(res));
}
function* graphRequestWrapper() {
return eventChannel(emit => {
const infoRequest = new GraphRequest(
'/me', {
httpMethod: 'GET',
version: 'v2.5',
parameters: {
'fields': {
'string': 'email,first_name,last_name,id'
}
}
}, (err, res) => {
if (err) {
emit(new Error(err));
} else {
emit(res);
}
emit(END);
});
// BTW infoRequest variable should be instantiated before we
// can add it with addRequest, just a side note
new GraphRequestManager().addRequest(infoRequest).start();
return () => {
// clean up
};
})
}
export function* mainSaga() {
const chan = yield call(graphRequestWrapper);
while (true) {
try {
const res = yield take(chan);
yield call(startFacebookRegister, null, res);
} catch (err) {
yield call(startFacebookRegister, err);
} finally() {
if (yield cancelled()) chan.close()
}
}
}
Store Dispatch
And with store, you can export your store from your main.js or index.js where you createStore it with all the reducers and middlewares, and use it here directly.
import store "../exported/from/somewhere";
function* startFacebookRegister({payload}) {
const {
id,
email,
first_name,
last_name
} = payload;
yield put(initFacebookLogin.success(res));
}
function graphRequestWrapper() {
const infoRequest = new GraphRequest(
'/me', {
httpMethod: 'GET',
version: 'v2.5',
parameters: {
'fields': {
'string': 'email,first_name,last_name,id'
}
}
}, (err, response) => {
store.dispatch({type: "INIT_FACEBOOK_LOGIN_REQUEST_SUCCESS_SAGA", payload: {...response}})
});
new GraphRequestManager().addRequest(infoRequest).start();
}
function* mainSaga() {
yield takeLatest("INIT_FACEBOOK_LOGIN_REQUEST_SUCCESS_SAGA", startFacebookRegister);
yield call(graphRequestWrapper);
}

Categories

Resources