expo-location startLocationUpdatesAsync triggered too rarely - react-native

This is where i trigger Location.startLocationUpdatesAsync
useEffect(() => {
if (isPermissionReady && destination && !intervalHandle.current) {
// Register location fetch to task manager
Location.startLocationUpdatesAsync(TASK_NAME, {
accuracy: Location.Accuracy.Balanced,
// activityType: Location.ActivityType.AutomotiveNavigation,
// deferredUpdatesTimeout: INTERVAL_MS,
})
// Repeatedly read local storage to update currentPos state
intervalHandle.current = setInterval(updateCurrentPos, INTERVAL_MS)
}
}, [isPermissionReady, destination])
And this is my TaskManager (declared separately in index.tsx not inside a lifecycle or hook):
TaskManager.defineTask(TASK_NAME, ({ data, error }) => {
if (error) {
console.error('Task Manager Failed')
return
}
if (data) {
const { locations } = (data as any) ?? { locations: [] }
const { latitude, longitude } = locations[0]?.coords ?? {}
try {
AsyncStorage.setItem(STORAGE_KEY, JSON.stringify({ latitude, longitude }))
} catch {
console.error('SetItem Failed')
}
}
})
My taskmanager reads location data from device and save to LocalStorage(AsyncStorage) and react native app fetches this data with setInterval. However, data set by TaskManager is updated too rarely and it is not called regularly in constant time(imo) and never called if I stay at the same place with all deferredOOO options disabled.
Does startLocationUpdatesAsync trigger only after some distance change even with default values? or am I doing something wrong? (I want it to be called in regular basis or at least have a clear understanding of when it is called)
Plus, is it normal for taskmanager to not show any console.log?

Related

Async Storage / Secure Store help for React Native

I'm doing a test app to learn react native, and I'm trying to use secure store (a bit like async storage) to store my individual goals and save them. So far it's working, however when I refresh the app only the last goal I entered gets loaded.
Where am I going wrong here? In my console log the full array is shown with both the old and the new ones I add, then I refresh and I only have one left.
const [goals, setGoals] = useState([])
const addGoal = async (goal) => {
try{
const goalJson = JSON.stringify({text: goal, id:`${Math.random()}`, todos:[], date: Date.now(), percentage:0})
await SecureStore.setItemAsync("Goal", goalJson)
load()
}
catch (err) {alert(err)}
}
const load = async() => {
try {
const goalValue = await SecureStore.getItemAsync("Goal")
const parsed = JSON.parse(goalValue)
if(goals !== null) {
setGoals([...goals, parsed])
console.log(goals)
}
}catch (err) {alert(err)}
}
useEffect(()=> {
load()
},[])
SecureStore is like a key-value database, so currently you're always writing to the same key Goal and your addGoal function is erasing the previous value with goalJson content.
Instead, load once the goals from storage, then update the goals state when a new goal is added, and write them all to on storage each time goals value is updated.
This how effects works, by "reacting" to a change of value. This is just a little bit more complicated because of SecureStorage async functions.
Here is my (untested) improved code. I renamed the storage key from Goal to Goals.
const [goals, setGoals] = useState([])
const [loaded, setLoaded] = useState(false)
useEffect(()=> {
async function load() {
try {
const goalsValue = await SecureStore.getItemAsync("Goals")
const goalsParsed = JSON.parse(goalsValue)
if (goalsParsed !== null) {
setGoals(goalsParsed)
}
setLoaded(true)
} catch (err) { alert(err) }
}
load()
}, []) // load only when component mount
const addGoal = (text) => {
const goal = { text, id:`${Math.random()}`, todos:[],
date: Date.now(), percentage:0 }
setGoals([...goals, goal])
})
useEffect(() => {
async function saveGoals() {
try {
// save all goals to storage
const goalsJson = JSON.stringify(goals)
await SecureStore.setItemAsync("Goals", goalsJson)
}
catch (err) {alert(err)}
}
if (loaded) { // don't save before saved goals were loaded
saveGoals();
}
}, [goals, loaded]) // run the effect each time goals is changed

