Redux Thunk handling network responses in a different order - javascript

I have a text entry field which (via a Thunk) queries the server of the validity of the user's text. As these requests can happen in very quick succession, they may be returned from the server in a different order than when they were sent. Therefor, the string in the text field may be shown as invalid, when in fact it is valid.
To fix this, I'm performing a check when I receive a response from the server - is the current content of the text field the same as what was checked? If not, check again. I feel there should be a better way to handle this situation than querying a DOM element value.
How can I follow through from pre to post server request?
export function updateUserCode(code) {
return dispatch => {
return dispatch(validateUserCode(code))
}
}
function validateUserCode(code) {
return dispatch => {
dispatch(updateCode(code))
return fetch(`/api/code/${code}`)
.then(response => response.json())
.then(json => dispatch(receiveValidatedCode(code, json)))
.catch(error => {Log.error(error)})
}
}
function receiveValidatedCode(code, data) {
const lastval = document.getElementById('usercode').value
if (code != lastval) {
// Code not the same as current value
// need to validate again
return updateUserCode(lastval)
}
if(data.success) {
return {
type: types.CODE_VALIDATED,
code: code,
}
}
return {
type: types.CODE_INVALID,
reason: data.reason,
}
}

Messing with DOM inside your logic is indeed less than ideal. I would suggest keeping last entered text field value in Redux store and perform your checks in reducer.
Also I don't see any point in re-validating user input if current entered value differs from one validated by last resolved request. Just ignore such responses and do not perform unnecessary request.
In terms of code you can do it like that:
// actions
const requestValidation = value => ({ type: 'request-validation', value });
const receiveValidation = (value, result) => ({ type: 'receive-validation', value, result });
export const validateUserCode = code => dispatch => {
dispatch(requestValidation(code));
return fetch(`/api/code/${code}`)
.then(response => response.json())
.then(json => dispatch(receiveValidation(code, json)))
.catch(error => {Log.error(error)})
}
// reducer
export const validationReducer = (state = {}, action) => {
if (action.type === 'request-validation')
return ({ value: action.value, isValidating: true });
if (action.type === 'receive-validation' && state.value === action.value)
return ({ value: action.value, isValid: !!action.success });
return state;
};
That is not production quality code, and I don't event sure if it works, but it reflects the idea.

Related

how to conditionally call an api depending on received value

i need to call an api depending on the value that im receiving on state,
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
const [selectedValue, setSelectedValue] = useState([])
const firstAPI = async (val) => {
return await axios
.get(`http://localhost:3000/api/v1/rendition?name=${val}&&excel=true`)
.then(res => console.log('excel', res))
}
const secondAPI = async (val) => {
const limit = pageSize * (page - 1);
return await axios
.get(`http://localhost:3000/api/v1/rendition?id=${val}&&skip=${limit}&&take=${pageSize}&&excel=true`)
.then((res) => {
console.log('excelID', res);
})
}
const handleExcelClick = (param) => {
if(param.filter(item => typeof item === 'string')){
firstAPI(param)
} else {
secondAPI(param)
}
}
<Button onClick={() => handleExcelClick(selectedValue)} > Excel </Button>
so im receveing a value from 2 different inputs, which are stored in selectedValue state, which can either be a string or a number, so i need to call an api depending on the value, if its a string, call the firstAPI, if it's a number, call the secondAPI. But whatever condition i put in, it ignores the if statement and just calls the first api which also triggers a 500 error because one of the apis only takes numbers as params, i don't know other way to approach this, any help will be much appreciated
Try with:
...
if(param.filter(item => isNaN(item) )){
firstAPI(param)
} else {
secondAPI(param)
}
...
So i solved it a lot of days ago, but i forgot to post my solution, so here it goes:
const handleExcelClick = (param: any) => {
if (typeof param === 'number') {
return firstAPI(param);
} else if (param.some((i: any) => typeof i === 'string')) {
return secondAPI(param);
}
};
i basically forgot to add a return to each statement, and there was also no point in filtering values based on just a filter method, so i used the "some" array method to just receive the values that return true if the condition is met, which is checking if the data type matches the statement
hope this helps anyone trying to do the same

Rendering component before fetching data has been finished

