React OnClick iteration - javascript

I want to do an onClick counter but I have a problem with the counter iterating correctly. In the app there are 3 "products" and after clicking "Add To Cart" button the state of the object is updated but all of the products are generated separately. I think that is cousing the problem where the counter is different for each of the products or everything will work correctly if I lift the state up, but the console.log is just freshly generated for all of the products. I'm not really sure so I need help with that.
Here is some code in the order from the parent to the last child:
import { useEffect, useState } from "react";
import ProductList from "./ProductList";
const Products = () => {
const [products, setProducts] = useState (null);
useEffect (() => {
fetch('http://localhost:8000/products')
.then(res => {
return res.json();
})
.then(data => {
setProducts(data);
})
}, []);
return (
<div className="ProductList">
{products && <ProductList products={products}/>}
</div>
);
}
export default Products;
import Card from "./Card";
const ProductList = (props) => {
const products = props.products;
return (
<div className="ProductList" >
{products.map((product) => (
<Card product={product} key={product.id} />))}
</div>
);
}
export default ProductList;
import { useState } from "react";
const Card= ({ product }) => {
const [showDescription, setShowDescription] = useState(false);
const [CartCounter, setCartCounter ] = useState(0);
console.log(CartCounter);
return (
<div className="Product-Preview" >
<div className="backdrop" style={{ backgroundImage: `url(${product.image})` }}></div>
<h2>{product.title}</h2>
<div>{product.price}</div>
<button className="ShowDescription" onClick={() => setShowDescription(!showDescription)}>Details</button>
<button className="AddToCart" onClick={() => setCartCounter(CartCounter + 1)}>Add To Cart </button>
{showDescription && <p>{product.description}</p>}
<br />
</div>
);
};
export default Card;

