Fetch data in redux reducer and action - how to do it - javascript

I have problem to find out how to replace my fetch function into my action and reducer. Here is how does it look like without redux
async getData() {
const amount = this.props.amount;
if (amount === isNaN) {
return;
} else {
try {
await fetch(
`https://api.exchangeratesapi.io/latest?base=${this.props.base}`,
)
.then(res => res.json())
.then(data => {
const date = data.date;
const result = (data.rates[this.props.convertTo] * amount).toFixed(
4,
);
this.setState({
result,
date,
});
}, 3000);
} catch (e) {
console.log('error', e);
}
}
}
How should look action or reducer or both in redux to get the same result ?

Redux doesn't allow you to do api calls in actions or reducers. Instead, you can use libraries like redux-observable, redux-saga or redux-thunk. I personally recommend redux-observable.
If you still want to try doing it on your own, you'll have to create three actions - one for request, one for api call success and one for failure. You can write the same code and instead of setState dispatch appropriate actions before and after the call resolves.

Related

How to access data outside of a Promise

I'm making a react app that sends an API call to OpenWeather to get the weather data for a city (specified by the user). Here's what the request for that call looks like:
async function getAPI() {
const apiCall = await axios.get(apiLink).then(res => {
res = {
temp : res.data.main.temp - 273.15,
weatherIcon : res.data.weather[0].icon,
windSpeed : res.data.wind.speed
}
return res
});
return apiCall
}
const weatherData = getAPI()
Notice that I try to store the data I want from the API response in a variable called weatherData. That way I can simply call that variable whenever I need, heres an example of HTML code that uses this variable:
<p>
temperature is {weatherData.temp} Celcius
</p>
This results in weatherData.temp simply not showing up on the browser side for some reason. A console.log(weatherData) prints this in the console:
Promise {<pending>}
[[Prototype]]: Promise
[[PromiseState]]: "fulfilled"
[[PromiseResult]]: Object
temp: 29.53
weatherIcon: "04d"
windSpeed: 1.59
[[Prototype]]: Object
How do I extract the data from the promise in a way that allows me to easily refer to said data for use in HTML code?
Answer below is if you are using functional components and react hooks.
You can can go two directions:
Using a try catch block:
const fetchWeather = async () => {
try {
const res = await axios.get(apiLink);
console.log(res);
setWeather(res.data); //Im not sure what the exact response is, but you can access the keys you need.
// you can then set the data you need to your state to render it.
} catch (error) {
// handle error
}
}
Or you can use .then .catch
const fetchWeather = async () => {
axios.get(apiLink)
.then((res) => {
setWeather(res.data); //Im not sure what the exact response is, but you can access the keys you need.
// set the data you need from the respones to your state.
})
.catch((err) => {
// handle error
})
}
In both cases you can just call the function in your useEffect hook.
useEffect(() => {
fetchWeather()
}, [])
In general my preference goes to set the response you get from the Api into the local state (meaning the state of your page/component). And then rendering the state to your jsx.
So if you are using react hooks, your state could look like this:
const [weather, setWeather] = useState({});
Last Edit:
Finally you can just refer to your state within your jsx/html. Assuming your weather state looks like this:
{
temp: '50 degrees'
}
In your JSX you can just refer to it this way:
<>
<div>{weather.temp}</div>
</>

Redux Toolkit w/ TS: what is the recommended approach for dispatching two consecutive async thunk actions

I encountered this problem recently when I tried to use two different versions of an API. The logic is if v2 of the API gives me a 404 error, then I would try v1, if no errors, I would use the results from v2
Here is my attempt: I created two separate async thunk actions for each version and then create a async thunk where I dispatch both actions.
export const getV2LoggingOptions = createAsyncThunk(
"settings/getV2LoggingOptions",
async () => {
return sdkClient.getV2LoggingOptions().promise();
}
);
export const getV1LoggingOptions = createAsyncThunk(
"settings/getV1LoggingOptions",
async () => {
return sdkClient.getV1LoggingOptions().promise();
}
);
export const getLoggingOptions = createAsyncThunk(
"settings/getLoggingOptions",
async (arg, thunkApi) => {
let response = await thunkApi.dispatch(getV2LoggingOptions());
if (response.error) {
if (
response.error.statusCode === "404"
) {
response = await thunkApi.dispatch(getV1LoggingOptions());
}
throw response.error;
}
return response.payload;
}
);
I think this approach works. but not sure if this is the best way of doing it. Right now there are a couple issues with this approach:
I don't know how I can properly type this response as in let response = await thunkApi.dispatch(getV2LoggingOptions());.
also, the error property inside of response(if v2 call failed) doesn't contain a statusCode property. so I cannot read it. This is really confusing to me as to why it doesn't contain the statusCode
Then another approach would be just create one async thunk and call two versions inside directly
export const getLoggingOptions = createAsyncThunk(
"settings/getLoggingOptions",
async () => {
let response;
try {
response = await sdkClient.getV2LoggingOptions().promise();
} catch (error) {
if (
error.statusCode === "404"
) {
response = await sdkClient.getV1LoggingOptions().promise();
}
throw error;
}
return response;
}
);
It seems to be working too. but the issues are, still I am not sure how to type the response here.
The API does offer typing for its response. GetV1LoggingOptionsResponse and GetV2LoggingOptionsResponse. but I am not sure should I type the response as
let response: GetV1LoggingOptionsResponse | GetV2LoggingOptionsResponse
since then I can only read the overlapped part of these two types from the response.
also in the second approach statusCode is missing in the error that got caught in the catch clause.
Since both apis have different return values, they should probably be different asyncThunks and you should handle their actions separately.
Also, you should do the error handling (including throwing) inside the respective asyncThunks, as that will lead to a rejected action that you can handle in the reducer.
Once you have those two asyncThunks, there is no reason for a third asyncThunk that will introduce it's own lifecycle actions that you really don't need as this is essentially just orchestration of the other two.
Write a normal thunk:
const getLogginOptions = () => async (dispatch) => {
let result = await dispatch(getV2LoggingOptions());
if (getV2LoggingOptions.rejected.match(result)) {
result = await dispatch(getV1LoggingOptions());
}
return result;
}

