Is my Vue.js computed function recursive? - javascript

I have a vue computed function and would like to know if there is a recursion in my code.
I have some comma seperated states within my prop this.searchQuery['by-multiple-states']. My computed function statesSearchQuery should be a 'wrapper' for my prop, it should just give me a way to work with my prop not as a string but as a array.
The getter just returns the states as array. The setter should toggle a given 'val'. If within the array, remove it, else add it. Finaly apply the changes on the prop itself.
Because the getter and setter is both called with this.statesSearchQuery and the getter is used within the first line of my setter, I'm not sure if there would be a weird interaction. Could someone enlighten me about this situation?
computed: {
statesSearchQuery: {
get() {
return this.searchQuery['by-multiple-states'].split(',').filter(t => t !== '');
},
set(val) {
let state = this.statesSearchQuery;
if (state.includes(val)) {
state = state.filter(t => t !== val);
} else {
state.push(val);
}
this.searchQuery['by-multiple-states'] = state.join(',');
},
},
},
Update: In retrospect, I can rephrase the question. Is it ok to use the getter of a computed function in the setter of the same?

Related

How to make props equal to state that does not exist yet?

Solved Thank you for your help
I am setting props of component
<Component myprops={state_variable}/>
The problem is that when I am creating the component and setting the props the state variable does not exist yet and my code breaks. What can I do to solve this problem? In addition when I change the state the prop is not updated.
<ServiceTicket
showOverlay={Tickets_disabled_withError[ticket_num]?.isDisabled}
showSelectedError={Tickets_disabled_withError[ticket_num]?.showError}
/>
My intial state initial variable:
const [Tickets_disabled_withError,setTickets_disabled_withError] = useState({})
I am trying to call function that will update state and change value that props is equal to.
const OverLayOnAll = (enable) =>
{
let tempobject = Tickets_disabled_withError
for (let key in tempobject)
{
if (enable == "true")
{
tempobject[key].isDisabled = true
}
else if (enable == "false")
{
tempobject[key].isDisabled = false
}
}
setTickets_disabled_withError(tempobject)
}
I fixed the issue. Thank you so much for your help. I had to set use optional chaining ?. and also re render the component.
The value exists. It's just that the value itself is undefined. You need to set an initial value when defining your state
const [statevariable, setstatevariable] = useState({
somekey: {
isDisabled: false // or whatever the initial value should be
}
}) // or whatever else you need it to be
For your second problem, you are using the same pointer. JavaScript does equality by reference. You've transformed the existing value, so React doesn't detect a change. The easiest way to fix this is to create a shallow copy before you start transforming
let tempobject = {...Tickets_disabled_withError}
Your question isn't very clear to me, but there's a problem in your setTickets_disabled_withError call.
When you update a state property (ticketsDisabledWithError) using its previous value, you need to use the callback argument.
(See https://reactjs.org/docs/state-and-lifecycle.html#state-updates-may-be-asynchronous)
overlayAll = (enable)=> {
setTicketsDisabledWithError((ticketsDisabledWithError)=> {
return Object.keys(ticketsDisabledWithError).reduce((acc,key)=> {
acc[key].isDisabled = (enabled=="true");
return acc;
}, {}); // initial value of reduce acc is empty object
})
}
Also, please learn JS variable naming conventions. It'll help both you, and those who try to help you.

Make setter an action using Mobx makeObservable in presence of getter

In mobx if I want to use interheritance I need to use makeObservable rather than makeAutoObservable. But using makeObservable requires I name the actions that mutate state so how can I declare a setter to be an action given it has the same name as the getter?
In other words what goes where I wrote SETTER_FOR_MYVAR or what is another way to achieve the same effect?
class BaseClass {
_myvar = null
set myvar(val) {
this._myvar = val;
}
get myvar() {
return this._myvar;
}
other_action() {
this._myvar = 5;
}
constructor() {
makeObservable(this, {
_myvar: observable,
other_action: action,
SETTER_FOR_MYVAR: action
});
}
}
Yes, I know I could farm it out to yet another helper function _myvar_setter and declare that an action but that seems ugly and I'm hoping there is a better way.
Just mark myvar as computed, everything should work out of the box (If I understand correctly what you want):
constructor() {
makeObservable(this, {
_myvar: observable,
myvar: computed,
other_action: action
});
}
Codesandbox
Excerpt from the docs:
It is possible to define a setter for computed values as well. Note that these setters cannot be used to alter the value of the computed property directly, but they can be used as an "inverse" of the derivation. Setters are automatically marked as actions.
Example:
class Dimension {
length = 2
constructor() {
makeAutoObservable(this)
}
get squared() {
return this.length * this.length
}
set squared(value) {
this.length = Math.sqrt(value)
}
}
More info in the docs

vue.js watch not updated

I'm new to vue.
I'm now trying to update a couple of variables based on the change of another computed variable.
This computed variable is taking the values from a Vuex store and works as should. I see the values change.
In order to calculate the derived variables I've created a watch that watches the computed variable and then updates these derived values.
This watch is called two times during start-up and then no longer, although the computed values keeps updating.
What am I doing wrong.
This is working:
...
computed: {
lastAndMarkPrice() {
return store.getters.getLastAndMarkPriceByExchange(
"deribit",
store.getters.getAsset
);
},
...
this part is not working:
...
data: () => ({
lastPriceUp: false,
lastPriceDn: false,
markPriceUp: false,
markPriceDn: false,
}),
...
watch: {
lastAndMarkPrice (newValue, oldValue) {
console.log(newValue, oldValue);
this.lastPriceUp = newValue.lastPrice > oldValue.lastPrice;
this.lastPriceDn = newValue.lastPrice < oldValue.lastPrice;
this.markPriceUp = newValue.markPrice > oldValue.markPrice;
this.markPriceDn = newValue.markPrice < oldValue.markPrice;
},
},
...
By default a watch is shallow. If a new object is assigned to lastAndMarkPrice then the handler will be called but it won't check for mutations of properties within that object.
To create a deep watcher you'd do something like this:
watch: {
lastAndMarkPrice: {
deep: true,
handler (newValue, oldValue) {
// ...
}
}
}
https://v2.vuejs.org/v2/api/#watch
Usually that would be the correct solution but your use-case is slightly more complicated because you need access to the old values. Using a deep watcher won't help with that as you'll just be passed the same object.
To get around that problem you'll need to take copies of the old values somewhere so that you still have them available to compare with the new values. One way to do that would be to have the computed property take a copy:
computed: {
lastAndMarkPrice() {
const prices = store.getters.getLastAndMarkPriceByExchange(
"deribit",
store.getters.getAsset
);
// I'm assuming that prices is initially null or undefined.
// You may not need this bit if it isn't.
if (!prices) {
return null;
}
return {
lastPrice: prices.lastPrice,
markPrice: prices.markPrice
}
}
}
With the code above, each time the values of lastPrice or markPrice change it will re-run the computed property and create a new object. That will trigger the watch handler and, importantly, you'll get two different objects passed as the old and new values. You don't need to use deep in this case as the object itself is changing, not just the properties within it.
You could also shorten it a little with...
return { ...prices }
...rather than explicitly copying the two properties across.

Why isn't the mobx #computed value?

Simple: the computed value isn't updating when the observable it references changes.
import {observable,computed,action} from 'mobx';
export default class anObject {
// THESE WRITTEN CHARACTERISTICS ARE MANDATORY
#observable attributes = {}; // {attribute : [values]}
#observable attributeOrder = {}; // {attribute: order-index}
#observable attributeToggle = {}; // {attribute : bool}
#computed get orderedAttributeKeys() {
const orderedAttributeKeys = [];
Object.entries(this.attributeOrder).forEach(
([attrName, index]) => orderedAttributeKeys[index] = attrName
);
return orderedAttributeKeys;
};
changeAttribute = (existingAttr, newAttr) => {
this.attributes[newAttr] = this.attributes[existingAttr].slice(0);
delete this.attributes[existingAttr];
this.attributeOrder[newAttr] = this.attributeOrder[existingAttr];
delete this.attributeOrder[existingAttr];
this.attributeToggle[newAttr] = this.attributeToggle[existingAttr];
delete this.attributeToggle[existingAttr];
console.log(this.orderedAttributeKeys)
};
}
After calling changeAttribute, this.orderedAttributeKeys does not return a new value. The node appears unchanged.
However, if I remove the #computed and make it a normal (non-getter) function, then for some reason this.orderedAttributeKeys does display the new values. Why is this?
EDIT: ADDED MORE INFORMATION
It updates judging by logs and debugging tools, but doesn't render on the screen (the below component has this code, but does NOT re-render). Why?
{/* ATTRIBUTES */}
<div>
<h5>Attributes</h5>
{
this.props.appStore.repo.canvas.pointerToObjectAboveInCanvas.orderedAttributeKeys.map((attr) => { return <Attribute node={node} attribute={attr} key={attr}/>})
}
</div>
pointerToObjectAboveInCanvas is a variable. It's been set to point to the object above.
The changeAttribute function in anObject is called in this pattern. It starts in the Attribute component with this method
handleAttrKeyChange = async (existingKey, newKey) => {
await this.canvas.updateNodeAttrKey(this.props.node, existingKey, newKey);
this.setState({attributeEdit: false}); // The Attribute component re-renders (we change from an Input holding the attribute prop, to a div. But the above component which calls Attribute doesn't re-render, so the attribute prop is the same
};
which calls this method in another object (this.canvas)
updateNodeAttrKey = (node, existingKey, newKey) => {
if (existingKey === newKey) { return { success: true } }
else if (newKey === "") { return { success: false, errors: [{msg: "If you'd like to delete this attribute, click on the red cross to the right!"}] } }
node.changeAttribute(existingKey, newKey);
return { success: true }
};
Why isn't the component that holds Attribute re-rendering? It's calling orderedAttributeKeys!!! Or am I asking the wrong question, and something else is the issue...
An interesting fact is this same set of calls happens for changing the attributeValue (attribute is the key in anObject's observable dictionary, attributeValue is the value), BUT it shows up (because the Attribute component re-renders and it pulls directly from the node's attribute dictionary to extract the values. Again, this is the issue, an attribute key changes but the component outside it doesn't re-render so the attribute prop doesn't change?!!!
It is because you have decorated changeAttribute with the #action decorator.
This means that all observable mutations within that function occur in a single transaction - e.g. after the console log.
If you remove the #action decorator you should see that those observables get updated on the line they are called and your console log should be as you expect it.
Further reading:
https://mobx.js.org/refguide/action.html
https://mobx.js.org/refguide/transaction.html
Try to simplify your code:
#computed
get orderedAttributeKeys() {
const orderedAttributeKeys = [];
Object.entries(this.attributeOrder).forEach(
([attrName, index]) => orderedAttributeKeys[index] = this.attributes[attrName])
);
return orderedAttributeKeys;
};
#action.bound
changeAttribute(existingAttr, newAttr) {
// ...
};
Also rename your Store name, Object is reserved export default class StoreName

How to modify a 'value' prop in VueJS before `$emit('input')` finishes updating it

I have a question about creating VueJS components that are usable with v-model which utilise underlying value prop and $emit('input', newVal).
props: {
value: Array
},
methods: {
moveIdToIndex (id, newIndex) {
const newArrayHead = this.value
.slice(0, newIndex)
.filter(_id => _id !== id)
const newArrayTail = this.value
.slice(newIndex)
.filter(_id => _id !== id)
const newArray = [...newArrayHead, id, ...newArrayTail]
return this.updateArray(newArray)
},
updateArray (newArray) {
this.$emit('input', newArray)
}
}
In the above code sample, if I do two modifications in quick succession, they will both be executed onto the "old array" (the non-modified value prop).
moveIdToIndex('a', 4)
moveIdToIndex('b', 2)
In other words, I need to wait for the value to be updated via the $emit('input') in order for the second call to moveIdToIndex to use that already modified array.
Bad solution 1
One workaround is changing updateArray to:
updateArray (newArray) {
return new Promise((resolve, reject) => {
this.$emit('input', newArray)
this.$nextTick(resolve)
})
}
and execute like so:
await moveIdToIndex('a', 4)
moveIdToIndex('b', 2)
But I do not want to do this, because I need to execute this action on an array of Ids and move them all to different locations at the same time. And awaiting would greatly reduce performance.
Bad solution 2
A much better solution I found is to just do this:
updateArray (newArray) {
this.value = newArray
this.$emit('input', newArray)
}
Then I don't need to wait for the $emit to complete at all.
However, in this case, VueJS gives a console error:
Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "value"
Does anyone have any better solution?
OK. These are your options as far as I understand your use case and application.
First of all, don't mutate the props directly save the props internally and then modify that value.
props: {
value: Array
},
data() {
return {
val: this.value
}
}
If the next modification to the array is dependent on the previous modification to the array you can't perform them simultaneously. But you need it to happen fairly quickly ( i will assume that you want the user to feel that it's happening quickly ). What you can do is perform the modification on the val inside the component and not make it dependent on the prop. The val variable is only initialized when the component is mounted. This way you can modify the data instantly in the UI and let the database update in the background.
In other words, your complete solution would look like this:
props: {
value: Array
},
data () {
return {val: this.value}
},
methods: {
moveIdToIndex (id, newIndex) {
const newArrayHead = this.val
.slice(0, newIndex)
.filter(_id => _id !== id)
const newArrayTail = this.val
.slice(newIndex)
.filter(_id => _id !== id)
const newArray = [...newArrayHead, id, ...newArrayTail]
return this.updateArray(newArray)
},
updateArray (newArray) {
this.val = newArray
this.$emit('input', newArray)
}
}
This solution fixes your problem and allows you to execute moveIdToIndex in quick succession without having to await anything.
Now if the array is used in many places in the application next best thing would be to move it to a store and use it as a single point of truth and update that and use that to update your component. Your state will update quickly not simultaneously and then defer the update to the database for a suitable time.
Emit a message to the parent to change the prop.
Put a watcher on the prop (in the child) and put your code to use the new value there.
This keeps the child from mutating the data it does not own, and allows it to avoid using nextTick. Now your code is asynchronous and reactive, without relying on non-deterministic delays.
How about making copy of the value ?
moveIdToIndex (id, newIndex) {
const valueCopy = [...this.value]
const newArrayHead = this.valueCopy
.slice(0, newIndex)
.filter(_id => _id !== id)
const newArrayTail = this.valueCopy
.slice(newIndex)
.filter(_id => _id !== id)
const newArray = [...newArrayHead, id, ...newArrayTail]
return this.updateArray(newArray)

Categories

Resources