How to initialize MobX store with async data (AsyncStorage) in React Native - react-native

We created a simple MobX store to save an object. This MobX store also syncs with AsyncStorage every time fooObject is changed:
import { observable } from 'mobx';
class FooStore {
#observable fooObject = {};
setFooObject(newFooObject) {
this.fooObject = newFooObject;
this.syncWithAsyncStorage();
}
syncWithAsyncStorage() {
AsyncStorage.setItem('fooObject', JSON.stringify(this.fooObject));
}
This mobx store is then consumed by various screens in the app.
As you can see, we use RN AsyncStorage, which is kind of a LocalStorage but for native apps, to always store the current state of fooObject.
The question is: when the app is closed, we loose FooStore state, so we want to retrieve the last 'save' we did to AsyncStorage (which is persisted).
The problem is AsyncStorage is (as the name states) an ASYNC function, and we can't wait for a promise resolution in FooStore's constructor.
How one would accomplish this initial state initialization of this MobX store based on the resolved return value of AsyncStorage?
We thought about writing an initialization method (async method) in FooStore like this:
async initializeFromAsyncStorage() {
this.fooObject = await JSON.parse(await AsyncStorage.getItem('fooObject'));
}
And then call this method on app initialization in our root App.js.
But I'm not sure if this would present unintended side effects and if this is idiomatic mobx/react-native.

Yes, I would create an initialization method. If the instance is not dependent of the initial data, you could call it right after the instantiation:
const fooStore = new FooStore()
fooStore.initializeFromAsyncStorage()
export default fooStore
Thill will not guarantee that the initial data is loaded when the app is constructed, but since the data is an optional observable, your app will react to it if/when it’s available.
Remember that AsyncStorage.getItem will resolve null if it has not been set, so you might want to do:
async initializeFromAsyncStorage() {
const result = await AsyncStorage.getItem('fooObject')
if (result !== null) {
this.fooObject = JSON.parse(result)
}
}

Related

mobx computed always update that computed doesn't work

import {makeAutoObservable} from "mobx";
class Test {
id = 0
constructor() {
makeAutoObservable(this)
}
get total() {
console.log('enss')
return 2;
}
}
const store = new Test();
export default store;
call:
import {isComputed, isComputedProp} from "mobx";
console.log(isComputedProp(Test, 'total'),Test.total,Test.total, Test.total, isComputedProp(Test, 'total'))
console output:
enss
enss
enss
true 2 2 2 true
the computed did not work and does not serve as a cache.
i using mobx 6.6 version in react 18.
Thank you very much for your answer!
This is explained in the docs here
It sometimes confuses people new to MobX, perhaps used to a library like Reselect, that if you create a computed property but don't use it anywhere in a reaction, it is not memoized and appears to be recomputed more often than necessary. For example, if we extended the above example with calling console.log(order.total) twice, after we called stop(), the value would be recomputed twice.
Basically it won't be cached if you use it outside of reactive context.
You can use computed({ keepAlive: true }) option to change this behaviour

AsyncStorage setItem inside loop

Is it ideal to setItem inside a loop? Assuming that i retrieved a list of data from the backend, which i need to do some heavy processing on each data before i can store it individually inside AsyncStorage store.
import { AsyncStorage } from "react-native";
async function getList(query){
/* some call setup in here */
try {
const response = await fetch(/* call parameters in here */);
const { data, err } = response;
if (err) throw new Error(err.message);
await processAndStoreItems(data);
return data;
} catch(err){
throw err;
}
}
async function processAndStoreItems(data){
/* data is a list of array that needs to be processed first before storing */
data.forEach( d => {
/* Some processing of data happening in here, and it may be a heavy processing that may take time */
const { processedKey, processedData } = await heavyProcessingHere(d);
await AsyncStorage.setItem(processedKey, JSON.stringify(processedData));
});
}
The reason why i did this was because, some of the process may take longer time, and the idea to just wait the whole process to finish then only store isn't ideal, since there might be case where user will logout from the app while the whole process is still running, and i need to clear the stored items upon logout.
If i do setItem inside loop, at least i can be assured that the store will always be updated for any completed processed data, and i can just delete whatever that is already stored on logout, rather than waiting for the whole process to complete, then only store it in AsyncStorage, which will be a problem if the user logout while the process is still incomplete, leaving the store outdated and making deletion hard for those items that are still in process.
So i need to know if performance wise, using setItem inside loop is okay? I read from somewhere about people facing crash issue with AsyncStorage when attempting to use setItem inside loop, which is bad for me.
Why you not putting your array to cookie
Like this;
await AsyncStorage.setItem(dataArray, JSON.stringify(data));

Is it valid inside a getter, call an action in vuex?

For example, I use a Getter for write a logic about a if object has a especify content.
const state = {
object = []
}
const getters = {
CHECK_OBJECT_STATE: (state) => {
if(Object.prototype.hasOwnProperty.call(state.object, "error")){
//return a message, in this case is not necessary proceed the logic.
}else{
//otherwise I need take state.object.id and fetch a new a action.
//CALL ACTION FETCH_OBJECT_ID
}
}
}
const actions = {
FETCH_OBJECT(...){
//SET MUTATION HERE
}
FETCH_OBJECT_ID (..) {..}
}
anyone can suggest me other method to do it, if it's not valid?
Vuex Getters are not designed for asynchronous tasks, those should be handled in the actions. I really think that getters operate best as pure functions which perform read-only operations on an existing state, so it should do only one job: retrieve the data in the state.
So you process the logic, the if statement in the actions, store the returned value in a state array object (which is pretty weird for me to name an array "object"), and the getter only has to retrieve such array. Things are reactive so that getter will always hold the latest data set.

Vuex mutations and actions responsibilities

Except that a mutation must be synchronous whereas an action can contain asynchronous code, I find it difficult to establish some guidelines on how to correctly choose where the logic is supposed to be implemented.
The Vuex doc describes the mutations as transactions, which makes me think that each mutation must ensure as much as possible state coherence, potentially doing multiple changes in the store within the same mutation.
Let’s take for example the following state, storing documents that can either be loading or loaded:
const state = {
loading: []
loaded: []
}
const mutations = {
setLoaded(state, doc) {
state.loaded.push(doc)
// Remove doc from loading:
const index = state.loading.find(loadingDoc => doc.id === loadingDoc.id)
if (index !== -1) {
state.loading.splice(index, 1)
}
}
}
Here the mutation ensures that the doc is either in loading or loaded state, not both. The problem doing so is that the mutations could eventually end up having a lot of logic, which could make them more difficult to debug through the Vuex dev tools.
Some prefer to keep the mutations as simple as possible (eg. only one state change per mutation), but then it’s up to the actions (or the components in case they directly call mutations) to ensure state coherence, which could be more error-prone since, in the above example, you would need to remove the doc from loading state each time you call the “setLoaded” mutation.
As for data validation, unluckily we have no simple way to do it in mutations since mutations cannot return anything. So an action calling a mutation has no way to know that the data is invalid. Eg.:
const mutations = {
setValue(state, intValue) {
if (intValue < 0) {
// error
return
}
state.value = intValue;
}
}
Thus, it seems that data validation needs to be done in actions, with mutations assuming that the data they receive are correct.
What are your thoughts about this, and more generally about actions and mutations? Do you have guidelines on how to properly use them from your own experience?
Data validation MUST be done in actions if it encompasses multiple mutations and/or needs to signal to the caller if the operation failed.
My rule (and this will be argued...) is to always use async CALLS (with async/await) whenever possible, even if I am calling sync methods. Then my code is future proof if the sync method becomes async. And in the case you mentioned, it also allows a return value and/or error from Vuex.
let response
response = await anAsyncMethod()
response = await aSyncMethod()
response = await aVuexMutateOp()
response = await aVueActionOp()
Yes this adds a bit of processing overhead, but it is also just as clear by using async/await syntax. It streamlines the error processing as well, routing it to a catch block.
Because mutations have the restriction that they MUST be synchronous AND CANNOT call other mutations AND DO NOT return a value demonstrates that the mutate should only be used in the simplest of situations.

React Native Expo Task Manager

Eventually, I would like to be able to run background tasks in my React Native app (Axios fetch to get some fresh data at least once a day). I am struggling to make this work:
https://docs.expo.io/versions/latest/sdk/task-manager/
import * as BackgroundFetch from 'expo-background-fetch';
import * as TaskManager from 'expo-task-manager';
const FETCH_TASKNAME = 'test_task'
const INTERVAL = 60
function test() {
console.log('function is running')
}
export async function registerFetchTask() {
TaskManager.defineTask(FETCH_TASKNAME, test());
const status = await BackgroundFetch.getStatusAsync();
switch (status) {
case BackgroundFetch.Status.Restricted:
case BackgroundFetch.Status.Denied:
console.log("Background execution is disabled");
return;
default: {
console.debug("Background execution allowed");
let tasks = await TaskManager.getRegisteredTasksAsync();
if (tasks.find(f => f.taskName === FETCH_TASKNAME) == null) {
console.log("Registering task");
await BackgroundFetch.registerTaskAsync(FETCH_TASKNAME);
tasks = await TaskManager.getRegisteredTasksAsync();
console.debug("Registered tasks", tasks);
} else {
console.log(`Task ${FETCH_TASKNAME} already registered, skipping`);
}
console.log("Setting interval to", INTERVAL);
await BackgroundFetch.setMinimumIntervalAsync(INTERVAL);
}
}
}
and calling this in App.js
import { registerFetchTask } from './helpers/backgroundFetch'
registerFetchTask();
I am getting a console logs up to this point:
function is running
Background execution allowed
Registering task
But I am unfortunately also getting following errors:
TaskManager.defineTask must be called during the initialization phase!
I read also in the documentation and as per example code, I am running in App.js directly and not in the component class.
And I am getting the following warning:
[Unhandled promise rejection: Error: Task 'test_task' is not defined. You must define a task using TaskManager.defineTask before registering.]
Which I don't understand since it is defined at the very top and before registering.
It is only unfortunate there is no proper working example anywhere to be found. An example would save countless hours for people struggling with this. Is there maybe an easier way of making background tasks running in react native apps?
Thanks so much for your kind help.
I can spot a couple of problems:
When you define the task, pass in a reference to the function rather than calling it:
TaskManager.defineTask(FETCH_TASKNAME, test); // no parentheses
registerFetchTask() is async which means it returns a promise when called in App.js. That probably doesn't count as the "initialization phase" of the app so try removing async
I don't know whether these changes will solve the problem but they should help.
change the line TaskManager.defineTask(FETCH_TASKNAME, test()); to
TaskManager.defineTask(FETCH_TASKNAME, test);
instead of passing the return value of the function pass the function reference.