Making ajax call using redux-saga and updating store - reactjs

im new to whole React env and im trying to create a GET request to Google Api using redux-saga library.
Im sort of missing 2 things. First problem is that my saga function is called again and again forever ( have no idea why ).
The second thing is how to pass the data properly to the reducer.
Here is my Saga:
function* watchAutoCompleteFetch() {
yield takeLatest(UPDATE_ZIP_AUTOCOMPLETE, requestAutoComplete);
}
function requestAutoCompleteApi() {
return fetch(
'some-url-here'
).then((response) => response.json())
.then((json) => json)
.catch((e) => {
put({type: "AUTOCOMPLETE_ZIP_FETCH_FAILED", message: e.message});
});
}
function* requestAutoComplete() {
const data = call(requestAutoCompleteApi);
yield put(updateZipAutoCompleteAction(data));
}
And reducer function:
const updateZipAutoComplete = (state, data) => {
debugger;
return state;
};
In reducer, I get the data as some sort of call object from the redux-saga, not a promise, nor the data.
Any ideas what im doing wrong?

So, turns out there was indeed 2 problems. One is that I was missing yield keyword, the second was I was calling always the same reducer, which triggered the dispatch event again and it went into loop and run forever.
The actual solutions looks like this:
function* watchAutoCompleteFetch() {
yield takeLatest(UPDATE_ZIP_AUTOCOMPLETE, requestAutoComplete);
}
function requestAutoCompleteApi() {
return fetch(
'some-url-here'
).then((response) => response.json())
.catch((e) => {
console.log(e)
});
}
function* requestAutoComplete() {
const data = yield call(requestAutoCompleteApi);
if(data.status ==="OK") {
yield put(updateZipAutoCompleteSucceedAction(data));
} else {
yield put(updateZipAutoCompleteFailedAction(data));
}
}

Related

Redux saga dispatching actions from inside map inside saga-action

I have API calls in the following manner:
Call main api which gives me an array of objects.
For each object inside array I have to call another API asynchronously.
As soon as the sub API call for the object is finished, update its data in redux store which is an array (ofc) and show it.
So the scenario is a list showing items which grow in dynamic fashion.
Since I am using redux-saga, I have to dispatch the second part from inside an redux-action. I have tried the follow way:
const response = yield call(get, 'endpoint')
const configHome = response.map(function* (ele) {
const data = yield call(get, ele.SomeURI + '?someParameter=' + ele.someObject.id)
}))
This doesn't work since map doesn't know anything about generator functions. So I tried this:
const response = yield call(get, 'endpoint')
const configHome = yield all(response.map((ele) => {
return call(get, paramsBuilder(undefined, ele.CategoryID))
}))
But this will block my UI from showing available data untill all sub API calls are finished.
I have also tried making a separate generator function which I call from inside map and call its .next() function but the problem here again is that saga doesn't control that generator function so the call effect doesn't return any value properly.
Completely stuck on this part. Would appreciate any help.
Have you tried this, I have created a sample this might help
import { put, takeLatest, all, call } from 'redux-saga/effects';
function* _fetchNews(id) {
const data = yield fetch(
`https://jsonplaceholder.typicode.com/todos/${id}`
).then(function(response) {
const data = response.json();
return data;
});
console.log(id);
yield put({ type: 'NEWS_RECEIVED', data });
return data;
}
function* _getData() {
const json = yield fetch('https://jsonplaceholder.typicode.com/todos').then(
response => response.json()
);
return json;
}
function* fetchNews() {
const json = yield _getData();
const configHome = json.map(ele => _fetchNews(ele.id));
for (var item of configHome) {
yield item;
}
}
function* actionWatcher() {
yield takeLatest('GET_NEWS', fetchNews);
}
export default function* rootSaga() {
yield all([actionWatcher()]);
}
yield all - the generator is blocked until all the effects are resolved or as soon as one is rejected
So you will need to dispatch events separately for each sub API
Let's assume you have 2 actions:
export const getMainApi =() => ({
type: types.GET_MAIN_API,
});
export const getSubApi = endpoint => ({
type: types.GET_SUB_API,
endpoint,
});
Then your operations will be:
const get = endpoint => fetch(endpoint).then(response => response);
function* fetchMainApi(action) {
try {
const response = yield call(get, 'endpoint');
for (let i = 0; i < response.length; i += 1) {
// dispatch here all sub APIs
yield put(
getSubApi(
response[i].SomeURI + '?someParameter=' + response[i].someObject.id,
),
);
}
} catch (e) {
console.log(e);
}
}
function* fetchSubApi(action) {
try {
const response = yield call(get, action.endpoint);
yield put({
type: types.RECEIVE_SUB_API,
response
});
} catch (e) {
console.log(e);
}
}
takeLatest(type.GET_MAIN_API, fetchMainApi)
takeEvery(types.GET_SUB_API, fetchSubApi)
So on success of receiving sub APIs you need to insert data into your state inside reducers.
This is just pseudo code.

