How to make other function gets called after first is executed? - javascript

I am working on a react app where I am using redux for state management and I have 2 functions to call and I want them to run only after 1st function is executed.
Here's a snippet of whats I am doing:
if (this.props.page_num < this.props.numPages) {
this.props.fetchCode(params, isFiltered, isSearched).then(() => {
this.props.setPageNumber(this.props.page_num + 1);
});
}
Here I am getting a error stating:
CodeTable.jsx?2468:132 Uncaught TypeError: this.props.fetchCode(...).then is not a function
fetchCode function:
export function* fetchCode(action) {
try {
const response = yield call(Services.fetchCode, action.params);
const { dtoList } = response.data.pagedList;
const num_pages = response.data.pagedList.numPages;
const total_records = response.data.pagedList.totalRecords;
const page_number = response.data.pagedList.pageNumber;
const postCodeSetsData = dtoList.map(({
}) => ({
}));
yield put(ActionCreator.setCodes(dtoList, num_pages, total_records, postCodeData, page_number, action.isFiltered, action.isSearched));
} catch (error) {
sagaException(error);
}
}

Since you are using redux saga, I believe the most appropriate thing to do is compose another saga.
export function* fetchCodeSetsAndSetPage(action) {
try {
yield put (ActionCreator.fetchCodes(...));
yield put (ActionCreator.setPageNumber(...));
} catch (error) {
sagaException(error);
}
}
And then call that one instead in your component.
Some docs.

make fetchCodeSets function async. You can write promise like then only with an async functions. also use a return statement inside the function. The declaration of the fetchCodeSets function should be like this
const fetchCodeSets = async (params) => {
//codes here
return;
}

Related

use route parameter in the Redux Saga file

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.

Declare an Async function

