What's the best way to declare global actions in Mobx-State-Tree? - mobx

Assume I have set of actions in different parts of the state tree that apart from their logic should modify certain property on the root node, for instance, toggle loading prop to indicate that UI should globally change progress indicator visibility.
const Contacts = types.model( 'Contacts', {
items: types.array(types.string)
}).actions(self=>({
show: flow(function* fetchData(){
// somehow indicate start of the loading process
self.items = yield fetch();
// somehow indicate end of the loading process
})
}));
const Store = types.model('AppStore', {
loading: types.optional(types.boolean, false),
contacts: Contacts
}).actions(self => ({
toggle() {
self.loading = !self.loading;
}
}));
While I certainly can use getRoot this will bring certain inconvenience to testing flow and downgrades overall design transparency.
Probably use of lazy composition and exporting instances along with model declarations from module can do, but this looks even weirder for me.
What is the suggested way to deal with this kind of issues in Mobx-State-Tree?

I think you can use types.reference and types.late in your models which down the tree for access to the root actions.

Related

Reuse components with different Vuex stores in NuxtJS - Create dynamic/multiple VueX store instances

I have a vue.js/nuxt.js component in my UI that displays news based on a backend which can be queried with selectors (e.g. news-type1, news-type2).
I want to add a second instance of that component which uses exactly the same backend, but allows the user to use a few different selectors (e.g. news-type3, news-type4). The UI kinda works dashboard-like. Implementing that distinction in the .vue component file is no problem (just accept some props and display stuff conditionally to the user), but:
How do I reuse the vuex store? The code of the store for the new card stays exactly the same since the same backend is used. But I can't use the same instance of the store because the selectors and the loaded news should be stored per component and should not be shared between them. Surprisingly I haven't been able to find any easy solutions for that in nuxt. I thought this would be a common use case.
MWE for my use case:
/** Vuex store in store/news.js **/
export const state = () => ({
// per default only news-type1 is selected, but not news-type2. the user can change that in the UI
currentSelectors: ['news-type1'],
news = [] // object array containing the fetched news
});
export const mutations = {
// some very simple mutations for the stae
setSelectors (state, data) {
state.currentSelectors = data;
},
setNews (state, data) {
state.news = data;
}
}
export const actions = {
// simplified get and commit function based on the currentSelectors
async loadNews ({ commit, state }) {
const news = await this.$axios.$get(`/api/news/${state.currentSelectors.join(',')}`);
commit('setNews', news);
// ... truncated error handling
},
// Helper action. In comparison to the mutation with the same name, it also calls the load action
async setSelectors ({ commit, dispatch }, selectors) {
commit('setSelectors', selectors);
dispatch('loadNews');
},
};
In my news-card.vue I simply map the two states and call the two actions loadNews (initial load) and setSelectors (after user changes what news to show in the UI). This should stay the same in both instances of the card, it just should go to different store instances.
My current alternative would be to simply copy-paste the store code to a new file store/news-two.js and then using either that store or the original store depending on which prop is passed to my news-card component. For obvious reasons, that would be bad practice. Is there a better complicated alternative that works with nuxt?
All related questions I have found are only for Vue, not for nuxt vuex stores: Need multiple instances of Vuex module for multiple Vue instances or How to re-use component that should use unique vuex store instance.

Vuejs Watch jumping one tick