redux-saga api call response filtering

I' trying to filter my redux-saga generator functions api response like this.
function* loadSingleDataAsync(id) {
console.log('Second Saga Works');
let wholeData = [];
let singleData = [];
agent
.get(
`https://pro-api.coinmarketcap.com/v1/cryptocurrency/listings/latest?CMC_PRO_API_KEY=xxx`
)
.then((res) => {
wholeData = [...res.body.data];
singleData = wholeData.filter((currencie) => currencie.id === id);
})
.catch((err) => {
console.log('--------err', err);
});
yield delay(700);
yield put({ type: 'RECEIVE_SINGLE_API_DATA_ASYNC', singleData });
}
After this I'm calling it this way:
export function* rootSaga() {
yield all([takeEvery('RECEIVE_API_DATA', loadDataAsync), takeEvery('RECEIVE_SINGLE_API_DATA', loadSingleDataAsync)]);
}
The first function is working but the second don't,Any suggestions why?
Adding a try, catch inside the saga helps you to know if the saga chain is broken because of any empty responses or data issues.

How to make await work with redux Saga in React?

The await does not seem to work with Redux saga. I need to wait for my API call to finish and then execute the remaining code. What happens now is that AFTER CALL gets printed before the RESPONSE which means await does not seem to work at all. I'm using async calls but not sure what needs to be done extra from the redux saga side?
async componentWillMount() {
console.log("BEFORE CALL")
await this.props.getUserCredit()
console.log("AFTER CALL")
}
mapDispatchToProps = (dispatch) => {
return {
getUserCredit: () => dispatch(getUserCredit()),
}
};
connect(null, mapDispatchToProps)(MyComponent);
Action
export const getUserCredit = () => {
return {
type: GET_USER_CREDIT,
};
};
Redux Saga
const getUserCreditRequest = async () => {
const response = await Api.get(getCreditUrl)
console.log("REPONSE!!!")
console.log(response)
return response
}
function* getUserCredits() {
try {
const response = yield call(getUserCreditRequest);
if (response.status === okStatus) {
yield put({
userCredit: response.data.userCredit
}
));
}
} catch (error) {}
}
export function* getUserCredit() {
yield takeLatest(GET_USER_CREDIT, getUserCredits);
}
export default function* rootSaga() {
yield all([fork(getUserCredit)]);
}
Normally, init / fetching takes place during componentDidMount and don't use async or await inside components. Let the saga middleware do its thing via yield.
// In your component
componentDidMount() { // not async
this.props.getUserCredit(); // dispatch `GET_USER_CREDIT` action
}
mapDispatchToProps = (dispatch) => {
return {
getUserCredit: () => dispatch(getUserCredit()),
}
};
connect(null, mapDispatchToProps)(YourComponent);
You shouldn't be using async/await pattern. As redux-saga handles it by the yield keyword. By the time call is resolved you will have the value available in response.
in actions.js, you should have an action that will carry your data to your reducer:
export function getUserCredits(userCredit) {
return {
type: types.GET_USER_CREDIT_SUCCESS,
payload: userCredit
};
}
Your saga should handle the API call like so:
function* getUserCredits() {
try {
const response = yield axios.get(getCreditUrl); <--- This should work
// No need for if here, the saga will catch an error if the previous call failed
yield put(actions.getUserCredits(response.data.userCredit));
} catch (error) {
console.log(error);
}
}
EDIT: example of using axios with redux-saga

Redux-Loop dispatch not returning promise from reducer

