React usestate array length varies depending on item i click - javascript

im trying to build a list you can add and delete components from.
Adding works but when i try to delete an item, every item that comes after that also gets deleted.
I found that the length of the use state array i use varies depending on which item i click delete on.
const Alerting = () => {
const [Alerts, setAlert] = useState([]);
const AddAlertingChoice = () => {
const addedAlerts = Alerts => [...Alerts, <AlertingCoinChoice coins={coins} id={new Date().getUTCMilliseconds()}];
setAlert(addedAlerts);
}
const DeleteAlertingChoice = id =>{
console.log("alerts "+Alerts.length) //This length always displays the item index-1?
const removedArr = [...Alerts].filter(alert => alert.props.id != id)
setAlert(removedArr)
}
return (
<>
<AlertingContainer>
<CoinChoiceContainer>
{Alerts.map((item, i) => (
item
))}
</CoinChoiceContainer>
<AddAlertButton onClick={AddAlertingChoice}>+</AddAlertButton>
</AlertingContainer>
</>
)
The items
const AlertingCoinChoice = ({coins, id, DeleteAlertingChoice}) => {
return (
<>
<AlertingCoin>
<CoinSelect id={'SelectCoin'}>
<OptionCoin value='' disabled selected>Select your option</OptionCoin>
</CoinSelect>
<ThresholdInput id={'LowerThresholdInput'} type='number'
pattern='^-?[0-9]\d*\.?\d*$'/>
<ThresholdInput id={'UpperThresholdInput'} type='number'
pattern='^-?[0-9]\d*\.?\d*$'/>
<SaveButton id={'AlertSaveAndEdit'} onClick={ClickSaveButton}>Save</SaveButton>
<DeleteAlertButton onClick={() => {DeleteAlertingChoice(id)}}>X</DeleteAlertButton>
</AlertingCoin>
</>
)
why cant it just delete the item i pass with the id parameter?

It sounds like you're only passing down the DeleteAlertingChoice when initially putting the new <AlertingCoinChoice into state, so when it gets called, the length is the length it was when that component was created, and not the length the current state is.
This also causes the problem that the DeleteAlertingChoice that a component closes over will only have the Alerts it closes over from the time when that one component was created - it won't have the data from further alerts.
These problems are all caused by one thing: the fact that you put the components into state. Don't put components into state, instead transform state into components only when rendering.
const Alerting = () => {
const [alerts, setAlerts] = useState([]);
const AddAlertingChoice = () => {
setAlerts([
...alerts,
{ coins, id: Date.now() }
]);
}
const DeleteAlertingChoice = id => {
console.log("alerts " + alerts.length);
setAlerts(alerts.filter(alert => alert.id !== id));
}
return (
<AlertingContainer>
<CoinChoiceContainer>
{Alerts.map((alertData) => (
<AlertingCoinChoice {...alertData} DeleteAlertingChoice={DeleteAlertingChoice} />
))}
</CoinChoiceContainer>
<AddAlertButton onClick={AddAlertingChoice}>+</AddAlertButton>
</AlertingContainer>
);
};
You also don't need fragments <> </> when you're already only rendering a single top-level JSX item.

Related

How to only render an individual component with state, when multiple components use that state in React?

I'm currently using state to display/hide an icon in my SwipeRow, but when I swipe one row, the icon becomes visible on all the rows, because the icons all reference that same state, so my question was how do I display the icon only on the row swiped?
I was thinking maybe I can utilize index somehow in .map(), like wrap StyledContainer with a ternary using index, I'm not sure. Any ideas on how to achieve this? If you have a method without using state, that's also fine.
const foobar = content.map((object, index) => {
return (
<SwipeRow
onRowOpen={() => setSwiped(true)}
onRowClose={() => setSwiped(false)}
>
<StyledContainer swiped={isSwiped}> <-- This is where it's displayed/hidden
<Icon />
</StyledContainer>
</SwipeRow>
)})
You will have to maintain a state for all Swipe rows. Maybe a map or an object. Something like this.
const [swiped, setSwiped] = React.useState({});
const content = content.map((object, index) => {
const id = object.id;
return (
<SwipeRow
onRowOpen={() => setSwiped((oldSwipe) => { oldSwipe.id = true; return oldSwipe;})}
onRowClose={() => setSwiped((oldSwipe) => { oldSwipe.id = false; return oldSwipe;})}
>
<StyledContainer swiped={swiped[id]}> <----- This is where it's displayed/hidden
<Icon />
</StyledContainer>
</SwipeRow>
)})

useEffect: not rendering after update of state

I have a list of items (I get the items with a GET-call - I didn't add it here, because I think it's irrelevant). When I delete an item, the list should be updated/ re-rendered.
To do this I use the useEffect-hook with a second parameter (productData).
Problem:
I have to refresh the page manually in order to see the new list of items. I don't understand why: I use useEffect with the second parameter. Could someone point me in the right direction what is wrong? Thanks a lot!
Here is my code:
export default function MemberSavedProducts() {
const [productData, setProductData] = useState([]);
const [successMessage, setSuccessMessage] = useState();
const [errorMessage, setErrorMessage] = useState();
useEffect(() => {}, [productData]);
const deleteProduct = async(prod) => {
try {
if (window.confirm("Are you sure you want to delete this product?")) {
const {
data
} = await fetchContext.authAxios.delete(
`savedProducts/${prod}`
);
setProductData(
productData.filter((prod) => prod !== data.deletedItem.Id)
);
setSuccessMessage(data.message);
}
} catch (err) {
const {
data
} = err.response;
setErrorMessage(data.message);
}
};
return (
<CardLarge>
<div className={styles.productWrapper}>
{successMessage && <SuccessMessage text={successMessage} />}
{errorMessage && <ErrorMessage text={errorMessage} />}
{productData.map((prod) => {
return (
<div
key={prod.id}
>
<ProductItem
prod={prod}
onClick={() => {
getInfo(prod.information);
}}
/>
<button
onClick={() => {deleteProduct(prod.Id)}}
>
Delete
</button>
</div>
);
})}
</div>
</CardLarge>
);
}
Discussed this in the comments, posting this answer for completeness.
Are you sure the filter function works? It seems the refresh works because the GET response returns the right array. I think it should be productData.filter((prod) => prod.Id !== data.deletedItem.Id));. Because in your code you are comparing an object to a string.
Or you can use the passed parameter prod instead of the response maybe like this productData.filter((p) => p.Id !== prod));
Also a small clarification: useEffect does not cause a rerender, changing state does trigger a rerender. useEffect is just a listener/callback that triggers on change of the declared dependencies.