I'm trying to fetch data by creating a function. In that function I am doing trying to set state and I am calling it from the componentDidMount method, but I am having a few problems:
I am not sure if while is good practice to be used, because I am looping and changing my endpoint so I can get new data every time.
I have tried to return data from the fetching function and use setState inside componentDidMount, but I had a problem, I suspect because componentDidMount is running before fetching has completed
I have tried to use res.json() on the data using a promise, but I got an error that res.json is not a function.
state = {
title: [],
image: [],
rating: [],
};
getData = () => {
let i = 1;
while (i <= 9) {
axios.get(`http://api.tvmaze.com/shows/${i}`)
.then(response => console.log(response))
.then(response => this.setState({
title:response.data.data.name[i],
}))
.catch(error => console.log(error));
i++;
}
};
componentDidMount() {
this.getData();
console.log(this.state.title);
}
If your goal is to render your JSX after you're done fetching information, then I'd suggest creating an additional item in your state, isLoading, that you can set to true or false and render your JSX conditionally.
Based on the example you provided below, it'd look like the following:
class Shows extends React.Component {
state = {
title: [],
image: [],
rating: [],
isLoading: true
}
componentDidMount() {
this.getData()
}
getData = () => {
// I've created a URL for each request
const requestUrls = Array.from({ length: 9 })
.map((_, idx) => `http://api.tvmaze.com/shows/${idx + 1}`);
const handleResponse = (data) => {
// `data` is an array of all shows that you've requested
// extract information about each show from the payload
const shows = data.map(show => show.data)
// handle shows data however you need it
// and don't forget to set `isLoading` state to `false`
this.setState({
isLoading: false,
title: shows.map(show => show.name),
image: shows.map(show => show.url),
rating: shows.map(show => show.rating.average),
})
}
const handleError = (error) => {
// handle errors appropriately
// and don't forget to set `isLoading` to `false`
this.setState({
isLoading: false
})
}
// use `Promise.all()` to trigger all API requests
// and resolve when all requests are completed
Promise.all(
requestUrls.map(url => axios.get(url))
)
.then(handleResponse)
.catch(handleError)
}
render() {
const { isLoading, title, image, rating } = this.state
// prevent showing your `JSX` unless data has been fetched
// ideally, show a loading spinner or something that will
// tell users that things are happening;
// returning `null` won't render anything at all
if (isLoading) {
return null
}
return (
<div>...</div>
)
}
}
This way, with Promise.all, it's a lot easier to reason about all these calls that you're making.
Other than that, using componentDidMount to fetch data from an API is the right place to do it, but I'd stay away from the while loop and use Promise.all for all your requests and map to create an array of promises (requests) that can be passed to Promise.all and handled all at once.
Working example:
CodeSandbox
The way in which you are setting state will result in the last data from api to be saved in state and it will render only last call
Do it like this
getData = () => {
let i = 1;
while (i <= 9) {
axios.get(`http://api.tvmaze.com/shows/${i}`)
.then(response =>{
let prevState=this.state.title
prevState.push(response.data.data.name[i])
this.setState({
title:prevState,
})})
.catch(error => console.log(error));
i++;
}
};

Redux observable epic takes few actions in a row but return only last

I have an epic which should proceed few actions of type VERIFY_INSURANCE_REQUEST in a row. Everything works good inside switchMap block (all items are proceeded as well) but only last one goes to map block, so I have only one successfully dispatched action instead of many.
function verifyInsuranceEpic(action$) {
return action$.pipe(
ofType(types.VERIFY_INSURANCE_REQUEST),
switchMap((action) => {
const { verifyInsuranceModel } = action;
const promise = InsuranceApi.verifyInsurance(verifyInsuranceModel).then(result => {
const returnResult = result && result.rejectReason === null;
const actionResponse = {
returnResult,
key: verifyInsuranceModel.key
}
return actionResponse;
})
return from(promise);
}),
map(result => {
return verifyInsuranceSuccess(result)
}),
catchError(error => of(verifyInsuranceFailure(error)))
);
}
Is there any way to make all responses go to map block?
As mentioned in comments, the solution is just change switchMap to concatMap.

Asynchronous ActionCreator in React Redux

