Redux/saga: How to fire an action (put) inside a callback without channels (use sagas as normal generator functions) - react-native

I'm looking for a way to fire an action from inside a callback. I know this is not possible by default, but I'm looking for a way around. Channels are a bad solution in my case (for so far I see it).
The library I use is react-native-ble-plx. In that library, there is a function to start scanning: startDeviceScan(UUIDs, options, listener).
To keep this clean, I want to disconnect the start/stop scan from the listener (hence channels are out of the question).
Before I had this solution:
const onScanChannel = () => eventChannel(emit => {
BleService.startDeviceScan(..., ..., (peripheral) => {
emit(peripheral);
}
);
return () => {BleService.stopScan();}
});
The problem is that this connects the channel with the start and the stop of the scan. Which causes you to connect a lot of sagas because you need to start and stop the scanning from application logic (cancel the channel, setup again, start a new saga to listen to the new channel, etc)
What I had in mind is using sagas as normal generator functions:
const startScanSaga = function* () {
BleService.scan(..., ..., (peripheral) => {
const generator = deviceFoundHandler(peripheral);
generator.next();
generator.next();
});
};
const deviceFoundHandler = function* (peripheral) {
yield put(actions.deviceFound(peripheral));
};
That way, the saga for listening to the found-device-actions can just keep running. But, although the put is executed correctly, no take ever receives the action, which suggests that put does not work without the saga-logic behind the scenes.
Does someone know more about this? Or does someone has an alternative approach to realize this?

I managed to fix the problem by using middleware.run(saga, ...args).
I needed to export the sagaMiddleWare: export const sagaMiddleware = createSagaMiddleware();
import {sagaMiddleware} from '.../file-where-sagaMiddleWare-is-exported';
const startScanSaga = function* () {
BleService.scan((peripheral) => {
sagaMiddleware.run(deviceFoundHandler, peripheral);
});
};
const deviceFoundHandler = function* (peripheral) {
yield put(actions.deviceFound(peripheral));
};
Works like a charm =)

Related

Listen for changes in redis list

I want to write a function that constantly listens for changes in a redis list (usually when elements are pushed to the list) and pops its elements whenever the list is not empty. Then it will execute a function on the popped elements. How should I implement this?
You can use notify-keyspace-events for that
example with Node.js but the idea is similar for other languages.
const Redis = require('ioredis')
const redis = new Redis()
;(async function () {
redis.on('ready', () => {
console.log('ready');
redis.config('set', 'notify-keyspace-events', 'KEl')
// KEl => see https://redis.io/topics/notifications to understand the configuration
// l is meant we are interested in list event
redis.psubscribe(['__key*__:*'])
redis.on('pmessage', function(pattern, channel, message) {
console.log('got %s', message);
});
})
})()
Example output

How to populate the store and sequentially await return using Redux Observable?

