Using put inside anonymous function callback - javascript

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));
}
});

Related

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

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;
}

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?

testing promises causing undefined values

I am getting this error when I am testing my code:
1) Sourcerer Testing: getStatusCode :
Error: Expected undefined to equal 200
I'm not sure why I am getting undefined in my tests but when I run the code I get 200. It might be from not handling promises properly
Test code:
import expect from 'expect';
import rp from 'request-promise';
import Sourcerer from './sourcerer';
describe("Sourcerer Testing: ", () => {
let sourcerer = new Sourcerer(null);
const testCases = {
"https://www.google.com": 200,
// "www.google.com":
};
describe("getStatusCode", () => {
it("", () => {
for (let testCase in testCases) {
sourcerer.setSourcererUrl(testCase);
expect(sourcerer.url).toEqual(testCase);
expect(sourcerer.getStatusCode()).toEqual(testCases[testCase]);
}
});
});
});
code:
import rp from 'request-promise';
export default class Sourcerer {
constructor(url) {
this.options = {
method: 'GET',
url,
resolveWithFullResponse: true
};
this.payload = {};
}
setSourcererUrl(url) {
this.url = url;
}
getSourcererUrl() {
return this.url;
}
analyzeSourcePage() {
rp(this.options).then((res) => {
console.log(res);
}).catch((err) => {
console.log("ERROR");
throw(err);
});
}
getStatusCode() {
rp(this.options).then((res) => {
console.log(res.statusCode);
return res.statusCode;
}).catch((err) => {
console.log("STATUS CODE ERROR");
return 0;
});
}
}
getStatusCode doesn't return anything. And it should return a promise:
getStatusCode() {
return rp(this.options)...
}
The spec will fail in this case, because it expects promise object to equal 200.
It is even more complicated because the spec is async and there are several promises that should be waited before the spec will be completed. It should be something like
it("", () => {
let promises = [];
for (let testCase in testCases) {
sourcerer.setSourcererUrl(testCase);
let statusCodePromise = sourcerer.getStatusCode()
.then((statusCode) => {
expect(sourcerer.url).toEqual(testCase);
expect(statusCode).toEqual(testCases[testCase]);
})
.catch((err) => {
throw err;
});
promises.push(statusCodePromise);
}
return promises;
});
co offers an awesome alternative to Promise.all for flow control:
it("", co.wrap(function* () {
for (let testCase in testCases) {
sourcerer.setSourcererUrl(testCase);
expect(sourcerer.url).toEqual(testCase);
let statusCode = yield sourcerer.getStatusCode();
expect(statusCode).toEqual(testCases[testCase]);
}
});
Disclaimer: I wouldn't run a for-loop in a single it(), since I want to know which iteration failed. granted that there are ways to achieve that, but that is another story. Also, this very much depends on you test runner, but here is some rules of thumb I find useful.
But for what you have asked, the test should not evaluate until the promise is resolved. sometimes (e.g. in mocha), that means returning the promise from the it() internal function. sometimes, it means getting a done function and calling it when you are ready for the test to evaluate. If you provide more info on your test framework, I may be able to help (others certainly would be)

How to avoid duplicate API requests with Redux-Saga?

So far I like Redux better than other Flux implementations, and I'm using it to re-write our front end application.
The main struggling points that I'm facing:
Maintaining the status of API calls to avoid sending duplicate requests.
Maintaining relationships between records.
The first issue could be solved by keeping a status field in the sub-state of each type of data. E.g.:
function postsReducer(state, action) {
switch(action.type) {
case "FETCH_POSTS":
return {
...state,
status: "loading",
};
case "LOADED_POSTS":
return {
status: "complete",
posts: action.posts,
};
}
}
function commentsReducer(state, action) {
const { type, postId } = action;
switch(type) {
case "FETCH_COMMENTS_OF_POST":
return {
...state,
status: { ...state.status, [postId]: "loading" },
};
case "LOADED_COMMENTS_OF_POST":
return {
status: { ...state.status, [postId]: "complete" },
posts: { ...state.posts, [postId]: action.posts },
};
}
}
Now I can make a Saga for Posts and another one for Comments. Each of the Sagas knows how to get the status of requests. But that would lead to a lot of duplicate code soon (e.g. Posts, Comments, Likes, Reactions, Authors, etc).
I'm wondering if there is a good way to avoid all that duplicate code.
The 2nd issue comes to existence when I need to get a comment by ID from the redux store. Are there best practices for handling relationships between data?
Thanks!
redux-saga now has takeLeading(pattern, saga, ...args)
Version 1.0+ of redux-saga has takeLeading that spawns a saga on each action dispatched to the Store that matches pattern. After spawning a task once, it blocks until the spawned saga completes and then starts to listen for a pattern again.
Previously I implemented this solution from the owner of Redux Saga and it worked really well - I was getting errors from API calls sometimes being fired twice:
You could create a higher order saga for this, which would look something like this:
function* takeOneAndBlock(pattern, worker, ...args) {
const task = yield fork(function* () {
while (true) {
const action = yield take(pattern)
yield call(worker, ...args, action)
}
})
return task
}
and use it like this:
function* fetchRequest() {
try {
yield put({type: 'FETCH_START'});
const res = yield call(api.fetch);
yield put({type: 'FETCH_SUCCESS'});
} catch (err) {
yield put({type: 'FETCH_FAILURE'});
}
}
yield takeOneAndBlock('FETCH_REQUEST', fetchRequest)
In my opinion this way is far way more elegant and also its behaviour can be easily customized depending on your needs.
I had the exact same issue in my project.
I have tried redux-saga, it seems that it's really a sensible tool to control the data flow with redux on side effects. However, it's a little complex to deal with the real world problem such as duplicate requests and handling relationships between data.
So I created a small library 'redux-dataloader' to solve this problem.
Action Creators
import { load } from 'redux-dataloader'
function fetchPostsRequest() {
// Wrap the original action with load(), it returns a Promise of this action.
return load({
type: 'FETCH_POSTS'
});
}
function fetchPostsSuccess(posts) {
return {
type: 'LOADED_POSTS',
posts: posts
};
}
function fetchCommentsRequest(postId) {
return load({
type: 'FETCH_COMMENTS',
postId: postId
});
}
function fetchCommentsSuccess(postId, comments) {
return {
type: 'LOADED_COMMENTS_OF_POST',
postId: postId,
comments: comments
}
}
Create side loaders for request actions
Then create data loaders for 'FETCH_POSTS' and 'FETCH_COMMENTS':
import { createLoader, fixedWait } from 'redux-dataloader';
const postsLoader = createLoader('FETCH_POSTS', {
success: (ctx, data) => {
// You can get dispatch(), getState() and request action from ctx basically.
const { postId } = ctx.action;
return fetchPostsSuccess(data);
},
error: (ctx, errData) => {
// return an error action
},
shouldFetch: (ctx) => {
// (optional) this method prevent fetch()
},
fetch: async (ctx) => {
// Start fetching posts, use async/await or return a Promise
// ...
}
});
const commentsLoader = createLoader('FETCH_COMMENTS', {
success: (ctx, data) => {
const { postId } = ctx.action;
return fetchCommentsSuccess(postId, data);
},
error: (ctx, errData) => {
// return an error action
},
shouldFetch: (ctx) => {
const { postId } = ctx.action;
return !!ctx.getState().comments.comments[postId];
},
fetch: async (ctx) => {
const { postId } = ctx.action;
// Start fetching comments by postId, use async/await or return a Promise
// ...
},
}, {
// You can also customize ttl, and retry strategies
ttl: 10000, // Don't fetch data with same request action within 10s
retryTimes: 3, // Try 3 times in total when error occurs
retryWait: fixedWait(1000), // sleeps 1s before retrying
});
export default [
postsLoader,
commentsLoader
];
Apply redux-dataloader to redux store
import { createDataLoaderMiddleware } from 'redux-dataloader';
import loaders from './dataloaders';
import rootReducer from './reducers/index';
import { createStore, applyMiddleware } from 'redux';
function configureStore() {
const dataLoaderMiddleware = createDataLoaderMiddleware(loaders, {
// (optional) add some helpers to ctx that can be used in loader
});
return createStore(
rootReducer,
applyMiddleware(dataLoaderMiddleware)
);
}
Handle data chain
OK, then just use dispatch(requestAction) to handle relationships between data.
class PostContainer extends React.Component {
componentDidMount() {
const dispatch = this.props.dispatch;
const getState = this.props.getState;
dispatch(fetchPostsRequest()).then(() => {
// Always get data from store!
const postPromises = getState().posts.posts.map(post => {
return dispatch(fetchCommentsRequest(post.id));
});
return Promise.all(postPromises);
}).then() => {
// ...
});
}
render() {
// ...
}
}
export default connect(
state => ()
)(PostContainer);
NOTICE The promised of request action with be cached within ttl, and prevent duplicated requests.
BTW, if you are using async/await, you can handle data fetching with redux-dataloader like this:
async function fetchData(props, store) {
try {
const { dispatch, getState } = store;
await dispatch(fetchUserRequest(props.userId));
const userId = getState().users.user.id;
await dispatch(fetchPostsRequest(userId));
const posts = getState().posts.userPosts[userId];
const commentRequests = posts.map(post => fetchCommentsRequest(post.id))
await Promise.all(commentRequests);
} catch (err) {
// error handler
}
}
First, you can create a generic action creator for fetching post.
function fetchPost(id) {
return {
type: 'FETCH_POST_REQUEST',
payload: id,
};
}
function fetchPostSuccess(post, likes, comments) {
return {
type: 'FETCH_POST_SUCCESS',
payload: {
post,
likes,
comments,
},
};
}
When you call this fetch post action, it'll trigger onFetchPost saga.
function* watchFetchPost() {
yield* takeLatest('FETCH_POST_REQUEST', onFetchPost);
}
function* onFetchPost(action) {
const id = action.payload;
try {
// This will do the trick for you.
const [ post, likes, comments ] = yield [
call(Api.getPost, id),
call(Api.getLikesOfPost, id),
call(Api.getCommentsOfPost, id),
];
// Instead of dispatching three different actions, heres just one!
yield put(fetchPostSuccess(post, likes, comments));
} catch(error) {
yield put(fetchPostFailure(error))
}
}

Categories

Resources