Component rerendering only after double click - javascript

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]);

Related

react functional component: After first value not able to continue type the value in input element

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.

How to prevent re-render child component when dispatch redux in it to render parent component?

I have child component and it has a button to dispatch redux a variable to use in parent component.
But I want to still open child component when I click on the button.
Child Component
const ServoActionFill = (e, jammerSelected) => {
dispatch({
type: "HIGHLIGHT_TARGET",
data: {
uniqueId: "546546644",
},
});
}
return <div>
{showPopup == true && target != null && subPopupServo == true &&
<div className="position-fixed d-flex" style={{ top: info.containerPoint.y - 130, left: info.containerPoint.x - 130, zIndex: 1000 }} onMouseLeave={() => disp("servo")}>
<PieMenu
radius="130px"
centerRadius="70px"
centerX={info.containerPoint.x}
centerY={info.containerPoint.y}
>
{/* Contents */}
{jammerList.activeJammers.map(x =>
//StatusName.forEach(y =>
// y == x.latinName ?
<Slice onMouseOver={(e) => ServoActionFill(e, x)} backgroundColor="#6b22f3"><div style={{ fontSize: "11px", fontWeight: "bold" }}>{x.latinName}</div></Slice>
)}
</PieMenu>
</div>
}
Parent Component
const handlePropChange = (prevProps, nextProps) => {
let returnState = true;
// filter targets based on types
if (nextProps.HIGHLIGHT_TARGET) {
filterTargetsBasedOnTypes(nextProps.targetTypeHiddenInfo);
}
};
const LeafletMap = React.memo(({ highlightedTarget }) => {
const [viewport, setViewport] = React.useState(DEFAULT_VIEWPORT)
const { props: { mapUrl } } = useTheme();
return (
<>
<TextField
classes={{ root: "target-search" }}
placeholder="Search Target"
title="searching target based on its id"
variant="filled"
type="search"
onChange={searchTargetHandler}
onKeyPress={(e) => {
if (e.key === 'Enter')
searchTargetBtnHandler()
}}
InputProps={{
classes: {
input: "py-2 text-black non-opacity",
},
endAdornment: <IconButton
className="p-0"
onClick={searchTargetBtnHandler}
>
<AiOutlineSearch />
</IconButton>
}}
/>
</>
);
}, handlePropChange);
const mapStateToProps = state => {
return {
highlightedTarget: state.BaseReducers.highlightedTarget,
};
};
export default connect(mapStateToProps)(LeafletMap);
When dispatch fire in Child component (Highlight_Target), Parent re-render by redux. But I want hold open Child component. Any solution?

How do I associate seperate state to each button?

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>

Cannot make element disappear/appear properly inside of my React App

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.

React Hook "useState" cannot be called inside a callback

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>)
});

Categories