I am attempting to use Redux Observable to call an action to fetch some data, wait for its return, then fetch some more data that relies on it.
I have an epic which populates a store from a fetch FetchTodos. This listens for the FETCH_TODOS action and then calls my todos API and populates {todos: [] } =
I also have a comments section in my store todoComments. However, I would like to only populate todoComments once FETCH_TODOS has returned and populated the store.
In imperative code, this might look like:
let todos = await api.get('/todos');
await dispatch("FETCH_TODO_COMPLETE", todos)
let firstId = getState().todos[0].id
let comments = await api.get(`/todos/${firstId}/comments')
await dispatch("FETCH_COMMENTS_COMPLETE", { todo_id: firstId, comments})
The closest I saw to this was this issue in the Redux Observable Repo, but I could not understand how to do this efficiently. This is a pretty common scenario for me.
I would like to reuse as much code as possible. In this example, I may dispatch FETCH_TODOS from multiple components.
How would i accomplish this with Redux-Observable?
Based on our conversation in the comments:
In redux-observable, you can sequence things in numerous ways. You could do it all in one epic using normal RxJS, or you could split them into multiple ones. If you split them, the subsequent epic would listen for the signal that the previous one has completed its task. Something like this:
// this assumes you make your `api.get` helper return an Observable
// instead of a Promise which is highly advisable.
// If it doesn't, you could do:
// Observable.from(api.get('/url'))
// but Promises are not truly cancellable which can cause max
// concurrent connections issues
const fetchTodosEpic = action$ =>
action$.ofType('FETCH_TODOS')
.switchMap(() =>
api.get('/todos')
.map(todos => ({
type: 'FETCH_TODOS_COMPLETE',
todos
}))
);
const fetchComments = action$ =>
action$.ofType('FETCH_TODOS_COMPLETE')
.switchMap(({ todos }) =>
api.get(`/todos/${todos[0].id}/comments`)
.map(comments => ({
type: 'FETCH_COMMENTS_COMPLETE',
comments
}))
);

redux-observable return multiple action types

I am using redux-observable and this is what I am trying to do.
When an actiontype of 'APPLY_SHOPPING_LIST' comes in dispatch 'APPLYING_SHOPPING_LIST' and after 5 seconds dispatch 'APPLIED_SHOPPING_LIST'. This is the code that I have come up with so far
const applyingShoppingListSource = action$.ofType('APPLY_SHOPPING_LISTS').mapTo({ type: 'APPLYING_SHOPPING_LISTS' });
const applyingShoppingListSourceOther = action$.ofType('APPLY_SHOPPING_LISTS').mapTo({ type: 'APPLIED_SHOPPING_LISTS' }).delay(5000);
const concatList = applyingShoppingListSource.concat(applyingShoppingListSourceOther);
return concatList;
Now the problem is that only 'APPLYING_SHOPPING_LISTS' gets fired, the 'APPLIED_SHOPPING_LISTS' does not get fired to the reducer at all. Am I missing something here?
Just to add to this, when I used flatMap it worked, given below is the code
return action$.ofType('APPLY_SHOPPING_LISTS')
.flatMap(() => Observable.concat(Observable.of({ type: 'APPLYING_SHOPPING_LISTS' }), Observable.of({ type: 'APPLYING_SHOPPING_LISTS' });
I am confused how this works and the other does not?
There's a couple issues. Since Observables are lazy, your second action$.ofType('APPLY_SHOPPING_LISTS') for applyingShoppingListSourceOther is being concat'd after the first applyingShoppingListSource, so it won't be listening for APPLY_SHOPPING_LISTS until after the first one is has completed, but it will never realistically complete because you're taking all actions that match, forever.
Said another way, your code does this:
Start listening for APPLY_SHOPPING_LISTS and when received map it to APPLYING_SHOPPING_LISTS
When that first Observable completes (it never does) start listening for APPLY_SHOPPING_LISTS again, this time when received map it to APPLIED_SHOPPING_LISTS but wait 5000 ms before emitting it.
You could solve the particular issue of the first not ever completing by using .take(1) or .first() (same thing), but you usually need to write your epics to not ever stop listening so they respond to actions at any time.
I think what you want is this:
const exampleEpic = action$ =>
action$.ofType('APPLY_SHOPPING_LISTS')
.mergeMap(() =>
Observable.of({ type: 'APPLYING_SHOPPING_LISTS' })
.concat(
Observable.of({ type: 'APPLIED_SHOPPING_LISTS' })
.delay(5000)
)
);
I used mergeMap but you may want to use switchMap to cancel any previously pending APPLIED_SHOPPING_LISTS that haven't emitted yet. Your call.

How to wait on / yield to multiple actions in single redux observable epic?

I have a redux Saga that runs three different actions every time 'WATCHLIST_FETCH_REQUEST' is dispatched:
function* watchFetchWatchlist() {
yield takeLatest('WATCHLIST_FETCH_REQUEST', fetchWatchlist);
}
function* fetchWatchlist() {
const activity = 'ACTIVITY_FETCH_WATCHLIST';
yield put(
addNetworkActivity(activity) // Action 1: enables a global loading indicator before request is made
);
const { response, error } = yield call(
api.fetchWatchlist // make an API request
);
yield put(
removeNetworkActivity(activity) // Action 2: removes the above global loading indicator after request completes
);
if (response) {
yield put(
updateUserWatchlist(response) // Action 3a: updates Redux store with data if response was successful
);
} else {
yield put(
watchlistFetchFailed(error) // Action 3b: updates Redux store with error if response failed
);
}
}
The flow of this saga is synchronous in nature. Action 1 must run first to set the global loading state for the app. Action 2 must run after Action 1 and after the API response comes back to remove the global loading state when the network activity is finished.
I'm pretty new to redux-observable but I have been digging around a lot trying to figure out how to convert this saga into an epic. The two goals here:
Perform actions sequentially, one after the other, as opposed to running in parallel
Perform these actions / flow in a single epic (kicks off when type: 'WATCHLIST_FETCH_REQUEST' is fired)
How do you achieve this with redux-observable? Thanks!
I found the answer to my question by piecing together parts of the conversation here: https://github.com/redux-observable/redux-observable/issues/62
I ended up with something along the lines of:
import { concat as concat$ } from 'rxjs/observable/concat';
import { from as from$ } from 'rxjs/observable/from';
import { of as of$ } from 'rxjs/observable/of';
export const fetchWatchlistEpic = (action$) => {
const activity = 'ACTIVITY_FETCH_WATCHLIST';
return action$.ofType('WATCHLIST_FETCH_REQUEST')
.switchMap(() =>
concat$(
of$(addNetworkActivity(activity)),
from$(api.fetchWatchlist())
.map((data) => Immutable.fromJS(data.response))
.switchMap((watchlist) =>
of$(
updateUserWatchlist(watchlist),
removeNetworkActivity(activity),
)
)
)
);
};
concat and of seem to be the go-to operators when trying to run multiple actions in sequence.

backbone view in router lost after creation

When I try to associate my router's public variable this.currentView to a newly created view, the view gets lost, the public variable is null instead of containing the newly created view.
var self=this;
var watchListsCollection = new WatchlistCollection;
watchListsCollection.url = "watchlists";
user.fetch().done(function() {
watchListsCollection.fetch().done(function () {
loggedUser.fetch().done(function () {
self.currentView = new UserView(user, watchListsCollection,loggedUser);
});
});
});
alert(this.currentView); //null
The fetch() calls you do are firing asynchronous AJAX requests, meaning the code in your done handlers are not going to be executed untill the server calls return. Once you've executed user.fetch() the browser will fire off a request and then continue running your program and alert this.currentView without waiting for the requests to finish.
The sequence of events is basically going to be
call user.fetch()
alert this.currentView
call watchListsCollection.fetch()
call loggedUser.fetch()
set the value of self.currentView
You will not be able to see the value of your currentView before the last server request have completed.
If you change your code to
var self=this;
var watchListsCollection = new WatchlistCollection;
watchListsCollection.url = "watchlists";
user.fetch().done(function() {
watchListsCollection.fetch().done(function () {
loggedUser.fetch().done(function () {
self.currentView = new UserView(user, watchListsCollection,loggedUser);
alertCurrentView();
});
});
});
function alertCurrentView() {
alert(this.currentView);
}
You should see the correct value displayed. Now, depending on what you intend to use your this.currentView for that might or might not let you fix whatever issue you have, but there's no way you're not going to have to wait for all the requests to complete before it's available. If you need to do something with it straight away you should create your UserView immediately and move the fetch() calls into that view's initialize().
fetch() is asynchronous, but you check your variable right after you've started your task. Probably these tasks, as they supposed to be just reads, should be run in parallel. And forget making a copy of this, try _.bind instead according to the Airbnb styleguide: https://github.com/airbnb/javascript
var tasks = [];
tasks.push(user.fetch());
tasks.push(watchListsCollection.fetch());
tasks.push(loggedUser.fetch());
Promise.all(tasks).then(_.bind(function() {
this.currentView = new UserView(user, watchListsCollection, loggedUser);
}, this));
or using ES6 generators:
function* () {
var tasks = [];
tasks.push(user.fetch());
tasks.push(watchListsCollection.fetch());
tasks.push(loggedUser.fetch());
yield Promise.all(tasks);
this.currentView = new UserView(user, watchListsCollection, loggedUser);
}