react native offline - dispatching changeQueueSemaphore action by action dispatched from offline queue - react-native

I’m using react-native-offline library in my app for offline queueing of requests to server and the action changeQueueSemaphore had caused me few difficulties.
I’ll try explaining by an example.
Let’s say my app posts offline issues in Stack Overflow by three basic actions:
const createIssue = (issue) => action("CREATE_ISSUE",{issue},{retry: true});
const addComment = (comment) => action("ADD_COMMENT",{comment},{retry:true});
const closeIssue = (timeOfClosing) => action("CLOSE_ISSUE", {timeOfCLosing}, {retry: true});
When my app works in offline mode the actions are entered by order to the queue and then being dispatched one after another when network connection is regained.
In order to dispatch the last two actions I first need to dispatch the create issue action and get a generated id from my server. Thus, when listening to createIssue action using redux saga,
in the handler I'm dispatching changeQueueSemaphore('RED') -> sends post request of creating an issue -> retrieving the id from the response and saving it in state, then releases the queue again:
function* createIssueHandler(action: ActionType<typeof createIssue>) {
const {issue} = action.payload;
const {changeQueueSemaphore} = offlineActionCreators;
// stops the queue before generating id
yield put(changeQueueSemaphore('RED');
try {
const id = // Api Call to create Issue
// saves the id in state
yield put(createIssueSuccess(id));
// releases queue again
yield put(changeQueueSemaphore('GREEN');
} catch(err) {
// error handeling
}
function* addCommentHandler(action: ActionType<typeof addComment>) {
const issueId = yield select(getIssueIdFromState);
enter code here
// api call to `SERVER_URL/addComment/${issueId}`;
}
function* closeIssue(action: ActionType<typeof closeIssue>) {
// same as addCommentHandler
}
After looking at the action logs I saw that addComment and closeIssue actions are being dispatched before changeQueueSemaphore causes the queue to stop.
Any help will be pleased <3.

Related

Abort an Updates.fetchUpdateAsync() after a certain time [Expo/React native]

Expo React Native SDK Version: 46
Platforms: Android/iOS
Package concerned : Expo.Updates
Hello everyone, I want to programmatically check for new updates, without using the fallbackToCacheTimeout in app.json that will trigger the check of the new updates when the application is launched because like that I can't put a custom loading page.
So by doing this all by code as follow :
try{
const update = await Updates.checkForUpdateAsync();
if(update.isAvailable){
await Updates.fetchUpdateAsync();
await Updates.reloadAsync();
}else{}
}catch(err){}
But I want to be able to abort all those calls after a certain time (thus, the user that have a bad connection can use the app without waiting a very long time).
I check the documentation and I cannot found any method that allow this.
I dont't think it's possible to cancel a Promise for now in Javascript, or maybe any connection ?
Or does the "fallbackToCacheTimeout" value in the app.json will automatically apply to the fetch updates call of the Expo API?
Do someone have any idea how to do it ? :(
First of all I am assuming you have set updates.checkautomatically field to ON_ERROR_RECOVERY in app.json or app.config.js file. If not, please check the documentation. The reason why you need this is to avoid automatic updates which can also block your app on splash screen.
Updated Solution
Because of the limitation in javascript we can't cancel any external Promise (not created by us or when its reject method is not exposed to us). Also the function fetchUpdateAsync exposed to us is not a promise but rather contains fetch promise and returns its result.
So, here we have two options:
Cancel reloading the app to update after a timeout.
But note that updates will be fetched in background and stored on
the device. Next time whenever user restarts the app, update will
be installed. I think this is just fine as this approach doesn't
block anything for user and also there is a default timeout for http
request clients like fetch and axios so, request will error out in
case of poor/no internet connection.
Here is the code:
try {
const update = await Updates.checkForUpdateAsync();
if (update.isAvailable) {
const updateFetchPromise = Updates.fetchUpdateAsync();
const timeoutInMillis = 10000; // 10 seconds
const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject("timedout"), timeoutInMillis))
// This will return only one Promise
Promise.race([updateFetchPromise, timeoutPromise])
.then(() => Updates.reloadAsync())
.catch((error) => {
if (error === 'timedout') {
// Here you can show some toast as well
console.log("Updates were not cancelled but reload is stopped.")
} else if (error === 'someKnownError') {
// Handle error
} else {
// Log error and/or show a toast message
}
})
} else {
// Perform some action when update is not available
}
} catch (err) {
// Handle error
}
Change the expo-updates package just for your app using a patch
Here you can return a cancel method with Updates.fetchUpdateAsync() and use it with setTimeout to cancel the fetch request. I won't be providing any code for this part but if you are curious I can definitely provide some help.
Please refer this section to understand use of fallbackToCacheTimeout in eas updates.
Old solution:
Now, for aborting or bypassing the promise i.e. Updates.fetchUpdateAsync in your case. You can basically throw an Error in setTimeout after whatever time duration you want, so that, catch block will be executed, bypassing the promises.
Here is the old code :
try{
const update = await Updates.checkForUpdateAsync();
if(update.isAvailable){
// Throw error after 10 seconds.
const timeout = setTimeout(() => { throw Error("Unable to fetch updates. Skipping..") }, 10000)
await Updates.fetchUpdateAsync();
// Just cancel the above timeout so, no error is thrown.
clearTimeout(timeout)
await Updates.reloadAsync();
}else{}
}catch(err){}

Firebase authentication and redux-saga synchronizing state

I have a design question on how to manage firebase auth & redux saga states with react-native-firebase.
Example use-case
Let's start from the scenario that I have an app that uses the idToken for a variety of use cases, some in the views using information from the claims, and some in redux actions to make api calls.
Using redux-saga, I would expect to implement these two cases like so:
// in selectors.js
const getIdToken = (state) => state.idTokenResult?.token
const getUserRole = (state) => state.idTokenResult?.claims.role
// in view.js
const role = useSelector(Selectors.getUserRole)
// in actions.js
const idToken = yield select(Selectors.getIdToken)
With this in mind I want to make sure the idTokenResult is available & up to date in my state. I can do this we a few actions and reducers, by calling a login method & then relying on the dispatched event onIdTokenChanged to update my state on login & tokenRefreshes. Something like the following:
// in actions.js
function* onLogin(email, password){
yield call([auth(), 'signInWithEmailAndPassword'], email, password)
}
// This action would be called by an eventChannel which emits on each onIdTokenChanged
function* onIdTokenChanged(user){
yield put({ type: "UPDATE_USER", user: user, })
if (user){
const idTokenResut = yield call([auth().currentUser, 'getIdTokenResult'])
yield put({ type: "UPDATE_ID_TOKEN_RESULT", idTokenResult: idTokenResult, })
}
}
// in reducers.js
const reducer = (state = {}, action) => {
switch (action.type) {
case 'UPDATE_USER':
return { ...state, user: action.user };
case 'UPDATE_ID_TOKEN_RESULT':
return { ...state, idTokenResult: action.idTokenResult }
}
}
Problem
Here is when we run into a problem. I recently learned that the onIdTokenChanged is dispatched lazily, only when the getIdTokenResult() method is invoked link. This means that with the code above we cannot expect our state to be accurate, because when we call yield select(Selectors.getIdToken) it doesn't check getIdTokenResult() and therefore the onIdTokenChanged event is never dispatched.
Potential solutions
How do we overcome this problem?
Set up a timer which periodically calls getIdTokenResult() before the token expires, to trigger the event.
Should work, but defeats the purpose of having an onIdTokenChanged event. Also this means it will refresh the token hourly, even if it isn't needed or being accessed
Somehow call getIdTokenResult() in the selector?
It's an async method so it seems like an anti-pattern here and I'm not even sure it's possible
Use the library directly to fetch user states with auth().currentUser, and forget redux-saga
We lose the nice rerender functionalities that redux's useSelector provides. By accessing the state directly we'll need to figure out another way to trigger rerenders on auth changes, which defeats the purpose of using redux-saga
Something I didn't consider/implemented incorrectly?
Your suggestions are welcome and thanks in advance for you help! :)

API response is not accessible in ComponentDidMount but in render I can use

Hi I am working on React Native app. I am using Redux and Saga. I call the API in componentDidMount.
async componentDidMount() {
let data = this.props.navigation.getParam("returnProductData");
if (data) {
console.log("Return Here");
this.props.getProductReturnAction(data)
this.setState({
returnQty:parseInt(this.props.product.item_ordered)-parseInt(this.props.product.already_return_qty)
});
console.log(this.state.returnQty,"Return quty"); //coming undefined
console.log(this.props.product, "product"); // undefined
console.log(this.props.product.item_ordered); //undefined
}
}
I have to set the state in componentDidMount for returnQty. But, state is not accessible here. It's working fine in render method. I can use all the product object. But, it is coming empty in componentDidMount. I tried using async and await but it's not working.
// Dispatch Methods
const mapDispatchToProps = dispatch => {
return bindActionCreators(
{ getProductReturnAction, submitProductReturnAction },
dispatch
);
};
// Props
const mapStateToProps = state => {
return {
product: state.myOrdersReducer.returnProduct
};
};
I can't be able to find out the bug please help to find out the best solution.
When you are making API calls through redux/saga, you can not use async await, as the frameworks will just dispatch an action and return back, the listeners which are registered for the action will be triggered and then after they complete their work they will dispatch a new action and respect reducer will handle the response.
Explained above is general scenario.
In your scenario,
You are dispatching the action returned by getProductReturnAction which will give say GET_PRODUCTS action.
A saga would be registered for GET_PRODUCTS, say getProducts, this get invoked.
This will perform the API call once the response is received it will dispatch GET_PRODUCTS_SUCCESS along with the products data.
Corresponding reducer which handles GET_PRODUCTS_SUCCESS will get called and that updates returnProduct and as you are registered for that in your component the render method gets called (as the props are changed) and hence product data is available in your render method.
This is working perfectly correct. I don't see anything wrong here.
As the data is available in props use the same u do not need to do a setState again on that.

Service Worker - Wait for clients.openWindow to complete before postMessage

I am using service worker to handle background notifications. When I receive a message, I'm creating a new Notification using self.registration.showNotification(title, { icon, body }). I'm watching for the click event on the notification using self.addEventListener('notificationclick', ()=>{}). On click I'm checking to see if any WindowClient is open, if it is, I'm getting one of those window clients and calling postMessage on it to send the data from the notification to the app to allow the app to process the notification. Incase there is no open window I'm calling openWindow and once that completes I'm sending the data to that window using postMessage.
event.waitUntil(
clients.matchAll({ type: 'window' }).then((windows) => {
if (windows.length > 0) {
const window = windows[0];
window.postMessage(_data);
window.focus();
return;
}
return clients.openWindow(this.origin).then((window) => {
window.postMessage(_data);
return;
});
})
);
The issue I am facing is that the postMessage call inside the openWindow is never delivered. I'm guessing this is because the postMessage call on the WindowClient happens before the page has finished loading, so the eventListener is not registered to listen for that message yet? Is that right?
How do I open a new window from the service worker and postMessage to that new window.
I stumble this issue as well, using timeout is anti pattern and also might cause delay larger then the 10 seconds limit of chrome that could fail.
what I did was checking if I need to open a new client window.
If I didn't find any match in the clients array - which this is the bottle neck, you need to wait until the page is loaded, and this can take time and postMessage will just not work.
For that case I created in the service worker a simple global object that is being populated in that specific case for example:
const messages = {};
....
// we need to open new window
messages[randomId] = pushData.message; // save the message from the push notification
await clients.openWindow(urlToOpen + '#push=' + randomId);
....
In the page that is loaded, in my case React app, I wait that my component is mounted, then I run a function that check if the URL includes a '#push=XXX' hash, extracting the random ID, then messaging back to the service worker to send us the message.
...
if (self.location.hash.contains('#push=')) {
if ('serviceWorker' in navigator && 'Notification' in window && Notification.permission === 'granted') {
const randomId = self.locaiton.hash.split('=')[1];
const swInstance = await navigator.serviceWorker.ready;
if (swInstance) {
swInstance.active.postMessage({type: 'getPushMessage', id: randomId});
}
// TODO: change URL to be without the `#push=` hash ..
}
Then finally in the service worker we add a message event listener:
self.addEventListener('message', function handler(event) {
if (event.data.type === 'getPushMessage') {
if (event.data.id && messages[event.data.id]) {
// FINALLY post message will work since page is loaded
event.source.postMessage({
type: 'clipboard',
msg: messages[event.data.id],
});
delete messages[event.data.id];
}
}
});
messages our "global" is not persistent which is good, since we just need this when the service worker is "awaken" when a push notification arrives.
The presented code is pseudo code, to point is to explain the idea, which worked for me.
clients.openWindow(event.data.url).then(function(windowClient) {
// do something with the windowClient.
});
I encountered the same problem. My error was that I registered event handler on the window. But it should be registered on service worker like this:
// next line doesn't work
window.addEventListener("message", event => { /* handler */ });
// this one works
navigator.serviceWorker.addEventListener('message', event => { /* handler */ });
See examples at these pages:
https://developer.mozilla.org/en-US/docs/Web/API/Clients
https://developer.mozilla.org/en-US/docs/Web/API/Client/postMessage
UPD: to clarify, this code goes into the freshly opened window. Checked in Chromium v.66.

Dispatch an action in background app refresh with react native

I'm using react-native-background-fetch to receive app refresh events and have been struggling to dispatch an action (that fetches data) when it's triggered. I'm able to do this outside of redux but not when I dispatch the action.
BackgroundFetch.configure({
stopOnTerminate: false
}, async () => {
await store.dispatch(getItemsAction);
BackgroundFetch.finish();
});
Action:
export function getItemsAction() {
// <-- Reaches here
return async (dispatch, getState) => {
// <-- But not here
const items = await findAll();
dispatch(itemsRetrieved(items));
}
}
If not a solution, I'd like to get some insight into what's happening here.
First of all you need to call action creator
await store.dispatch(getItemsAction());
Then you'll need a middleware to handle functions as actions. I assume you are aware of redux-thunk.
If it's a headless task running in the background, It does not have access to the redux store from what I experienced.
You will want to use something like AsyncStorage (https://github.com/react-native-community/async-storage) when running the task as headless js, which is what happens when the app is running background events.