I am pretty new on JavaScript, React, and hooks and I have created a React useState's hook to increase the counter. Based on the following code, the number changes and it goes up on one record, but when I have more than one record all counters increase no matter which button I click. I would like any suggestion as to why it behaves like this will be greatly appreciated:
import React, {useState, useEffect } from 'react';
import { Link } from 'react-router-dom'
import vendors from './vendors.css'
const VendorsShow = (props) => {
const handleDelete = (vendor) => {
props.deleteVendor(vendor.id);
}
const [ count, setCount ] = useState(0)
// similar to componentDidMount:
useEffect(() => {
document.title = `You clicked ${count} times`
})
return (
<div className="vendor-show-div" style={vendors}>
{props.vendors.map((vendor) =>
<ul key={vendor.id}>
<Link to={`/vendors/${vendor.id}`}>{vendor.name}
- ${vendor.total_cost}
</Link>
- <button onClick={() => handleDelete(vendor)}>X</button>
- <button onClick={() => setCount(count + 1)}>{count}</button>
</ul>)}
</div>
);
}
export default VendorsShow;
Each element you are mapping would need its own counter state. Either an array/object of counters, or you abstract a component that maintains its own count state.
I suggest using an object to store count values using the vendor's id property.
const VendorsShow = (props) => {
const handleDelete = (vendor) => {
props.deleteVendor(vendor.id);
};
const [counts, setCounts] = useState({}); // <-- initial empty object for counts
// similar to componentDidMount:
useEffect(() => {
document.title = `You clicked ${Object.values(counts).reduce(
(total, { count }) => total + count,
0
)} times`;
});
// initialize/update state when vendors array changes
useEffect(() => {
setCounts(
props.vendors.reduce(
(counts, { id }) => ({
...counts,
[id]: 0 // <-- store counts by vendor id
}),
{}
)
);
}, [props.vendors]);
const handleCount = (id) => () =>
setCounts((counts) => ({
...counts,
[id]: counts[id] + 1 // <-- update specific vendor's count
}));
return (
<div className="vendor-show-div" style={vendors}>
{props.vendors.map((vendor) => (
<ul key={vendor.id}>
<Link to={`/vendors/${vendor.id}`}>
{vendor.name}- ${vendor.total_cost}
</Link>
- <button onClick={() => handleDelete(vendor)}>X</button>-{" "}
<button onClick={handleCount(vendor.id)}>{counts[vendor.id]}</button>
</ul>
))}
</div>
);
};
In your code, count is an independent state value not depends on your venders.
You need to include count variable for each vender and update them accordingly.
Something like this :
const [venders, setVenders] = setState(venders);
<button onClick={() => {
var temp = [...venders];
temp [i].count = temp [i].count + 1;
setVenders(temp);
}>{venders[i].count}</button>
Related
I was watching a tutorial on how to make todos, though my main focus was local storage use.
But when he made the delete button then I was a bit confused, the code below shows how he did it but I am not getting it.
Can anyone explain that I tried using the splice method to remove items from the array but I am not able to remove the items from the page?
Can you also suggest what should I do after using splice to return the array on the page?
Below is the code,
import "./styles.css";
import { useState, useEffect } from 'react'
import Todoform from './TodoForm'
export default function App() {
const [list, setlist] = useState("");
const [items, setitems] = useState([])
const itemevent = (e) => {
setlist(e.target.value);
}
const listofitem = () => {
setitems((e) => {
return [...e , list];
})
}
const deleteItems = (e) => {
// TODO: items.splice(e-1, 1);
// Is there any other way I can do the below thing .i.e
// to remove todos from page.
// this is from tutorial
setitems((e1)=>{
return e1.filter((er , index)=>{
return index!=e-1;
})
})
}
return (
<>
<div className='display_info'>
<h1>TODO LIST</h1>
<br />
<input onChange={itemevent} value={list} type="text" name="" id="" />
<br />
<button onClick={listofitem} >Add </button>
<ul>
{
items.map((e, index) => {
index++;
return (
<>
<Todoform onSelect={deleteItems} id={index} key={index} index={index} text={e} />
</>
)
})
}
</ul>
</div>
</>
)
}
And this is the TodoForm in this code above,
import React from 'react'
export default function Todoform(props) {
const { text, index } = props;
return (
<>
<div key={index} >
{index}. {text}
<button onClick={() => {
props.onSelect(index)
}} className="delete">remove</button>
</div>
</>
)
}
Here is the codeSandbox link
https://codesandbox.io/s/old-wood-cbnq86?file=/src/TodoForm.jsx:0-317
I think one issue with your code example is that you don't delete the todo entry from localStorage but only from the components state.
You might wanna keep localStorage in sync with the components state by using Reacts useEffect hook (React Docs) and use Array.splice in order to remove certain array elements by their index (Array.splice docs).
// ..
export default function App() {
const [list, setlist] = useState("");
const [items, setitems] = useState([])
/* As this `useEffect` has an empty dependency array (the 2nd parameter), it gets called only once (after first render).
It initially retrieves the data from localStorage and pushes it to the `todos` state. */
useEffect(() => {
const todos = JSON.parse(localStorage.getItem("notes"));
setitems(todos);
}, [])
/* This `useEffect` depends on the `items` state. That means whenever `items` change, this hook gets re-run.
In here, we set sync localStorage to the current `notes` state. */
useEffect(() => {
localStorage.setItem("notes", JSON.stringify(items));
}, [items])
const itemevent = (e) => {
setlist(e.target.value);
}
const listofitem = () => {
setitems((e) => {
return [...e , list];
})
}
const deleteItems = (index) => {
// This removes one (2nd parameter) element(s) from array `items` on index `index`
const newItems = items.splice(index, 1)
setitems(newItems)
}
return (
<>
{/* ... */}
</>
)
}
There are multiple ways to remove an item from a list in JS, your version of splicing the last index is correct too and it is able to remove the last item. What it can't do is update your state.
His code is doing two things at the same time: Removing the last item of the Todo array and then, setting the resulted array in the state which updates the todo list.
So, change your code as
const deleteItems = (e) => {
let newItems = [...items];
newItems.splice(e-1, 1);
setitems(newItems);
}
I have the following component which shows a question, there's a button within it that allows you to reveal the answer, this is handled through the revealedResults property/state.
const Question = ({
item
}: {
item: QuestionType;
}) => {
const [revealedResults, setRevealedResults] = useState(false);
const { question, answers } = item;
useEffect(() => {
setRevealedResults(false);
}, [item]);
const handleResultReveal = () => {
setRevealedResults(true);
};
return (
<section>
<h1>Question: {question}</h1>
<button onClick={() => handleResultReveal()}>Reveal Answer</button>
<div>
{revealedResults && answers.map((answer) => <p>{answer}</p>)}
</div>
</section>
);
};
export default Question;
const Questionaire = () => {
const [question, setQuestion] = useState(questions[0]);
const [correctAnswers, setCorrectAnswers] = useState(0);
const [incorrectAnswers, setIncorrectAnswers] = useState(0);
const handleQuestionAnswer = (isCorrect: boolean): void => {
if (isCorrect) {
setCorrectAnswers(correctAnswers + 1);
} else {
setIncorrectAnswers(incorrectAnswers + 1);
}
setQuestion(questions[1]);
};
return (
<>
<Question item={question} />
<section>
<div>
<p> Did you get the answer correct?</p>
<button onClick={() => handleQuestionAnswer(true)}>Yes</button>
<button onClick={() => handleQuestionAnswer(false)}>No</button>
</div>
</section>
</>
);
};
export default Questionaire;
The question updates through the item prop. The idea is that when the item prop updates setRevealedResults is ran again to hide the revealed result of the next question.
The problem I'm having is that the prop of the new question is being flashed right before the useEffect side effect is being ran. You can see this here:
What is the correct way to deal with this?
useEffect runs after the render is done. That's why you see the page change for a moment there.
Try to use useMemo instead. It should update during the render.
I am creating a web app, which is basically an image gallery for a browser game.
The avatars are stored in the game in this format:
https://websitelink.com/avatar/1
https://websitelink.com/avatar/2
https://websitelink.com/avatar/3
So i want to build 2 navigation buttons, one will increment the counter, to move to next image and another one will decrement the counter to move to previous image.
I tried to use props, but since props are immutable it didn't work.
How do I approach building this web app?
Here is the minimal code which may help you to understand about the React Component, props and state.
// parent compoment
import { useState } from "react"
export const GameImageGallery = () => {
const [num, setNum] = useState(0)
const increaseDecrease = (state) => {
if (state === "+") {
setNum(num + 1)
}
if (state === "-") {
setNum(num - 1)
}
}
return (
<>
<button onClick={() => increaseDecrease("-")}>--</button>
<button onClick={() => increaseDecrease("+")}>++</button>
<Image url={`https://websitelink.com/avatar/${num}`} />
</>
)
}
// child component to show image
const Image = ({ url }) => {
return <img src={url} alt="image" />
}
you can do this thing,
const [id,setId]=useState(0);
useEffect(() => {
},[id])
const increment = () => {
setId(id++);
}
const decrement = () => {
setId(id--);
}
return(
<button onClick={increment}>Add</button>
<button onClick={decrement}>remove</button>
<img url={`https://websitelink.com/avatar/${id}`} />
)
useRef is ideal to manage data persistently in a component.
Example:
import { useRef } from 'react'
...
const App = () => {
const links = useRef({url1Ctr : 1})
const onBtnClick = () => {
links.current = { url1Ctr: links.current.url1Ctr + 1}
}
...
}
I'm new to React and I have a short and stupid question, but my poor phrasing makes it so that I haven't been able to find the answer by searching for it.
Basically, I have 2 password fields. I want to show and hide each one independently, but I would like a more elegant way than having 2 different functions with their own variables like this:
const [showPassword1, setShowPassword1] = useState(false);
const [showPassword2, setShowPassword2] = useState(false);
const togglePasswordVisiblity1 = () => {
setShowPassword1(showPassword1 ? false : true);
};
const togglePasswordVisiblity2 = () => {
setShowPassword2(showPassword2 ? false : true);
};
With the respective buttons below:
<span onClick={togglePasswordVisiblity1}>Show/Hide</span>
<span onClick={togglePasswordVisiblity2}>Show/Hide</span>
I'm sure there's a way to regroup these into a single function that changes the right variable based on which span is clicked, but I haven't had any luck finding the syntax. Sorry again for this question, hopefully it can be answered quickly!
Thanks in advance for your help.
You can try to use an array for the state. Check this sandbox demo:
import React, { useState } from "react";
export default function App() {
const [showPassword, setShowPassword] = useState([false, false]);
return (
<div className="App">
<button
onClick={() => setShowPassword([!showPassword[0], showPassword[1]])}
>
{JSON.stringify(showPassword[0])}
</button>
<button
onClick={() => setShowPassword([showPassword[0], !showPassword[1]])}
>
{JSON.stringify(showPassword[1])}
</button>
</div>
);
}
Refactored version by extracting state update into a function:
import React, { useState } from "react";
export default function App() {
const [showPassword, setShowPassword] = useState([false, false]);
const togglePassword = (idx) => {
const newShowPassword = [...showPassword];
newShowPassword[idx] = !newShowPassword[idx]; // toggle
setShowPassword(newShowPassword); // update the state
};
return (
<div className="App">
<button onClick={() => togglePassword(0)}>
{JSON.stringify(showPassword[0])}
</button>
<button onClick={() => togglePassword(1)}>
{JSON.stringify(showPassword[1])}
</button>
</div>
);
}
const [state , setState] = useState({
showPassword1:false,
showPassword2: false
})
const togglePasswordVisiblity1= e => {
const {name , value} = e.target
setState( prevState => ({
...prevState,
[name]: prevState[name] ? false : true
}))
}
//
<span name='showPassword1' onClick={togglePasswordVisiblity1}>Show/Hide</span>
<span name='showPassword2' onClick={togglePasswordVisiblity1}>Show/Hide</span>
I'm working on a component that adds images to items. You can either upload your own image or pick an image, loaded from an API based on the name of the item.
Here is the root component:
const AddMedia = (props) => {
const [currentTab, setCurrentTab] = useState(0);
const [itemName, setItemName] = useState(props.itemName);
return (
<div>
<Tabs
value={currentTab}
onChange={() => setCurrentTab(currentTab === 0 ? 1 : 0)}
/>
<div hidden={currentTab !== 0}>
<FileUpload />
</div>
<div hidden={currentTab !== 1}>
{currentTab === 1 && <ImagePicker searchTerm={itemName} />}
</div>
</div>
);
};
And here is the <ImagePicker />:
import React, { useState, useEffect } from "react";
function ImagePicker({ searchTerm, ...props }) {
const [photos, setPhotos] = useState([]);
const searchForImages = async (keyword) => {
const images = await api.GetImagesByKeyword(keyword);
return images;
};
useEffect(() => {
const result = searchForImages(searchTerm);
setPhotos(result);
}, []);
return (
<>
{photos.map(({ urls: { small } }, j) => (
<img alt={j} src={small} className={classes.img} />
))}
</>
);
}
const areSearchTermsEqual = (prev, next) => {
return prev.searchTerm === next.searchTerm;
};
const MemorizedImagePicker = React.memo(ImagePicker, areSearchTermsEqual);
export default MemorizedImagePicker;
What I'm struggling with is getting the component to not fetch the results again if the searchTerm hasn't changed. For example, when the component loads it's on tab 0 (upload image), you switch to tab 1 (pick an image) and it fetches the results for searchTerm, then you switch to 0 and again to 1 and it fetches them again, although the searchTerm hasn't changed. As you can see, I tried using React.memo but to no avail. Also, I added the currentTab === 1 to stop it from fetching the photos when the root component renders and fetch them only if the active tab is 1.
You should add the searchTerm as dependency of the useEffect so that it will not fetch again if searchTerm hasn't change:
useEffect(() => {
const result = searchForImages(searchTerm);
setPhotos(result);
}, [searchTerm]);
Additional information, if you are using eslint to lint your code, you can use the react-hooks/exhaustive-deps rule to avoid this kind of mistake.