Using Action/Saga in another Saga - javascript

I have a Saga where I need to do 3 asynchronous requests, then use the responses from the 3 requests in a follow-up request. Here's some psuedo-code to explain:
function* useOtherActionsAndSagas(action) {
try {
const [response1, response2, response3] = yield [
request1,
request2,
request3
];
const orderData = {
...response1,
...response2,
...response3,
};
const response4 = yield request4;
yield put({ type: 'SUCCESS', data: response4 });
} catch (e) {
// handle error
}
The 3 requests request1, request2 and request3 correspond to 3 separate Sagas. For example, for request1 there's a Saga along the lines of:
export function* request1(action) {
try {
const result = yield api.get(`/api/endpoint`);
yield put({...action, type: ACTION1_SUCCESS, data: result});
} catch (e) {
yield put({...action, type: ACTION1_FAIL, errors: e});
}
}
function* watchAction1() {
yield* takeLatest(ACTION1, request1);
}
export default function* () {
yield [fork(watchAction1)];
}
where api.get is a wrapper for Axios.get().
This watcher in that Saga is connected to a corresponding action/reducer.
export const ACTION1 = "actions/ACTION1";
export const ACTION1_SUCCESS = "actions/ACTION1_SUCCESS";
export const ACTION1_FAIL = "actions/ACTION1_FAIL";
const initialState = {
// Initial state
};
export default function reducer(state = initialState, action = {}) {
switch (action.type) {
case ACTION1:
// return state
case ACTION1_SUCCESS:
// return state
case ACTION1_FAIL:
// return state
};
default:
// return state;
}
}
export function request1(data) {
return {type: ACTION1, data};
}
To keep my code DRY I was hoping to take advantage of the existing action and saga in the parent saga. To do this I tried:
const [response1, response2, response3] = yield [
put({type: ACTION1, data: data1}),
put({type: ACTION2, data: data2}),
put({type: ACTION3, data: data3})
];
This correctly initiates each action and their corresponding sagas. However, the response from the requests are not available in the assigned variables. That is, response1, response2 and response3 are references to their actions {type: "actions/ACTION1", data: data1} and not a Promise.
I know it would be possible to duplicate the Axios requests in this parent Saga but I'd lose the bonus of having the success/fail actions for the individual requests.
Is it possible to use a setup like this? If so, how can the responses from the asynchronous requests be retrieved for use in a follow-up request?
If not, what is the correct method for accomplishing this?
Update
I can use the workers from the other Sagas within the parent saga, like this:
import request1 from request1Saga;
const [response1, response2, response3] = yield [
call(request1, data1),
call(request2, data2),
call(request3, data3),
];
where request1, request2 and request3 are the worker functions from other Sagas. That gives the benefit of the ACTION1_SUCCESS and ACTION1_FAIL actions from those Sagas being used.

All you need is combine all combinator with call effect (docs for composing sagas and running tasks in parallel):
const [response1, response2, response3] = yield all([
call(request1),
call(request2),
call(request3)
]);
This will execute sagas in parallel and return results from each of them. It works as Promise.all.
The sagas above (request1 to request3) need to return some data at the end of saga:
export function* request1(action) {
try {
const result = yield call(url => api.get(url), `/api/endpoint`);
yield put({...action, type: ACTION1_SUCCESS, data: result});
// This will be assigned to result1
return result
} catch (e) {
yield put({...action, type: ACTION1_FAIL, errors: e});
}
}
Note: You don't need to fork takeEvery, because it is already "forked":
// Example of root saga:
export default function* () {
yield takeLatest(ACTION1, request1);
yield takeLatest(ACTION2, request2);
// ...
}

Related

Race condition when updating redux state?

I'm using redux-saga with takeEvery which allows multiple actions to execute simultaneously:
export function* watchSaga() {
yield all([
takeEvery(actionTypes.FETCH_DATA, fetchDataSaga)
])
}
and the fetchDataSaga looks like:
export function* fetchDataSaga(action) {
yield put(actions.fetchDataStart())
try {
const response = yield axios.post(url, body)
yield put(actions.fetchDataSuccess(response.data))
} catch (error) {
yield put(actions.fetchDataFail(error.response.data.error))
}
}
in fetchDataSuccess, I'm adding a new field (or updating if exist) into my redux state object:
const initialState = {
myObj: {}
}
...
const fetchDataSuccess = (state, action) => {
const newKey = action.newKey
return {
...state,
myObj: {
...myObj,
[newKey]: {
...myObj[newKey],
timestamp: new Date().getTime()
}
}
}
}
So I make concurrent calls to the saga ,let's say 100 times:
yield all([
call(fetchDataSaga, action[0])
call(fetchDataSaga, action[1])
call(fetchDataSaga, action[2])
...
call(fetchDataSaga, action[99])
])
There should be 100 fields in myObj afterward.
My question is, am I going to get the latest state every time in the fetchDataSuccess reducer? Or will there be a race condition that one of the reducers read the old previous state, and if so, how can I prevent it?