I'm new in React and I'm trying to call an async function, but I get the Unexpected reserved word 'await'. ERROR
So my async function is in a Helper class who is supposed to do all the API call, and I'm calling this Helper class in my Game function
Here is my code from Class Helper :
class Helper {
constructor() {
this.state = {
movieAnswer: {},
profile_path:"",
poster_path:"",
myInit: { method: "GET", mode: "cors" }
}
}
fetchMovieFunction = async (randomMovie) => {
return new Promise((resolve) => {
fetch(`${MOVIE_KEY}${randomMovie}?api_key=${API_KEY}`, this.myInit)
.then(error => resolve({ error }))
.then(response => resolve({ poster_path: response.poster_path }));
});
}
and here is where I call fetchMovieFunction in my game function :
const helper = new Helper();
function Game() {
useEffect(() => {
setRandomMovie(Math.floor(Math.random() * (500 - 1) + 1));
const { poster_path, error } = await helper.fetchMovieFunction(randomMovie)
if (error)
return console.log({error});
setApiMovieResponse({poster_path})
}, [page]);
return ();
}
And I don't understand why on this line const { poster_path, error } = await helper.fetchMovieFunction(randomMovie) I get the Unexpected reserved word 'await'. Error like my function is not declare as an async function
two issues:
You should use await inside an async function
But the callback passed to useEffect should not be async. So, you can create an IIFE and make it async:
useEffect(() => {
(async () => {
setRandomMovie(Math.floor(Math.random() * (500 - 1) + 1));
const { poster_path, error } = await helper.fetchMovieFunction(randomMovie);
if (error) return console.log({ error });
setApiMovieResponse({ poster_path });
})();
}, [page]);
The callback itself shouldn't be async because that would make the callback return a promise and React expects a non-async function that is typically used to do some cleanup.
You will necessarily need to wrap the hook callback logic into an async function and invoke it. React hook callbacks cannot be asynchronous, but they can call asynchronous functions.
Also, since React state updates are asynchronously processed, you can't set the randomMovie value and expect to use it on the next line in the same callback scope. Split this out into a separate useEffect to set the randomMovie state when the page dependency updates, and use the randomMovie as the dependency for the main effect.
useEffect(() => {
setRandomMovie(Math.floor(Math.random() * (500 - 1) + 1));
}, [page]);
useEffect(() => {
const getMovies = async () => { // <-- declare async
const { poster_path, error } = await helper.fetchMovieFunction(randomMovie);
if (error) {
console.log({error});
} else {
setApiMovieResponse({poster_path});
}
};
if (randomMovie) {
getMovies(); // <-- invoke
}
}, [randomMovie]);
FYI
fetch already returns a Promise, so wrapping it in a Promise is superfluous. Since you are returning the Promise chain there is also nothing to await. You can return the fetch (and Promise chain).
fetchMovieFunction = (randomMovie) => {
return fetch(`${MOVIE_KEY}${randomMovie}?api_key=${API_KEY}`, this.myInit)
.then(response => return {
poster_path: response.poster_path
})
.catch(error => return { error });
}
In order to do so, It's not suggested to asynchronize a function as a callback function being used in useEffect() since Effect callbacks are synchronous to prevent race conditions.
the better approach would be inside the useEffect callback
check this out:
useEffect(() => {
async function [name] () {
.....
.....
}
[name]();
},string[])
fetchMovieFunction doesn't need to be async because you're already returning a promise. Instead just return the fetch (also a promise), and catch any errors from that call in that method:
fetchMovieFunction = (randomMovie) => {
try {
return fetch(`${MOVIE_KEY}${randomMovie}?api_key=${API_KEY}`, this.myInit)
} catch(err) {
console.log(err);
}
});
But your useEffect, or rather the function you should be calling within the useEffect, does need to be async (because you're using await, but now you can just pick up the data from the fetch when it resolves.
useEffect(() => {
async function getData() {
setRandomMovie(Math.floor(Math.random() * (500 - 1) + 1));
const { poster_path } = await helper.fetchMovieFunction(randomMovie);
setApiMovieResponse({ poster_path });
}
getData();
}, [page]);

Javascript yield inside object

So I am using redux-saga and I have this generator function:
export function * getData(api, action) {
const response = yield call(api.getData)
if (response.ok) {
let { data } = response;
yield put(MyActions.responseSuccess(data))
}
}
Everything works as expected, there is a request and if the response is ok I get change of state in my component.
Now I wanted to have a websocket connection, WAMP to be precise. I wrote the wamp function that looks like this:
const default_callbacks = {
onopen: () => console.log('Default callback - onopen.')
}
const openConnection = (data, callbacks = default_callbacks) => {
const wsuri = "wss://wssuri";
let connection = new wamp.Connection({
url: wsuri,
realm: "realm1"
});
connection.onopen = () => {
console.log('Connection opened')
callbacks.onopen();
}
// Some code managing the data
connection.open()
}
export default { openConnection }
Now what I wanted to do is to use this openConnection function inside my generator function, and provide callbacks that will change the state of my application (as there will be a subscription to a channel inside this connection). So I modified my generator functions to look like this:
export function * getData(api, action) {
const response = yield call(api.getData)
if (response.ok) {
let { data } = response;
yield put(MyActions.responseSuccess(data))
const callbacks = {
onopen: yield put(MyActions.wampConnectionOpened())
}
WAMP.openConnection(data, callbacks);
}
}
Now in my console I see the log saying that connection was opened, but there is no change in the state.
What am I doing wrong? Maybe my whole approach to this problem is wrong?

testing redux-saga inside if statement and using real values

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?

Using put inside anonymous function callback

I am implementing Pusher into my React+Redux Saga application, but I am having a few problems with some callbacks where I can not hit the put(...) methods. Using console.log(...) etc. in the methods does show, but I am not able to put to the state of my application.
I could be wrong on some of the implementation of async/generator functions, but I am pretty much stuck right now.
My code to illustrate what will not fire:
import { takeLatest } from 'redux-saga'
import { call, put } from 'redux-saga/effects'
// Pusher actions
export const pusherConnecting = () => {
return {
type: ActionTypes.PUSHER_CONNECTING
}
};
export const pusherConnectSucceeded = (client) => {
return {
type: ActionTypes.PUSHER_CONNECT_SUCCEEDED,
client: client
}
};
const pusherConnectFailed = (exception) => {
return {
type: ActionTypes.PUSHER_CONNECT_FAILED,
message: exception
}
};
// Pusher Saga
function * connectPusher(action) {
try {
const pusher = yield call(Api.connectPusher, action.directory, function(subscription) {
subscription.bind(PUSHER_BIND_RELOAD, function() {
location.reload(true);
});
subscription.bind(PUSHER_BIND_REQUEST_DATA, function(data) {
if (data) {
put(updateDirectory(data));
} else {
put(requestDirectory(action.directory.id));
}
});
});
pusher.connection.bind('connected', function() {
put(pusherConnectSucceeded(pusher));
});
yield put(pusherConnecting());
} catch (e) {
yield put(pusherConnectFailed(e));
}
}
export default function * pusherSaga() {
yield * takeLatest(ActionTypes.DIRECTORY_FETCH_SUCCEEDED, connectPusher);
}
// My Api.ConnectPusher
export function * connectPusher(directory, subscription) {
var pusher = new Pusher(PUSHER_KEY, {
encrypted: true
});
var channels = ["test1", "test2" ];
for (var i = 0; i < channels.length; i++) {
// Take each channel and callback with the subscription
yield subscription(pusher.subscribe(channels[i]));
}
return pusher;
}
Solution based on #Sebastien
yield put(yield onConnect(pusher));
function onConnect(pusher) {
return new Promise((resolve, reject) => {
pusher.connection.bind('connected', function() {
resolve(pusherConnectSucceeded(pusher));
});
});
}
Redux-saga does not permit to put without using keyword yield. The put creates a simple json object/effect that must be interpreted/executed, and it won't if you don't yield.
Also, even with yield put(...), if this is done in a callback, it won't be interpreted, because Redux-saga does not have the ability to run callbacks in its interpreter. They'll simply be run as normal callbacks and nothing will happen.
If subscription.bind is supposed to return a single result, you can instead wrap that call into a function that returns a promise, and then yield that promise.
If subscription.bind is supposed to return a stream of results, you might need instead of create a channel. I guess in the future someone will ship something that can easily permits to transform Observables to Redux-saga streams
Note that if you don't need to unsubscribe/resubscribe multiple times, it may be simpler to you to put this code outside the saga, and just do
subscription.bind(PUSHER_BIND_RELOAD, function() {
location.reload(true);
});
subscription.bind(PUSHER_BIND_REQUEST_DATA, function(data) {
if (data) {
reduxStore.dispatch(updateDirectory(data));
} else {
reduxStore.dispatch((requestDirectory(action.directory.id));
}
});

Categories

Resources