Function inside component not receiving latest version of Redux-state to quit polling

I have an issue where I am trying to use the Redux state to halt the execution of some polling by using the state in an if conditional. I have gone through posts of SO and blogs but none deal with my issue, unfortunately. I have checked that I am using mapStateToProps correctly, I update state immutably, and I am using Redux-Thunk for async actions. Some posts I have looked at are:
Component not receiving new props
React componentDidUpdate not receiving latest props
Redux store updates successfully, but component's mapStateToProps receiving old state
I was kindly helped with the polling methodology in this post:Incorporating async actions, promise.then() and recursive setTimeout whilst avoiding "deferred antipattern" but I wanted to use the redux-state as a single source of truth, but perhaps this is not possible in my use-case.
I have trimmed down the code for readability of the actual issue to only include relevant aspects as I have a large amount of code. I am happy to post it all but wanted to keep the question as lean as possible.
Loader.js
import { connect } from 'react-redux';
import { delay } from '../../shared/utility'
import * as actions from '../../store/actions/index';
const Loader = (props) => {
const pollDatabase = (jobId, pollFunction) => {
return delay(5000)
.then(pollFunction(jobId))
.catch(err => console.log("Failed in pollDatabase function. Error: ", err))
};
const pollUntilComplete = (jobId, pollFunction) => {
return pollDatabase(jobId, pollFunction)
.then(res => {
console.log(props.loadJobCompletionStatus) // <- always null
if (!props.loadJobCompletionStatus) { <-- This is always null which is the initial state in reducer
return pollUntilComplete(jobId, pollFunction);
}
})
.catch(err=>console.log("Failed in pollUntilComplete. Error: ", err));
};
const uploadHandler = () => {
...
const transferPromise = apiCall1() // Names changed to reduce code
.then(res=> {
return axios.post(api2url, res.data.id);
})
.then(postResponse=> {
return axios.put(api3url, file)
.then(()=>{
return instance.post(api3url, postResponse.data)
})
})
transferDataPromise.then((res) => {
return pollUntilComplete(res.data.job_id,
props.checkLoadTaskStatus)
})
.then(res => console.log("Task complete: ", res))
.catch(err => console.log("An error occurred: ", err))
}
return ( ...); //
const mapStateToProps = state => {
return {
datasets: state.datasets,
loadJobCompletionStatus: state.loadJobCompletionStatus,
loadJobErrorStatus: state.loadJobErrorStatus,
loadJobIsPolling: state.loadJobPollingFirestore
}
}
const mapDispatchToProps = dispatch => {
return {
checkLoadTaskStatus: (jobId) =>
dispatch(actions.loadTaskStatusInit(jobId))
};
};
export default connect(mapStateToProps, mapDispatchToProps)(DataLoader);
delay.js
export const delay = (millis) => {
return new Promise((resolve) => setTimeout(resolve, millis));
}
actions.js
...
export const loadTaskStatusInit = (jobId) => {
return dispatch => {
dispatch(loadTaskStatusStart()); //
const docRef = firestore.collection('coll').doc(jobId)
return docRef.get()
.then(jobData=>{
const completionStatus = jobData.data().complete;
const errorStatus = jobData.data().error;
dispatch(loadTaskStatusSuccess(completionStatus, errorStatus))
},
error => {
dispatch(loadTaskStatusFail(error));
})
};
}
It seems that when I console log the value of props.loadJobCompletionStatus is always null, which is the initial state of in my reducer. Using Redux-dev tools I see that the state does indeed update and all actions take place as I expected.
I initially had placed the props.loadJobCompletionStatus as an argument to pollDatabase and thought I had perhaps created a closure, and so I removed the arguments in the function definition so that the function would fetch the results from the "upper" levels of scope, hoping it would fetch the latest Redux state. I am unsure as to why I am left with a stale version of the state. This causes my if statement to always execute and thus I have infinite polling of the database.
Can anybody point out what might be causing this?
Thanks
I'm pretty sure this is because you are defining a closure in a function component, and thus the closure is capturing a reference to the existing props at the time the closure was defined. See Dan Abramov's extensive post "The Complete Guide to useEffect" to better understand how closures and function components relate to each other.
As alternatives, you could move the polling logic out of the component and execute it in a thunk (where it has access to getState()), or use the useRef() hook to have a mutable value that could be accessed over time (and potentially use a useEffect() to store the latest props value in that ref after each re-render). There are probably existing hooks available that would do something similar to that useRef() approach as well.

Infinite loop when dispatching in componentWillMount

I'm working in a React + Redux + redux-thunk codebase and I'm seeing some odd behavior. If I attempt to execute TWO actions in componentWillMount, the second action will infinitely loop.
Here's the componentWillMount:
componentWillMount() {
const { actions } = this.props;
// Action #1 (synchronous)
actions.openLoader();
// Action #2 (promise-based fetch)
actions.getListingsPurchased().then(() => {
actions.closeLoader();
})
.catch(error => {
console.error(error);
});
}
The first action, openLoader() is a simple state update. The second action does a fetch to the server. Action file here:
export function openLoader() {
return {
type: TYPES.SET_LOADER_OPEN
};
}
export function getListingsPurchased() {
return dispatch => {
return fetch'URL GOES HERE', { 'credentials': 'include' })
.then(response => {
return response.json();
})
.then(response => {
return dispatch({ type: TYPES.SET_LISTINGS, data: response.data });
});
};
}
If I was to remove the first action openLoader() from componentWillMount the infinite loop does not happen. Otherwise the fetch call will keep repeating endlessly.
Any help would be appreciated, I seem to have hit a wall.
I believe the best place for breaking infinite loop is in Redux reducer. Reducer is the place where you have to decide if you going to update the state of your app -> will trigger re-render of your components -> will trigger fetch action.
So try to put in place some reducer condition where you can recognize that state was already fetched before and you not going to update the state.

