update item in two dimensional array within the Vuex store - javascript

I would like to get into VueJs development and created a simple Minesweeper game.The two dimensional grid is managed by a Vuex state. When clicking on a cell I would like to reveal it so my current code is
[MutationTypes.REVEAL_CELL]: (state, { rowIndex, columnIndex }) => {
state.board[rowIndex][columnIndex].isRevealed = true;
}
Unfortunately this has no affect to the UI. This problem is known and described here
https://v2.vuejs.org/v2/guide/list.html#Caveats
The docs told me to use something like this
import Vue from "vue";
[MutationTypes.REVEAL_CELL]: (state, { rowIndex, columnIndex }) => {
const updatedCell = state.board[rowIndex][columnIndex];
updatedCell.isRevealed = true;
Vue.set(state.board[rowIndex], columnIndex, updatedCell);
Vue.set(state.board, rowIndex, state.board[rowIndex]);
}
but it did not help. Lastly I tried to create a copy of the board, modify the values and assign that copy to the board.
[MutationTypes.REVEAL_CELL]: (state, { rowIndex, columnIndex }) => {
const newBoard = state.board.map((row, mapRowIndex) => {
return row.map((cell, cellIndex) => {
if (mapRowIndex === rowIndex && cellIndex === columnIndex) {
cell = { ...cell, isRevealed: true };
}
return cell;
});
});
state.board = newBoard;
}
This didn't work neither. Does someone got an idea?
I created a Codesandbox showing my project
https://codesandbox.io/s/vuetify-vuex-and-vuerouter-d4q2b
but I think the only relevant file is /store/gameBoard/mutations.js and the function REVEAL_CELL

The problem is in Cell.vue and the issue is that you're checking an unchanging variable to determine the state of reveal. You've abstracted this.cell.isRevealed into a variable called isUnrevealed which is never told how to change after the initial load.
Option 1
isUnrevealed seems like an unnecessary convenience variable. If you get rid of isUnrevealed and change the references to it to !cell.isRevealed, the code will work as expected.
Option 2
If you're set on using this variable, change it to a computed so that it constantly updates itself whenever the Vuex state propagates a change to the cell isRevealed prop:
computed: {
isUnrevealed() {
return !this.cell.isRevealed;
}
}
If you go this route, don't forget to remove the property from data and remove the assignment in mounted (first line).
You'll also have the same problem with isMine and cellStyle. So, completely remove data and mounted and make them both computed as well.
computed: {
isMine() {
return this.cell.isMine;
},
cellStyle() {
if (!this.cell.isRevealed) {
return "unrevealedCell";
} else {
if (this.isMine) {
return "mineCell";
} else {
let neighbourCountStyle = "";
... // Switch statement
return `neutralCell ${neighbourCountStyle}`;
}
}
}
}

Related

Why my delete function is not returning on page array after I delete element from it

Below is the delete function which is working, it takes an argument and deletes the element with the index in the argument.
Setitem is a useState hook and is defined like this
when we click the on add list is added to item array
const listofitem = () => {
setitems((e) => {
return [...e, list];
});
};
This function is working.
const deleteItems = (e) => {
setitems((e1)=> {
return e1.filter( (er, index)=> {
return index!=e;
});
});
}
Why is this not working the splice method is working fine.
const deleteItems = (e) => {
items.splice(e-1, 1);
setitems((e1)=>{
return e1;
})
};
here is the code sandbox for the same, the function is working in this. But I want to know why the other is not working.
https://codesandbox.io/s/old-wood-cbnq86?file=/src/App.js:0-1169
This is how I got it to work, in your example you try to mutate the state directly and then not really set the state. If you want to use the state on setState you need to mutate what you call e1.
However you have some issues with your codesandbox that will trigger the function more than once sometimes
setitems((e1) => {
e1.splice(e, 1);
return [...e1];
});
Edit author updated question, this is not longer relevant.
In the first deleteItems function you never use e which I assume is the index you want to delete.
const deleteItems = (deleteIndex) => {
setitems((list)=> list.filter((_, index)=> index !== deleteIndex));
};

How do i return an object from my react state

