How to pass current route parameters to the saga function?
// Saga
function* findOneReleaseWithFilesSaga() {
const { productName, releaseId } = useParams()
try {
const releaseResponse: AxiosResponse = yield call(findOneWithFiles, releaseId)
^^^Error Here^^^
yield put(getAllReleasesSuccessAction(releaseResponse))
} catch (error) {
yield put(getAllReleasesErrorAction(error))
}
}
// Fetch Data
export const findOneWithFiles = async (releaseId:string) => {
return await Api.get(`/releases/${releaseId}/?populate=data_files`)
.then((res) => res.data.data)
.catch((err) => err)
}
I think the issue in the type of releaseId you get from useParams. Make sure it is a string. Also the desctructuring might be a problem, there were some fixes for that in TS4.6, but assuming you are on older version I would try to assign to a variable instead:
const params = useParams()
yield call(findOneWithFiles, params.releaseId)
On a side note, I would avoid prefixing a function with use unless it is a react hook.
I'm having trouble with destructuring an object that is returned from the fetch api in my Aztro class.
I've tried various way to destructure it at random with no success....
If i return it instead of console logging, how do i access it? See the code and comments for further clarification of my questions.
The reason i want to do this is because the api will only return one zodiac sign at a time.
class Aztro {
constructor(sign){
this.sign = sign
}
getData(){
return fetch('https://example/?sign=' + this.sign + '&day=today', {
method: 'POST'
})
.then( response => response.json() )
.then( data => {
console.log(data) // how do I destructure this if it's returned and not just console.log()
})
}
}
let aries = new Aztro('aries') // Can pass in zodiac signs to fetch data
let aquarius= new Aztro('aquarius')
aries.getData() // this logs the json data in the console....
// EDIT this is how I tried to destructure it
const {description} = aries
const {description} = aries.getData() // this returns Object promise when i tried to output it to the dom
const {description} = Aztro
You can access the fetched data in two manner:
1- using promise chain, like this:
aries.getData().then(data => console.log(data))
2- using async/await to fetch data.The important point in using async/await is that you have to call await keyword just inside an async function (the reason I defined app function in below code) like this:
class Aztro {
constructor(sign){
this.sign = sign
}
async getData(){
const response = await fetch('https://example/?sign=' + this.sign + '&day=today', {
method: 'POST'
})
const data = await response.json();
return data;
}
}
async function app(){
let aries = new Aztro('aries') // Can pass in zodiac signs to fetch data
let aquarius= new Aztro('aquarius')
const data = await aries.getData();
console.log(data);
}
app();
After an input change in my input element, I run an empty string check(if (debouncedSearchInput === "")) to determine whether I fetch one api or the other.
The main problem is the correct promise returned faster than the other one, resulting incorrect data on render.
//In my react useEffect hook
useEffect(() => {
//when input empty case
if (debouncedSearchInput === "") autoFetch();
//search
else searchvalueFetch();
}, [debouncedSearchInput]);
searchvalueFetch() returned slower than autoFetch() when I emptied the input. I get the delayed searchvalueFetch() data instead of the correct autoFetch() data.
What are the ways to tackle this? How do I queue returns from a promises?
I read Reactjs and redux - How to prevent excessive api calls from a live-search component? but
1) The promise parts are confusing for me
2) I think I don't have to use a class
3) I would like to learn more async/await
Edit: added searchvalueFetch, autoFetch, fetcharticles code
const autoFetch = () => {
const url = A_URL
fetchArticles(url);
};
const searchNYT = () => {
const url = A_DIFFERENT_URL_ACCORDING_TO_INPUT
fetchArticles(url);
};
const fetchArticles = async url => {
try{
const response = await fetch(url);
const data = await response.json();
//set my state
}catch(e){...}
}
This is an idea how it could looks like. You can use promises to reach this. First autoFetch will be called and then searchvalueFetch:
useEffect(() => {
const fetchData = async () => {
await autoFetch();
await searchvalueFetch();
};
fetchData();
}, []);
You can also use a function in any lifecycle depends on your project.
lifecycle(){
const fetchData = async () => {
try{
await autoFetch();
await searchvalueFetch();
} catch(e){
console.log(e)
}
};
fetchData();
}
}
This code works fine:
const { response } = yield call(makeGetRequest, requestUrl);
return response.data;
However, this does not:
function makeCall() {
...
const { response } = yield call(makeGetRequest, requestUrl);
return response;
}
function returnData() {
const response = makeCall();
return response.data;
}
The typescript error is in the return response.data line:
Property data does not exist on type.
I assume this is because it is treating response as an any, but I have no idea how I would give the method the right info about what type of object response is. I tried typeof response and just got Object.
is makeCall meant to be a generator function? It has to be if you're using using yield, so presumably you meant to type function* makeCall(). But if it's a generator, then when you do this:
function returnData() {
const response = makeCall();
return response.data;
}
... the return value of makeCall() is an iterator object. Iterators have no .data property, which is why typescript is giving you an error.
As for how to correct this, i'll need more information about what you're trying to do. Is this redux-saga code? If so, you shouldn't be calling sagas yourself, but instead you have it listen to actions (eg, with take, takeEvery, or takeLatest) and redux saga will run the saga when the actions occur.
If you want to call a saga from another saga you can, but you'll either need to use yield*:
function otherSaga*() {
const response = yield* makeCall()
console.log(response.data);
}
Or the call effect:
function otherSaga*() {
const response = yield call(makeCall);
console.log(response.data);
}
How do I test a function inside an if statement or try/catch? For instance,
export function* onFetchMessages(channel) {
yield put(requestMessages())
const channel_name = channel.payload
try {
const response = yield call(fetch,'/api/messages/'+channel_name)
if(response.ok){
const res = yield response.json();
const date = moment().format('lll');
yield put(receiveMessages(res,channel.payload,date))
}
} catch (error){
yield put(rejectMessages(error))
}
}
I need to input a real channel name that actually exist in the database for it to return a valid response for the yields that follow to execute, otherwise it will throw an error. In addition, I will get an error message, cannot read property json of undefined, so the yield after that cannot be reached due to this error message.
So my first problem is 'if(response.ok)' but even if I remove it, yield response.json() would return an error and in addition the yield after that wont be executed.
If anyone can show me how to test these, would be much appreciated.
Pass the response object to the previous execution and test conditional, I would do it like this, hope this helps:
export function* onFetchMessages(channel) {
try {
yield put(requestMessages())
const channel_name = channel.payload
const response = yield call(fetch,'/api/messages/'+channel_name)
if(response.ok){
const res = yield response.json();
const date = moment().format('lll');
yield put(receiveMessages(res,channel.payload,date))
}
} catch (error){
yield put(rejectMessages(error))
}
}
describe('onFetchMessages Saga', () => {
let output = null;
const saga = onFetchMessages(channel); //mock channel somewhere...
it('should put request messages', () => {
output = saga.next().value;
let expected = put(requestMessages()); //make sure you import this dependency
expect(output).toEqual(expected);
});
it('should call fetch...blabla', ()=> {
output = saga.next(channel_name).value; //include channel_name so it is avaiable on the next iteration
let expected = call(fetch,'/api/messages/'+channel_name); //do all the mock you ned for this
expect(output).toEqual(expected);
});
/*here comes you answer*/
it('should take response.ok into the if statemenet', ()=> {
//your json yield is out the redux-saga context so I dont assert it
saga.next(response).value; //same as before, mock it with a ok property, so it is available
output = saga.next(res).value; //assert the put effect
let expected = put(receiveMessages(res,channel.payload,date)); //channel should be mock from previous test
expect(output).toEqual(expected);
});
});
Notice your code probably does more stuff I'm not aware of, but this at least should put u in some line to solve your problem.
You might want to use an helper library for that, such as redux-saga-testing.
Disclaimer: I wrote this library to solve that exact same problem
For your specific example, using Jest (but works the same for Mocha), I would do two things:
First, I would separate the API call to a different function
Then I would use redux-saga-testing to test your logic in a synchronous way:
Here is the code:
import sagaHelper from 'redux-saga-testing';
import { call, put } from 'redux-saga/effects';
import { requestMessages, receiveMessages, rejectMessages } from './my-actions';
const api = url => fetch(url).then(response => {
if (response.ok) {
return response.json();
} else {
throw new Error(response.status); // for example
}
});
function* onFetchMessages(channel) {
try {
yield put(requestMessages())
const channel_name = channel.payload
const res = yield call(api, '/api/messages/'+channel_name)
const date = moment().format('lll');
yield put(receiveMessages(res,channel.payload,date))
} catch (error){
yield put(rejectMessages(error))
}
}
describe('When testing a Saga that throws an error', () => {
const it = sagaHelper(onFetchMessages({ type: 'foo', payload: 'chan1'}));
it('should have called the API first, which will throw an exception', result => {
expect(result).toEqual(call(api, '/api/messages/chan1'));
return new Error('Something went wrong');
});
it('and then trigger an error action with the error message', result => {
expect(result).toEqual(put(rejectMessages('Something went wrong')));
});
});
describe('When testing a Saga and it works fine', () => {
const it = sagaHelper(onFetchMessages({ type: 'foo', payload: 'chan2'}));
it('should have called the API first, which will return some data', result => {
expect(result).toEqual(call(api, '/api/messages/chan2'));
return 'some data';
});
it('and then call the success action with the data returned by the API', result => {
expect(result).toEqual(put(receiveMessages('some data', 'chan2', 'some date')));
// you'll have to find a way to mock the date here'
});
});
You'll find plenty of other examples (more complex ones) on the project's GitHub.
Here's a related question: in the redux-saga docs, they have examples where take is listening for multiple actions. Based on this, I wrote an auth saga that looks more or less like this (you may recognize that this is a modified version of an example from the redux-saga docs:
function* mySaga() {
while (true) {
const initialAction = yield take (['AUTH__LOGIN','AUTH__LOGOUT']);
if (initialAction.type === 'AUTH__LOGIN') {
const authTask = yield fork(doLogin);
const action = yield take(['AUTH__LOGOUT', 'AUTH__LOGIN_FAIL']);
if (action.type === 'AUTH__LOGOUT') {
yield cancel(authTask);
yield call (unauthorizeWithRemoteServer)
}
} else {
yield call (unauthorizeWithRemoteServer)
}
}
}
I don't think this is an anti-pattern when dealing with Sagas, and the code certainly runs as expected outside the test environment (Jest). However, I see no way to handle the if statements in this context. How is this supposed to work?