Update a MobX nested observable.map property - mobx

There is a simplified store with an observable.map:
class ThStore {
constructor() {
this.store = mobx.observable.map({});
}
}
mobx.decorate(ThStore, {
_store: mobx.observable,
}
The store holds data with this format (abbreviated), it is a javascript object literal that is managed by mobx as an observable.map. The data itself is a collection of items keyed with its id that has some nested child collection with the same format:
{
"some-id": {
id:"some-id",
label:"some-value",
childs: {
"child-id": {...some properties},
"other-child-id": {...some properties},
}
},
"other-id": {
id:"other-id",
label:"other-value",
childs: {
...
}
}
}
A change in a whole entry in the store works as expected
storeInstance.store.set("some-id", newItem);
But I cannot find a way to update only a property of an item. Getting an item gives a Proxy object which I don't know how to use to update some of its properties.
const item = storeInstance.store.get("childs");
// item is a Proxy instance and has no `.get` method to retrive a child
Trying to update the item like this works, but forces to update all the item and not just the required property triggering more re-renders than necesary.
const copiedItem = mobx.toJS(storeInstance.store.get("some-id"));
// This change does not trigger mobx
copiedItem.child["child-id"].property = someValue;
// This change does but forces updating all the item
storeInstance.set("some-id", copiedItem);
So I don't know how to do fine grainned updates to a mobX store using observable.map with nested child maps.
EDIT: the problem is that the nested childs property is not converted to an observable.map by mobx but is left as a plain object. How can be the nested property be forced to be converted to observable.map?

Related

V-model is updating unreferenced state data

I am trying to copy an object in the vuex store to a local component object so that I can change it locally and not modify the store state object. However, when I associate the local object with a textarea v-model, it changes the store state data even though it is not directly referenced. I am not sure how this is happening or even possible.
<v-textarea v-model="currentObj.poltxt" solo light></v-textarea>
watch: { //watch for current UID changes
"$store.state.currentUID"(nv) {
//Clearing the local temporary object
this.currentObj = {};
//Moving the store state data into the local object
this.currentObj = this.$store.state.docuMaster[this.$store.state.currentUID];
}
}
The watch function is executing and when I console log this.$store.state.docuMaster[this.$store.state.currentUID] I can see that v-model is directly updating it even though its referencing currentObj. Any idea why this is happening? The text box is not referencing the store in any other place in code.
If this.$store.state.docuMaster[this.$store.state.currentUID] is not a nested object then try using
//Moving the store state data into the local object
this.currentObj = Object.assign({}, this.$store.state.docuMaster[this.$store.state.currentUID]);
If this.$store.state.docuMaster[this.$store.state.currentUID]) is a nested object then you got to do deep clone
this.currentObj = JSON.parse(JSON.stringify(this.$store.state.docuMaster[this.$store.state.currentUID]));
Note:
Nested object in the sense I mean
docuMaster: {
UID: {
...
xyz: {}
},
....
}
Not a nested obj means
docuMaster: {
UID: {
//no inner objects
},
....
}

How to reset all states when property value is changed from javascript?

I am using stencil framework. In my component I am using different states to trigger different events. I am also updating the property value of component from javascript.
I would like to reset all states value and reload the component with updated property value.
New property value is responsible for many actions like calling api, generating the cache key etc.
Can anyone suggest me the best approach to fulfill my requirement. Currently I am reset all the states in watcher method of property and call the componentWillLoad event but I am facing many issue in this approach.
Sample code
#Prop() symbol!: string;
#Watch('symbol')
symbolChanged(newSymbol: string, prevSymbol: string) {
if (newSymbol && newSymbol !== prevSymbol) {
this.resetStates();
}
}
resetStates() {
//Reset all state values here
this.componentWillLoad();
}
By setting key property on root element of render method would solve my issue like below code snippet.
uniqKeyId = uniqid.get();
#Prop() symbol!: string;
#Watch('symbol')
sysmbolWatcher(newSymbol: string, prevSysmbol: string) {
if (newSymbol != prevSysmbol) {
//update key attribute each switch of exchange
this.uniqKeyId = uniqid.get();
//Set default values based on properties as to consider this as fresh request.
this.setDefaultValues();
}
}
And in render method like below
render() {
return (
<section class="cid-minichart" key={this.uniqKeyId}>
//Render markup
</section>
);
}

Vue Keys do not delete from Object

I'm trying to delete a key from an object in a parent component. A child component emits an event (with an item value) back to the parent method that triggers the delete in the parent's data object.
Parent component:
data() {
return {
savedNews: Object
}
},
methods: {
containsKey(obj, key) {
var result = Object.keys(obj).includes(key)
return result
},
handleSaveNews(item) {
if (!this.containsKey(this.savedNews, item.url)) {
this.savedNews = {
[item.url]: item,
...this.savedNews
}
} else {
console.log(this.containsKey(this.savedNews, item.url))
var res = delete(this.savedNews, item.url)
console.log(res)
console.log(this.containsKey(this.savedNews, item.url))
}
}
}
All of the console.logs in the last else statement return true. It's saying that the delete was successful yet the key is still there. How do I delete this key?
From the docs:
Vue cannot detect property addition or deletion
Use this.$delete:
this.$delete(this.savedNews, item.url)
or this.$set (which also should be used for property changes):
this.$set(this.savedNews, item.url, undefined);
Extra info: The $ is a naming convention Vue uses for its built-in methods that are available on each component instance. There are some plugins which opt to follow this pattern too. You can also use built-ins inside other modules if you import Vue and use Vue.delete, for example. You could add your own methods like Vue.prototype.$mymethod = ....

Vue does not update items in v-for from Vuex with dynamic component