React returns older state value onClick

I am adding a component onclick and keeping track of the components using useState Array. However when I go to remove one of the added components, it doesn't recognize the full component Array size, only the state that was there when that component was initially added.
Is there a way to have the current state recognized within that delete function?
https://codesandbox.io/s/twilight-water-jxnup
import React, { useState } from "react";
export default function App() {
const Span = props => {
return (
<div>
<span>{props.index}</span>
<button onClick={() => deleteSpan(props.index)}>DELETE</button>
Length: {spans.length}
</div>
);
};
//set initial span w/ useState
const [spans, setSpans] = useState([<Span key={0} index={Math.random()} />]);
//add new span
const addSpan = () => {
let key = Math.random();
setSpans([...spans, <Span key={key} index={key} />]);
};
//delete span
const deleteSpan = index => {
console.log(spans);
console.log(spans.length);
};
//clear all spans
const clearInputs = () => {
setSpans([]);
};
return (
<>
{spans}
<button onClick={() => addSpan()}>add</button>
<button onClick={() => clearInputs()}>clear</button>
</>
);
}
UPDATE - Explaining why you are facing the issue descibed on your question
When you are adding your new span on your state, it's like it captures an image of the current values around it, including the value of spans. That is why logging spans on click returns you a different value. It's the value spans had when you added your <Span /> into your state.
This is one of the benefits of Closures. Every <Span /> you added, created a different closure, referencing a different version of the spans variable.
Is there a reason why you are pushing a Component into your state? I would suggest you to keep your state plain and clean. In that way, it's also reusable.
You can, for instance, use useState to create an empty array, where you will push data related to your spans. For the sake of the example, I will just push a timestamp, but for you might be something else.
export default function App() {
const Span = props => {
return (
<div>
<span>{props.index}</span>
<button onClick={() => setSpans(spans.filter(span => span !== props.span))}>DELETE</button>
Length: {spans.length}
</div>
);
};
const [spans, setSpans] = React.useState([]);
return (
<>
{spans.length
? spans.map((span, index) => (
<Span key={span} index={index} span={span} />
))
: null}
<button onClick={() => setSpans([
...spans,
new Date().getTime(),
])}>add</button>
<button onClick={() => setSpans([])}>clear</button>
</>
);
}
I hope this helps you find your way.