Vuex Getter not pulling data

I have a vuex store that I am pulling data from into a component. When the page loads the first time, everything behaves as expected. Yay.
When I refresh the page data is wiped from the store as expected and pulled again into the store as designed. I have verified this is the case monitoring the state using Vuex dev tools. My getter however doesn't pull the data this time into the component. I have tried so many things, read the documentation, etc and I am stuck.
Currently I am thinking it might be an issue with the argument?...
If I change the argument in the getter, 'this.id' to an actual value (leaving the dispatch alone - no changes there), the getter pulls the data from the store. So it seems the prop, this.id has the correct data as the dispatch statement works just fine. So why then wouldn't the getter work?
this.id source - The header includes a search for the person and passes the id of the person that is selected as the id prop. example data: playerId: 60
Thoughts? Appreciate any help.
This code works on initial page load, but not on page refresh.
props: ["id"],
methods: {
fetchStats() {
this.$store.dispatch("player/fetchPlayer", this.id).then(() => {
// alert(this.id);
this.player = this.$store.getters["player/getPlayerById"](this.id);
this.loading = false;
});
}
},
This code (only changing this.id to '6' on getter) works both on initial load and page refresh.
props: ["id"],
methods: {
fetchStats() {
this.$store.dispatch("player/fetchPlayer", this.id).then(() => {
// alert(this.id);
this.player = this.$store.getters["player/getPlayerById"](6);
this.loading = false;
});
}
},
Here is the getPlayerById getter:
getPlayerById: state => id => {
return state.players.find(plr => plr.playerId === id);
},
Here is the fetchPlayer action:
export const actions = {
fetchPlayer({ state, commit, getters }, id) {
// If the player being searched for is already in players array, no other data to get, exit
if (getters.getIndexByPlayerId(id) != -1) {
return;
}
// If the promise is set another request is already getting the data. return the first requests promise and exit
if (state.promise) {
return state.promise;
}
//We need to fetch data on current player
var promise = EventService.getPlayer(id)
.then(response => {
commit("ADD_PLAYER", response.data);
commit("CLEAR_PROMISE", null);
})
.catch(error => {
console.log("There was an error:", error.response);
commit("CLEAR_PROMISE", null);
});
//While data is being async gathered via Axios we set this so that subsequent requests will exit above before trying to fetch data multiple times
commit("SET_PROMISE", promise);
return promise;
}
};
and mutations:
export const mutations = {
ADD_PLAYER(state, player) {
state.players.push(player[0]);
},
SET_PROMISE(state, data) {
state.promise = data;
},
CLEAR_PROMISE(state, data) {
state.promise = data;
}
};

Simplest way to merge two useState values

I have a react native hook where trips must be updated every time a createdTrips is added to the state:
const [trips, setTrips] = useState([]);
function fetchCreatedTrips() {
try {
API.graphql(graphqlOperation(onCreateTrip)).subscribe({
next: (result) => {
console.log(result);
const updatedTrips = [...trips, result.value.data.onCreateTrip]
setTrips(updatedTrips)
}
})
} catch (err) { console.log(err) }
}
Now, when i first open the screen, it renders all the trips items of the list.
However with the current code, after i create a trip and go back to that screen, it doesn't currently return all the trips + the newly created one, but only one, that is the newly created one. How can i return all the items of the list? Sorry in advance, i'm a beginner.
While setting the new state on your effect you should include the previous values of the trips. You can use the spread operator to do the same.
const createdTrips = [result.value.data.onCreateTrip];
setTrips([...createdTrips, ...trips]);
or better merge the newly created trip in the updateTrips variable and then set it as the state value,
const udpatedTrips = [...trips,result.value.data.onCreateTrip];
setTrips(udpatedTrips);
The problem in your code is you are firing those two methods at the same time and there can be race conditions, so you see random updates on the screen.
Ideally, you need to bring in some consistency in the API calls and the state update. So first fetchTrips()->then->fetchCreatedTrips(). Try the below code wherein I don't update the state immediately in fetchTrips() but rather pass on the results to fetchCreatedTrips() which completes the API call and updates the state together.
const [trips, setTrips] = useState([]);
useEffect(() => {
fetchTrips();
}, [])
async function fetchTrips() {
try {
const tripData = await API.graphql(graphqlOperation(listTrips));
const trips = tripData.data.listTrips.items
fetchCreatedTrips(trips);
} catch (err) { console.log(err) }
}
function fetchCreatedTrips(fetchedTrips) {
try {
API.graphql(graphqlOperation(onCreateTrip)).subscribe({
next: (result) => {
console.log(result);
const updatedTrips = [...fetchedTrips,...updatedTrips];
setTrips(updatedTrips)
}
})
} catch (err) { console.log(err) }
}
PS: Please handle exceptions correctly.