Redux Saga wait for action creator that makes an api call

I have an action creator which is being watched like this:
yield takeLatest('actionCreator', actionFlow);
in file X.
Inside action flow there's an api call and another dispatch to store this data into the store.
Then, from file Y, I have a saga, and I want to call this actionCreator.
So I go:
yield put({ type: 'actionCreator', etc.. });
However, after this, I do yield select(selector) where selector selects this data and it returns null (as it was before the api call.
Is there a way to wait for that "actionFlow" to end?
File X:
export const aCreator= ({ data, type }) => ({
type: ACTION_A,
payload: { data, type },
});
function* aFlow() {
//api call
//put
}
export default function*() {
yield takeLatest(ACTION_A, aFlow);
}
File Y
import { aCreator } from 'fileX';
function* bFlow(){
yield put(aCreator({ data }));
const something = yield select(selector); //this should return the data saved in the reducer by aFlow but return null, as if it didn't wait for aFlow to finish.
}
export default function*() {
yield takeLatest(ACTION_B, bFlow);
}
put is non blocking so it won't wait until the aFlow has finished.
function* bFlow() {
yield put(aCreator({ data }));
const something = yield select(selector); // this will happen immediately
}
You could add a take effect before select, so it will wait until the call from aFlow finished.
function* bFlow() {
yield put(aCreator({ data }));
yield take(dispatched action from aFlow after api call);
const something = yield select(selector); // will wait until aFlow finishes
}
The dispatched action can be actually anything:
function* aFlow() {
//api call
//put
yield put('api_finished');
}
function* bFlow() {
yield put(aCreator({ data }));
yield take('api_finished');
const something = yield select(selector); // will wait until aFlow finishes
}

How to make an async request using redux-saga

I'm trying to make a request to get some user info with redux sagas.
so far I have:
function* getUserDetails() {
const userDetails = axios.get('http://localhost:3004/user').then(response => response)
yield put({ type: 'USER_DATA_RECEIVED', user: userDetails})
}
function* actionWatcher() {
yield takeLatest('GET_USER_DATA', getUserDetails)
}
export default function* rootSaga() {
yield all([
actionWatcher(),
]);
}
but when I log that out user either comes back as undefined or Promise<pending>. so I tried to add in yield call(axios stuff in here)
but that didn't seem to work either
anyone got any ideas either a) how to use call properly? and b) how to pass through a payload with the action?
The correct way to use the call effect in your case would be this:
function* getUserDetails() {
const userDetails = yield call(axios.get, 'http://localhost:3004/user');
yield put({ type: 'USER_DATA_RECEIVED', user: userDetails})
}
The first argument for call is the function you want to call, subsequent arguments are arguments you want to pass to the called function.
Improved Version
Calls to external APIs can always go wrong, so it's a good practice to safeguard against this by wrapping a try/catch block around the Axios call.
In the catch block, you could, for example, dispatch an action that signals an error, which you can use to show an error message to the user.
function* getUserDetails() {
let userDetails;
try {
userDetails = yield call(axios.get, 'http://localhost:3004/user');
} catch (error) {
yield put({ type: 'USER_DATA_ERROR', error });
return;
}
yield put({ type: 'USER_DATA_RECEIVED', user: userDetails})
}

How to wait for all actions(success/failure after request) to dispatch in redux-saga?

