How to conditionally update properties of items within a collection in React? - javascript

I have a collection of items stored in state:
this.state = {
items: [
{ name: "foo", description: "a foo", index: 0 },
{ name: "bar", description: "a bar", index: 1 },
{ name: "herp", description: "a herp", index: 2 },
{ name: "derp", description: "a derp", index: 3 }
]
};
The index property represents the ordinal position of each item in the collection. At some point I need to re-order these items. For example, "derp" may need to be moved to the front, so the indices of the other items need to be updated:
{ name: "derp", description: "a derp", index: 0 },
{ name: "bar", description: "a bar", index: 1 },
{ name: "herp", description: "a herp", index: 2 },
{ name: "foo", description: "a foo", index: 3 }
I am currently updating the state using update from the immutability-helper package. However, I am certain this is not the correct way to do it (although it works):
// originalIndex is a variable holding the original index
// newIndex is a variable holding the new index
// initialise updatedItems so we can update within the loop
let updatedItems = update(this.state.items, { [originalIndex]: {'index': {$set: newIndex}}});
for (var i = newIndex; i < this.state.items.length; i++) {
if (i !== originalIndex) {
updatedItems = update(updatedItems, { [i]: {'index': {set$: parseInt(this.state.items[i].index) + 1}}});
}
}
This feels like a massive hack.
My question is, is it possible to call update with conditional logic, and so can this loop be replaced with a single call to update?

Assuming that we pull the index property out of each item, you can create the new list like this:
const items = this.state.items.slice();
const value = items[oldIndex];
items.splice(oldIndex, 1); // remove the one you want to move
items.splice(newIndex, 0, value); // add it back to the desired index
this.setState({ items });
That is, use slice to make a (shallow) copy of the list, then use splice to swap the elements around.
Since you're only moving one element at a time, you can save a line using:
const [value] = items.splice(oldIndex, 1);
This assigns the first element of the array returned by splice to value.
If you want to keep index (why?), then you need to reassign the indices:
this.setState({ items: items.map((item, index) => ({ ...item, index })) });

Why not sort the item before hand, on render() :
render(){
let toDisplay = this.state.items.sort( (a,b) => {
if (a.index <= b.index) {
return -1;
}
if (a.index > b.index) {
return 1;
}
return 0;
});
return(
<div className='foo'>
{
toDisplay.map((item, i) => {
return(
<div className="bar" key={i}>{ item.name }</div>
);
})
}
</div>
);
}
Then you can update the state.items only by using :
this.setState({
items: yourUpdatedItems
});

Related

Add dynamic key to set state, react

