Hello
I am trying to associate a like button with each PaperCard component as shown in the code below. I have included the relevant code. Currently, The like button shows up and every time you click it the counter increases BUT all the buttons share the same state. So I am trying to fix that. I am new to JS and React.
Any help will be appreciated. Thanks!
function Home() {
const [likes, setLikes] = useState(0);
const incrementLikes = () => {
const addToLikes = likes + 1;
setLikes(addToLikes)
}
const loadMorePapers = () => {
setVisible((prevValue) => prevValue + 3);}
return (
<div>
<div style={{display:'flex', justifyContent:'center'}}>
<h1>Latest Papers</h1>
</div>
{apiData.slice(0, visible).map((paper) => (
<Grid key={paper.title}>
<button onClick={incrementLikes}>Likes: {likes}</button>
<PaperCard title={paper.title} abstract={paper.abstract}/>
</Grid>
))}
<div style={{display:'flex', justifyContent: 'center'}}>
<Button variant="contained" onClick={loadMorePapers}>Load More</Button>
</div>
</div>
)
}
The element from the map callback is extracted as a component, and now every button has its own state.
function Home() {
return (
<div>
<div style={{ display: "flex", justifyContent: "center" }}>
<h1>Latest Papers</h1>
</div>
{apiData.slice(0, visible).map((paper) => (
<LikeButton paper={paper} key={paper.title} />
))}
<div style={{ display: "flex", justifyContent: "center" }}>
<button variant="contained" onClick={loadMorePapers}>Load More</button>
</div>
</div>
);
}
function LikeButton(paper) {
const [likes, setLikes] = useState(0);
const incrementLikes = () => {
const addToLikes = likes + 1;
setLikes(addToLikes);
};
return (
<div key={paper.title}>
<button onClick={incrementLikes}>Likes: {likes}</button>
<PaperCard title={paper.title} abstract={paper.abstract}/>
</div>
);
}
Create a new functional component called LikeButton (or something relevant) to house the state for each button independently.
In that component, add the state values you want to track per each button. In your case it seems to just be the likes.
So could be something like:
const LikeButton = () => {
const [likes, setLikes] = useState(0); //likes controlled by state of component
const incrementLikes = () => {
setLikes((prevState) => prevState + 1);
};
return <button onClick={incrementLikes}>Likes: {likes}</button>;
};
Then add that component in place of your existing button and remove the state for likes in the Home component. E.g.:
function Home() {
const loadMorePapers = () => {
setVisible((prevValue) => prevValue + 3);
};
return (
<div>
<div style={{ display: "flex", justifyContent: "center" }}>
<h1>Latest Papers</h1>
</div>
{apiData.slice(0, visible).map((paper) => (
<Grid key={paper.title}>
<LikeButton/>
<PaperCard title={paper.title} abstract={paper.abstract} />
</Grid>
))}
<div style={{ display: "flex", justifyContent: "center" }}>
<Button variant="contained" onClick={loadMorePapers}>
Load More
</Button>
</div>
</div>
);
}
Should you want to control state from the Home component, you can pass the likes as props, but it doesn't seem necessary for what you want.
In this situation you should consider using a reusable button component in order to control state within the component itself. Then you do not have to worry about the buttons sharing the same state. Here would be a simple example of a button component that will track it's count independent of the other buttons that are rendered:
import React, { useState } from 'react';
export default function CounterButton() {
const [count, setCount] = useState(0);
function incrementLikes() {
setCount(count + 1);
}
return (
<button onClick={incrementLikes}>
{count} likes
</button>
);
}
You could simply render these buttons like in the pseudo code below:
{[0, 1, 2, 3].map((num: number, index: number) => (
<div key={index}>
<CounterButton />
</div>
))}
I think you're doing too much in one component. The "likes" in your example are for an individual paper, not for the whole site, right?
Maybe something like this...
function Home() {
const loadMorePapers = () => {
setVisible((prevValue) => prevValue + 3);
}
return (
<div>
<div style={{display:'flex', justifyContent:'center'}}>
<h1>Latest Papers</h1>
</div>
{apiData.slice(0, visible).map((paper) => (
<Paper {...paper} key={paper.title} />
))}
<div style={{display:'flex', justifyContent: 'center'}}>
<Button variant="contained" onClick={loadMorePapers}>Load More</Button>
</div>
</div>
);
}
function Paper(props){
const [likes, setLikes] = useState(0);
const incrementLikes = () => setLikes(likes + 1)
return (
<Grid>
<button onClick={incrementLikes}>Likes: {likes}</button>
<PaperCard title={paper.title} abstract={paper.abstract}/>
</Grid>
)
}
If the data from the api has a key/id you can pass that to your incrementLikes function and use it to increment the likes for the right item.
const [apiData, setApidData] = useState(...)
const incrementLikes = (id) => {
const updated = apiData.map((paper) => {
if (paper.id === id) {
return {
...paper,
likes: paper.likes + 1
};
}
return paper;
});
setApidData(updated);
};
Then pass the id in the button
<button onClick={() => incrementLikes(paper.id)}>Likes: {paper.likes}</button>
// Get a hook function
const { useState } = React;
const PaperCard = ({ title, abstract }) => {
return (
<div>
<p>{title}</p>
<p>{abstract}</p>
</div>
);
};
const Header = () => {
const [apiData, setApidData] = useState([
{
title: 'Testing likes',
id: 1,
likes: 0,
abstract: 'abs',
},
{
title: 'More likes',
id: 3,
likes: 5,
abstract: 'abstract',
}
]);
const incrementLikes = (id) => {
const updated = apiData.map((paper) => {
if (paper.id === id) {
return {
...paper,
likes: paper.likes + 1
};
}
return paper;
});
setApidData(updated);
};
const loadMorePapers = (e) => {};
return (
<div>
<div style={{ display: 'flex', justifyContent: 'center' }}>
<h1>Latest Papers</h1>
</div>
{apiData.map((paper) => (
<div key={paper.title}>
<button onClick={() => incrementLikes(paper.id)}>Likes: {paper.likes}</button>
<PaperCard title={paper.title} abstract={paper.abstract} />
</div>
))}
<div style={{ display: 'flex', justifyContent: 'center' }}>
<button variant='contained' onClick={loadMorePapers}>
Load More
</button>
</div>
</div>
);
};
// Render it
ReactDOM.render(<Header />, document.getElementById('react'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="react"></div>
Related
I'm trying to update value in react functional compoenent through input element but after the first value I'm unable to type
My Code:
import React from "react";
import "./App.css";
const { useState } = React;
function App() {
const [items, setItems] = useState([
{ value: "" },
{ value: "" },
{ value: "" },
]);
const [count, setCount] = useState(0);
const Item = React.memo(({ id, value, onChange }) => {
return (
<div className="item">Item
<input
onChange={(e) => onChange(id, e.target.value)}
value={value}
/>
</div>
);
});
return (
<div className="app">
<h1>Parent</h1>
<p style={{marginTop: '20px'}}>Holds state: {count}, Does't passes to it items</p>
<p style={{marginTop: '20px'}}>{JSON.stringify(items)}</p>
<button onClick={() => setCount((prev) => prev + 1)} style={{marginTop: '20px'}}>
Update Parent
</button>
<ul className="items" style={{marginTop: '20px'}}>
{items.map((item, index) => {
return (
<Item
key={index}
id={index}
value={item.value}
onChange={(id, value) =>
setItems(
items.map((item, index) => {
return index !== id
? item
: { value: value };
})
)
}
/>
);
})}
</ul>
</div>
);
}
export default App;
You should move the Item declaration outside the App component. Having a component declaration inside another one is almost always a bad idea. Explanation below.
import React, { useState } from "react";
const Item = React.memo(({ id, value, onChange }) => {
return (
<div className="item">
Item
<input onChange={(e) => onChange(id, e.target.value)} value={value} />
</div>
);
});
function App() {
const [items, setItems] = useState([
{ value: "" },
{ value: "" },
{ value: "" }
]);
const [count, setCount] = useState(0);
return (
<div className="app">
<h1>Parent</h1>
<p style={{ marginTop: "20px" }}>
Holds state: {count}, Does't passes to it items
</p>
<p style={{ marginTop: "20px" }}>{JSON.stringify(items)}</p>
<button
onClick={() => setCount((prev) => prev + 1)}
style={{ marginTop: "20px" }}
>
Update Parent
</button>
<ul className="items" style={{ marginTop: "20px" }}>
{items.map((item, index) => {
return (
<Item
key={index}
id={index}
value={item.value}
onChange={(id, value) =>
setItems(
items.map((item, index) => {
return index !== id ? item : { value: value };
})
)
}
/>
);
})}
</ul>
</div>
);
}
export default App;
When a component definition is inside another component, React will re-declare the inner component every time the parent re-renders. This means, that any state held by the inner component will be lost.
In your case, since every time there is an entirely new component, the input was not the same input as in the previous render. This means that the input that was in focus in the previous render is not present anymore, and the new input is not focused anymore.
You should also probably change
setItems(
items.map((item, index) => {
return index !== id ? item : { value: value };
})
)
to
prev.map((item, index) => {
return index !== id ? item : { value: value };
})
)
It's a good idea to use the function notation for set state when the new state depends on the old state value.
I have a parent component that is passing products down into a subcomponent as state along with the product's filters. For some reason I have to double click the "filters" in order for the parent component to rerender with the filtered products. I understand because it is running asynchronously it is not updating the state immediately, but how can I force the update and rerender to run as soon as I add a filter without using forceUpdate? Is this where redux would come in to play?
Parent component
const [products, setProducts] = React.useState(data.pageContext.data);
const handleCount = () => {
setCount(count + 24);
}
return (
<div style={{width: "100%"}}>
<Header/>
<div style={{display: "flex", flexDirection: "row", justifyContent: "center"}}>
<Sidebar
products={products}
setProducts={setProducts}
baseProducts={data.pageContext.data}
/>
<div style={{display: "flex", flexDirection: "column"}}>
<h1 style={{width: "50%"}}>Cast vinyl</h1>
<h3>Product Count: {products.length}</h3>
<ProductList>
{products.slice(0, count).map(product => {
return (
<a href={`/vinyl/${product.data.sku}`}><div>
{product.data.field_product_image.length > 0 ?
<ProductImage images={data.data.allFiles} sku={`${product.data.sku}`}/> :
<StaticImage src="http://stagingsupply.htm-mbs.com/sites/default/files/default_images/drupalcommerce.png" width={250} alt=""/>}
<h3>{product.data.title}</h3>
<h5>{product.data.sku}</h5>
</div></a>
)
})}
</ProductList>
<h3 onClick={handleCount}>Load more</h3>
</div>
</div>
</div>
)
Child Component
const Sidebar = ({ setProducts, baseProducts }) => {
const [filters, setFilters] = React.useState([]);
const [click, setClick] = React.useState(false);
const handleClick = () => {
setClick(!click);
}
const onChange = (e) => {
if (!filters.includes(e)) {
setFilters([...filters, e])
}
if (filters.length > 0) {
const filteredProducts = baseProducts.filter(product => filters.includes(product.data.field_product_roll_size));
setProducts(filteredProducts);
}
}
const clearFilters = () => {
setFilters([]);
setProducts(baseProducts);
setClick(false);
}
const rollSize = [...new Set(baseProducts.map(fields => fields.data.field_product_roll_size))]
return (
<SidebarContainer>
<h3>Mbs Sign Supply</h3>
<ul>Sub Categories</ul>
<li>Calendered Vinyl</li>
<li>Cast Vinyl</li>
<h3>Filters</h3>
{filters.length > 0 ? <button onClick={clearFilters}>Clear Filters</button> : null}
<li onClick={() => handleClick()}>Roll Size</li>
{/*map through roll size array*/}
{/*each size has an onclick function that filters the products array*/}
{click ? rollSize.sort().map(size => {
return (
<span style={{display: "flex", flexDirection: "row", alignItems: "center", height: "30px"}}>
<Checkbox onClick={() => {onChange(size)}} />
<p >{size}</p>
</span>
)
}) : null}
<li>Width</li>
demo can be found at http://gatsby.htm-mbs.com:8000/cast-vinyl, clicking "Roll Size" from the left and then double clicking a filter
Thanks in advance
All I needed was a little useEffect
React.useEffect(() => {
if (filters.length > 0) {
const filteredProducts = baseProducts.filter(product => filters.includes(product.data.field_product_roll_size));
setProducts(filteredProducts);
}
}, [filters]);
So I have built a movie search app.
On the 4th page we have the ability to search for a specific movie or TV show.
Now I have built a logic that will display "Movies(Tv Shows) not found" when there are no search results.
Here is the code of the entire "Search" Component:
const Search = () => {
const [type, setType] = useState(0);
const [page, setPage] = useState(1);
const [searchText, setSearchText] = useState("");
const [content, setContent] = useState([]);
const [numOfPages, setNumOfPages] = useState();
const [noSearchResults, setNoSearchResults] = useState(false);
const fetchSearch = async () => {
try {
const { data } = await axios.get(`https://api.themoviedb.org/3/search/${type ? "tv" : "movie"}?api_key=${process.env.REACT_APP_API_KEY}&language=en-US&query=${searchText}&page=${page}&include_adult=false`);
setContent(data.results);
setNumOfPages(data.total_pages);
} catch (error) {
console.error(error);
}
};
const buttonClick = () => {
fetchSearch().then(() => {
if (searchText && content.length < 1) {
setNoSearchResults(true);
} else {
setNoSearchResults(false);
}
});
};
useEffect(() => {
window.scroll(0, 0);
fetchSearch();
// eslint-disable-next-line
}, [page, type]);
return (
<div>
<div style={{ display: "flex", margin: "25px 0" }}>
<TextField className="textBox" label="Search" variant="outlined" style={{ flex: 1 }} color="secondary" onChange={e => setSearchText(e.target.value)} />
<Button variant="contained" style={{ marginLeft: "10px" }} size="large" onClick={buttonClick}>
<SearchIcon color="secondary" fontSize="large" />
</Button>
</div>
<Tabs
value={type}
indicatorColor="secondary"
onChange={(event, newValue) => {
setPage(1);
setType(newValue);
}}
style={{
marginBottom: "20px",
}}
>
<Tab style={{ width: "50%" }} label="Search Movies" />
<Tab style={{ width: "50%" }} label="Search TV Shows" />
</Tabs>
<div className="trending">
{content && content.map(c => <SingleContent key={c.id} id={c.id} poster={c.poster_path} title={c.title || c.name} date={c.first_air_date || c.release_date} media_type={type ? "tv" : "movie"} vote_average={c.vote_average} />)}
{noSearchResults && (type ? <h2>Tv Shows not found</h2> : <h2>Movies not found</h2>)}
</div>
{numOfPages > 1 && <CustomPagination setpage={setPage} numOfPages={numOfPages} />}
</div>
);
};
You can see this in action here.
The problem that happens is that even when I have something in my search results, it still shows the Movies(Tv Shows) not found message.
And then if you click the search button again it will disappear.
A similar thing happens when there are no search results.
Then the Movies(Tv Shows) not found message will not appear the first time, only when you press search again.
I don't understand what is going on. I have used .then after my async function and still it does not execute in that order.
Try adding noSearchResults to your useEffect hook. That hook is what tells React when to re-render, and right now it's essentially not listening to noSearchResult whenever it changes because it's not included in the array.
Is there another way to do this without having to place the useState variables outside the map function?
const slimy = [];
{
slimy.map(function (item) {
const [count, setCount] = useState("");
return (
<ListItem
key={item.createdAt}
style={{ display: "flex", justifyContent: "center" }}
>
<div>
<p>{count}</p>
<button onClick={() => setCount(count + 1)} />
</div>
</ListItem>
);
});
}
You can define a custom component.
{
slimy.map(function (item) {
return (
<CustomItem key={item.createdAt} names={item.names} />
);
});
}
const CustomItem = (props) => {
console.log(props.names); // <----------------
const [count, setCount] = useState(0);
return (
<ListItem style={{ display: "flex", justifyContent: "center" }}>
<div>
<p>{count}</p>
<button onClick={() => setCount(count + 1)} />
</div>
</ListItem>
)
}
You can put an array into state instead:
const [countArr, setCountArr] = useState(
() => new Array(slimy.length).fill(0)
);
return slimy.map(function(item, i){
<ListItem key={item.createdAt} style={{ display: 'flex', justifyContent: 'center'}}>
<div>
<p>{countArr[i]}</p>
<button onClick={() => { setCountArr(
countArr.map((count, j) => i === j ? count + 1 : count)
) }} />
</div>
</ListItem>)
});
So I'm new to React, I'm trying to pass the data after fetching with axios to Component:
index.js
class App extends React.Component {
state = { pokemon: null, errorMsg: '' };
componentDidMount() {
api
.get('', {
params: {
limit: 50,
offset: showrandom(),
},
})
.then((res) => {
const temporary = res.data.results;
const details = { pokemon: [] };
temporary.forEach((e) => {
api
.get('/' + e.name)
.then((res) => details.pokemon.push(res.data))
.catch((e) => console.log(e));
});
this.setState({ pokemon: details.pokemon });
})
.catch((e) => this.setState({ errorMsg: e }));
}
render() {
if (this.state.pokemon) {
return (
<React.Fragment>
<div
className="container-fluid"
style={{ textAlign: 'center', justifyContent: 'center', marginBottom: '5em' }}
>
<img alt="logo" src="logo" />
<p>by: Ihsan Fajar Ramadhan</p>
</div>
<div className="ui five column grid" style={{ padding: '2em' }}>
<Cards data={this.state.pokemon} />
</div>
</React.Fragment>
);
} else {
return (
<React.Fragment>
<div className="ui" style={{ textAlign: 'center', justifyContent: 'center', marginBottom: '5em' }}>
<img alt="logo" src="logo" />
<p>by: Ihsan Fajar Ramadhan</p>
</div>
<div className="ui" style={{ textAlign: 'center', justifyContent: 'center' }}>
<i className="compass loading icon massive blue"></i>
<p className="ui text blue">Loading...</p>
</div>
</React.Fragment>
);
}
}
}
Card.js
import React from 'react';
const Card = (props) => {
let pokemon = props.data.map((pokemon, i) => {
return (
<div className="column" key={i}>
<div className="ui fluid card">
<a className="image" href="/">
<img alt="nama" src={pokemon.sprites.front_default} />
</a>
<div className="content" style={{ textAlign: 'center' }}>
<a className="header" href="/">
{pokemon.species.name}{' '}
</a>
</div>
</div>
</div>
);
});
if (props.data.length > 0) {
return <>{pokemon}</>;
} else {
return <>fetching</>;
}
};
export default Card;
The loading screen rendered successfully, and after the pokemon state updated it passes the condition where if(this.state.pokemon) which means the state has been updated, then it tries to render the element with component. But the problem is the data props to component is not passed yet, and it renders "fetching" instead {pokemon}. I've been searching for solution but I'm stuck. The strange part is that when I changed content (adding or deleting word), the data is passed to the Component.
first of all, thank you to Chris G for the hint in the comment.
There's the problem: a forEach loop doesn't wait for async code inside, only a for loop does, and you haven't even used await. When you call this.setState({ pokemon: details.pokemon });, none of the api calls has finished yet, and it's still [], exactly what causes the behavior you observe.
So here is my solution:
async loadApi(){
const temp= await api.get('',{
params:{
limit:50,
offset:showrandom()
}
});
const pokemon=[];
for(let i=0; i<temp.data.results.length; i++){
await api.get('/'+temp.data.results[i].name)
.then(res => pokemon.push(res.data))
}
this.setState({pokemon:pokemon});
return pokemon;
}
componentDidMount(){
this.loadApi();
}