I'm developing an app using Vuejs and Vuex.
I've got a Vuex module called settings_operations. This module has the following action:
async changePassword ({ commit }, { password, id }) {
commit(CHANGE_PASSWORD_PROCESSING, { id })
const user = auth.currentUser
const [changePasswordError, changePasswordSuccess] = await to(user.updatePassword(password))
if (changePasswordError) {
commit(CHANGE_PASSWORD_ERROR, { id, error: changePasswordError })
} else {
commit(CHANGE_PASSWORD_SUCCESS, changePasswordSuccess)
}
}
Edit: the to() is https://github.com/scopsy/await-to-js
With the following mutations:
[CHANGE_PASSWORD_PROCESSING] (state, { id }) {
state.push({
id,
status: 'processing'
})
},
[CHANGE_PASSWORD_ERROR] (state, { id, error }) {
state.push({
id,
error,
status: 'error'
})
}
And then, in the component I want to use this state slice:
computed: {
...mapState({
settings_operations: state => state.settings_operations
})
},
watch: {
settings_operations: {
handler (newSettings, oldSettings) {
console.log(newSettings)
},
deep: false
}
}
The problem is that when the changePassword action results in an error, the watch doesn't stop in the PROCESSING step, it goes directly to the ERROR moment so the array will be filled with 2 objects. It literally jumps the "processing" watching step.
A funny thing that happens is that if I add a setTimeout just like this:
async changePassword ({ commit }, { password, id }) {
commit(CHANGE_PASSWORD_PROCESSING, { id })
setTimeout(async () => {
const user = auth.currentUser
const [changePasswordError, changePasswordSuccess] = await to(user.updatePassword(password))
if (changePasswordError) {
commit(CHANGE_PASSWORD_ERROR, { id, error: changePasswordError })
} else {
commit(CHANGE_PASSWORD_SUCCESS, changePasswordSuccess)
}
}, 500)
},
It works! The watch stops two times: the first tick displaying the array with the processing object and the second tick displaying the array with 2 objects; the processing one and the error one.
What am I missing here?
Edit:
I reproduced the problem here: https://codesandbox.io/s/m40jz26npp
This was the response given in Vue forums by a core team member:
Watchers are not run every time the underlying data changes. They are only run once on the next Tick if their watched data changed at least once.
your rejected Promise in the try block is only a microtask, it doesn’t
push execution to the next call stack (on which the watchers would be
run), so the error handling happens before the watchers are run.
additionally, when you mutat an object or array, the newValue and
oldValue in a deep watcher will be the same. See the docs:
Note: when mutating (rather than replacing) an Object or an Array, the old value will be the same as new value because they reference the
same Object/Array. Vue doesn’t keep a copy of the pre-mutate value.
and as a final sidenote, I’ve never seen anyone use an aray as the
root state of a module, I have no idea if that will work for vuex in
all possible circumstances. I certainly would not recommend doing
this.
Edit with a better and more complete answer from the same member:
Why watchers are asynchronous at all? Because in the vast majority of
use cases, watchers only need to react to the last synchrnous change
that was done. In most cases (in the context of a component), it would
be couterproductive to to react to every change since you would
re-trigger the same behaviour mutliple times even though in the end,
only the last state is the important one.
In other words: Running a watcher on each change by default would
probably lead to apps that burn a lot of CPU cycles doing useless
work. So watchers are implemented with an asynchronous queue that is
only flushed on nexTick. And we don’t allow duplicate watchers then
because the older instance of a watcher would apply to data that
doesn’t “exist” anymore in that state once the queue is flushed.
An important note would be that this only applies to synchronous
changes or those done within a microtask, i.e. in an immediatly
resolving or failing promise - it would, for example, not happen with
an ajax request.
Why are they implemented in a way that they are still not run after a
microtask (i.e. an immediatly resolved promise? That’s a bit more
coplicated to explain and requires a bit of history.
Originally, in Vue 2.0, Vue.nextTick was implemented as a microtask
itself, and the watcher queue is flushed on nextTick. That meant that
back then, a watcher watching a piece of data that was changed two
times, with a microtask (like a promise) in between, would indeed run
two times.
Then, around 2.4 I think, we discovered a problem with this
implementation and switched Vue.nextTick to a macroTask instead. under
this behaviour, both data chhanged would happen on the current call
stack’s microtaks queue, while the watcher queue would be flushed at
th beginning of the next call stack, wich means it will only run once.
We found a couple of new problems with this implementation that are
much more common than the original issue with microtasks, so we will
likely switch back to the microtask implementation in 2.6. Ugly, but
necessary.
So, this should do the trick for now:
await Vue.nextTick();

Using one vuex module store in multiple sibling components

I have one global state with some modules.
now i have vue components for various parts of my page.
i have everything setup so /foo uses the foo store (this works).
the created method loads data from an API and writes it to the store
now i have /foo/bar as another (sibling) component, but it needs to access the same store as /foo, but i can't get it to work.
if i enter /foo/bar/ in the URL, there is nothing in the store.
but if i switch to /foo, and then back to /foo/bar, the data is in the store and being output correctly
I've tried registering /foo/bar as a child, which seemed to have no effect (and actually it's not really a child, but just another page with the same data..)
I also tried
state: {
...mapState([
'foo'
)]
}
in /foo/bar, but that doesn't seem to be the right way either
what is the best practice to
load data from API on created on any of a specified set of pages
access said data on any of those pages (i.e. sharing the same store)
i've tried all day to find a solution, but it seems I didn't understand something.
thanks for your help :)
EDIT
actually, while i read my question again, i think my whole problem is the data not being loaded (because the created method is not called). how can i make sure this happens on any page using the store and just once? i can't just write an api call in every created method, can i?
Well, I think just to summarize your problem could be called like you're not being able to access the same state between two different componentes.
What I do normally is that I make an API call from one component inside the method beforeMount, that will guarantee that once my component is created, the data will be available to be used.
Furthermore, after calling the api, I update my state so after that I can call it from everywhere.
One thing that you have to take care with is which component is loaded first?
If A is B's parent, then you should load data inside A.
However, if A and B are siblings, then you should load data inside both of them because you can access first either Component A or B, then you don't know when the data is going to be available. In that case, I would load the data in both of the components.
Also, add cache to your server so you don't need to load the same data again.
For example:
State
{
data: {}
}
Component A
export default {
name: 'Batch',
beforeMount() {
this.getDataFromAPI();
},
methods: {
// getDataFromAPI will store its return inside data with a mutation
...mapActions(['getDataFromAPI']),
randomMethod() {
// Now I can Use my state
const data = this.$store.state.data;
}
}
};
Component B
export default {
name: 'Batch',
methods: {
randomMethodB() {
// If component A was loaded first than component B and A is B's parent, then the state will be accessible in the same manner and it should be populated
const data = this.$store.state.data;
}
}
};
Actions
const getDataFromAPI = ({ commit }) => new Promise((resolve, reject) => {
// Call server
const data = await callServer();
commit('updateMyStateWithData');
resolve(data);
});
export default {
getDataFromAPI
}
Mutations
const mutations = {
updateMyStateWithData(state, newData) {
state.data = newData;
}
}
export default mutations;
Another thing that I do is to define getters, that way is a good approach to load data once, and inside the getter you update the data to return only the things that your UI needs.
I hope that it helps!