I have this state
this.state = {
dropdown1: false,
dropdown2: false,
dropdown3: false
}
I want to access to these dropdowns in state using this.setState but the number after 'dropdown' comes from API
onMaca = (ev) => {
this.setState({
dropdown + ev: true
})
}
So I want the key to be dynamic 'dropdown1' for example.
Thanks for your answers
you can access the object property like this object['property name']
onMaca = (ev) => {
this.state['dropdown' + ev]= true;
this.setState({
...this.state
})
}
https://codezup.com/add-dynamic-key-to-object-property-in-javascript/
You can use any of these to set key dynamically. I will try to update the answer with an example in a while for setState.
The state is a JS object, so you can get its keys as usual, like so:
const stateKeys = this.state.keys()
Now you have an array: [ "dropdown1", "dropdown1", "dropdown1" ]
One way to use it would be:
const keysMap = statekeys.map(( item, i ) => return {
key: item,
idx: i,
number: item.replace( /dropdown/, '' )
}
keysMap will look like so: [ { key: 'dropdown1', idx: 0, number "1" }, { key: 'dropdown1', idx: 1, number "2" }, { key: 'dropdown1', idx: 2, number "3" } ]
You can query keysMap for a given dropDownNumber like so:
let k = keysMap.find( kmap => kmap.key = dropDownNumber )
To set the dropdown's state:
this.setState({ k: <whatever> })

saveData method saves twice

I am building a React app that includes one separate component for CRUD functionality of Products and another separate component for CRUD functionality of Suppliers.
I am using the same saveData method for both components (the Create functionality of CRUD.. that is triggered when the User presses Save after filling in the input fields of Product or Supplier). The saveData method is located in a central ProductsAndSuppliers.js file that is available to both the Products and Supplier components.
In both of the Product & Supplier components, there is a table showing the Products or Suppliers already present as dummy data.
I made a button at the bottom of each page to add a new Product or Supplier... depending on which tab the user has selected on the left side of the screen (Product or Supplier).
Since I am using the same saveData method in both cases, I have the same problem whenever I try to add a new Product or Supplier to each respective table after filling out the input fields. My new Product or Supplier is added.. but twice and I can't figure out why.
I have tried using a spread operator to add the new item to the collection but am having no success:
saveData = (collection, item) => {
if (item.id === "") {
item.id = this.idCounter++;
this.setState((collection) => {
return { ...collection, item }
})
} else {
this.setState(state => state[collection]
= state[collection].map(stored =>
stored.id === item.id ? item : stored))
}
}
Here is my original saveData method that adds the new Product or Supplier, but twice:
saveData = (collection, item) => {
if (item.id === "") {
item.id = this.idCounter++;
this.setState(state => state[collection]
= state[collection].concat(item));
} else {
this.setState(state => state[collection]
= state[collection].map(stored =>
stored.id === item.id ? item : stored))
}
}
my state looks like this:
this.state = {
products: [
{ id: 1, name: "Kayak", category: "Watersports", price: 275 },
{ id: 2, name: "Lifejacket", category: "Watersports", price: 48.95 },
{ id: 3, name: "Soccer Ball", category: "Soccer", price: 19.50 },
],
suppliers: [
{ id: 1, name: "Surf Dudes", city: "San Jose", products: [1, 2] },
{ id: 2, name: "Field Supplies", city: "New York", products: [3] },
]
}
There are issues with both of your implementations.
Starting with the top one:
// don't do this
this.setState((collection) => {
return { ...collection, item }
})
In this case, collection is your component state and you're adding a property called item to it. You're going to get this as a result:
{
products: [],
suppliers: [],
item: item
}
The correct way to do this with the spread operator is to return an object that represents the state update. You can use a computed property name to target the appropriate collection:
this.setState((state) => ({
[collection]: [...state[collection], item]
})
)
* Note that both this and the example below are using the implicit return feature of arrow functions. Note the parens around the object.
In the second code sample you're
mutating the existing state directly which you should not do.
returning an array instead of a state update object.
// don't do this
this.setState(state =>
state[collection] = state[collection].concat(item)
);
Assignment expressions return the assigned value, so this code returns an array instead of an object and I'd frankly be surprised if this worked at all.
The correct implementation is the same as above except it uses concat instead of spread to create the new array:
this.setState(state => ({
[collection]: state[collection].concat(item)
})
);
needlessly fancy, arguably silly id generators:
const nextId = (function idGen (start = 100) {
let current = start;
return () => current++;
})(100);
console.log(nextId()); // 100
console.log(nextId()); // 101
console.log(nextId()); // 102
// ----------------
// a literal generator, just for fun
const ids = (function* IdGenerator(start = 300) {
let id = start;
while (true) {
yield id++;
}
})();
console.log(ids.next().value); // 300
console.log(ids.next().value); // 301
console.log(ids.next().value); // 302

bug from element showing

Hi all I have following code: my code
In code I am recieving data from backend, In my case I hardcoded that part (see below data)
const skills = [
{
id: 1,
name: "Html "
},
{ id: 2, name: "CSS" },
{ id: 3, name: "Scss" },
{ id: 4, name: "Bootstrap 4" },
{ id: 5, name: "JavaScript" },
{ id: 6, name: "Jquery" },
{ id: 7, name: "React JS" },
{ id: 8, name: "Angular " },
{ id: 9, name: "Vue.js " },
{ id: 10, name: "SQL" },
{ id: 11, name: "php" },
{ id: 12, name: "Laravel" }
];
I am counting all my skill names char length and if that length is greater then allowCharCount I am hiding all rest skills and showing in number how many skills is hided. That part is working.
let lengthCount = 0;
let maxIndex = 0;
const allowCharCount = 20;
const skill = [];
export const Skill = ({ data }) => {
if (data === undefined) {
return null;
} else {
data.map((item) => {
if (lengthCount <= allowCharCount) {
maxIndex = item.id;
skill.push(item);
}
lengthCount += item.name.length;
});
const mySkills = skill.map((perSkill) => (
<span key={perSkill.id} className="skillItem">
{perSkill.name}
</span>
));
return (
<div className="skillWrapper">
<div>{mySkills}</div>
{lengthCount > allowCharCount ? (
<div className="skillNumber">+{data.length - maxIndex}</div>
) : null}
</div>
);
}
};
but when my chars count is less then allowCharCount it's not working.
If I only have first 3 items (Html, CSS, Scss) I see following view
Html CSS Scss Html CSS Scss +0.
Please help me to fix this code for that case (if chars count is less than allowCharCount) I need to show only correct items with no any +0 count
You should rethink the way you write the component and code in general
You have to store dynamic variables inside the component
To save the result of array operations, you should either mutate state or create a new variable inside the component (which is more preferable in your case).
.map method return an array of values returned by callback inside of it. If you're not going to return anything, use .forEach instead
Use .length property instead of incrementing the size of array by yourself to avoid bugs.
The reason why you get duplicated elements is that you don't clear the array before the component updates, so the algorithm just pushes these values again.
Working code example:
export const Skill = ({ data }) => {
// here I invert condition to get the rest code out of brackets
if (!data) return null;
// all of the dynamic data should be inside the component
const failIndex = React.useMemo(() => {
let charCount = 0;
for (let i = 0; i < data.length; i++) {
charCount += data[i].name.length;
if (charCount > allowCharCount) return i;
}
return -1;
}, [data]);
// check if data has element that doesn't pass the condition
const dataBeforeFail = failIndex === -1 ? data : data.slice(0, failIndex + 1);
const hiddenSkillsCount = data.length - dataBeforeFail.length;
return (
<div className="skillWrapper">
<div>
{dataBeforeFail.map((perSkill) => (
<span key={perSkill.id} className="skillItem">
{perSkill.name}
</span>
))}
</div>
{hiddenSkillsCount > 0 && (
<div className="skillNumber">+{hiddenSkillsCount}</div>
)}
</div>
);
};

What is the most efficient functional way for refactoring forEach loop?

I have a method which accepts items and available items from props. While iteration over an array I should filter items by two conditions inside the forEach loop. The first condition is passed item if item id doesn't exist or item id equal 1. The second one should return item and filter 'description' field, otherwise, we push our items into 'nextAvailableItems' array. What is the most efficient functional way for replacing the forEach loop in this situation?
Items structure:
[{
id: 23740416,
display_name: "test",
date_from: "1970-12-31"
}]
Available items structure:
[{
id: 23740416,
display_name: "test",
description: "Text"
}]
Expected output:
[{
id: 23740416,
display_name: "test"
}]
Current code:
buildAvailableItems() {
const { items, availableItems } = this.props
const nextAvailableItems = [...availableItems]
items.forEach(item => {
if (!item.id || item.id === -1) {
return
}
const availableItem = availableItems.find(availableItem => availableItem.id === item.id)
if (availableItem) {
const { id, display_name } = availableItem
return { id, display_name }
}
const { id, display_name } = item
nextAvailableItems.push({ id, display_name })
})
return nextAvailableItems
}
buildAvailableItems() {
const { items, availableItems } = this.props
const itemIds = items.reduce((c, i) => (c[i.id] = true, c), {})
const nextAvailableItems = availableItems
.filter(a => itemIds[a.id])
.map(a => ({ id: a.id, display_name: a.display_name }))
return [...availableItems, ...nextAvailableItems]
}
First, I would turn items into an ID lookup. Then filter your availableItems using that lookup. Then do the array merge last.

ReactJs - How to update state in array?

I'm creating a Page Builder with React.
I have a component which contains the structure of the page.
var LayoutPage = React.createClass({
getInitialState: function getInitialState() {
return {
items: {
'78919613':{
id: '78919613',
component : 'OneColumn',
cols:{
'565920458':{
id: '565920458',
content:{
'788062489':{
id: '788062489',
component : 'Text',
params: 'Lorem ipsum'
},
'640002213':{
id: '640002213',
component : 'Text',
params: 'Lorem ipsum'
}
}
}
}
}
}
};
},
.....
});
I have a system with drag'n drop to put a new element on the page and it works. But when the new element is dropped I want to update the state to add a new item in the array.
So how can I push a new item ? I did a test with that :
this.state.items.push({.....});
But I have an error :
TypeError: this.state.items.push is not a function
Can you help me ?
Thank you.
Instead of using an object in your state you should change it to the array like below :
this.state = {
items: [ // items array
{
id: 1,
name: "Stack"
},
{
id: 2,
name: "Overflow"
}],
count: 3, // another state
textValue : '' // and this is state too
}
Where the items it's an array of objects. And then you will be able to add new items to an array.
const newItem = {
id : this.state.count,
name: this.state.textValue
};
const newArr = this.state.items.concat(newItem);
this.setState({
items: newArr,
textValue: '',
count: this.state.count + 1
})
The whole example is here.
I hope it will help you!
Thanks
You'll start to get headaches if you directly mutate the state of your application. If you forget to call this.setState then it won't re-render!
Assuming you can't use an array (which would be easier), then you'll have to generate a unique key if you want to add another item to the object.
// create a new copy of items based on the current state
var newItems = Object.assign({}, this.state.items),
newItem = { id: '', component: '', cols: {} },
uniqueId = generateUniqueId();
// safely mutate the copy
newItems[uniqueId] = newItem;
// update the items property in state
this.setState({ items: newItems });
This is even easier with ES7/Babel.
const newItem = { id: '', component: '', cols: {} },
uniqueId = generateUniqueId(),
items = { [uniqueId]: newItem, ...this.state.items };
this.setState({ items });
You can generate a similar unique ID to the one you have there using Math.random.
function generateUniqueId() {
// removing leading '0.' from number
return Math.random()
.toString()
.slice(3);
}

Categories

Resources