React Native - How to access promise values from TaskManager in Expo?

I am testing background location tasks in react native with Expo's taskmanager to test when I enter a geofence. I've got the basics working but I am now trying to access the values within a promise that is returned from one of the tasks so i can display if you've entered or exited the geofence. Can anyone help me figure out how to properly access the values I want?
Below is the task portion relevant to the geofence. I return the value region which comes as a promise. I haven't been able to figure out how to send directly from task manager yet:
// 3 Define geofencing task
TaskManager.defineTask(TASK_CHECK_GEOFENCE, ({ data: { eventType, region }, error }) => {
if (error) {
// check `error.message` for more details.
return;
}
if (eventType === Location.GeofencingEventType.Enter) {
console.log("You've entered region:", region);
//this.props.setEnterRegion({ inRegion: region })
const final = region
return final
} else if (eventType === Location.GeofencingEventType.Exit) {
console.log("You've left region:", region);
const final = region
return final
}
});
Then within App.js I call the app in componentDidMount:
componentDidMount() {
this.getLocationsPermissions();
this.startBackgroundUpdate();
this.startGeofence();
}
//define and start geofence
startGeofence = async () => {
console.log('starting geofencing test ...')
let x = Location.startGeofencingAsync('TASK_CHECK_GEOFENCE',
[
{
identifier: 'court',
latitude: this.state.myLocation.latitude,
longitude: this.state.myLocation.longitude,
radius: 20,
notifyOnEnter: true,
notifyOnExit: true,
}
]
)
this.sendTask(x)
};
When I try to access the value I can see the promise and I am unsure where I am going wrong. I would perfer to send to the store directly from taskmanager but I am having trouble with that:
//send to store from task
sendTask = (x) => {
console.log('entered region...', x)
this.props.setEnterRegion(x)
entered region... Promise {
"_40": 0,
"_55": null,
"_65": 0,
"_72": null,
}

React redux async data sync then re-render view

I have a number of actions and reducers setup for different content types, e.g. pages, events and venues. These actions and reducers get data which has been saved to AsyncStorage, by another action called sync, and puts it into the store.
Sync performs an async call to Contentful and retrieves any new/updated/deleted entries, which I then save to AsyncStorage.
What is the best way to ensure the view correctly is re-rendered after the async call is finished?
Should syncReducer merge data into the store that would normally be pulled out by pagesReducer, venuesReducer etc or should there be some kind of event emitted after syncReducer is done?
Data is pulled in asynchronously for offline viewing and keeping things fast, so I really don't want to wait for the sync before rendering.
data/sync.js
import { AsyncStorage } from 'react-native';
import database from './database';
const cache = {
getByType: async (query) => {
return new Promise(async(resolve, reject) => {
// Get results from AsyncStorage
resolve(results);
});
},
sync: async () => {
return new Promise(async(resolve, reject) => {
database
.sync(options)
.then(async results => {
// Save results to AsyncStorage
resolve(results);
});
});
}
};
export default cache;
actions/sync.js
import actionTypes from '../constants/actionTypes';
import cache from '../data/cache';
export function sync() {
return dispatch => {
dispatch(syncRequestedAction());
return cache
.sync()
.then(() => {
dispatch(syncFulfilledAction());
})
.catch(error => {
console.log(error);
dispatch(syncRejectedAction());
});
};
}
function syncRequestedAction() {
return {
type: actionTypes.SyncRequested
};
}
function syncRejectedAction() {
return {
type: actionTypes.SyncRejected
};
}
function syncFulfilledAction(data) {
return {
type: actionTypes.SyncFulfilled,
data
};
}
actions/getPages.js
import actionTypes from '../constants/actionTypes';
import cache from '../data/cache';
export function getPages() {
return dispatch => {
dispatch(getPagesRequestedAction());
return cache
.getByType('page')
.then(results => {
dispatch(getPagesFulfilledAction(results));
})
.catch(error => {
console.log(error);
dispatch(getPagesRejectedAction());
});
};
}
function getPagesRequestedAction() {
return {
type: actionTypes.GetPagesRequested
};
}
function getPagesRejectedAction() {
return {
type: actionTypes.GetPagesRejected
};
}
function getPagesFulfilledAction(settings) {
return {
type: actionTypes.GetPagesFulfilled,
pages
};
}
reducers/pagesReducer.js
import { merge } from 'lodash';
import actionTypes from '../constants/actionTypes';
const pagesReducer = (state = {}, action) => {
switch (action.type) {
case actionTypes.GetPagesRequested: {
return merge({}, state, { loading: true });
}
case actionTypes.GetPagesRejected: {
return merge({}, state, { error: 'Error getting pages', loading: false });
}
case actionTypes.GetPagesFulfilled: {
const merged = merge({}, state, { error: false, loading: false });
return { ...merged, data: action.pages };
}
default:
return state;
}
};
export default pagesReducer;
In the end I was able to solve this by importing the other actions into my sync action, and dispatching depending on which data needs to be updated.
import { getEvents } from './getEvents';
import { getPages } from './getPages';
import { getVenues } from './getVenues';
export function sync() {
return dispatch => {
dispatch(syncRequestedAction());
return cache
.sync()
.then(results => {
dispatch(syncFulfilledAction());
if (results.includes('event')) {
dispatch(getEvents());
}
if (results.includes('page')) {
dispatch(getPages());
}
if (results.includes('venue')) {
dispatch(getSettings());
}
})
.catch(error => {
console.log(error);
dispatch(syncRejectedAction());
});
};
}
Your sync action should be a thunk function (redux middleware) that makes the call to Contentful, resolves the promise, and contains the data, or error. Then you can dispatch another action, or actions to reduce the data into the store.
On each component that you want to re-render (based on the data being updated in the store via the actions we just dispatched and reduced), if you have connect(mapStateToProps, mapDispatchToProps) and have included those parts of the store in MSTP, those props will be updated which will re-render the components.
You can even be more explicit about the resolution of data if necessary by creating another action where you can dispatch and reduce to some part of your store the current state of the fetch.
So when you make the call, you could dispatch 'FETCH_IN_PROGRESS', then either 'FETCH_ERROR' or 'FETCH_SUCCESS' and if that was mapStateToProps into your component, you could choose to evaluate it in shouldComponentUpdate() and based on where in the process it is, you could either return true or false based on if you wanted to rerender. You could also force render in componentWillReceiveProps. I'd start with just relying on props changing and adding this if necessary.
You should use Redux Persist for this kind of thing, it supports AsyncStorage and a range of other options.
https://github.com/rt2zz/redux-persist
Actions and Reducers should be just designed to update the Redux store. Any other action is known as a side effect, and should be managed in a Middleware or Store Enhancer.
I would strongly advise against using Redux-Thunk it is way too powerful for the few things that it is useful for and very easy to create unmaintainable anti-patten code as it blurs the boundaries between actions and middleware code.
If you think you need to use Redux-Thunk first look to see if their is already a middleware that does what you need and if not learn about Redux-Sagas.