Here is my store:
import { observable, action, flow, reaction } from "mobx";
export default class Demo {
#observable obj = {
flag: false,
name: "",
age: 20
};
#action
turnFlag = () => {
this.obj.flag = true;
};
constructor() {
reaction(
() => this.obj,
obj => {
console.log(obj.flag);
}
);
}
}
What I want do is, if any property in obj changed, the reaction callback will be invoked.
But when the action turnFlag executed, nothing happened.
So what's wrong with my code? If I want supervisor any change in obj, what should I do?
To make the reaction work you need to have it watch a property on the observable, rather than the root obj.
reaction(
() => this.obj.flag,
flag => { console.log(`FOO: ${flag}`); }
);
There's a working example of that here: https://codesandbox.io/s/km3n38yrj7
(Open your browser console to see the output.)
The documentation covers this here:
It is important to notice that the side effect will only react to data that was accessed in the data expression, which might be less then the data that is actually used in the effect.
In your original code you weren't accessing anything on 'obj'.
Since you want to do something when anything on 'obj' is changed:
What I want do is, if any property in obj changed, the reaction callback will be invoked.
It sounds like you instead want 'observe'.
observe(this.obj, change => {
console.log(
`${change.type} ${change.name} from ${change.oldValue}` +
` to ${change.object[change.name]}`
);
});
I've updated the codesandbox link to show that.
let's try so as
`import { observable, action, flow, reaction } from "mobx";
class Demo {
#observable obj = {
flag: false,
name: "",
age: 20
};
#action
turnFlag = () => {
this.obj.flag = true;
};
constructor() {
reaction(
() => this.obj,
obj => {
console.log(obj.flag);
}
);
}
}
export default Demo;
`
Related
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
Linking.removeEventListener('url', onReceiveURL);
removeEventListener is deprecated.
This is what my IDE suggests :
EventEmitter.removeListener('url', ...): Method has been deprecated.
Please instead use remove() on the subscription returned by
EventEmitter.addListener.
// Custom function to subscribe to incoming links
subscribe(listener: (deeplink: string) => void) {
// First, you may want to do the default deep link handling
const onReceiveURL = ({url}: {url: string}) => listener(url);
// Listen to incoming links from deep linking
Linking.addEventListener('url', onReceiveURL);
const handleDynamicLink = (
dynamicLink: FirebaseDynamicLinksTypes.DynamicLink,
) => {
listener(dynamicLink.url);
};
const unsubscribeToDynamicLinks = dynamicLinks().onLink(handleDynamicLink);
return () => {
unsubscribeToDynamicLinks();
Linking.removeEventListener('url', onReceiveURL);
};
I tried many things but nothing seems to work.
Didn't find any concrete information about it.
Any help to figure it out ?
EDIT -> I will investigate further but so far it's working :
const unsubscribeToDynamicLinks : any = ...
then in return :
return () => {
unsubscribeToDynamicLinks().remove('url', onReceiveURL);};
For the new API, this is how it works:
useEffect (() => {
const subscription = Linking.addEventListener('url', onReceiveURL);
return () => subscription.remove();
}, [])
For class components you can use something like below:
class MyClass extends Component {
constructor(props){
super(props)
this.changeEventListener = null
}
componentDidMount(){
// or you can use constructor for this
this.changeEventListener = AppState.addEventListener('change', () => {
// your listener function
})
}
componentWillUnmount() {
this.changeEventListener.remove()
}
}
I cannot figure out why the details computed property in the following component is not updating when the fetch() method is called:
<template>
<div>
{{ haveData }} //remains undefined
</div>
</template>
<script>
export default {
props: {
group: {
type: Object,
required: true
},
},
computed: {
currentGroup() {
return this.$store.getters['user/navbar_menu_app_current_group'](
this.group.id
)
/*-- which is the following function
navbar_menu_app_current_group: state => item => {
return state.menu.find(m => {
return m.id == item
})
}
*/
/*-- this function returns an object like so
{
id: 1,
label: 'kity cats',
}
***details --> IS NOT DEFINED. If I add it to the current group as null, my problem goes away. However, this is a previous API call that does not set the `details` parameter.
*/
},
details() {
let c = this.currentGroup.details
console.log(c) // returns undefined, which makes sense, but it should be updated after this.fetch() is called
return c
},
haveData() {
return this.details != null
}
},
methods: {
async fetch() {
await this.$store.dispatch(
'user/navbar_menu_app_details_get',
this.group.id
)
//This is setting the "details" part of the state on menu which is referred to in the computed properties above
//Previous to this there is no state "this.group.details"
//If I add a console log to the mutation the action calls, I get what is expected.
}
},
created() {
if (!this.haveData) {
this.fetch()
}
}
}
</script>
If I change the array items to include details, it works:
{
id: 1,
label: 'kity cats',
details: null // <-- added
}
The unfortunate part is that the array is created from a large API call, and adding the details seems unnecessary, as it may never be needed.
How can I get the computed properties to work without adding the details:null to the default state?
Attempt 1:
// Vuex mutation
navbar_menu_app_details_set(state, vals) {
let app = state.menu.find(item => {
return item.id == vals[0] //-> The group id passing in the dispatch function
})
//option 1 = doesn't work
app = { app, details: vals[1] } //-> vals[1] = the details fetched from the action (dispatch)
//option 2 = doesnt work
app.details = vals[1]
//option 3 = working but want to avoid using Vue.set()
import Vue from 'vue' //Done outside the actual function
Vue.set( app, 'details', vals[1])
},
Attempt 2:
// Vuex action
navbar_menu_app_details_get(context, id) {
return new Promise((resolve, reject) => {
setTimeout(() => {
context.commit('navbar_menu_app_details_set', [
context.getters.navbar_menu_app_current(id), //-> the same as find function in the mutation above
apps[id]
])
resolve()
}, 1000)
})
}
// --> mutation doesn't work
navbar_menu_app_details_set(state, vals) {
vals[0].details = vals[1]
},
The Vue instance is available from a Vuex mutation via this._vm, and you could use vm.$set() (equivalent to Vue.set()) to add details to the menu item:
navbar_menu_app_details_set(state, vals) {
let app = state.menu.find(item => {
return item.id == vals[0]
})
this._vm.$set(app, 'details', vals[1])
},
All Objects in Vue are reactive and are designed in a way such that only when the object is re-assigned, the change will be captured and change detection will happen.
Such that, in your case, following should do fine.
app = { ...app, details: vals[1] }
How can i destroy this watcher? I need it only one time in my child component, when my async data has loaded from the parent component.
export default {
...
watch: {
data: function(){
this.sortBy();
},
},
...
}
gregor ;)
If you construct a watcher dynamically by calling vm.$watch function, it returns a function that may be called at a later point in time to disable (remove) that particular watcher.
Don't put the watcher statically in the component, as in your code, but do something like:
created() {
var unwatch = this.$watch(....)
// now the watcher is watching and you can disable it
// by calling unwatch() somewhere else;
// you can store the unwatch function to a variable in the data
// or whatever suits you best
}
More thorough explanation may be found from here: https://codingexplained.com/coding/front-end/vue-js/adding-removing-watchers-dynamically
Here is an example:
<script>
export default {
data() {
return {
employee: {
teams: []
},
employeeTeamsWatcher: null,
};
},
created() {
this.employeeTeamsWatcher = this.$watch('employee.teams', (newVal, oldVal) => {
this.setActiveTeamTabName();
});
},
methods: {
setActiveTeamTabName() {
if (this.employee.teams.length) {
// once you got your desired condition satisfied then unwatch by calling:
this.employeeTeamsWatcher();
}
},
},
};
</script>
If you are using vue2 using the composition-api plugin or vue3, you can use WatchStopHandle which is returned by watch e.g.:
const x = ref(0);
setInterval(() => {
x.value++;
}, 1000);
const unwatch = watch(
() => x.value,
() => {
console.log(x.value);
x.value++;
// stop watch:
if (x.value > 3) unwatch();
}
);
For this kind of stuff, you can investigate the type declaration of the API, which is very helpful, just hover the mouse on it, and it will show you a hint about what you can do:
My async actions tend to look something like this:
anAsyncAction: process(function* anAsyncAction() {
self.isLoading = true;
const service = getEnv<IMyMarksPageStoreEnv>(self).myService;
try
{
yield service.doSomething();
}
finally
{
self.isLoading = false;
}
}),
Then I let the view handle what toasts to show:
toaster = Toaster.create({
position: Position.TOP
});
render() {
return <button disabled={this.props.store.isLoading} onClick={this.handleButtonClicked}>Do Async Thing</button>
}
handleButtonClicked = () => {
const store = this.props.store;
try
{
await store.anAsyncAction();
toaster.show({ message: "Saved!", intent: Intent.SUCCESS });
}
catch(e)
{
toaster.show({ message: "Whoops an error occured: "+e, intent: Intent.DANGER });
}
}
But im starting to think that the toasts handling should live in the async try-catch of the store and not the view, but then its mixing business logic with view, so im not sure.
Any suggestions?
I'd argue that messages are part of the application.
In my app I have an array at root level
export default types.model('AppStore', {
....
flashMessages: types.optional(types.array(FlashMessage), []),
})
.actions((self) => {
/* eslint-disable no-param-reassign */
const addFlashMessage = (message) => {
self.flashMessages.push(FlashMessage.create({
...message,
details: message.details.toString(),
}));
};
function addErrorMessage(text, details = '') {
addFlashMessage({ type: 'error', text, details });
}
function addInfoMessage(text, details = '') {
addFlashMessage({ type: 'info', text, details });
}
function addSuccessMessage(text, details = '') {
addFlashMessage({ type: 'success', text, details });
}
Then
#inject('appStore')
#observer
class App extends Component {
render() {
const app = this.props.appStore;
return (
<BlockUI tag="div" blocking={app.blockUI}>
<Notifications messages={app.unseenFlashMessages} />
...
And in a component
this.props.appStore.addSuccessMessage(`User ${name} saved`);
This will also allow you to implement a 'last 5 messages' sort of thing which might be useful if you've missed a to
Guess that's not specific to mobx or mobx-state-tree, but I'd probably consider adding a dedicated NotificationStore to the picture. Service try/catch/finally would be one producer of notifications with a source of service, another might be a fetch/xhr wrapper with a source of transport.
It would be up to the business logic to decide how to present/handle those.