I am trying to find an item from a collection, from the code below, in order to update my react component, the propertState object isnt empty, it contains a list which i have console logged, however I seem to get an underfined object when i console log the value returned from my findProperty function... I am trying update my localState with that value so that my component can render the right data.
const PropertyComponent = () => {
const { propertyId } = useParams();
const propertyState: IPropertiesState = useSelector(
propertiesStateSelector
);
const[property, setProperty] = useState()
const findProperty = (propertyId, properties) => {
let propertyReturn;
for (var i=0; i < properties.length; i++) {
if (properties[i].propertyId === propertyId) {
propertyToReturn = properties[i];
break;
}
}
setProperty(propertyReturn)
return propertyReturn;
}
const foundProperty = findProperty(propertyId, propertyState.properties);
return (<>{property.propertyName}</>)
}
export default PropertyComponent
There are a few things that you shall consider when you are finding data and updating states based on external sources of data --useParams--
I will try to explain the solution by dividing your code in small pieces
const PropertyComponent = () => {
const { propertyId } = useParams();
Piece A: Consider that useParams is a hook connected to the router, that means that you component might be reactive and will change every time that a param changes in the URL. Your param might be undefined or an string depending if the param is present in your URL
const propertyState: IPropertiesState = useSelector(
propertiesStateSelector
);
Piece B: useSelector is other property that will make your component reactive to changes related to that selector. Your selector might return undefined or something based on your selection logic.
const[property, setProperty] = useState()
Piece C: Your state that starts as undefined in the first render.
So far we have just discovered 3 pieces of code that might start as undefined or not.
const findProperty = (propertyId, properties) => {
let propertyReturn;
for (var i=0; i < properties.length; i++) {
if (properties[i].propertyId === propertyId) {
propertyToReturn = properties[i];
break;
}
}
setProperty(propertyReturn)
return propertyReturn;
}
const foundProperty = findProperty(propertyId, propertyState.properties);
Piece D: Here is where more problems start appearing, you are telling your code that in every render a function findProperty will be created and inside of it you are calling the setter of your state --setProperty--, generating an internal dependency.
I would suggest to think about the actions that you want to do in simple steps and then you can understand where each piece of code belongs to where.
Let's subdivide this last piece of code --Piece D-- but in steps, you want to:
Find something.
The find should happen if you have an array where to find and a property.
With the result I want to notify my component that something was found.
Step 1 and 2 can happen in a function defined outside of your component:
const findProperty = (propertyId, properties) => properties.find((property) => property.propertyId === propertyId)
NOTE: I took the liberty of modify your code by simplifying a little
bit your find function.
Now we need to do the most important step, make your component react at the right time
const findProperty = (propertyId, properties) => properties.find((property) => property.propertyId === propertyId)
const PropertyComponent = () => {
const { propertyId } = useParams();
const propertyState: IPropertiesState = useSelector(
propertiesStateSelector
);
const[property, setProperty] = useState({ propertyName: '' }); // I suggest to add default values to have more predictable returns in your component
/**
* Here is where the magic begins and we try to mix all of our values in a consistent way (thinking on the previous pieces and the potential "undefined" values) We need to tell react "do something when the data is ready", for that reason we will use an effect
*/
useEffect(() => {
// This effect will run every time that the dependencies --second argument-- changes, then you react afterwards.
if(propertyId, propertyState.properties) {
const propertyFound = findProperty(propertyId, propertyState.properties);
if(propertyFound){ // Only if we have a result we will update our state.
setProperty(propertyFound);
}
}
}, [propertyId, propertyState.properties])
return (<>{property.propertyName}</>)
}
export default PropertyComponent
I think that in this way your intention might be more direct, but for sure there are other ways to do this. Depending of your intentions your code should be different, for instance I have a question:
What is it the purpose of this component? If its just for getting the property you could do a derived state, a little bit more complex selector. E.G.
function propertySelectorById(id) {
return function(store) {
const allProperties = propertiesStateSelector(store);
const foundProperty = findProperty(id, allProperties);
if( foundProperty ) {
return foundProperty;
} else {
return null; // Or empty object, up to you
}
}
}
Then you can use it in any component that uses the useParam, or just create a simple hook. E.G.
function usePropertySelectorHook() {
const { propertyId } = useParams();
const property = useSelector(propertySelectorById(propertyId));
return property;
}
And afterwards you can use this in any component
functon AnyComponent() {
const property = usePropertySelectorHook();
return <div> Magic {property}</div>
}
NOTE: I didn't test all the code, I wrote it directly in the comment but I think that should work.
Like this I think that there are even more ways to solve this, but its enough for now, hope that this helped you.
do you try this:
const found = propertyState.properties.find(element => element.propertyId === propertyId);
setProperty(found);
instead of all function findProperty

VueJS component does not update data

I know there is many similar questions about data not being updated in VueJS component but I still could not find the answer. I have the following component:
<template>
<div class="mt-5">
[...]
<div v-for="dataSource in dataSources" v-bind:key="dataSource.id">
{{ dataSource}}
</div>
[...]
</div>
</template>
<script>
import { chain, find, merge, toUpper } from "lodash";
import Mixins from "../utils/Mixins.vue";
import Pagination from "../utils/Pagination.vue";
import DataSourceTable from "./DataSourceTable.vue";
export default {
mixins: [Mixins],
components: {
"data-source-table": DataSourceTable,
"data-source-pagination": Pagination
},
data: function() {
return {
dataSources: [],
//[...]
};
},
methods: {
getAllDataSources(page) {
//[...]
},
search() {
//[...]
},
setSortAttribute(attribute) {
//[...]
},
updateDataSource(updatedDataSource){
for (let i = 0; i < this.dataSources.length; i++) {
if (this.dataSources[i].id == updatedDataSource.id) {
this.dataSources[i] = updatedDataSource;
break; // Stop this loop, we found it!
}
}
}
},
created: function() {
this.getAllDataSources(this.currentPage);
// Capture updated data source via websocket listener
this.$options.sockets.onmessage = function(data) {
let message = JSON.parse(data.data);
if (message.id == "dataSource" && message.type == "data") {
let updatedDataSource = message.payload.data.listen.relatedNode;
this.updateDataSource(updatedDataSource);
}
}
}
};
</script>
In the created hook, I capture changes coming from a websocket and I update the corresponding item in the array dataSources. I can see the item is properly updated in Vue Dev Tools but it is still not updated in the component template. Example below:
This is a common mistake with vuejs.
You can check the document here: Why isn’t the DOM updating?
In Your case, you can use push or $set to achieve your purpose.
When you modify an Array by directly setting an index (e.g. arr[0] = val) or modifying its length property. Similarly, Vue.js cannot pickup these changes. Always modify arrays by using an Array instance method, or replacing it entirely. Vue provides a convenience method arr.$set(index, value) which is syntax sugar for arr.splice(index, 1, value).
Example:
Replace this.dataSources[i] = updatedDataSource;
By this.dataSources.splice(i, 1, updatedDataSource);
When you modify an Array by directly setting an index (e.g. arr[0] = val) or modifying its length property. Similarly, Vue.js cannot pickup these changes. Always modify arrays by using an Array instance method, or replacing it entirely. Vue provides a convenience method arr.$set(index, value) which is syntax sugar for arr.splice(index, 1, value).
updateDataSource(updatedDataSource){
for (let i = 0; i < this.dataSources.length; i++) {
if (this.dataSources[i].id == updatedDataSource.id) {
//this.dataSources[i] = updatedDataSource;
this.dataSources.$set(i, updatedDataSource);
break; // Stop this loop, we found it!
}
}
}

The value of object's property is not being changed in the Local Storage in Ember

I'm building a shop in Ember with the list of products that are being added to Local Storage when the user clicks on the
add to cart button. Each product is an object that has a property called ordered_quantity, I'm trying to change this property's value when the user tries to remove the product from the cart. (example: ordered quantity: 8, when the button is clicked it should be 7).
I have the following code in my service file:
remove(item) {
let new_arr = this.get('items');
let elementIndex = new_arr.findIndex(obj => {
return obj.id === item.id;
});
if (elementIndex !== -1) {
new_arr[elementIndex].ordered_quantity = new_arr[elementIndex].ordered_quantity - 1;
}
this.set('cart.items', new_arr);
}
I'm using Local Storage add-on (https://github.com/funkensturm/ember-local-storage#methods)
and I have the following action:
actions: {
removeFromCart(){
this.get('cart').remove(this.product);
}
}
When I try to run the following code I get an error:
Uncaught Error: Assertion Failed: You attempted to update [object Object].ordered_quantity to "7", but it is being tracked by a tracking context, such as a template, computed property, or observer. In order to make sure the context updates properly, you must invalidate the property when updating it. You can mark the property as #tracked, or use #ember/object#set to do this.
I tried using the set function like this:
let updated = item.ordered_quantity - 1;
set(item, 'ordered_quantity', updated);
https://api.emberjs.com/ember/release/functions/#ember%2Fobject/set and the code worked with no errors as expected, but the value of my property ordered_quantity was not updated in the Local Storage.
You need to adapt the set function to your use-case:
remove(item) {
let new_arr = this.get('items');
let elementIndex = new_arr.findIndex(obj => {
return obj.id === item.id;
});
if (elementIndex !== -1) {
set(new_arr[elementIndex], 'ordered_quantity', new_arr[elementIndex].ordered_quantity - 1);
}
this.set('cart.items', new_arr);
}
Based of #Lux's original answer (and to also update for the Ember 3.16+ era), I wanted to add that you could use functional programming patterns to clean things up a little bit:
If in your cart Service, items is #tracked, like so:
import Service from '#ember/service';
import { tracked } from '#glimmer/tracking';
export default class CartService extends Service {
#tracked items = [];
}
Note: assumes that ordered_quantity is not tracked.
remove(item) {
for (let obj of this.items) {
if (item.id !== obj.id) continue;
// set must be used because quantity is not known to the tracking system
set(obj, 'ordered_quantity', obj.ordered_quantity - 1);
});
// invalidate the reference on the card service so that all things that
// reference cart.items re-compute
this.cart.items = this.items;
}
Here is a live demo: https://ember-twiddle.com/e18433b851091b527512e27ae792640c?openFiles=components.demo%5C.js%2C
There is also a utility addon, which allows for more ergonomic interaction with arrays, objects, etc: https://github.com/pzuraq/tracked-built-ins
the above code would then look like this:
import Service from '#ember/service';
import { tracked } from '#glimmer/tracking';
import { TrackedArray } from 'tracked-built-ins';
export default class CartService extends Service {
#tracked items = new TrackedArray([]);
}
and then in your component:
remove(item) {
for (let obj of this.items) {
if (item.id !== obj.id) continue;
// set must be used because quantity is not known to the tracking system
set(obj, 'ordered_quantity', obj.ordered_quantity - 1);
});
}
Here is a live demo: https://ember-twiddle.com/23c5a7efdb605d7b5fa9cd9da61c1294?openFiles=services.cart%5C.js%2C
You could then take it a step further, and add tracking to your "items".
so then your remove method would look like this:
remove(item) {
for (let obj of this.items) {
if (item.id !== obj.id) continue;
obj.ordered_quantity = obj.ordered_quantity - 1;
});
}
And here is a live demo: https://ember-twiddle.com/48c26e2e0f0e5f3ac7685e4bdc0eda4e?openFiles=components.demo%5C.js%2C