Ok, you want to keep track of an aggregated value. I'll list code in some high level.
const ProductList = () => {
const [count, setCount] = useState(0)
const addOrRemove = n => { setCount(v => v + n) }
return products.map(p => <Card addOrRemove={addOrRemove} />)
}
const Card = ({ addOrRemove }) => {
// optional if you want to track card count
// const [count, setCount] = useState(0)
return (
<>
<button onClick={() => { addOrRemove(1) }>Add</button>
<button onClick={() => { addOrRemove(-1) }>Remove</button>
</>
)
}
Essentially either you track the local count or not, you need to let the parent to decide what is the final count, otherwise there'll be some out of sync issue between the child and parent.

Related

How can I change the state of individual elements in a map function?

I want the content to display when the tab is clicked. The issue that I'm having is that once the tab is clicked, all the tabs open... and likewise close when clicked again. I've been trying for hours to figure out how to fix this. I thought I had an answer by having a state that I could set the index to and then write a condition for the tab to open when the index of the state is the same but I noticed that after clicking on another tab, the other one closes. I would appreciate it so much if someone could help me open an individual tab when it's clicked and always stay open until clicked again, meaning, I could have multiple tabs open at once.
Here's a demo:
https://codesandbox.io/s/orrigenda-react-question-5oxg47
import React, { useEffect, useState } from 'react'
import axios from 'axios';
import LeaguesStyle from '../components/styles/LeaguesStyle.css';
const Leagues = () => {
const [teamz, setTeams] = useState([]);
const [loading, setLoading] = useState(false)
const [isOpen, setOpen] = useState(false);
const getTeams = async () => {
try {
const res = await axios.get('https://api-football-standings.azharimm.site/leagues');
setTeams(res.data.data)
setLoading(true);
console.log(res.data)
} catch (err) {
alert(err.message)
}
}
useEffect(() => {
getTeams();
}, []);
return (
<div className="leagues">
{loading &&
teamz.map(item => (
<div className='teamCard' key={item.id}>
<div onClick={() => setOpen(!isOpen)} className="teamDiv">
<img src={item.logos.dark} className='teamLogo' />
<h1>{item.name}</h1>
</div>
{isOpen && <div className='card-content-active'>{item.abbr}</div>}
</div>
))}
</div>
);
}
You need to track the individual truthy values per item.id. This can be easily done by using an object to keep track of all the previous states via the spread operator. Once an initial state is set per tab, then it's just a matter of toggling that individual state between true and false. You delineate between tabs by dynamically assigning the id to the truthy value ([id]: !isOpen[id]). Here is the code in totality:
import React, { useEffect, useState } from "react";
import axios from "axios";
import LeaguesStyle from "./LeaguesStyle.css";
const Leagues = () => {
const [teamz, setTeams] = useState([]);
const [loading, setLoading] = useState(false);
const [isOpen, setOpen] = useState({});
const getTeams = async () => {
try {
const res = await axios.get(
"https://api-football-standings.azharimm.site/leagues"
);
setTeams(res.data.data);
setLoading(true);
console.log(res.data);
} catch (err) {
alert(err.message);
}
};
useEffect(() => {
getTeams();
}, []);
const handleOpen = (id) => {
setOpen((prevTruthys) => ({ ...prevTruthys, [id]: !isOpen[id] }));
};
console.log(isOpen);
return (
<div className="leagues">
{loading &&
teamz.map((item) => (
<div className="teamCard" key={item.id}>
<div onClick={() => handleOpen(item.id)} className="teamDiv">
<img src={item.logos.dark} className="teamLogo" alt="logo" />
<h1>{item.name}</h1>
</div>
{isOpen[item.id] === true && (
<div className="card-content-active">{item.abbr}</div>
)}
</div>
))}
</div>
);
};
export default Leagues;
Here is the code sandbox: https://codesandbox.io/s/orrigenda-react-question-forked-42lbfo?file=/src/App.js
The solution is to store all clicked tabs in a list using the item ID, when the tab is open and you clicked again the ID is removed from the list
here is the code with the solution:
I created a function to update the state. setOpenById(tabId) and a function for checking if the tab is open isTabOpen(tabId)
the onClick now uses that function onClick={() => setOpenById(item.id)}
import React, { useEffect, useState } from "react";
import axios from "axios";
import LeaguesStyle from "./LeaguesStyle.css";
const Leagues = () => {
const [teamz, setTeams] = useState([]);
const [loading, setLoading] = useState(false);
const [openTab, setOpenTab] = useState([])
const getTeams = async () => {
try {
const res = await axios.get(
"https://api-football-standings.azharimm.site/leagues"
);
setTeams(res.data.data);
setLoading(true);
//console.log(res.data);
} catch (err) {
alert(err.message);
}
};
useEffect(() => {
getTeams();
}, []);
const setOpenById = (tabId) => {
if(!isTabOpen(tabId)){
setOpenTab([...openTab, tabId])
} else{
var array = [...openTab] // make a separate copy of the array
var index = array.indexOf(tabId)
if (index !== -1) {
array.splice(index, 1)
setOpenTab(array)
}
}
}
const isTabOpen = (tabId) => {
return openTab.indexOf(tabId) !== -1
}
return (
<div className="leagues">
{loading &&
teamz.map((item) => (
<div className="teamCard" key={item.id}>
<div onClick={() => setOpenById(item.id)} className="teamDiv">
<img src={item.logos.dark} className="teamLogo" alt="logo" />
<h1>{item.name}</h1>
</div>
{isTabOpen(item.id) && <div className="card-content-active">{item.abbr}</div>}
</div>
))}
</div>
);
};
export default Leagues;

How can I send the state (useState) of one file component to another file's component?

REACT.js:
Let say I have a home page with a search bar, and the search bar is a separate component file i'm calling.
The search bar file contains the useState, set to whatever the user selects. How do I pull that state from the search bar and give it to the original home page that
SearchBar is called in?
The SearchBar Code might look something like this..
import React, { useEffect, useState } from 'react'
import {DropdownButton, Dropdown} from 'react-bootstrap';
import axios from 'axios';
const StateSearch = () =>{
const [states, setStates] = useState([])
const [ stateChoice, setStateChoice] = useState("")
useEffect (()=>{
getStates();
},[])
const getStates = async () => {
let response = await axios.get('/states')
setStates(response.data)
}
const populateDropdown = () => {
return states.map((s)=>{
return (
<Dropdown.Item as="button" value={s.name}>{s.name}</Dropdown.Item>
)
})
}
const handleSubmit = (value) => {
setStateChoice(value);
}
return (
<div>
<DropdownButton
onClick={(e) => handleSubmit(e.target.value)}
id="state-dropdown-menu"
title="States"
>
{populateDropdown()}
</DropdownButton>
</div>
)
}
export default StateSearch;
and the home page looks like this
import React, { useContext, useState } from 'react'
import RenderJson from '../components/RenderJson';
import StateSearch from '../components/StateSearch';
import { AuthContext } from '../providers/AuthProvider';
const Home = () => {
const [stateChoice, setStateChoice] = useState('')
const auth = useContext(AuthContext)
console.log(stateChoice)
return(
<div>
<h1>Welcome!</h1>
<h2> Hey there! Glad to see you. Please login to save a route to your prefered locations, or use the finder below to search for your State</h2>
<StateSearch stateChoice={stateChoice} />
</div>
)
};
export default Home;
As you can see, these are two separate files, how do i send the selection the user makes on the search bar as props to the original home page? (or send the state, either one)
You just need to pass one callback into your child.
Homepage
<StateSearch stateChoice={stateChoice} sendSearchResult={value => {
// Your Selected value
}} />
Search bar
const StateSearch = ({ sendSearchResult }) => {
..... // Remaining Code
const handleSubmit = (value) => {
setStateChoice(value);
sendSearchResult(value);
}
You can lift the state up with function you pass via props.
const Home = () => {
const getChoice = (choice) => {
console.log(choice);
}
return <StateSearch stateChoice={stateChoice} giveChoice={getChoice} />
}
const StateSearch = (props) => {
const handleSubmit = (value) => {
props.giveChoice(value);
}
// Remaining code ...
}
Actually there is no need to have stateChoice state in StateSearch component if you are just sending the value up.
Hello and welcome to StackOverflow. I'd recommend using the below structure for an autocomplete search bar. There should be a stateless autocomplete UI component. It should be wrapped into a container that handles the search logic. And finally, pass the value to its parent when the user selects one.
// import { useState, useEffect } from 'react' --> with babel import
const { useState, useEffect } = React // --> with inline script tag
// Autocomplete.jsx
const Autocomplete = ({ onSearch, searchValue, onSelect, suggestionList }) => {
return (
<div>
<input
placeholder="Search!"
value={searchValue}
onChange={({target: { value }}) => onSearch(value)}
/>
<select
value="DEFAULT"
disabled={!suggestionList.length}
onChange={({target: {value}}) => onSelect(value)}
>
<option value="DEFAULT" disabled>Select!</option>
{suggestionList.map(({ id, value }) => (
<option key={id} value={value}>{value}</option>
))}
</select>
</div>
)
}
// SearchBarContainer.jsx
const SearchBarContainer = ({ onSelect }) => {
const [searchValue, setSearchValue] = useState('')
const [suggestionList, setSuggestionList] = useState([])
useEffect(() => {
if (searchValue) {
// some async logic that fetches suggestions based on the search value
setSuggestionList([
{ id: 1, value: `${searchValue} foo` },
{ id: 2, value: `${searchValue} bar` },
])
}
}, [searchValue, setSuggestionList])
return (
<Autocomplete
onSearch={setSearchValue}
searchValue={searchValue}
onSelect={onSelect}
suggestionList={suggestionList}
/>
)
}
// Home.jsx
const Home = ({ children }) => {
const [result, setResult] = useState('')
return (
<div>
<SearchBarContainer onSelect={setResult} />
result: {result}
</div>
)
}
ReactDOM.render(<Home />, document.getElementById('root'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.9.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.9.0/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Just pass a setState to component
parent component:
const [state, setState] = useState({
selectedItem: ''
})
<StateSearch state={state} setState={setState} />
change parent state from child component:
const StateSearch = ({ state, setState }) => {
const handleStateChange = (args) => setState({…state, selectedItem:args})
return (...
<button onClick={() => handleStateChange("myItem")}/>
...)
}

OnClick toggles all items

I want to toggle each of the item that I clicked on but Its keeps toggling all the Items. Using the useContext api
import React, { useState, useEffect } from "react";
const MyContext = React.createContext({
addToFavorites: () => {},
likeHandler: () => {},
fetchRequest: () => {},
});
export const MyContextProvider = (props) => {
const [favorites, setfavorites] = useState([]);
const [toggle, setToggle] = useState(false);
const [items, setItems] = useState([]);
const fetchRequest = async () => {
const api_Key = "oLfD9P45t23L5bwYmF2sib88WW5yZ8Xd7mkmhGSy";
const response = await fetch(
`https://api.nasa.gov/mars-photos/api/v1/rovers/curiosity/photos?
sol=50&api_key=${api_Key}`
);
const data = await response.json();
const allItems = data.photos.map((item) => {
return {
id: item.id,
title: item.camera.full_name,
img: item.img_src,
date: item.rover.launch_date,
like: false,
};
});
setItems(allItems);
};
const likeHandler = (item) => {
const found = items.find((x) => x.id === item.id);
setToggle((found.like = !found.like));
console.log(found); //this logs the particular item that is clicked on
};
return (
<MyContext.Provider
value={{
likeHandler,
fetchRequest,
toggleLike: toggle,
data: items,
}}
>
{props.children}
</MyContext.Provider>
);
};
export default MyContext;
I also have a NasaCard component where I call the likeHandler function and the toggle state, onClick of the FavoriteIcon from my context.And I pass in the toggle state to a liked props in my styled component to set the color of the favorite Icon
import {
Container,
Image,
Name,
InnerContainer,
Titlecontainer,
Date,
FavouriteContainer,
FavouriteIcon,
ImageContainer,
SocialContainer,
} from "./index";
import MyContext from "../../Context/store";
import React, { useState, useEffect, useContext } from "react";
const NasaCards = (props) => {
const { likeHandler, toggleLike } = useContext(MyContext);
return (
<Container>
<InnerContainer>
<ImageContainer>
<Image src={props.Image} alt="" />
</ImageContainer>
<Titlecontainer>
<Name>{props.title}</Name>
<Date>{props.date}</Date>
<FavouriteContainer>
<FavouriteIcon
liked={toggleLike}
onClick={() => {
likeHandler({
id: props.id,
title: props.title,
Image: props.Image,
});
}}
/>
</InnerContainer>
</Container>
);
};
export default NasaCards;
I think you're making this a little more complicated than it needs to be. For starters you are using a single boolean toggle state for all the context consumers, and then I think you're mixing your like property on the items state array with the toggle state.
The items array objects have a like property, so you can simply toggle that in the context, and then also use that property when mapping that array.
MyContextProvider - Map the items state to a new array, updating the like property of the matching item.
const likeHandler = (item) => {
setItems(items => items.map(
el => el.id === item.id
? { ...el, like: !el.like }
: el
));
console.log(item); // this logs the particular item that is clicked on
};
NasaCards - Use item.like property for the liked prop on FavouriteIcon and pass the entire props object to the likeHandler callback.
const NasaCards = (props) => {
const { likeHandler } = useContext(MyContext);
return (
<Container>
<InnerContainer>
<ImageContainer>
<Image src={props.Image} alt="" />
</ImageContainer>
<Titlecontainer>
<Name>{props.title}</Name>
<Date>{props.date}</Date>
<FavouriteContainer>
<FavouriteIcon
liked={props.like} // <-- use like property
onClick={() => {
likeHandler(props); // <-- props has id property
}}
/>
</FavouriteContainer>
</Titlecontainer?
</InnerContainer>
</Container>
);
};

Passing value up in react

I have a component with a lot of buttons, which increment the counter. I need the counter all the way up in the main component, so I want to pass the Counter value;
import { useState } from "react";
import Card from "./Card";
const ProductList = (props) => {
const products = props.products;
const [ Counter, setCounter ] = useState(0);
const Count = n => { setCounter(v => v + n) }
return (
<div className="ProductList" >
{products.map((product) => (
<Card product={product} key={product.id} C={Count} />))}
</div>
);
}
export default ProductList;
up to the parent component:
import { useEffect, useState } from "react";
import ProductList from "./ProductList";
const Products = () => {
const [products, setProducts] = useState (null);
useEffect (() => {
fetch('http://localhost:8000/products')
.then(res => {
return res.json();
})
.then(data => {
setProducts(data);
})
}, []);
return (
<div className="ProductList">
{products && <ProductList products={products}/>}
</div>
);
}
export default Products;
It's a simple question, but I'm not sure how it works. Can anyone help? Thanks!
If you need the Counter reference in the Products component you should define it in the component itself and pass the function reference down to the child component.
Try to change your code like this:
Products component:
const Products = () => {
const [products, setProducts] = useState(null);
const [Counter, setCounter] = useState(0);
const count = (n) => {
setCounter((v) => v + n);
};
useEffect(() => {
fetch('http://localhost:8000/products')
.then((res) => {
return res.json();
})
.then((data) => {
setProducts(data);
});
}, []);
return (
<div className='ProductList'>
{products && <ProductList products={products} count={count}/>}
</div>
);
};
export default Products;
ProductList component
import Card from "./Card";
const ProductList = (props) => {
const products = props.products;
const count = props.count
return (
<div className="ProductList" >
{products.map((product) => (
<Card product={product} key={product.id} C={count} />))}
</div>
);
}
export default ProductList;
However, if you need to pass the function down multiple levels, you should probably use the Context API, but from the question, I presume you just need to go down one level.
Define the counter state in the parent component then pass both the counter and setCounter to the child as props. Update counter with setCounter and it will be update in the parent component also.
Child:
import Card from "./Card";
const ProductList = ({counter, setCounter}) => {
const products = props.products;
const Count = n => { setCounter(v => v + n) }
return (
<div className="ProductList" >
{products.map((product) => (
<Card product={product} key={product.id} C={Count} />))}
</div>
);
}
export default ProductList;
Parent:
import ProductList from "./ProductList";
const Products = () => {
const [products, setProducts] = useState (null);
const [counter, setCounter] = useState(0);
useEffect (() => {
fetch('http://localhost:8000/products')
.then(res => {
return res.json();
})
.then(data => {
setProducts(data);
})
}, []);
return (
<div className="ProductList">
{products &&
<ProductList
counter={counter}
setCounter={setCounter}
products={products}
/>}
</div>
);
}
export default Products;

How to use React component's custom hook with "map"

I'm trying to make a Checkbox component.
Here is my Checkbox.tsx.
import React from "react";
import * as S from "./style";
const Checkbox: React.FC<S.ICheckboxProps> = ({ checked, setChecked }) => {
return <S.StyledCheckbox checked={checked} onClick={setChecked} />;
};
and this is my useCheckbox.tsx,
import { useState } from "react";
export const useCheckbox = (initialState: boolean) => {
const [checked, _setChecked] = useState<boolean>(initialState);
const setCheckedToggle = () => _setChecked((prev) => !prev);
const setCheckedTrue = () => _setChecked(true);
const setCheckedFalse = () => _setChecked(false);
return { checked, setCheckedToggle, setCheckedTrue, setCheckedFalse };
};
export default Checkbox;
It works good. I can use this like
import Layout from "components/Layout";
import { useCheckbox } from "hooks/useCheckbox";
import Checkbox from "components/Checkbox";
const Home = () => {
const { checked, setCheckedToggle } = useCheckbox(false);
return (
<Layout>
<Checkbox checked={checked} setChecked={setCheckedToggle} />
</Layout>
);
};
export default Home;
But I have trouble in the List component.
List has a Checkbox component, and I have to use this List with data.
const Home = ({data}) => {
return (
<Layout>
{data.map((d) => <List />)}
</Layout>
);
};
In this case, is there a way to determine if the list is selected?
If the List has useCheckbox, the Home component doesn't know the checked state.
Should I use useCheckbox in the Home component for data.length times? I think this is not good.
Thanks for reading, and Happy new year.
If you want the checkbox state to exist at the level of Home then you'll need state in the Home component that can handle multiple items, either as an array or object.
Then where you map over data you can pass down checked and setChecked as props to List, with all the logic defined in Home using the item index (or preferably an ID if you have one) in relation to your Home state.
Here's an example of a hook you could use in Home
import { useState } from "react";
export const useCheckboxes = () => {
const [checkedIds, setCheckedIds] = useState([]);
const addToChecked = (id) => setCheckedIds((prev) => [...prev, id]);
const removeFromChecked = (id) =>
setCheckedIds((prev) => prev.filter((existingId) => existingId !== id));
const isChecked = (id) => !!checkedIds.find(id);
const toggleChecked = (id) =>
isChecked(id) ? removeFromChecked(id) : addToChecked(id);
return { isChecked, toggleChecked };
};
And you would use it like this
const Home = ({ data }) => {
const { isChecked, toggleChecked } = useCheckboxes();
return (
<Layout>
{data.map((d) => (
<List
key={d.id}
checked={isChecked(d.id)}
toggleChecked={() => toggleChecked(d.id)}
/>
))}
</Layout>
);
};

Categories

Resources