Vue component .$on use case

Its not clear to me how to use the .$on(...) method available in every Vue instance. I am sure I am probably missing some use case where an event would be emitted and consumed by the same Vue component (?) but currently I am not able to imagine many. Also, where would this wiring be performed. Would that be in a lifecycle method ?
My problem: I have unrelated (that is non-sibling, non-descendant or non-common-parent) components which change view based on interactions made on a different component. And, $on(...) does not seem to help my purpose.
And, there arises the need to understand how/why .$on(..) is made available in the framework. Thank you.
You can use the $on-method for implementation of CommunicationHub -- common mixin, for non parent <--> child communication (like in your case).
For example: you have two Vue root applications: RootAppA and RootAppB. To communicate between them, you can create CommunicationHub mixin with next code:
let CommunicationHub = new Vue();
Vue.mixin({
data: function () {
return {
communicationHub: CommunicationHub
}
}
});
Now you can send data by emitting custom event from RootAppA with $emit-method, and get this data by subscribing on this event in RootAppB, with method $on:
let RootAppA = {
methods: {
sendData(){
this.communicationHub.$emit('customEvent', {foo: 'bar', baz: 1, comment: 'custom payload object'});
}
}
}
let RootAppB = {
created(){
this.communicationHub.$on('customEvent', (payload) => {
console.log(payload); //{foo: 'bar', baz: 1, comment: 'custom payload object'}
});
}
}
By the way, please mention that CommunicationHub-pattern is not so flexible solution for bigger apps. So if your application will grow up, perhaps you will want to use Vuex-library (see my example in previous so-answer)