React and jest mock module

I am creating an application in which I use redux and node-fetch for remote data fetching.
I want to test the fact that I am well calling the fetch function with a good parameter.
This way, I am using jest.mock and jasmine.createSpy methods :
it('should have called the fetch method with URL constant', () => {
const spy = jasmine.createSpy('nodeFetch');
spy.and.callFake(() => new Promise(resolve => resolve('null')));
const mock = jest.mock('node-fetch', spy);
const slug = 'slug';
actionHandler[FETCH_REMOTE](slug);
expect(spy).toHaveBeenCalledWith(Constants.URL + slug);
});
Here's the function that I m trying to test :
[FETCH_REMOTE]: slug => {
return async dispatch => {
dispatch(loading());
console.log(fetch()); // Displays the default fetch promise result
await fetch(Constants.URL + slug);
addLocal();
};
}
AS you can see, I am trying to log the console.log(fetch()) behavior, and I am having the default promise to resolve given by node-fetch, and not the that I've mock with Jest and spied with jasmine.
Do you have an idea what it doesn't work ?
EDIT : My test displayed me an error like my spy has never been called
Your action-handler is actually a action handler factory. In actionHandler[FETCH_REMOTE], you are creating a new function. The returned function taskes dispatch as a parameter and invokes the code you are showing.
This means that your test code will never call any function on the spy, as the created function is never invoked.
I think you will need to create a mock dispatch function and do something like this:
let dispatchMock = jest.fn(); // create a mock function
actionHandler[FETCH_REMOTE](slug)(dispatchMock);
EDIT:
To me, your actionHandler looks more like an actionCreator, as it is usually called in redux terms, though I personally prefer to call them actionFactories because that is what they are: Factories that create actions.
As you are using thunks(?) your actionCreater (which is misleadingly named actionHandler) does not directly create an action but another function which is invoked as soon as the action is dispatched. For comparison, a regular actionCreator looks like this:
updateFilter: (filter) => ({type: actionNames.UPDATE_FILTER, payload: {filter: filter}}),
A actionHandler on the other hand reacts to actions being dispatched and evaluates their payload.
Here is what I would do in your case:
Create a new object called actionFactories like this:
const actionFactories = {
fetchRemote(slug): (slug) => {
return async dispatch => {
dispatch(loading());
console.log(fetch()); // Displays the default fetch promise result
let response = await fetch(Constants.URL + slug);
var responseAction;
if (/* determine success of response */) {
responseAction = actionFactories.fetchSuccessful(response);
} else {
responseAction = actionFactories.fetchFailed();
}
dispatch(responseAction);
};
}
fetchFailed(): () => ({type: FETCH_FAILED, }),
fetchSuccessful(response): () => ({type: FETCH_FAILED, payload: response })
};
Create an actionHandler for FETCH_FAILED and FETCH_SUCCESSFUL to update the store based on the response.
BTW: Your console.log statement does not make much sense too me, since fetch just returns a promise.

Categories