I have following actions:
export function createRequest(inputValues) {
return {
type: actions.PERSON_CREATE_REQUEST,
payload: {
inputValues
}
};
}
export function createSuccess(person) {
return {
type: actions.PERSON_CREATE_SUCCESS,
payload: {
person: person
}
};
}
export function createFailure(error) {
return {
type: actions.PERSON_CREATE_FAILURE,
payload: {
error
}
};
}
Saga:
export function* createPerson(action) {
try {
const data = yield call((async () => {
const person = action.payload.inputValues;
return await api.post(person);
}));
yield put(createSuccess(data));
} catch (error) {
yield put(createFailure(error));
}
}
export function* watchCreatePerson() {
yield takeEvery('PERSON_CREATE_REQUEST', createPerson);
}
export function* rootSaga() {
yield [
fork(watchCreatePerson),
];
}
Function that is called on form submit:
handleSubmit = async (event, values) => {
event.preventDefault();
await store.dispatch(action.createRequest(values));
if (this.props.error === null) {
store.dispatch(reset(this.props.form));
}
};
After await store.dispatch(action.createRequest(values)); I want to check if PERSON_CREATE_FAILURE or PERSON_CREATE_SUCCESS was dispatched. On PERSON_CREATE_FAILURE I set error in store that is mapped to props. But next line of code with if is called after PERSON_CREATE_REQUEST is dispatched. But I need to wait until PERSON_CREATE_FAILURE or PERSON_CREATE_SUCCESS dispatch. How to change saga to wait for that actions dispatches?
In the handleSubmit function, just dispatch the createRequest action, don't await it's response at this point in your code.
Now, if the request fails, write a reducer to handle the PERSON_CREATE_FAILURE action. Within this reducer you can then set the application state appropriately i.e. reset the form state. Then using MapStateToProps for your component which encompasses your form, you form will automatically re-render to match the updated (and in this case reset) form state.
Note that with this approach you will need to create your form's state in the Redux store, but doing so will simplify the application's state lifecyle.

How to avoid duplicate API requests with Redux-Saga?

