I have a component with a method to add/create a new sub-component and insert it into an array at a specific index (i.e. [elem1, elem2, elem3] becomes [elem1, elem2, new elem, elem3]).
When using something like array.splice(currentIndex + 1, 0, newElement), in the DOM the last element that already exists is just duplicated, though it appears to update correctly for the array in state.
The only way I have found that works is to just use array.push(newElement), which does render properly but is not the desired effect. I've tried a few more ways of inserting the new element into the array but so far everything other than .push() causes the duplication issue. Any suggestions or tips would be greatly appreciated!
const Notef = ({ fetchedParts }) => {
const [parts, setParts] = useState(fetchedParts);
const addNewPart = (currentPart) => {
const index = parts.map((p) => p.id).indexOf(currentPart.id);
const updatedParts = [...parts];
const newPart = { id: pid(), html: 'Type here...', tag: 'p' }
// This will cause duplicated element
updatedParts.splice(index + 1, 0, newPart);
// This does not create duplicate
//updatedParts.push(newPart);
setParts(updatedParts);
}
return (
<>
{parts.map((part) => {
const position = parts.map((p) => p.id).indexOf(part.id) + 1;
return (
<Part
key={part.id}
position={position}
id={part.id}
tag={part.tag}
html={part.html}
addPart={addNewPart}
/>
);
})}
</>
);
}
export default Notef;
Element duplication w/ .splice()
Element addition w/ .push()
edit: re-worded intro to specify new element to be inserted rather than replaced.
Dominic found the issue - I was using indices for keys in the rendered component. Replaced that with the ID and it solved the issue!
return (
<>
{parts.map((part) => {
const position = parts.map((p) => p.id).indexOf(part.id) + 1;
return (
<Part
key={part.id}
position={position}
id={part.id}
tag={part.tag}
html={part.html}
addPart={addNewPart}
/>
);
})}
</>
);
So I am new to javascript and I tried making a todo list. This works well with adding elements. The issue is when I am removing some item, the first one gets removed too, why is it so? I know I am missing a small thing and this may be really basic but I am not able to find out what that is.
const App1 = () => {
const [item, updatedItem]=useState('');
const [Items, setItems]=useState([]);
function inputEvent(event) {
updatedItem(event.target.value);
}
const addItem = (event) => {
event.preventDefault();
setItems((prev) => {
return[
...prev,
item
]
});
updatedItem('');
}
let key=0;
return(<>
<div className='back'>
<div className='list'>
<header>ToDo List</header>
<form onSubmit={addItem}>
<input type='text' placeholder='Add an item' value={item} onChange={inputEvent}/>
<button type='submit'>+</button>
</form>
<div className='items'>
<ol>
{Items.map((val) => <li><button id={key++} onClick={(event) => {
setItems((Items) => {
return Items.filter((val, index) => {
if(index!==Number(event.target.id)){
return index;
}
}
);
});
key=0;
}}>x</button>{val}</li>)}
</ol>
</div>
</div>
</div>
</>);
You return the index in your filter, expecting this to always be true, yet 0 (the index of the first element) is a falsy value.
Try this instead:
return Items.filter((_val, index) => index !== Number(event.target.id));
Some unrelated code-quality notes:
In React, you should always set a key prop on each element when looping through them, rather than id.
map has a second argument, index, which it passes into the callback --- you don't have to keep track of this yourself with e.g. key++ etc.
If you use map's index parameter, then you can pass that directly into your filter rather than using Number(event.target.id), which is not very idiomatic in React.
If you don't use an argument of a callback, it's a good idea to prefix it with a _ (like I've done with _val here), to make it explicit that you're not using it.
Your filter callback should return a flag. index is a number. When treated as a flag, 0 is false (more on MDN). Instead:
return Items.filter((val, index) => index !== Number(event.target.id));
However, your code is returning an array of li elements without setting key on them (see: keys), which React needs in order to manage that list properly (you should be seeing a warning about it in devtools if you're using the development version of the libs, which is best in development). You can't use the mechanism you're using now for keys when doing that, it will not work reliably (see this article linked by the React documentation). Instead, assign each Todo item a unique ID when you create it that doesn't change, and use that as the key (and as the value to look for when removing the item):
// Outside the component:
let lastId = 0;
// Inside the component:
const addItem = (event) => {
event.preventDefault();
setItems((prev) => {
return [
...prev,
{text: item, id: ++lastId}
];
});
updatedItem("");
};
// Add a remove function:
const removeItem = ({currentTarget}) => {
const id = +currentTarget.getAttribute("data-id"); // Get the ID, convert string to number
setItems(items => items.filter(item => item.id !== id));
};
// When rendering:
{Items.map((item) => <li key={item.id}><button data-id={item.id} onClick={removeItem}>x</button>{item.text}</li>)}
In some cases it may be useful to use useCallback to memoize removeItem to avoid unnecessary rendering, but often that's overkill.
I am making a card game in React JS that requires 3 sets of unique cards.
The way the format works is there are ingredient cards that can create potions. The ingredients are dealt in the Top Row of the game, so I have the component called TopRow.
Since these are not normal playing cards I had to generate arrays with 10 of each of 5 different cards for the deal. ( shuffle(a) )
Then I am splicing the deal to only get 5 cards ( a.splice(5); )
So I want the value of the ingredients to increment based on the number of times the ingredients appear, example: function handleHoneyIncrement should increase countHoney by 1.
I've tried a couple different things and I guess I am having a brain fart on how to make a for loop for this.
function TopRow(props) {
let a=["Honey0", "Bone0", "Herbs0", "Mushroom0", "Seeds0",
"Honey1", "Bone1", "Herbs1", "Mushroom1", "Seeds1",
"Honey2", "Bone2", "Herbs2", "Mushroom2", "Seeds2",
"Honey3", "Bone3", "Herbs3", "Mushroom3", "Seeds3",
"Honey4", "Bone4", "Herbs4", "Mushroom4", "Seeds4",
"Honey5", "Bone5", "Herbs5", "Mushroom5", "Seeds5",
"Honey6", "Bone6", "Herbs6", "Mushroom6", "Seeds6",
"Honey7", "Bone7", "Herbs7", "Mushroom7", "Seeds7",
"Honey8", "Bone8", "Herbs8", "Mushroom8", "Seeds8",
"Honey9", "Bone9", "Herbs9", "Mushroom9", "Seeds9"
];
shuffle(a);
function shuffle(a) {
for (let i = a.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[a[i], a[j]] = [a[j], a[i]];
}
return a.splice(5);
}
let imageIngredients = a.map(image => {
return <img key={image} src={require(`../pngs/${image}.png`)}
alt="ingredients" className="img-responsive"
style={{width:"15%", float:"left"}}
/>
});
let handleHoneyIncrement = () => {
if (shuffle.length= "Honey0" ||"Honey1" ||"Honey2" ||"Honey3" ||"Honey4" ||"Honey5" ||"Honey6" ||"Honey7" || "Honey8" || "Honey9" ){
this.setState({countHoney: this.state.countHoney + 1})
};
};
return (
<div className="row" id="topRow"
style={{WebkitBorderRadius:2, WebkitTextStrokeColor: "red", width:"90%", maxHeight:"30%", padding:0}} >
<div className="col-6-md">
<img src={require('../pngs/IngredientBacks.png')} alt="ingredientsBack" style={{width:"15%", float:"left"}} />
</div>
<div className="col-6-md">
{imageIngredients}
{handleHoneyIncrement}
{a}
</div>
</div>
);}
export default TopRow;
Not 100% sure if this is what you were going for, but it sounds like you just need to turn the ingredients list into a collection of ingredient/count pairs?
const ingredientCounts = a.reduce((obj, curr) => ({
...obj,
[curr]: obj[curr] ? obj[curr] + 1 : 1
}), {})
ingredientCounts["Honey0"] // 1
If you're looking to count all Honeys together like Honey0 + Honey1, etc., this should work:
const ingredientCounts = a.reduce((obj, curr) => {
const keys = ["Honey", "etc"]; // maybe this list should be somewhere else, IDK
const key = keys.find(k => curr.includes(k)); // missing null check :)
return {
...obj,
[key]: obj[key] ? obj[key] + 1 : 1
}
}, {})
ingredientCounts["Honey"] // 10
Then we can set state for all of them like:
this.setState({
counts: ingredientCounts
})
And have a state of counts like:
{
Honey: 10,
etc: 0
}
I'm not 100% sure that I understand your goals correctly, but I think a simplified version is that you want to show:
5 random cards from your deck
A button or trigger that shuffles the deck and displays a new hand of 5 cards from the same deck
A count of the total number of honey cards accumulated as the hand is updated
There are a number of confusing things in your code sample, so rather than try to make corrections I threw up a quick demo of how I would approach that problem with some comments explaining what I did differently, given these assumptions. https://codesandbox.io/s/trusting-mclean-kwwq4
import React, { useState, useEffect } from "react";
// The deck of cards is probably a constant whose values never change directly.
// It's possible that I'm wrong and the deck *does* change, but even so I imagine
// it would come from a prop or context from a parent component. Either way the
// cards array should not be mutable.
const CARDS = [
"Honey0", "Bone0", "Herbs0", "Mushroom0", "Seeds0",
"Honey1", "Bone1", "Herbs1", "Mushroom1", "Seeds1",
"Honey2", "Bone2", "Herbs2", "Mushroom2", "Seeds2",
"Honey3", "Bone3", "Herbs3", "Mushroom3", "Seeds3",
"Honey4", "Bone4", "Herbs4", "Mushroom4", "Seeds4",
"Honey5", "Bone5", "Herbs5", "Mushroom5", "Seeds5",
"Honey6", "Bone6", "Herbs6", "Mushroom6", "Seeds6",
"Honey7", "Bone7", "Herbs7", "Mushroom7", "Seeds7",
"Honey8", "Bone8", "Herbs8", "Mushroom8", "Seeds8",
"Honey9", "Bone9", "Herbs9", "Mushroom9", "Seeds9"
];
const initialCards = [];
function TopRow(props) {
// Keep the current hand of cards in state rather than mutating an array
// directly in the function body. React function components should be pure,
// with all side effects occurring inside of effect hooks.
let [cards, setCards] = useState(initialCards);
let [honeyCount, setHoneyCount] = useState(
countSubstrings(initialCards, "Honey")
);
let imageIngredients = cards.map(image => (
<img
key={image}
src={require(`../pngs/${image}.png`)}
alt={humanReadableAltTag}
className="img-responsive"
style={{ width: "15%", float: "left" }}
/>
));
function shuffleCards() {
// Reset your hand of cards with the original array (the deck)
setCards(shuffleArray(CARDS));
}
// Return all state to initial values
function reset() {
setCards(initialCards);
setHoneyCount(countSubstrings(initialCards, "Honey"));
}
// Any time our cards are updated, we want to increment the number of Honey
// cards in our hand. useState accepts a lazy initializer to access the
// previous state, which is very useful for effects like this!
useEffect(() => {
setHoneyCount(count => count + countSubstrings(cards, "Honey"));
}, [cards]);
return (
<div
{...props}
className="row"
id="topRow"
style={
{
WebkitBorderRadius: 2,
WebkitTextStrokeColor: "red",
width: "90%",
maxHeight: "30%",
padding: 0
}
}
>
<button onClick={shuffleCards}>
{cards.length ? "Shuffle" : "Deal"}
</button>
<button onClick={reset}>Reset</button>
<hr />
<div className="col-6-md">
<img
src={require("../pngs/IngredientBacks.png")}
alt="Back of ingredient card"
style={{ width: "15%", float: "left" }}
/>
</div>
<div className="col-6-md">
{imageIngredients}
</div>
<hr />
<div>
<strong>TOTAL HONEY COUNT:</strong> {honeyCount}
</div>
</div>
);
}
export default TopRow;
// I put these utility functions outside of the component body since there is no
// real reason to recreate them on each render.
/**
* #param {any[]} array
*/
function shuffleArray(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
// Use slice instead of splice here to prevent mutating the original array
return array.slice(0, 5);
}
/**
* #param {string[]} array
* #param {string} subs
*/
function countSubstrings(array, subs) {
return array.filter(card => card.includes(subs)).length;
}
I have a View with nested items, implementing list-like block displaying received props array and I want to get height of this block from sum of height of all of its children. So my component looks like this:
class MultiColumnBox extends Component {
state = {
calculatedMaxHeight: null,
};
measureBlock = () => {
let totalHeight = 0;
//this.props.data is array
this.props.data.forEach((item, index) => {
this['checkbox_${index}'].measure((x , y, width, height) => {
console.log(height)
totalHeight += height;
})
if (index === (this.props.data.length - 1)) {
this.setState({
calculatedMaxHeight: totalHeight
})
}
})
};
render() {
return (
<View
onLayout={() => this.measureBlock()}
>
{this.props.data.map((item, index) => (
<View ref={node => this[`checkbox_${index}`] = node}>
// some content....
</View>
)}
</View>
)
}
}
But if there will be for example 5 blocks of height 40dp, it will console log 40 five times and set state.calculatedMaxHeight to 0. But if I move if (index === (this.props.data.length - 1)) condition inside measure function, it will work okay and set state.calculatedMaxHeight to 200. Why is it works like that? Regardless of nesting inside measure function I work with the same index variable and increase same totalHeight varable, defined once outside the loop.
I think the issue is that setState function is called before the callback function inside measure invoked for first node.
console.log(height) would be printing the 40 for 5 times, But the calculatedMaxHeightt is set to 0 before first print of height value.
setState forces the component to re-render... It is not good practice to call setState within a render (which is what you are doing) as it will cause an infinite render loop (which I believe is the issue you are running into).
I would say pull the calculation out of the state. Put it on the this level and use it however you plan from there.
I'm creating a scheduling system for a grooming salon. What needs to happen is when a client is scheduling their pet, the system only gives available times. Once the client selects an available time, if there is more than one groomer available, the system needs to assign the pet to the groomer with the highest seniority.
I currently have a nested array with each groomer's information, and an array of
time slots they have available.
Here is an example of the array:
timeslots (
0 => array (
groomer => array (
ID => 0,
Seniority => 5,
...
),
slots => array (
0 => array (
start => 200,
end => 450,
length => 250
),
1 => array (
start => 500,
end => 600,
length => 100
)
)
),
1 => array (...),
2 => array (...)
)
I need to combine all of the slots into a single array in order to fill a box. However, any overlapping timeslots would need to be merged, and I still need a way to track which groomer is the most senior for a given timeslot that is selected.
Thanks!
This is more of a suggestion than an answer, but I think using classes will help you find the answer to your own problem.
class Groomer {
constructor(id, seniority) {
this.id = id,
this.seniority = seniority,
this.timeslots => {
someFunctionThatGetsTimeslots();
}
}
}