React useEffect with multiple dependencies getting called multiple time - javascript

I'm using redux-saga to fetch data from API and for that my code looks something like this.
useEffect(()=>{
getListFromAPI(); // dispatches an action that fetches data
getDataByResponseFromList(list.userName); // I also need to call another API depending on response from first API.
}, []);
getDataByResponseFromList(list.userName) fetches data from API depending on response from the getListFromAPI().
This resulted in an error "list.userName not defined", which was obvious because list is not defined yet.
to fix this I wrote another useEffect like below.
useEffect(()=>{
if(!Empty(list))
getDataByResponseFromList(list.userName);
}, [list]);
This worked fine, but in other situation I also need to call this code when one more state changes which is a general "connection" state. So my code becomes something like this.
useEffect(()=>{
if(!Empty(list) && connection)
getDataByResponseFromList(list.userName);
}, [list, connection]);
But now on page load this code runs two times, once when list is populated and once connection is setup, I know exactly the problem is occurring but I'm not sure about the right way to fix this. what is right way to fix such issues.
A Solution I tried :
As a solution I created a global variable to keep track for only one time execution.
let firstTimeExecution = true; // for only onetime execution
export const MyComponent = ({propsList}) => {
...
useEffect(()=>{
if(!Empty(list) && connection && firstTimeExecution){
getDataByResponseFromList(list.userName);
firstTimeExecution = false;
}
}, [list, connection]);
}
This worked perfectly but I'm not sure if this is the best practice and if I should do that.