So far I like Redux better than other Flux implementations, and I'm using it to re-write our front end application.
The main struggling points that I'm facing:
Maintaining the status of API calls to avoid sending duplicate requests.
Maintaining relationships between records.
The first issue could be solved by keeping a status field in the sub-state of each type of data. E.g.:
function postsReducer(state, action) {
switch(action.type) {
case "FETCH_POSTS":
return {
...state,
status: "loading",
};
case "LOADED_POSTS":
return {
status: "complete",
posts: action.posts,
};
}
}
function commentsReducer(state, action) {
const { type, postId } = action;
switch(type) {
case "FETCH_COMMENTS_OF_POST":
return {
...state,
status: { ...state.status, [postId]: "loading" },
};
case "LOADED_COMMENTS_OF_POST":
return {
status: { ...state.status, [postId]: "complete" },
posts: { ...state.posts, [postId]: action.posts },
};
}
}
Now I can make a Saga for Posts and another one for Comments. Each of the Sagas knows how to get the status of requests. But that would lead to a lot of duplicate code soon (e.g. Posts, Comments, Likes, Reactions, Authors, etc).
I'm wondering if there is a good way to avoid all that duplicate code.
The 2nd issue comes to existence when I need to get a comment by ID from the redux store. Are there best practices for handling relationships between data?
Thanks!
redux-saga now has takeLeading(pattern, saga, ...args)
Version 1.0+ of redux-saga has takeLeading that spawns a saga on each action dispatched to the Store that matches pattern. After spawning a task once, it blocks until the spawned saga completes and then starts to listen for a pattern again.
Previously I implemented this solution from the owner of Redux Saga and it worked really well - I was getting errors from API calls sometimes being fired twice:
You could create a higher order saga for this, which would look something like this:
function* takeOneAndBlock(pattern, worker, ...args) {
const task = yield fork(function* () {
while (true) {
const action = yield take(pattern)
yield call(worker, ...args, action)
}
})
return task
}
and use it like this:
function* fetchRequest() {
try {
yield put({type: 'FETCH_START'});
const res = yield call(api.fetch);
yield put({type: 'FETCH_SUCCESS'});
} catch (err) {
yield put({type: 'FETCH_FAILURE'});
}
}
yield takeOneAndBlock('FETCH_REQUEST', fetchRequest)
In my opinion this way is far way more elegant and also its behaviour can be easily customized depending on your needs.
I had the exact same issue in my project.
I have tried redux-saga, it seems that it's really a sensible tool to control the data flow with redux on side effects. However, it's a little complex to deal with the real world problem such as duplicate requests and handling relationships between data.
So I created a small library 'redux-dataloader' to solve this problem.
Action Creators
import { load } from 'redux-dataloader'
function fetchPostsRequest() {
// Wrap the original action with load(), it returns a Promise of this action.
return load({
type: 'FETCH_POSTS'
});
}
function fetchPostsSuccess(posts) {
return {
type: 'LOADED_POSTS',
posts: posts
};
}
function fetchCommentsRequest(postId) {
return load({
type: 'FETCH_COMMENTS',
postId: postId
});
}
function fetchCommentsSuccess(postId, comments) {
return {
type: 'LOADED_COMMENTS_OF_POST',
postId: postId,
comments: comments
}
}
Create side loaders for request actions
Then create data loaders for 'FETCH_POSTS' and 'FETCH_COMMENTS':
import { createLoader, fixedWait } from 'redux-dataloader';
const postsLoader = createLoader('FETCH_POSTS', {
success: (ctx, data) => {
// You can get dispatch(), getState() and request action from ctx basically.
const { postId } = ctx.action;
return fetchPostsSuccess(data);
},
error: (ctx, errData) => {
// return an error action
},
shouldFetch: (ctx) => {
// (optional) this method prevent fetch()
},
fetch: async (ctx) => {
// Start fetching posts, use async/await or return a Promise
// ...
}
});
const commentsLoader = createLoader('FETCH_COMMENTS', {
success: (ctx, data) => {
const { postId } = ctx.action;
return fetchCommentsSuccess(postId, data);
},
error: (ctx, errData) => {
// return an error action
},
shouldFetch: (ctx) => {
const { postId } = ctx.action;
return !!ctx.getState().comments.comments[postId];
},
fetch: async (ctx) => {
const { postId } = ctx.action;
// Start fetching comments by postId, use async/await or return a Promise
// ...
},
}, {
// You can also customize ttl, and retry strategies
ttl: 10000, // Don't fetch data with same request action within 10s
retryTimes: 3, // Try 3 times in total when error occurs
retryWait: fixedWait(1000), // sleeps 1s before retrying
});
export default [
postsLoader,
commentsLoader
];
Apply redux-dataloader to redux store
import { createDataLoaderMiddleware } from 'redux-dataloader';
import loaders from './dataloaders';
import rootReducer from './reducers/index';
import { createStore, applyMiddleware } from 'redux';
function configureStore() {
const dataLoaderMiddleware = createDataLoaderMiddleware(loaders, {
// (optional) add some helpers to ctx that can be used in loader
});
return createStore(
rootReducer,
applyMiddleware(dataLoaderMiddleware)
);
}
Handle data chain
OK, then just use dispatch(requestAction) to handle relationships between data.
class PostContainer extends React.Component {
componentDidMount() {
const dispatch = this.props.dispatch;
const getState = this.props.getState;
dispatch(fetchPostsRequest()).then(() => {
// Always get data from store!
const postPromises = getState().posts.posts.map(post => {
return dispatch(fetchCommentsRequest(post.id));
});
return Promise.all(postPromises);
}).then() => {
// ...
});
}
render() {
// ...
}
}
export default connect(
state => ()
)(PostContainer);
NOTICE The promised of request action with be cached within ttl, and prevent duplicated requests.
BTW, if you are using async/await, you can handle data fetching with redux-dataloader like this:
async function fetchData(props, store) {
try {
const { dispatch, getState } = store;
await dispatch(fetchUserRequest(props.userId));
const userId = getState().users.user.id;
await dispatch(fetchPostsRequest(userId));
const posts = getState().posts.userPosts[userId];
const commentRequests = posts.map(post => fetchCommentsRequest(post.id))
await Promise.all(commentRequests);
} catch (err) {
// error handler
}
}
First, you can create a generic action creator for fetching post.
function fetchPost(id) {
return {
type: 'FETCH_POST_REQUEST',
payload: id,
};
}
function fetchPostSuccess(post, likes, comments) {
return {
type: 'FETCH_POST_SUCCESS',
payload: {
post,
likes,
comments,
},
};
}
When you call this fetch post action, it'll trigger onFetchPost saga.
function* watchFetchPost() {
yield* takeLatest('FETCH_POST_REQUEST', onFetchPost);
}
function* onFetchPost(action) {
const id = action.payload;
try {
// This will do the trick for you.
const [ post, likes, comments ] = yield [
call(Api.getPost, id),
call(Api.getLikesOfPost, id),
call(Api.getCommentsOfPost, id),
];
// Instead of dispatching three different actions, heres just one!
yield put(fetchPostSuccess(post, likes, comments));
} catch(error) {
yield put(fetchPostFailure(error))
}
}

Categories

Resources