I'm attempting to both add and remove items in a multidimensional array stored in Vuex.
The array is a group of categories, and each category and have a sub-category (infinity, not simply a two dimensional array).
Example data set is something like this:
[
{
id: 123,
name: 'technology',
parent_id: null,
children: [
id: 456,
name: 'languages',
parent_id: 123,
children: [
{
id:789,
name: 'javascript',
parent_id: 456
}, {
id:987,
name: 'php',
parent_id: 456
}
]
}, {
id: 333,
name: 'frameworks',
parent_id 123,
children: [
{
id:777,
name: 'quasar',
parent_id: 333
}
]
}
]
}
]
....my question is, how do I best add and remove elements to this array, which is inside of a Vuex Store?
I normally manipulate simple arrays inside the Vuex Store using Vue.Set() to get reactivity. However, because I'm not sure how deep the nested array being manipulated is - I simply can't figure it out.
Here's an example of how I thought I could add a sub-category element using recursion:
export const append = (state, item) => {
if (item.parent_uid !== null) {
var categories = []
state.data.filter(function f (o) {
if (o.uid === item.parent_uid) {
console.log('found it')
o.push(item)
return o
}
if (o.children) {
return (o.children = o.children.filter(f)).length
}
})
} else {
state.data.push(item)
}
}
The first thing to understand is that vuex, or any other state management library based on flux architecture, isn't designed to handle nested object graph, let alone arbitrary/infinity nested objects that you mentioned. To make the matter worse, even with shallow state object, vuex works best when you define the shape of the state (all desired fields) upfront.
IMHO, there are two possible approaches you can take
1. Normalize your data
This is an approach recommended by vue.js team member [here][2].
If you really want to retain information about the hierarchical structure after normalization, you can use flat in conjunction with a transformation function to flatten your nested object by name to something like this:
const store = new Vuex.Store({
...
state: {
data: {
'technology': { id: 123, name: 'technology', parent_id: null },
'technology.languages': { id: 456, name: 'languages', parent_id: 123 },
'technology.languages.javascript': { id: 789, name: 'javascript', parent_id: 456 },
'technology.languages.php': { id: 987, name: 'php', parent_id: 456 },
'technology.frameworks': { id: 333, name: 'frameworks', parent_id: 123 },
'technology.frameworks.quasar': { id: 777, name: 'quasar', parent_id: 333 },
}
},
});
Then you can use Vue.set() on each item in state.data as usual.
2. Make a totally new state object on modification
This is the second approach mentioned in vuex's documentation:
When adding new properties to an Object, you should either:
Use Vue.set(obj, 'newProp', 123), or
Replace that Object with a fresh one
...
You can easily achieve this with another library: object-path-immutable. For example, suppose you want to add new category under languages, you can create a mutation like this:
const store = new Vuex.Store({
mutations: {
addCategory(state, { name, id, parent_id }) {
state.data = immutable.push(state.data, '0.children.0.children', { id, name, parent_id });
},
},
...
});
By reassigning state.data to a new object each time a modification is made, vuex reactivity system will be properly informed of changes you made to state.data. This approach is desirable if you don't want to normalize/denormalize your data.
Related
Given a list:
let names = [{name: "bobby"}, {name: "sydney"}, {name: "Paul"}, {name: "Grace"}
I want the output to be ["bobby", "sydney", "Paul", "Grace"]
Here is what I have tried:
var items = Object.keys(names).map(function(i) {
return names[i];
})
const items = Object.keys(names).map((key)=>names[key]);
this.setState({items});
console.log(this.state.items);
names.map(({ name }) => name)
const names = [{
name: "bobby"
}, {
name: "sydney"
}, {
name: "Paul"
}, {
name: "Grace"
}];
const keys = names.map(({
name
}) => name);
console.log(keys);
A note about react keys, they should be unique within the rendered siblings, i.e. they should be unique within the dataset. Names alone may not provide sufficient uniqueness.
A second note, you might not want to generate your react keys separately from where you need them, i.e. generally they are created when you are mapping JSX.
This is not really related to React. You can do that with JavaScript, for instance using API like map().
Here is an example:
let arr = names.map(obj => obj.name);
I want to remove an element from my array when click on a specific row.
When I click on an element it does nothing or the last row gets deleted.
I tried to remove the element like this:
ondeleterow(e: any) {
const array = [...this.state.fields.columns]; // make a separate copy of the array
const index = array.indexOf(e.target.id);
if (index !== -1) {
array.splice(index, 1);
this.setState({ fields: { columns: array }});
}
}
My array/json object looks like this:
[ {index: 0, name: "person 1", age: 12},
{index: 1, name: "person 2", age: 19},
{index: 2, name: "person 3", age: 34},
]
My result should be when I click on a row with ID=1 the row with index: 1 gets deleted from my state array.
I can't give them an Id because when I submit the json structure then does not get accepted.
I feel like your Array.splice might be causing the issue here (because even though you created a new array, the objects in the array are still passed by reference).
I would recommend a completely different method of doing this operation which I've found to be far cleaner and robust.
First you have to add a unique id field to each row. (this is good practice in react anyway, instead of using index for keys).
ondeleterow(id: string) {
return (e: any) => {
const array = this.state.fields.column.filter(item => item.id != id)
this.setState({ fields: { columns: array }});
}
}
and when you're mapping over your rows, you can simply add the function to the onClick like this
<Row key={item.id} onClick={ondeleterow(item.id)} />
Never use splice in react especially with state. They directly mutate the data. Use non mutating operations like slice.
Your code should as follows
ondeleterow(e: any) {
const array = [...this.state.fields.columns]; // make a separate copy of the array
const index = array.indexOf(e.target.id);
if (index !== -1) {
array.splice(index, 1);
this.setState({ fields: {
columns: [ ...array.slice(0, index), ...array.slice(index + 1, array.length) ]
}});
}
}
You can use Array.filter. This will allow you to create a new array with only the items you want based on a certain criteria. In this case, you want an array with items that have a different ID that the one you want to remove. So it will look like this
// Actual processing
const filterByIndex = (arr, idx) => arr.filter(x => x.index !== idx);
// Your data
const json = [{
index: 0,
name: "person 1",
age: 12
},
{
index: 1,
name: "person 2",
age: 19
},
{
index: 2,
name: "person 3",
age: 34
},
];
// Printing the result
console.log(filterByIndex(json, 1));
In your React app
ondeleterow(e: any) {
const columns = this.state.fields.columns.filter(x => x.index !== e.target.id);
this.setState({ fields: { columns }});
}
Try this
onDeleteRow(e) {
const afterRemoval = this.setState.fields.columns.filter(item => item.index != e.target.id);
this.setState(prevState => ({ fields: { ...prevState.fields, columns: afterRemoval } }));
}
The other solution above sets the fields field directly, It may work but will cause problem if fields has some other attribute other than columns (those attributes will get removed)
I have some json data in file called countryData.json structured as so:
{
"info":"success",
"stats":
[{
"id":"1",
"name":"USA",
"type":"WEST"
},
//...
I'm using graphQL to access this data. I have created an object type in the schema for countries using the following:
const CountryType = new GraphQLObjectType({
name: "Country",
fields: () => ({
id: { type: GraphQLID },
name: { type: GraphQLString },
type: { type: GraphQLString },
})
});
I want to write a query that will allow me to access all of the elements of this array that have a certain "name" value(There can be multiple with the same name). I've written the following query, but it only returns the first match in the array:
const RootQuery = new GraphQLObjectType({
name:"RootQueryType",
fields:{
country: {
type: CountryType,
args: { type: { name: GraphQLString } },
resolve(parent, args){
return _.find(countryData.stats, {name: args.name});
}
}
}
});
The "_" comes from const _ = require('lodash');
Also, how can I just get every single item in the array?
I have not recreated the code, therefore I can not check if it would be executed correctly. This is code, that should work in my opinion (without trying). If you want to return array of elements you need to implement https://lodash.com/docs/#filter. Filter will return all objects from stats, which match the argument name. This will return correctly inside resolver function, however, your schema needs adjustments to be able to return array of countries.
You need probably rewrite the arguments as follows as this is probably not correct. You can check out how queries or mutation arguments can be defined https://github.com/atherosai/express-graphql-demo/blob/feature/2-json-as-an-argument-for-graphql-mutations-and-queries/server/graphql/users/userMutations.js. I would rewrite it as follows to have argument "name"
args: { name: { type: GraphQLString } }
You need to add GraphQLList modifier, which defines, that you want to return array of CountryTypes from this query. The correct code should look something like this
const RootQuery = new GraphQLObjectType({
name:"RootQueryType",
fields:{
country: {
type: CountryType,
args: { name: { type: GraphQLString } },
resolve(parent, args){
return _.find(countryData.stats, {name: args.name});
}
},
countries: {
type: new GraphQLList(CountryType),
args: { name: { type: GraphQLString } },
resolve(parent, args){
return _.filter(countryData.stats, {name: args.name});
}
}
}
});
Now if you call query countries, you should be able to retrieve what you are expecting. I hope that it helps. If you need some further explanation, I made the article on implementing lists/arrays in GraphQL schema as I saw that many people struggle with similar issues. You can check it out here https://graphqlmastery.com/blog/graphql-list-how-to-use-arrays-in-graphql-schema
Edit: As for the question "how to retrieve every object". You can modify the code in resolver function in a way, that if the name argument is not specified you would not filter countries at all. This way you can have both cases in single query "countries".
I have entities in very nested JSON that already follow the normalizr format where the idAttribute is already the key where the object is defined:
groups: [{
id: 'foo',
families: {
smiths: {
people: [{
id: 'sam',
}, {
id: 'jake',
}],
},
jones: {
people: [{
id: 'john',
}, {
id: 'sue',
}],
},
},
}];
In this example, notice that the families attribute is using the id (smiths, jones) to identify the people who are an array of objects with ids.
The schemas for this might look like:
const person = new Entity('person');
const family = new Entity('family', {
people: [person],
}, {idAttribute: ???});
const group = new Entity('family', {
family: [family],
});
QUESTION: Is there a way to specify that a schema's idAttribute is the key where it is defined? In other words, how would I define the schema for Family as it's related to groups and people?
Another question, is there a way to denormalize a flattened state so that the families families: {[id]: obj} pattern stays the same as it is in the example json above?
Is there a way to specify that a schema's idAttribute is the key where it is defined?
Yes. The idAttribute function takes 3 arguments: value, parent, and key. Please read the docs. In your case, you can use the key, along with schema.Values
const family = new schema.Entity('families', {
people: [ person ]
}, (value, parent, key) => key);
const families = new schema.Values(family);
const group = new schema.Entity('groups', {
families
});
For denormalize, you'll need a separate schema for family, since the ID can't be derived from the key.
I have my state with the following structure:
{ messages:
[
{ id: 1, comments: []}
]
}
And I would like to add a new comment in my message, I have the message id so I can easily create a new state, loop over the messages, and add the new comment, but it doesn't seem to be the right way...
Thank you for your help.
Try this:
var commentToAdd = { id: 1, comment: "text" };
this.setState({ messages: [...this.state.messages.map(i => i.id === commentToAdd.id ? { id: i.id, comments: [...i.comments, commentToAdd.comment] } : i) ] });
The best practice is to keep state in the normalized shape, so instead of arrays, you would have objects indexed by id, and instead of storing comments inside messages you would store only ids.
For your example the state would look like this:
{
messages: {
byId: {
'1': { id: '1', comments: ['comment1'] },
...
},
allIds: ['1', ...]
},
comments: {
byId: {
'comment1': { id: 'comment1', ... },
...
},
allIds: ['comment1', ...]
}
}
In such structure in order to add a new comment you will have to update comments part and add a new id in messages[id].comments... but it's now much easier to update single comment.
For normalizing objects to such form I recommend normalizr.
More information about normalizing state shape in redux documentation Normalizing State Shape