In Vue.js, how do you prevent navigation for a subroute?

The nice thing about beforeRouteLeave is that you can prevent navigating away under certain conditions.
I have a setup that uses a subroute to render part of the page. I would like a navigation guard on the subroute to prevent switching to another one if the data is not saved.
{
path: '/customers/view',
component: ViewCustomerShell,
children: [
{path: ':id', name: 'ViewCustomer', component: ViewCustomer}
]
},
So when I visit /customers/view/12 and make a change, if they try to load /customers/view/13, I want to pop up the usual confirmation and potentially stop navigation. Since beforeRouteLeave is not called in this situation, what is the recommended approach for preventing navigation? It seems that watching $route would be too late, because then the navigation has already occurred.
Note: As mentioned above, beforeRouteLeave is not called in this situation; it doesn't work.
Note: Using onbeforeunload doesn't work because it only triggers when the entire page changes.
I have also posted the same answer here.
Dynamic route matching is specifically designed to make different paths or URLs map to the same route or component. Therefor, changing the argument does not technically count as leaving (or changing) the route, therefor beforeRouteLeave rightly does not get fired.
However, I suggest that one can make the component corresponding to the route responsible for detecting changes in the argument. Basically, whenever the argument changes, record the change then reverse it (hopefully reversal will be fast enough that it gets unnoticed by the user), then ask for confirmation. If user confirms the change, then use your record to "unreverse" the change, but if the user does not confirm, then keep things as they are (do not reverse the reverse).
I have not tested this personally and therefor I do not gurantee it to work, but hopefully it would have cleared up any confusion as to which part of the app is responsible for checking what change.
I know that this post is very old. but it was the first one I found when looking for the same problem.
I have no idea if there is a better solution nowadays but for those who are looking for a solution, I can share mine:
1. Define a global state
let isRouteChangeBlocked: boolean = false;
export function blockRouteChange(set?: boolean): boolean {
if (arguments.length == 1) {
isRouteChangeBlocked = !!set;
return isRouteChangeBlocked;
}
return isRouteChangeBlocked;
}
2. Replace the route function
const originalPush = VueRouter.prototype.push;
VueRouter.prototype.push = function(location: RawLocation) {
if (blockRouteChange()) {
if (confirm("Du hast ungespeicherte Änderungen, möchtest du fortfahren?")) {
blockRouteChange(false);
return originalPush.call(this, location) as any;
}
return;
}
return originalPush.call(this, location) as any;
};
3. Set the state
#Watch("note.text")
private noteTextChanged() {
blockRouteChange(true);
}
This does exactly what I want. If nowadays there is a better solution, let me know. You can get the full runnable example here: https://github.com/gabbersepp/dev.to-posts/tree/master/blog-posts/vuejs-avoid-routes/code/example
You could use a $route object inside your component to watch if it changes and then raise up the confirmation modal... This will get called whenever your route changes!
const Baz = {
data () {
return { saved: false }
},
template: `
<div>
<p>baz ({{ saved ? 'saved' : 'not saved' }})<p>
<button #click="saved = true">save</button>
</div>
`,
watch: {
'$route': function () {
if (this.saved || window.confirm('Not saved, are you sure you want to navigate away?')) {
// do something ...
}
}
}