I'm using v2.2.2 of redux-loop to handle my side-effects from a server call.
Having dispactched an action from my component like so:
checkStatus() {
const user = Utils.toJS(this.props.user);
this.props.dispatch(UserActions.getUserData(user._id))
.then((res) => {
console.log(res)
}
}
I expect my promise to come back from the dispatch, but it always returns
[]
My action looks like so...
export async function getUserData(data) {
return await getUser(data)
.then((res) => ({type: USER_GET_SUCCESS, payload: res}))
.catch((err) => ({type: USER_GET_FAILURE, payload: err}));
}
where getUser looks like:
export async function getUser(data) {
return await get(`/users/${data}`)
}
and gets caught in the reducer and saved to the state like so:
case USER_GET_SUCCESS:
return state
.set('user', fromJS(action.payload.data));
The data always comes back properly but for some reason never gets returned back as a promise to the original dispatch.
Any suggestions would be amazing!
I think part of the issue is mixing your promise .then code in with the async/await calls within a single function. Try this instead:
export async function getUserData(data) {
try {
const result = await getUser(data);
return { type: USER_GET_SUCCESS, payload: result };
} catch (err) {
return {type: USER_GET_FAILURE, payload: err};
}
}

React, Redux and Axios - trying to make API call

It's my first experience with React, Redux and I am totally lost. The problem is my action :
import axios from 'axios';
import { FETCH_MOVIE } from '../constants/actionTypes';
const API_KEY = <API_KEY>;
const ROOT_URL = `<API_URL>`;
export function fetchMovies(pop){
const url = `${ROOT_URL}?api_key=${API_KEY}&sort_by=${pop}`;
axios.get(url)
.then(function (response) {
console.log("response is",response)
})
.catch(function (error) {
console.log(error);
});
return{
type: FETCH_MOVIE,
payload: response.data
};
}
On Console.log it seems just fine - I can see the response has the data I need. But when I am trying to send response.data to payload it returns the error - response is not defined. What am I doing wrong?
P.s. I also tried to create const result = [] and than result = [...response.data]. The error was - SyntaxError: "result" is read-only
The const error is because, result being a variable that changes over the course of the execution, you must use 'let' and not 'const'.
Now, for the fix, response is not defined comes from the last return. A good approach would be to, instead of returning the action on this function fetchMovies, you should dispatch a new action, e.g dispatch(fetchMoviesSuccess(payload)) instead of "console.log("response is",response)", which will dispatch an action that will trigger the reducer, and , in turn, update the state of the app.
You are performing async request using axios. You should dispatch your action using redux-thunk. Installation is easy, read more about thunk here.
Then your action should look like this:
export function fetchMovies(pop) {
return dispatch => {
const url = `${ROOT_URL}?api_key=${API_KEY}&sort_by=${pop}`;
axios.get(url)
.then(function (response) {
console.log("response is",response);
dispatch({
type: FETCH_MOVIE,
payload: response.data
});
})
.catch(function (error) {
console.log(error);
// You can dispatch here error
// Example
dispatch({
type: FETCH_MOVIE_FAILED,
payload: error
});
});
}
}
The issue with your code is that by the time you return, response is still undefined because this code run synchronously till the return statement.
As you can see response is defined in console.log("response is",response)
So this is where you need to do your actual magic return but in another way.
You can use redux-thunk to do these thing because this is redux async. but as I feel you are a beginner from the code I have seen, Just use the simpler way and read redux-thunk or redux-promise. if you feel your project needs this then go one.
//try to make the caller pass this.props.dispatch as param
export function fetchMovies(dispatch, pop){
const url = `${ROOT_URL}?api_key=${API_KEY}&sort_by=${pop}`;
axios.get(url)
.then(function (response) {
// only here is response define so use dispatch to triger another action (fetched data with response)
dispatch({
type: FETCH_MOVIE,
payload: response.data
})
})
.catch(function (error) {
//if you had a loader state, you call also toggle that here with erro status
console.log(error);
});
}
//now on the caller (onClick for instance) do this instead
fetchMovies(this.props.dispatch, pop)
As you can see from #loelsonk answer down. if you use redux-thunk then you won't need to pass dispatch from the caller redux-thunk for you. But also notice how you would return and anonymous arrow function which accept dispatch as a parameter.
You can use redux promise middleware. I have used this in my new project. It is very simple and keeps our code and state manageable.
For every async action dispatch, it dispatches
$action_type_PENDING immediately after our action dispatch , $action_type_FULFILLED if api call success, $action_type_REJECTED if api call failure
See documentation- https://github.com/pburtchaell/redux-promise-middleware
Example from my project-
your action is
export function getQuestions() {
return {
type: types.GET_QUESTIONS,
payload: axios.get('http://localhost:3001/questions')
};
}
reducer is
const initialState = {
isLoading: false,
questions: []
};
const questions = (state = initialState.questions, action) => {
switch(action.type) {
case types.GET_QUESTIONS_FULFILLED:
return [...action.payload.data];
default: return state;
}
};
For displaying loader while api call we can use following reducer
const isLoading = (state = initialState.isLoading, action) => {
switch(action.type) {
case (action.type.match(/_PENDING/) || {}).input:
return true;
case (action.type.match(/_FULFILLED/) || {}).input:
return false;
default: return state;
}
};
Comment me if you need any more details on above stuff.

Resources