Error: Attempting to set key on an object that is immutable and has been frozen

I am having a rather hard time tracking down the issue related to this error, obviously the implication is I'm trying to update an immutable object. Oddly, I have used this implementation before without a hitch which is why I am finding this behaviour so perplexing.
Below is the method I am using, it simply alters the object of an array, changing the property and returns the updated state. As referenced here, using prevState appears to be the optimal way for getting around immutability.
onCheck = (id) => {
this.setState((prevState) => {
prevState.options[id].checked = !prevState.options[id].checked;
return {
options: prevState.options,
dataSource: prevState.cloneWithRows(prevState.options)
};
});
}
I have also tried a number of variations of copying the prevState, however it is still giving me the same immutability error. It appears as if it still references rather than duplicates the prevState.
onCheck = (id) => {
this.setState((prevState) => {
let options = [...prevState.options];
options[id].checked = !options[id].checked;
return {
options: options,
dataSource: this.state.cloneWithRows(options)
};
});
}
I eventually found a solution, it appears I needed to copy not just the array but each element of the array. As the individual elements / objects of the array are still immutable / frozen.
onCheck = (id) => {
this.setState((prevState) => {
const newOptions = prevState.options.map((option, index) => {
let copyOption = {...option};
if (id == index) {
copyOption.checked = !copyOption.checked;
}
return copyOption;
});
return {
options: newOptions,
dataSource: this.state.dataSource.cloneWithRows(newOptions)
};
});
}
In your question, you acknowledge that you are mutating the state, which you cannot do. A fix for this is to clone prevState.options before fiddling with it.
eg.
var options = Object.assign({}, prevState.options)
options[id].checked
return {
options,
...
As your reference pointed out, you should not directly mutate your data like you do
prevState.options[id].checked = !prevState.options[id].checked;
In your link, they return a new array by concat or by "spread syntax" (...). So i suggest you to copy your prevState first and mutate it
let state = [...prevState.options];
state[id].checked = !state[id].checked;
...

Categories