Since you are using sagas, it might be easier to do the orchestration there rather than in the component.
function * someSaga() {
// wait for connections & list in any order
const [_, result] = yield all([
take(CONNECTION), // action dispatched once you have connection
take(FETCH_LIST_SUCCESS)
])
// call a saga with a userName from the fetch list success action
yield call(getDataByResponseFromList, result.userName);
}
If you expect FETCH_LIST_SUCCESS to happen multiple times and want to call the getData saga every time:
function * someSaga() {
// Do nothing until we have connection
yield take(CONNECTION)
// call a saga with a userName from the fetch list success action
yield takeEvery(FETCH_LIST_SUCCESS, function*(action) {
yield call(getDataByResponseFromList, action.userName);
})
}
If you need to get the data for every getListFromAPI but it can be called multiple times before you get the connection (I am assuming here you don't the the connection for the getListFromAPI itself), you can also buffer the actions and then process them once you have it.
function * someSaga() {
const chan = yield actionChannel(FETCH_LIST_SUCCESS)
yield take(CONNECTION)
yield takeEvery(chan, function*(action) {
yield call(getDataByResponseFromList, action.userName);
})
}

Related

Async/Await not executing as expected

I have the below method where I am updating store and after store updating, I am performing certain activities based on store values -
useEffect(()=>{
const asyncUpdateStore=async()=>{
await updateStore();
getDetails(storeValues) //this is api call made based on updated store values above
.then(()=>{...})
}
asyncUpdateStore();
},[applyChange]);
Upon execution of this code , I find that getDetails which is internally a axios call is not waiting for store values to be get updated within updateStore() method.
When useEffect is getting called second time , I find store is updated.
I want to wait execution of getDetails , till updateStore method finishes its execution.
I have also tried with -
useEffect(()=>{
const asyncUpdateStore=async()=>{
await updateStore();
}
asyncUpdateStore();
getDetails(storeValues) //this is api call made based on updated store values above
.then(()=>{...})
},[applyChange]);
Edit 1:
updateStore() method involves a dispatch call.
const updateStore=()=>{
const data:IData={
value1:valuestate1
value2:valuestate2
}
dispatch(updateData(data));
}
In redux, all dispatches are synchronous. Using await has no effect there. If updateData() is an asynchronous action, then you may need look at the documentation of the middleware you are using, to handle async actions (i.e redux-thunk, etc).
Usually, the middleware will provide 3 states ("pending", "success", "failed") that you can store in your redux store, and then read in your component. The logic flow could look like this.
//example route that would store the current status of an async response
const asyncState = useSelector(state => state.storeValues.status)
const storeValues = useSelector(state => state.storeValues.data)
useEffect(()=>{
//runs everytime applyChange changes
dispatch(updateData(data));
},[applyChange, dispatch, updateData]);
//runs everytime an async status changes, due to the api request done above
useEffect(()=>{
//success indicates the promise resolved.
if(asyncState === "success")
getDetails(storeValues) //this is api call made based on updated store values above.then(()=>{...})
},[asyncState]);
To understand how async patterns work in redux, or see more examples, you can check out redux's own tutorial and docs here. The have a conceptual diagram of state flow, as well as a ton of guides.
Note: dispatch actions should never be anything but synchronous, as reducers should not have side effects. Redux will throw errors and complain if an action is async, and you may see unexpected behavior in your store if async actions aren't handled outside reducers first.

Is it synchronous to dispatch three actions inside a onClick functions and then map the data using that reponses

I want to parse one excel sheet and before parsing I want some data from backend to map it.
So after clicking on Submit button, I want to trigger three actions one by one and store the response inside store. I am using redux-saga for this.
After the three action (api calls), I will call the parsing function and do the parsing and mapping using that response I will be fetching from store.
I have tried dispatching the three actions one by one. But as soon as it reaches the network client i.e axios instance to call api it becomes async and the next line gets executed.
onSubmit = () => {
/* I will set the loader on submit button till the api is called and all parsing of excel sheet is done. */
this.setState({
showLoader: true,
}, () => {
this.props.getData1(); //Will be saving it in store as data1
this.props.getData2(); //Will be saving it in store as data2
this.props.getData3(); //Will be saving it in store as data3
/* After this I want to call the parsing function to parse the excel sheet data and map accordingly */
parseExcelData(sheetData); //sheet data is the excel data
}
So I expected that when I will call the 'parseExcelData' function, the data from store i.e data1, data2,and data3 will be available in that function.
But all the api call happens after the sheet is being parsed.
I have done it using saga generator functions and is working fine. But I want to know how to deal with this situation with redux.
Putting an api call (or any other async operation) into a saga does not make that action synchronous, it is still async. Separately, redux-saga really does not support getting a result from an action -- you trigger a saga with an action, so when the saga completes, it has no way to return a result to the code that originally triggered it. (You can try to work around this by passing a callback along with the action that triggers the saga, and have the saga call the callback, but I wouldn't recommend this approach.)
I would recommend implementing this without redux-saga, using traditional action creators. The action creators would return promises that make the async api calls, and resolve with the result when they're finished. That might look something like this:
// action creator getData1, getData2, getData3
export const getData1 = () => {
return fetch(apiUrl).then(result => {
return result.json();
}).then(resultJson => {
// also fire an action to put it in the store here
// if other parts of your app need the data
return resultJson;
}).catch(err => {
console.error(err);
});
};
// react component
// assumes 1, 2, and 3 cannot be parallelized
// could also be written with .then instead of await
onSubmit = async () => {
this.setState({showLoader: true}, () => {
const result1 = await this.props.getData1();
const result2 = await this.props.getData2(result1);
const result3 = await this.props.getData3(result2);
});
}
You could have the action creators dispatch an action to put the data in the store instead of resolving the promise with the result. But that means you have to pick up the new data via the component's props, which probably means something in componentDidUpdate that checks if the new props are different from the old props, and if so, calls the next data-fetcher. IMO that approach is much more awkward.

Redux saga channel blocking inconsistently

I have a redux saga setup where for some reason my channel blocks when I'm trying to take from it and I can't work out why.
I have a PubSub mechanism which subscribes to an event and when received calls this function:
const sendRoundEnd = (msg, data) => {
console.log('putting round end')
roundEndChannel.put({
type: RUN_ENDED,
data: data.payload
})
}
I have a watcher for this channel defined like this:
function* watchRoundEndChannel() {
while(true) {
console.log('before take')
const action = yield take(roundEndChannel)
console.log('after take')
yield put(action)
}
}
And I have a reducer setup which is listening for the put of RUN_ENDED like this:
case RUN_ENDED:
console.log(action)
return {
...state,
isRunning: false,
roundResult: action.data
}
Finally, I have a roundEndChannel const within the file (but not within the functions) and I export the following function as part of an array which is fed into yield all[]:
takeEvery(roundEndChannel, watchRoundEndChannel)
So if my understanding is right, when I get the msg from my pubsub I should first hit sendRoundEnd which puts to roundEndChannel which should in turn put the RUN_ENDED action.
What's weird however is that when I run these functions and receive the message from the pubsub, the following is logged:
putting round end
before take
I never get to the after take which suggests to me that the channel doesn't have anything in it, but I'm pretty sure that isn't the case as it should have been put to in the event handler of the pubsub immediately prior.
It feels like I'm missing something simple here, does anyone have any ideas (or ways I can examine the channel at different points to see what's in there?)
Arg, managed to fix this. The problem was I had exported the watchRoundEndChannel wrapped in a takeEvery which was snatching my pushed events up.
I exported the function like this fork(watchRoundEndChannel) and things work as I expected.

How to make sure all actions are performed after some action is fired with Saga?

I have some initial data that is loaded from server on start of the application.
It loads and then fires an action like InitialDataLoaded.
This data is saved to a state and is used for all future communications with server.
While the app is loading that data, some other requests can be sent by takeEvery(action). If initial data is not available by the moment, I cannot send these requests as they don't make sense without initial data in them.
How do I make sure that all dependent actions cause requests only when the needed action is fired?
I'm not sure if the other requests that can be sent to takeEvery are initiated by the UI (in which case you would want to disable the UI by checking some prop if the initial data is loaded).
But if you are talking about within the saga, you can wait for the InitialDataLoaded action like this:
function* watchAppInit() {
while (true) {
// when this completes it dispatches the InitialDataLoaded action
yield fork(loadInitialData);
yield take(actionTypes.InitialDataLoaded);
// this won't execute until InitialDataLoaded action is dispatched
yield fork(doOtherStuff);
}
}
or you can check the state in the other actions:
function* watchDoOtherStuff() {
while (true) {
const action = yield take(actionTypes.DO_OTHER_STUFF);
// check the state to see if you have initial data
const initialized = yield select(selectors.getInitialDataLoaded);
if (initialized) {
yield fork(doIt);
}
}
}

How can I get redux-saga to wait for two actions to happen at least once in any order?

Redux saga noob here.
I need to create a saga that loads the initial state for the redux store from my API server.
This involves using two async sagas: getCurrentUser and getGroups.
I need to issue these ajax requests in parallel and wait for the GET_CURRENT_USER_SUCCESS and GET_GROUPS_SUCCESS actions before issuing the pageReady action which tells the UI it's time to render the react components.
I came up with a hacky solution:
function * loadInitialState () {
yield fork(getCurrentUser)
yield fork(getGroups)
while (true) {
yield take([
actions.GET_GROUPS_SUCCESS,
actions.GET_CURRENT_USER_SUCCESS
])
yield take([
actions.GET_GROUPS_SUCCESS,
actions.GET_CURRENT_USER_SUCCESS
])
yield put(actions.pageReady())
}
}
The problem with this code is that if for some reason GET_GROUPS_SUCCESS is issued twice, the pageReady action will be called to early.
How can I get redux saga to wait for GET_GROUPS_SUCCESS and GET_CURRENT_USER_SUCCESS to happen at least once in any order?
I think you want the all effect
function * loadInitialState () {
// start loading state...
yield all([
take(actions.GET_GROUPS_SUCCESS)
take(actions.GET_CURRENT_USER_SUCCESS)
]);
yield put(actions.pageReady())
}

Categories