I have an indexed list of users in the JS object (not array). It's part of the React state.
{
1: { id: 1, name: "John" }
2: { id: 2, name: "Jim" }
3: { id: 3, name: "James" }
}
What's the best practice to:
add a new user { id: 4, name: "Jane" } with id (4) as key
remove a user with id 2
change the name of user #2 to "Peter"
Without any immutable helpers. I'm using Coffeescript and Underscore (so _.extend is ok...).
Thanks.
This is what i would do
add: var newUsers = _.extend({}, users, { 4: { id: 4, ... } })
remove: var newUsers = _.extend({}, users) then delete newUsers['2']
change: var newUsers = _.extend({}, users) then newUsers['2'].name = 'Peter'
If you're not using Flux, you use this.setState() to update the state object.
delUser(id) {
const users = this.state.users;
delete users[id];
this.setState(users);
}
addChangeUser(id, name) {
const users = this.state.users;
users[id] = {id: id, name: name};
this.setState(users);
}
Then you can execute your test cases with this:
addChangeUser(4, 'Jane);
addChangeUser(2, 'Peter');
delUser(2);
Apart from _.extend you can use Map for storing user
let user = new Map();
user.set(4, { id: 4, name: "Jane" }); //adds with id (4) as key
user.myMap.set(2, { id: 2, name: "Peter" }); // set user #2 to "Peter"
user.delete(3); //deletes user with id 3
setState also accepts a function, which you might find more intuitive
function add( user ) {
this.setState( users => {
users[ user.id ] = user
return users
}
}
function remove( id ) {
this.setState( users => {
delete users[ id ]
return users
}
These functions assume that your state object is your users object, if it is actually state.users then you'd have to pick users out, passing a function to setState will always be called passing the actual state object.
In this example add can also be used to amend, depending on your actual use-case you may want to create separate helpers.
Using spreads:
Adding
this.setState({
...this.state,
4: { id: 4, name: "Jane" },
}
Removing id 2
let prevState = this.state;
let {"2": id, ...nextState} = prevState;
this.setState({
...nextState,
}
Changing id 2
this.setState({
...this.state,
2: {
...this.state["2"],
name: "Peter",
}
}
Related
I have a possible infinite category tree and I would like to add, update or remove categories at any level with setState in react. I know this is possible with recursion but I don't have enough experience to manage this problem on my own. Here is how the data could possible look like:
const categories = [
{
id: "1",
name: "category1",
subCategories: [
{
id: "sub1",
name: "subcategory1",
subCategories: [
{ id: "subsub1", name: "subsubcategory1", subCategories: [] },
{ id: "subsub2", name: "subsubcategory2", subCategories: [] }
]
},
{ id: "sub2", name: "subcategory2", subCategories: [] }
]
},
{
id: "2",
name: "category2",
subCategories: []
}
]
Considering that your top level categories object is an object and not an array, the add and remove function could be the following (same pattern for update)
function add (tree, newCategory, parentId) {
if(tree.id === parentId)
return {
...tree,
subCategories: tree.subCategories.concat(newCategory)
}
return {
...tree,
subCategories: tree.subCategories.map(c => add(c, newCategory, parentId))
}
}
function remove (tree, idToRemove) {
if(tree.subCategories.map(c => c.id).includes(idToRemove))
return {
...tree,
subCategories: tree.subCategories.filter(c => c.id !== idToRemove)
}
return {
...tree,
subCategories: tree.subCategories.map(c => remove(c, idToRemove))
}
}
Prologue
To update a nested property in an immutable way, you need to copy or perform immutable operations on all its parents.
Setting a property on a nested object:
return {
...parent,
person: {
...parent.person,
name: 'New Name'
}
}
Arrays: You may pre-clone the array, or use a combination of .slice(), .map(), .filter() and the concatenation operator (...); Warning: .splice mutates the array.
(this can be a long topic, so I just gave a veryfast overview)
immer
As this can quickly get very ugly on objects with deep nesting, using the immer lib quite becomes a must at some point. Immer "creates new immutable state from mutations".
const newState = immer(oldState, draft => {
draft[1].subcategories[3].subcategories[1].name = 'Category'
})
In the extreme case, you can combine immer with lodash to create mutations in arbitrary places:
import set from 'lodash/set'
const newState = immer(oldState, draft => {
set(draft, [0, 'subcategories', 5, 'subcategories', 3], { id: 5, name:'Cat' })
})
"You might not need lodash" website has the recursive implementation for lodash/set. But, seriously, just use lodash.
PLUSES:
If you are using redux-toolkit, immer already is auto-applied on reducers, and is exposed as createNextState (check docs with care).
Deeply nested state can be an interesting use case for normalizr (long talk).
this is how the recursive function would look like.
the arguments:
id: id to look for
cats: the categories array to loop
nestSubCategory: (boolean) if we want to add the subcategory object in the subCategories array or not
subCategory: the category object we want to insert
const addCategories = (id, cats, nestSubCategory, subCategory)=> {
const cat = cats.find(item=> item.id === id)
const arrSubs = cats.filter(item => item.subCategories?.length)
.map(item => item.subCategories)
if(cat){
if(nestSubCategory){
cat.subCategories.push(subCategory)
return
}else{
cats.push(subCategory)
return
}
}
else{
return addCategories(id, arrSubs[0], nestSubCategory, subCategory)
}
}
addCategories("blabla1", categories, true, { id: "blabla2", name: "blablacategory1", subCategories: [] })
//console.log(categories)
console.log(
JSON.stringify(categories)
)
remember to update the object in the state replacing the entire categories array once the function is executed.
be careful with recursion 🖖🏽
you can do in a similar way to remove items, i leave to you the pleasure to experiment
I have a list of members and each member has the same set of applications. I created 2 classes to associate each member and application and ended up with a general list of members and applications, which I wrote in the initial state.
Then, using the function, I get the ID of the member that was clicked and I want to change the state of a particular application for it from false to true. But I get a change in the state of the application from false to work for ALL members. Why is this happening and where could there be an error ???
list members
let teamMembers = [
{ 'idMember': 0, 'name': 'John Littel', 'email': 'Delores_Barrows5#hotmail.com' },
{ 'idMember': 1, 'name': 'Tom Hamill', 'email': 'Luigi0#gmail.com' },
{ 'idMember': 2, 'name': 'Ann Quitzonl', 'email': 'AnnQ#hotmail.com' },
{ 'idMember': 3, 'name': 'Frances Schuster', 'email': 'Frances.Schuster#yahoo.com' },
{ 'idMember': 4, 'name': 'Morrison Mohr', 'email': 'Rafael.Hilll64#yahoo.com' }
];
list apps:
let appsFromBase = ['App 0', 'App 1', 'App 2', 'App 3', 'App 4'];
class Application {
constructor(idApp, isSelected, appName) {
this.idApp = idApp;
this.isSelected = isSelected;
this.appName = appName;
}
toggleSelected() { *this method changed apps from false => true*
this.isSelected = !this.isSelected
}
};
const apps = appsFromBase.map((app, i) => new Application(i, false, app));
const membersObject = teamMembers.map((member, i) => new Member(i, member.name, member.email, apps));
State:
const [members, setMembers] = useState(membersObject);
Handler method:
const handleChoosenApp = (app, member_i) => {
let newMembers = { ...members };
let _id = app.idApp;
if (_id !== 'btn-all-app') {
let myUpdatingMember = newMembers[member_i].apps[_id].toggleSelected()
setMembers({ ...newMembers });
}
}
member_i, _id - This is the member's ID and his application, which was clicked from the interface and he needs to change the value from true to false
in the console myUpdatingMember I get the member and application I need, but why does the state change for everyone ???
The selected state is a property of the Application, not the Member.
You create one Application array and pass it to every Member constructor.
Every Member object you create shares the same Application array.
I have a two dictionaries:
featurePermissionMap = {'2':2,'3':1,'4':1} where key is the feature id and it's value represents the permission type.
Like '2':2 means for a feature id 2 we have a permission 2(Read and Write)
and '3':1 means for a feature id 3 we have a permission 1(Read-Only)
Second Dictionary:
feature_with_sub_feature =
[
{ name: 'FeatureA',
subfeatures: [
{ id: 2, name: 'Feature2' },
{ id: 3, name: 'Feature3' },
},
.......
];
I need a resultant dictionary like below:
read_write_access_feature = {
'read':{},
'write':{}
}
I just want to iterate over feature_with_sub_feature and based on subfeature id, I want output like
read_write_access_feature = {
'read':{'FeatureA':['Feature3',....],......},
'write':{'FeatureA':['Feature2',.....],....}
}
I am trying to achieve this using the two forEach. I am new to javascript.
Any optimized way would be much appreciated.
Any help/suggestions would be much appreciated.
Added function getFeatureWithPermission which will return features with permission passed in parameter. Added code explanation in comment.
call getFeatureWithPermission will required permission as below.
let read_write_access_feature = {
'read': getFeatureWithPermission(1),
'write': getFeatureWithPermission(2)
};
Try it below.
let featurePermissionMap = {'2': 2, '3': 1, '4': 1};
// return features with permission passed in parameter.
function getFeatureWithPermission(permission) {
// use reduce to update & return object as requiment
return feature_with_sub_feature.reduce((a, x) => {
// return object with key as x.name
// value as array of names from subfeatures which have respective permission
// first filter subfeatures for respective permission
// then use map to select only name from subfeatures
a[x.name] = x.subfeatures
.filter(y => featurePermissionMap[y.id] === permission)
.map(y => y.name);
return a;
}, {}); // <- pass empty object as input
}
let feature_with_sub_feature = [{
name: 'FeatureA',
subfeatures: [
{ id: 2, name: 'Feature2' },
{ id: 3, name: 'Feature3' },
]
}];
let read_write_access_feature = {
'read': getFeatureWithPermission(1),
'write': getFeatureWithPermission(2)
};
console.log(read_write_access_feature);
I'm using normalizr util to process API response based on non-ids model. As I know, typically normalizr works with ids model, but maybe there is a some way to generate ids "on the go"?
My API response example:
```
// input data:
const inputData = {
doctors: [
{
name: Jon,
post: chief
},
{
name: Marta,
post: nurse
},
//....
}
// expected output data:
const outputData = {
entities: {
nameCards : {
uniqueID_0: { id: uniqueID_0, name: Jon, post: uniqueID_3 },
uniqueID_1: { id: uniqueID_1, name: Marta, post: uniqueID_4 }
},
positions: {
uniqueID_3: { id: uniqueID_3, post: chief },
uniqueID_4: { id: uniqueID_4, post: nurse }
}
},
result: uniqueID_0
}
```
P.S.
I heard from someone about generating IDs "by the hood" in normalizr for such cases as my, but I did found such solution.
As mentioned in this issue:
Normalizr is never going to be able to generate unique IDs for you. We
don't do any memoization or anything internally, as that would be
unnecessary for most people.
Your working solution is okay, but will fail if you receive one of
these entities again later from another API endpoint.
My recommendation would be to find something that's constant and
unique on your entities and use that as something to generate unique
IDs from.
And then, as mentioned in the docs, you need to set idAttribute to replace 'id' with another key:
const data = { id_str: '123', url: 'https://twitter.com', user: { id_str: '456', name: 'Jimmy' } };
const user = new schema.Entity('users', {}, { idAttribute: 'id_str' });
const tweet = new schema.Entity('tweets', { user: user }, {
idAttribute: 'id_str',
// Apply everything from entityB over entityA, except for "favorites"
mergeStrategy: (entityA, entityB) => ({
...entityA,
...entityB,
favorites: entityA.favorites
}),
// Remove the URL field from the entity
processStrategy: (entity) => omit(entity, 'url')
});
const normalizedData = normalize(data, tweet);
EDIT
You can always provide unique id's using external lib or by hand:
inputData.doctors = inputData.doctors.map((doc, idx) => ({
...doc,
id: `doctor_${idx}`
}))
Have a processStrategy which is basically a function and in that function assign your id's there, ie. value.id = uuid(). Visit the link below to see an example https://github.com/paularmstrong/normalizr/issues/256
I need to be able to create a user and add it's favourite movies (An array of objects with a reference to the Movies collection and his personal rating for each movie) in a single request.
Something that could look like this (pseudocode)
var exSchema = `
type Mutation {
addUser(
name: String!
favMovies: [{ movie: String! #ref to movies coll
personal_rating: Int! # this is different for every movie
}]
) : User
}
...
`
What is the graphql way of doing this in a single request? I know I can achieve the result with multiple mutations/requests but I would like to do it in a single one.
You can pass an array like this
var MovieSchema = `
type Movie {
name: String
}
input MovieInput {
name: String
}
mutation {
addMovies(movies: [MovieInput]): [Movie]
}
`
Then in your mutation, you can pass an array like
mutation {
addMovies(movies: [{name: 'name1'}, {name: 'name2'}]) {
name
}
}
Haven't tested the code but you get the idea
I came up with this simple solution - NO JSON used. Only one input is used. Hope it will help someone else.
I had to add to this type:
type Option {
id: ID!
status: String!
products: [Product!]!
}
We can add to mutation type and add input as follows:
type Mutation {
createOption(data: [createProductInput!]!): Option!
// other mutation definitions
}
input createProductInput {
id: ID!
name: String!
price: Float!
producer: ID!
status: String
}
Then following resolver could be used:
const resolvers = {
Mutation: {
createOption(parent, args, ctx, info) {
const status = args.data[0].status;
// Below code removes 'status' from all array items not to pollute DB.
// if you query for 'status' after adding option 'null' will be shown.
// But 'status': null should not be added to DB. See result of log below.
args.data.forEach((item) => {
delete item.status
});
console.log('args.data - ', args.data);
const option = {
id: uuidv4(),
status: status, // or if using babel status,
products: args.data
}
options.push(option)
return option
},
// other mutation resolvers
}
Now you can use this to add an option (STATUS is taken from first item in the array - it is nullable):
mutation{
createOption(data:
[{
id: "prodB",
name: "componentB",
price: 20,
producer: "e4",
status: "CANCELLED"
},
{
id: "prodD",
name: "componentD",
price: 15,
producer: "e5"
}
]
) {
id
status
products{
name
price
}
}
}
Produces:
{
"data": {
"createOption": {
"id": "d12ef60f-21a8-41f3-825d-5762630acdb4",
"status": "CANCELLED",
"products": [
{
"name": "componentB",
"price": 20,
},
{
"name": "componentD",
"price": 15,
}
]
}
}
}
No need to say that to get above result you need to add:
type Query {
products(query: String): [Product!]!
// others
}
type Product {
id: ID!
name: String!
price: Float!
producer: Company!
status: String
}
I know it is not the best way, but I did not find a way of doing it in documentation.
I ended up manually parsing the correct schema, since JavaScript Arrays and JSON.stringify strings were not accepted as graphQL schema format.
const id = 5;
const title = 'Title test';
let formattedAttachments = '';
attachments.map(attachment => {
formattedAttachments += `{ id: ${attachment.id}, short_id: "${attachment.shortid}" }`;
// { id: 1, short_id: "abcxyz" }{ id: 2, short_id: "bcdqrs" }
});
// Query
const query = `
mutation {
addChallengeReply(
challengeId: ${id},
title: "${title}",
attachments: [${formattedAttachments}]
) {
id
title
description
}
}
`;
What i understand by your requirement is that if you have the following code
const user = {
name:"Rohit",
age:27,
marks: [10,15],
subjects:[
{name:"maths"},
{name:"science"}
]
};
const query = `mutation {
createUser(user:${user}) {
name
}
}`
you must be getting something like
"mutation {
createUser(user:[object Object]) {
name
}
}"
instead of the expected
"mutation {
createUser(user:{
name: "Rohit" ,
age: 27 ,
marks: [10 ,15 ] ,
subjects: [
{name: "maths" } ,
{name: "science" }
]
}) {
name
}
}"
If this is what you wanted to achieve, then gqlast is a nice tag function which you can use to get the expected result
Simply grab the js file from here and use it as:
const user = {
name:"Rohit",
age:27,
marks: [10,15],
subjects:[
{name:"maths"},
{name:"science"}
]
};
const query = gqlast`mutation {
createUser(user:${user}) {
name
}
}`
The result stored in the variable query will be :
"mutation {
createUser(user:{
name: "Rohit" ,
age: 27 ,
marks: [10 ,15 ] ,
subjects: [
{name: "maths" } ,
{name: "science" }
]
}) {
name
}
}"
Pass them as JSON strings. That's what I do.
For those of you who don't need to pass in an array for one request, and are open to the idea of making a request for every mutation. (I am using Vue3, compisition Api, but React and Angular developers still can understand this).
You cannot for loop the mutation like this:
function createProject() {
for (let i = 0; i < state.arrOfItems.length; i++) {
const { mutate: addImplementation } = useMutation(
post_dataToServer,
() => ({
variables: {
implementation_type_id: state.arrOfItems[i],
sow_id: state.newSowId,
},
})
);
addImplementation();
}
}
this will give you an error, because the mutation must be in the setup().
(here is the error you will recieve: https://github.com/vuejs/vue-apollo/issues/888)
Instead create a child component, and map the array in the parent.
in Parent.vue
<div v-for="(card, id) in state.arrOfItems">
<ChildComponent
:id="id"
:card="card"
/>
</div>
in ChildComponent.vue
recieve props and:
const { mutate: addImplementation } = useMutation(
post_dataToServer,
() => ({
variables: {
implementation_id: props.arrOfItems,
id: props.id,
},
})
);