Mobx how to compute values in state - mobx

I'm new to Mobx and I can't work out how to compute a value in my store.
I'm following the docs description of using the 'computed' modifier.
Here's my (cut down) appState:
export default class AppState {
constructor() {
/* MOBX STATE */
extendObservable(this, {
// Links
'links': [],
'updateLinks': action((newlinks) => {
this.links = newlinks;
}),
'linksWithComments': computed(() => this.links),
});
}
}
'links' works fine in my React components, but when I add 'linksWithComments' I see this error:
Uncaught Error: [MobX] 'keys()' can only be used on observable objects, arrays, sets and maps
What am I doing wrong? 'links' is an array, and as far as I can tell it's observable, so what does the error mean? I've googled for the error message but haven't found anything that explains what's going on.
I've tried this form also:
get linksWithComments () { return 2*3; },
In this case 'linksWithComments' is undefined.
Versions:
"mobx": "^6.1.8",
"mobx-react": "^7.1.0",
"mobx-react-lite": "^3.2.0",
Thank you!

Preferred way to use MobX#6 with classes is to use makeAutoObservable or makeObservable:
class AppState {
links = [];
constructor() {
makeAutoObservable(this);
}
updateLinks = (newLinks) => {
this.links = newLinks;
};
get linksWithComments() {
return this.links.filter((link) => link > 0.5);
}
}
Have you tried it?
I've made Codesanbox example for you to explore!
More info in the official docs

Related

extending of observable subclassing: [MobX] Options can't be provided for already observable objects

When I create a deep structure with few extends I got this kind of error:
Uncaught Error: [MobX] Options can't be provided for already observable objects.
"mobx": "^6.4.2",
"mobx-react-lite": "^3.3.0",
All of code is just dirty example. real structure more complex.
Code example:




import { makeObservable, observable, action } from 'mobx';
class DateMobx {
date ;
constructor(data) {
this.date = new Date(data.date);
makeObservable(this, { date: observable }, { autoBind: true });
}
}
class Todo extends DateMobx {
id = 0;
title = 'title';
text = 'text';
constructor(todo) {
super(todo);
this.id = todo.id;
this.title = todo.title;
makeObservable(this, { id: observable, title: observable, changeTitle: action }, { autoBind: true });
}
changeTitle= (e) => {
const { value } = e.target;
this.title = value;
}
}
class TodoList {
todo = [];
constructor() {
const todoData = [{ id: 1, title: 'title', date: '123' }];
this.todo = todoData.map(item => new Todo(item)); // ERROR happen here
makeObservable(this, { todo: observable }, { autoBind: true });
}
}


Error happen in constructor of TodoList class.

if remove makeObservable from Todo class, error is not reproduced but I need reactivity in that class.

If remove extends DateMobx from Todo class error also is not reproduces (but I have a lot of general classes with basic logic where I need reactivity too).



Why its happen and what should I do if I really need such kind of deep structure ?
Guys on github bring me the right solution
Don't pass the third param ({ autoBind: true }) to makeObservable in subclasses. All options are "inherited" and can't be changed by subsequent makeObservable calls on the same object (this).
options argument can be provided only once. Passed options are
"sticky" and can NOT be changed later (eg. in subclass).
https://mobx.js.org/observable-state.html#limitations

How to trigger watch AFTER I read array from db?