We have a dynamic component for tab body, which defined as
<component :is="currentTab.itemType" :itemId="currentTab.itemId"></component>
Template has a span, which reflects itemId - it changes every time when the currentTab changed in tabs host component.
Each component of tab.itemType has Vuex module, belongs to it specific type.
For example, there is store module product with described state:
{
products: { [itemId: string]: IProduct }
}
When component created or itemId changed, it tries to run load action and put loaded product to products of vuex state.
So, there is Vue computed property, looks like
#State(productNamespace)
state: IProductState;
get currentProduct() {
return this.state.products[this.itemId];
}
or even
#Getter(GetterNames.GET_PRODUCT_BY_ID, bindingOptions)
getProductById: (itemId: string) => IProduct;
get currentProduct() {
return this.getProductById(this.itemId);
}
Each product has an attributes list, which is iterated by v-for with :key.
<v-list :key="itemId"><!-- itemId has no effect there -->
<v-list-item v-for="attribute in currentProduct.attributes" :key="attribute.id">
...
</v-list-item>
</v-list>
The problem is:
when we change itemId, the attributes list displays all attributes from last added product and does not refresh it when switching to previous "tabs" with another itemId but the same itemType.
I've tried to set :key of parent div as itemId but with no effect.
When I set :key to <component>, vuex state becomes broken.
Vue version is 2.6.10
UPDATE:
It does not work with simple property of product too:
{{ currentProduct.name }}
Summary:
There is the itemId property in. And computed property which depends on it. So computed property does not reflect changes when itemId prop changed while Vuex collection does not changed.
Confirmed:
Computed property renews only when state.products collection changed. I've emulate this by run createProduct action for each tab switching. Collection in vuex state accepts unwatched product stub and reflect changes to legal currentProduct with given itemId
UPDATE 2: component with watcher. Still no way...
#Component
export default class Product extends Vue {
#Prop({ type: Object, required: true })
readonly tabItem: ITabItem;
#State(productNamespace)
state: IProductState;
itemId: string;
created() {
//...
this.initCurrentProduct();
}
// No changes until state.products was changed.
get currentProduct(): IProduct | {} {
if (!this.state) return {};
return this.state.products[this.itemId];
}
#Watch('tabItem')
onTabItemChanged()
{
DEBUG && console.log('Tab changed: keep moving!');
this.initCurrentProduct();
}
private async initCurrentProduct() {
const { isNew, itemId } = this.tabItem;
if (itemId === this.itemId)
return;
DEBUG && console.log('ItemId changed.');
this.itemId = itemId;
// ...
}
// ...
}
Okay so the property you're passing to the dynamic component is currentTab.itemId which means itemId is actually an element in the currentTab object not the root Vue data object?
Vue does not track nested objects by default, it will only trigger redraw when the entire object is changed (for example if you do something like currentTab = {...}). You can either:
Use a watcher on currentTab with deep: true attribute: https://v2.vuejs.org/v2/api/#watch, and then trigger redraw with this.$forceUpdate whenever it is called.
Move itemId to the root of data and just update it from there
in your vuex mutation
let items = [...state.items]; // create a new copy
// mutate it
items.map(item => item.selected = true);
// return the new copy
state.items = items;

UI not updating when nested array property value deleted, only when added

I have a page where an object with nested array values are passed in from the parent component. The user can then, using a series of events and components manage the data in these subscriptions. Currently I'm facing an issue where when a subscriptionId is removed from the props, conditions on the page aren't changing, but they do when it's added.
Child Component
export default {
props: {
// Format of this object is:
// { "gameId": [
// 'subscriptionId',
// 'subscriptionId',
// ] }
subscriptions: {
type: Object,
required: true
}
},
watch: {
subscriptions: {
handler: function (newSubscriptions, oldSubscriptions) {
// NEVER gets fired when `subscriptionId` deleted from array list, but is fired when a new subscription is added
console.log('handler');
}
},
deep: true
}
},
I suspect this might be related to how I'm removing the array from the object. Essentially I'm copying the array, deleting the index in question and overwriting the original array. My hope with this approach is that the watcher wouldn't be needed but it appears to have no impact. Here's the code that exists on the parent component to update the subscriptions:
Parent Component
// Works great, don't have any issues here
handleSubscribed (subscriptionId) {
let newSubscriptions = [subscriptionId];
if (this.subscriptions.hasOwnProperty(this.currentGame.id)) {
newSubscriptions = this.subscriptions[this.currentGame.id];
newSubscriptions.push(subscriptionId);
}
this.$set(this.subscriptions, this.currentGame.id, newSubscriptions);
},
handleUnsubscribed (subscriptionId) {
// if there's more than one, delete only the one we're looking for
if (this.subscriptions.hasOwnProperty(this.currentGame.id) && this.subscriptions[this.currentGame.id].length > 1) {
let newSubscriptions = this.subscriptions[this.currentGame.id];
delete newSubscriptions[newChannels.indexOf(subscriptionId)];
this.$set(this.subscriptions, this.currentGame.id, newSubscriptions);
// shows my subscription has been removed, but GUI doesn't reflect the change
console.log('remove-game', newSubscriptions);
return;
}
this.$delete(this.subscriptions, this.currentGame.id);
},
I was hoping watch might be the solution, but it's not. I've looked over the reactive docs several times and don't see a reason for why this wouldn't work.
VueJS version: 2.5.7
Use Vue.delete instead of the delete keyword.
The object is no longer observable when using delete, therefore not reactive.
Delete a property on an object. If the object is reactive, ensure the deletion triggers view updates. This is primarily used to get around the limitation that Vue cannot detect property deletions, but you should rarely need to use it.