I'm pretty new in React-Redux. Was working on an application. The thing is that I faced some issues with asynchronous execution of Redux actionCreator, may be.
Below is my component. Say, I want to call an actionCreator from componentDidMount() or from an onclick event listener.
class Dashboard extends PureComponent {
componentDidMount() {
this.props.getProductsAndPackages();
let something = [];
something = this.props.products;
}
....................................
}
Or , the function this.props.getProductsAndPackages(); can be an onClick event handler that does the same thing, context is the same. I'll ask my question after first explaining my code.
At the lower side of my Dashboard container:
Dashboard.propTypes = {
getProductsAndPackages: PropTypes.func.isRequired,
products: PropTypes.array.isRequired,
.......................
};
const mapStateToProps = (state) => {
return {
.....................
products: state.products.products,
...................
};
};
const mapDispatchToProps = (dispatch) => {
return {
getProductsAndPackages: () => dispatch(getProductsAndPackagesActionCreator()),
};
};
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Dashboard));
My actionCreator goes like:
export const getProductsAndPackagesActionCreator = () => {
return (dispatch) => {
dispatch(productsIsLoading(true));
let url = 'xyz';
if(!!localStorage.getItem('_token')) {
const local_token = localStorage.getItem('_token');
const fullToken = 'Bearer '.concat(local_token);
axios.get(url, {headers: {Authorization: fullToken}})
.then(response => {
dispatch(productsIsLoading(false));
if (response.data.statusCode === 200) {
dispatch(productsFetched(true));
dispatch(products(response.data.data));
} else {
dispatch(productsFetched(false));
dispatch(productsErrors(response.data.message));
}
})
.catch(error => {
});
} else {
axios.get(url)
.then(response => {
dispatch(productsIsLoading(false));
if (response.data.statusCode === 200) {
dispatch(productsFetched(true));
dispatch(products(response.data.data));
} else {
dispatch(productsFetched(false));
dispatch(productsErrors(response.data.message));
}
})
.catch(error => {
console.log(error);
dispatch(productsIsLoading(false));
dispatch(productsErrors(error.message));
});
}
};
};
Now, I want my getProductsAndPackagesActionCreator() to return a Promise or anything that would allow my something variable to get the actual data returned from the server. Right now, by the time I'm getting actual data, the line something=this.props.products has already been executed and I get back the initialValue that was set for products.
I know, whenever I'll receive the populated products, component will re-render, but that does not help my decision making.
I'm using redux-thunk, by the way.
What should I do now ? Sorry for such a long post.
Actually I wanted getProductsAndPackagesActionCreator() to return a promise, which was pretty straightforward, to be honest. I figured out that if you just return the axios.get() or axios.post(), it will return a promise. So, the modified code looked like below:
export const getProductsAndPackagesActionCreator = () => {
return (dispatch) => {
dispatch(productsIsLoading(true));
let url = 'xyz';
if(!!localStorage.getItem('_token')) {
return axios.get(url, {headers: {Authorization: fullToken}})
.then(response => {
............
............
})
.catch(error => {
});
} else {
return axios.get(url)
.then(response => {
...........
...........
})
.catch(error => {
console.log(error);
});
}
};
};
And then, I could do something like below in componentDidMount() or on any onClick event:
this.props.getProductsAndPackages().then(() => {
this.setState({
...this.state,
clicked_product: this.props.product_by_id
}, () => {
//do other stuffs
});
});
Feel free to let me know if there's any issue.
I think you are close to getting what you want. First of all, you should understand that redux actions and react actions like setState are asynchronous, so you have to apply your logic keeping this in mind. I'm going to explain what i think in some points:
You have called the action creator in the correct place componentDidMount, also you can call this action in any onClick if you want.
As soon as you dispatch the action you are changing your redux state setting loading true I suppose. So now you can access this property in your render function, so you can render a Loader until your api call finishes.
When your ajax function finishes, with an error or not, I suppose you are setting loading to false and updating your products data, so you can render now your loaded products in your dashboard.
Are you sure that you have to compare your empty products array with the received data? Maybe you can check in your render function if (!this.props.products.length) return null, when you load your page you will see a loader function and later your dashboard with the products.
If you really need to compare previous products with received products componentDidUpdate is your method. In this method, you can access your previous props and compare with actual props, be careful comparing arrays, remember [] === [] is false. Maybe you can compare the length, something like
componentDidUpdate(prevProps){
if(prevProps.products.length !=== this.props.products.lenth){
doSomething()
}
}
Just to say that componentDidUpdate is executed after render, so be careful with your code to no-execute extra renderings.
Hope it helps, if you dont understand anyting just tell me :)