Change state of individual div (that is inside a map) instead of changing the state of all div's inside the map

I have a component that is checking if some state is true or false. I show a <p> tag if true and hide a <h3>. I am pulling the data from a gaphQL query so there is a map method and there are three <Card>'s now if I click the card and run my showFull function it shows the p tags on all the elements, instead I just want to isolate the specific one it is clicking on.
Here is my component
<Testimonials className="testimonaials">
{data.allDatoCmsTestimonial.edges.map(({ node: testimonial }) => (
<Card onClick={showFull} background={testimonial.testimonialImage.url}>
{testimonialFull ?
<p>{testimonial.fullLengthQuote}</p>
:
<h3>{testimonial.shortQuote}</h3>
}
</Card>
))}
</Testimonials>
Here is my state and my function
const [testimonialFull, setTestimonialFull] = useState(false)
const showFull = () => {
setTestimonialFull(true)
}
Attempting Alexander's answer. The issue I am having now is Cannot read property 'testimonialImage' of undefined
Here is the component
const IndexPage = ({ data }) => {
const TestimonialCard = ({testimonial})=>{
const [showFull, setShowFull] = useState(false)
const handleClick = useCallback(()=>{
setShowFull(true)
//setShowFull(s=>!s)//If you want toggle behaviour
},[])
return <Card onClick={handleClick} background={testimonial.testimonialImage.url}>
{showFull ?
<p>{testimonial.fullLengthQuote}</p>
:
<h3>{testimonial.shortQuote}</h3>
}
</Card>
}
return (
...
Here is where I invoke it in the map function
...
return (
... (bunch of other jsx/html)
<Testimonials className="testimonaials">
{data.allDatoCmsTestimonial.edges.map(({ node: testimonial }) => (
<TestimonialCard/>
))}
</Testimonials>
...
Wrap the cards in a custom component
const TestimonialCard = ({testimonial})=>{
const [showFull, setShowFull] = useState(false)
const handleClick = useCallback(()=>{
setShowFull(true)
//setShowFull(s=>!s)//If you want toggle behaviour
},[])
return <Card onClick={handleClick} background={testimonial.testimonialImage.url}>
{showFull ?
<p>{testimonial.fullLengthQuote}</p>
:
<h3>{testimonial.shortQuote}</h3>
}
</Card>
}

Why won't my controlled Input component in React update with the state?

I've got a list of ingredients in an "editIngreds" array in my state, e.g. ['Apple', 'Banana', 'Orange']. They are rendered to the DOM via react map function. I have an edit mode that replaces the text with Input boxes so I can edit the items and save them back to the state array. However, when typing in the box, nothing happens. console.log shows that editIngreds[i] is indeed being updated when I type a character, however the input boxes do not update with this new value.
export default function RecipeCard({ recipe, onEditRecipe, onRemoveRecipe }) {
const ingredients = Object.values(recipe.ingredients);
const ingredientKeys = Object.keys(recipe.ingredients);
const [editIngreds, setEditIngreds] = React.useState(ingredients)
...
const onChangeEditIngreds = (event, i) => {
const value = event.target.value;
const ingreds = editIngreds
ingreds[i] = value;
setEditIngreds(ingreds);
};
...
return (
<ul>
{ingredients.map((ingred, i) => (
<li key={ingredientKeys[i]}>
{editMode ? (
<Input
type="text"
value={editIngreds[i]}
onChange={(e) => onChangeEditIngreds(e,i)}
/>
) : (
<>{ingred}</>
)}
</li>
))}
</ul>
You are mutating the state. Make a shallow copy of ingredients before updating
const onChangeEditIngreds = (event, i) => {
const value = event.target.value;
const ingreds = [...editIngreds];
ingreds[i] = value;
setEditIngreds(ingreds);
};
Rather than using this -> ingredients.map((ingred, i)
use this ->
editIngreds.map((ingred, i){
....
<Input
type="text"
value={ingred}
onChange={(e) => onChangeEditIngreds(e,i)}
/>

Categories

Resources