In my vue/cli 4/vuex opening page I need to fill select input with default value from
vuex store. To fill select input I need have selection items read from db and I have a problem that watch is triggered
BEFORE I read data from db in mount event.
I do as :
watch: {
defaultAdSavedFilters: {
handler: function (value) {
console.log('WATCH defaultAdSavedFilters value::')
console.log(value)
if (!this.isEmpty(value.title)) {
this.filter_title = value.title
}
if (!this.isEmpty(value.category_id)) {
this.categoriesLabels.map((nexCategoriesLabel) => { // this.categoriesLabels IS EMPTY
if (nexCategoriesLabel.code === value.category_id) {
this.selection_filter_category_id = {code: value.category_id, label: nexCategoriesLabel.label};
}
});
}
}
}, //
}, // watch: {
mounted() {
retrieveAppDictionaries('ads_list', ['ads_per_page', 'categoriesLabels']); // REQUEST TO DB
bus.$on('appDictionariesRetrieved', (response) => {
if (response.request_key === 'ads_list') { // this is triggered AFTER watch
this.ads_per_page = response.ads_per_page
this.categoriesLabels = response.categoriesLabels
// this.$forceUpdate() // IF UNCOMMENT THAT DOES NOT HELP
Vue.$forceUpdate() // THAT DOES NOT HELP
}
})
this.loadAds(true)
}, // mounted() {
I found this Can you force Vue.js to reload/re-render?
branch and tried some decisions, like
Vue.$forceUpdate()
but that does not work.
If there is a right way to trigger watch defaultAdSavedFilters AFTER I read array from db ?
Modified BLOCK :
I use Vuex actions/mutations when I need to read / keep /use /update data of the logged user, like
defaultAdSavedFilters, which is defined as :
computed: {
defaultAdSavedFilters: function () {
return this.$store.getters.defaultAdSavedFilters
},
Data ads_per_page(used for pagionaion), categoriesLabels(used for selection input items) has nothing to do with
logged user, that is why I do not use vuex for them, and I use retrieveAppDictionaries method to read them from the db
and bus to listen to them, which is defined as :
import {bus} from '#/main'
Sure I have data( block :
export default {
data() {
return {
...
ads_per_page: 20,
categoriesLabels: [],
...
}
},
"vue": "^2.6.10",
"vue-router": "^3.1.3",
"vuex": "^3.1.2"
Thanks!
Please add the data() method from you component. But I'm pretty sure it is NOT triggering because of the way you are assigning the result from the API call.
Try this:
mounted() {
retrieveAppDictionaries('ads_list', ['ads_per_page', 'categoriesLabels']); // REQUEST TO DB
bus.$on('appDictionariesRetrieved', (response) => {
if (response.request_key === 'ads_list') { // this is triggered AFTER watch
this.ads_per_page = [ ...response.ads_per_page ]
this.categoriesLabels = [ ...response.categoriesLabels ]
}
})
this.loadAds(true)
}
However, I don't understand what bus is doing for you and why you are NOT using Vuex actions/mutations

Dispatching Redux Data and Get the State

I stuck on Redux Implementation during developing an app using React Native and Redux. I do this for the first time and followed this example.
I've already installed Redux and React Native Navigation. I would like to save the state containing data for countries (the user picked a country and would like to keep the choice by the time when it browses to all screens).
Good. I've created a component that could be seen to all screens like this:
LinksScreen.navigationOptions = {
headerTitle: 'Links',
headerRight: <CountriesPickButton/>,
};
Next, I visualize the button and wait for a change in the component. By default, it should show primary country. Next, the user clicks on the button and it opens a modal where has a dropdown menu. For example, I show you the default fetching a country:
import { connect } from 'react-redux'
import store from '../../redux/countries'
export default class CountriesPick extends Component {
render() {.... // here is the button and modal, etc. It's work.
}
constructor(props, context) {
super(props, context);
this.state = store.getState();
store.subscribe(() => {
this.setState(store.getState());
});
this.defaultCountry(251);
}
async defaultCountry(countryId) {
return fetch(URL)
.then((response) => response.json())
.then((responseJson) => {
for (const key of Object.keys(responseJson.result)) {
// this works for current screen: this.setState({ defaultCountry: responseJson.result[key], selectedCountry: responseJson.result[key].country_id });
store.dispatch({ defaultCountry: responseJson.result[key], selectedCountry: responseJson.result[key].country_id , type: 'countries' });
}
return responseJson.result;
})
.catch((error) => {
console.error(error);
});
state = {
showModal: false,
countries: [],
selectedCountry: 0,
defaultCountry: [],
type: 'countries'
};
}
Without store.dispatch({}) I can change the state with the country but it has not to share between screens. That's because I started with Redux.
Here is the Redux code ():
import { createStore } from 'redux'
const defaultState = {
showModal: false,
countries: [],
selectedCountry: 0,
defaultCountry: [],
type: 'countries'
};
function store(state = defaultState) {
return state;
}
export default createStore(store);
Something is not like it should be. When I invoke store.dispatch({...}) it's not changing the state, it returns the default array. I guess I should use <Provider></Provider> in App.js to catch every change but first, I need to understand what I wrong?
Is it connected at all? In the example that I followed, I did not see connect(). Also, I'm not sure I'm using type properly.
Thank you in advance.
Problems here are the following:
Example on the link you provided is bad to say the least. Do not follow it
You said to be using react-native-navigation, but the code you provided comes from react-navigation. I suggest using the latter, especially for starters
Your createStore code is not going to work, as reducer for the store should be a function of state and action
With that being said, you should definitely see Basic Tutorial of redux with examples. You will almost never have to do store.getState() or store.dispatch() while using react with redux, as react-reduxpackage (included in the tutorial I linked) will do this for you. You will instead declare dependency between your store state and props your component receives

undefined is not an object (evaluating '_effects.buffers.expanding')

I'm getting an error when tried to copy&paste this solution in my app.
undefined is not an object (evaluating 'effects.buffers.expanding')
My code is pretty similar:
export function* handleBleState() {
const {
bleConnections: { manager },
} = yield select()
const stateChannel = yield eventChannel((emit) => {
const subscription = manager.onStateChange((state) => {
emit(state)
}, true)
return () => {
subscription.remove()
}
}, buffers.expanding(1))
try {
for (;;) {
const newState = yield take(stateChannel)
const power = newState === 'PoweredOn'
yield put(BLEActions.updateBleState(power))
}
} finally {
if (yield cancelled()) {
stateChannel.close()
}
}
}
But what is more strange is that I have extracted code from the documentation of redux-saga and the error is similar.
I replaced part of the code with this:
const stateChannel = yield eventChannel((emitter) => {
const iv = setInterval(() => {
secs -= 1
if (secs > 0) {
emitter(secs)
} else {
// this causes the channel to close
emitter(END)
}
}, 1000)
// The subscriber must return an unsubscribe function
return () => {
clearInterval(iv)
}
})
And I get:
undefined is not a function (evaluating '(0, _effects.eventChannel)')
I'm using another version of redux-saga:
"redux-saga": "^1.0.2",
But I tried with the same version that he use
"redux-saga": "0.16.2"
And then the issue is another one:
console.error: "unchaught at root", "at root at takeLatest" at handleBleState etc, etc.
Any help will be welcome, thanks in advance.
PD:
"react": "16.6.1",
"react-native": "0.57.5",
"react-native-ble-plx": "^1.0.1",
the eventChannel is nor part of the effects but defined directly in the main module, see the relevant API doc. So if you're using ES6 import/export you import it with the following statement:
import { eventChannel } from 'redux-saga'
While the require equivalent should be:
const eventChannel = require('redux-saga').eventChannel

Realm & React Native - Best practice to implement auto-updates?

What are the best practices/patterns make realm a reactive datasource in a react native app? Especially for presentational and container components pattern?
Here is an example which I'd like to make reactive: Realm with React Native
The docs on auto-updates/change-events are a bit thin and the official example does not make use of this feature (to my knowledge).
You can make your example reactive by subscribing to events and updating the ui when you receive a change event. Right now events are only sent when write transactions are committed, but finer grained change events will be added in the future. For now you could add the following constructor to update the ui on changes:
constructor(props) {
super(props);
this.realm = new Realm({schema:[dogSchema]})
this.realm.addListener('change', () => {
this.forceUpdate()
});
}
You need to hold onto a Realm instance to keep the notifications alive, and you can use this Realm instance throughout the rest of the component.
Instead of calling forceUpdate, you could instead set the component's state or props within the event listener to trigger the refresh, like so:
constructor(props) {
super(props);
this.realm = new Realm({schema:[dogSchema]})
this.state = {...}; // Initial state of component.
this.realm.addListener('change', () => {
this.setState({...}); // Update state instead of using this.forceUpdate()
});
}
I think #Ari gave me a good answer for redux folks as i was also struggling. I'm not sure if it's immutable enough but it works!
I'm simpliy dispatching getVehicles action inside addListener and it just works!
Below is UI component whose constructor function makes the magic!
//- importing my realm schema
import realm from '../../db/models';
//- Importing my action
import { getVehicles } from './../../actions/vehicle';
#connect((store) => {
return {
vehicle: store.vehicle.vehicles
}
})
export default class Devices extends Component {
constructor(props) {
super(props);
realm.addListener('change', () => {
props.dispatch(getVehicles());
});
}
}
Below is db/models file used up there in the constructor.
import Realm from 'realm';
class VehicleSchema {};
VehicleSchema = {
name: 'vehicleInfo',
properties: {
vehicleName: 'string',
vehicleNumber: 'string',
vehiclePassword: 'string',
vehiclePasswordTrigger: 'bool',
vehicleType: 'string',
vehiclePicture: { type: 'data', optional: true }
}
};
export default new Realm({schema: [VehicleSchema]});
Below is the actions/vehicle file, which gets dispatched in the constructor above.
import { queryVehicle } from './../db/queryVehicle';
export function getVehicles() {
const vehicles = queryVehicle();
return function(dispatch) {
dispatch({type: "GOT_VEHICLES", payload: vehicles});
}
}
Below is my queryVehicle function that does the querying called in action file above.
import vehicleModel from './models';
const queryVehicle = (queryInfo="vehicleInfo", filter='') => {
const objects = vehicleModel.objects(queryInfo);
if(filter.length === 0) return objects;
let results = objects.filtered(filter);
return results;
};
export { queryVehicle };
disclaimer I don't know if this code looks immutable enough, or following good redux practice cause i'm just starting out with redux so give me some comments advising if i'm doing something wrong.
I'll also guess reducer implementation wouldn't matter much in this here.
Recently ran into an issue with Realm ListView auto-updating. When the ListView rows have varied heights, you can get overlaps on rows in the UI. The below was the only way I could get the ListView to re-render without causing UI overlaps. It seems a bit "dirty" to me, so if there is a better way, I welcome the input. But this is working perfectly so far; incase anyone else runs into this issue.
Basically it just wipes the dataSource, then inserts it again using the setState callback when there are insertions or deletions, but modifications simply roll through and auto-update.
let feed = this.props.store.feed;
feed.addListener((name, changes) => {
if (changes.insertions.length || changes.deletions.length) {
this.setState({dataSource: this.ds.cloneWithRows([])},
() => this.setState({dataSource: this.ds.cloneWithRows(feed)})
);
} else {
this.setState({dataSource: this.ds.cloneWithRows(feed)});
}
});