JS - Promises executing in the wrong order

what my code is trying to do is create an array of objects that have some dynamic properties, these properties are to be filled as a result of some functions. I'm trying to make use of promises otherwise my template is rendered before the function has finished and these objects' properties will be null or undefined, causing errors in the template.
This is the first function
fetchUserPortfolioCoins({ commit, dispatch, state, rootGetters }) {
const promises = []
promises.push(dispatch('utilities/setLoading', true, { root: true })) // start loader
if (!rootGetters['auth/isAuthenticated']) {
// if user isn't logged, pass whatever is in the store, so apiDetails will be added to each coin
let coins = state.userPortfolioCoins
coins.forEach(coin => { promises.push(dispatch('createAcqCostConverted', coin)) })
commit('SET_USER_COINS', { coins, list: 'userPortfolioCoins' })
} else {
// otherwise, pass the response from a call to the DB coins
Vue.axios.get('/api/coins/').then(response => {
let coins = response.data
coins.forEach(coin => { promises.push(dispatch('createAcqCostConverted', coin)) })
commit('SET_USER_COINS', { coins, list: 'userPortfolioCoins' })
})
}
Promise.all(promises)
.then(() => {
commit('SET_USER_PORTFOLIO_OVERVIEW')
dispatch('utilities/setLoading', false, { root: true })
})
.catch(err => { console.log(err) })
},
that calls this one:
createAcqCostConverted({ dispatch, rootState }, coin) {
const promises = []
// this check is only going to happen for sold coins, we are adding sell_price_converted in case user sold in BTC or ETH
if (coin.sell_currency === 'BTC' || coin.sell_currency === 'ETH') {
const URL = `https://min-api.cryptocompare.com/data/pricehistorical?fsym=${coin.coin_symbol}&tsyms=${rootState.fiatCurrencies.selectedFiatCurrencyCode}&ts=${coin.sold_on_ts}`
promises.push(Vue.axios.get(URL, {
transformRequest: [(data, headers) => {
delete headers.common.Authorization
return data
}]
}))
}
// if user bought with BTC or ETH we convert the acquisition cost to the currently select fiat currency, using the timestamp
if (coin.buy_currency === 'BTC' || coin.buy_currency === 'ETH') {
const URL = `https://min-api.cryptocompare.com/data/pricehistorical?fsym=${coin.coin_symbol}&tsyms=${rootState.fiatCurrencies.selectedFiatCurrencyCode}&ts=${coin.bought_on_ts}`
promises.push(Vue.axios.get(URL, {
transformRequest: [(data, headers) => {
delete headers.common.Authorization
return data
}]
}))
} else {
// if the selected fiatCurrency is the same as the buy_currency we skip the conversion
if (coin.buy_currency === rootState.fiatCurrencies.selectedFiatCurrencyCode) {
coin.acquisition_cost_converted = NaN
return coin
// otherwise we create the acq cost converted property
} else promises.push(dispatch('fiatCurrencies/convertToFiatCurrency', coin, { root: true }))
}
Promise.all(promises)
.then(response => {
const value = response[0].data[coin.coin_symbol][rootState.fiatCurrencies.selectedFiatCurrencyCode]
if (coin.sell_currency === 'BTC' || coin.sell_currency === 'ETH') coin.acquisition_cost_converted = value
if (coin.buy_currency === 'BTC' || coin.buy_currency === 'ETH') coin.acquisition_cost_converted = value
return coin
})
.catch(err => { console.log(err) })
},
The problem is that the first function is not waiting for the second one to complete. How can I adjust this code to fix the issue?
Thanks
You are executing all promises at the same time. Promise.all does not execute them in order. The order of the array you pass it is not relevant. It simply resolves when they all finish regardless of order.
The execution happens when you call the functions, which is before you even push them into the array.
If you need to wait for the first to finish before calling the second. You need to call the second inside the first .then function. For example...
dispatch('utilities/setLoading', true, { root: true }).then(resultOfSetLoading => {
return Promise.all(coins.map(coin => dispatch('createAcqCostConverted', coin)))
}).then(resultOfCreateQcqCostConverted => {
// all createAcqCostConverted are complete now
})
So now, dispatch('utilities/setLoading') will run first. Then, once complete, dispatch('createAcqCostConverted') will run once for each coin (at the same time since I used Promise.all).
I recommend you read up a bit more on how Promise.all works. It is natural to assume it resolves them in order, but it does not.
This is how I managed to make it work (both for logged off user and logged in user, 2 different approaches) after I read some of you guys replies, not sure if it's the cleanest approach.
First function:
fetchUserPortfolioCoins({ commit, dispatch, state, rootGetters }) {
const setCoinsPromise = []
let coinsToConvert = null
// start loader in template
dispatch('utilities/setLoading', true, { root: true })
// if user is logged off, use the coins in the state as dispatch param for createAcqCostConverted
if (!rootGetters['auth/isAuthenticated']) setCoinsPromise.push(coinsToConvert = state.userPortfolioCoins)
// otherwise we pass the coins in the DB
else setCoinsPromise.push(Vue.axios.get('/api/coins/').then(response => { coinsToConvert = response.data }))
// once the call to the db to fetch the coins has finished
Promise.all(setCoinsPromise)
// for each coin retrived, create the converted acq cost
.then(() => Promise.all(coinsToConvert.map(coin => dispatch('createAcqCostConverted', coin))))
.then(convertedCoins => {
// finally, set the portfolio coins and portfolio overview values, and stop loader
commit('SET_USER_COINS', { coins: convertedCoins, list: 'userPortfolioCoins' })
commit('SET_USER_PORTFOLIO_OVERVIEW')
dispatch('utilities/setLoading', false, { root: true })
}).catch(err => { console.log(err) })
},
createAcqCostConverted function:
createAcqCostConverted({ dispatch, rootState }, coin) {
const promises = []
// this check is only going to happen for sold coins, we are adding sell_price_converted in case user sold in BTC or ETH
if (coin.sell_currency === 'BTC' || coin.sell_currency === 'ETH') {
const URL = `https://min-api.cryptocompare.com/data/pricehistorical?fsym=${coin.coin_symbol}&tsyms=${rootState.fiatCurrencies.selectedFiatCurrencyCode}&ts=${coin.sold_on_ts}`
promises.push(Vue.axios.get(URL, {
transformRequest: [(data, headers) => {
delete headers.common.Authorization
return data
}]
}))
}
// if user bought with BTC or ETH we convert the acquisition cost to the currently select fiat currency, using the timestamp
if (coin.buy_currency === 'BTC' || coin.buy_currency === 'ETH') {
const URL = `https://min-api.cryptocompare.com/data/pricehistorical?fsym=${coin.coin_symbol}&tsyms=${rootState.fiatCurrencies.selectedFiatCurrencyCode}&ts=${coin.bought_on_ts}`
promises.push(Vue.axios.get(URL, {
transformRequest: [(data, headers) => {
delete headers.common.Authorization
return data
}]
}))
} else {
// if the selected fiatCurrency is the same as the buy_currency we skip the conversion
if (coin.buy_currency === rootState.fiatCurrencies.selectedFiatCurrencyCode) {
promises.push(coin.acquisition_cost_converted = NaN)
// otherwise we create the acq cost converted property
} else promises.push(dispatch('fiatCurrencies/convertToFiatCurrency', coin, { root: true }))
}
return Promise.all(promises)
.then(response => {
if (coin.sell_currency === 'BTC' || coin.sell_currency === 'ETH') {
const value = response[0].data[coin.coin_symbol][rootState.fiatCurrencies.selectedFiatCurrencyCode]
coin.acquisition_cost_converted = value
}
if (coin.buy_currency === 'BTC' || coin.buy_currency === 'ETH') {
const value = response[0].data[coin.coin_symbol][rootState.fiatCurrencies.selectedFiatCurrencyCode]
coin.acquisition_cost_converted = value
}
return coin
})
.catch(err => { console.log(err) })
},
In the second function I didn't have to adjust much, I just added a "return" for the Promise.all and corrected the if/else to use the response only in specific causes, because the "value" variable generated from the response is valid only in those 2 cases, in the other cases I could simply return the "coin".
Hope it makes sense, here to explain anything better if needed and/or discuss ways to make this code better (I have a feeling it's not, not sure why though :P )

Categories

Resources