React router dom, component doesn't get unmounted when changing with id - javascript

I have a route like this: <Route exact path="/product/:id" component={Product} /> inside a Switch. Inside the component Product I have a custom hook that makes a request to my api. I also have another component inside Product called: Configurator.
Inside the Configurator component I make another api request to get a specific choice. Then inside the render of the Configurator component I check which component I need to render.
{loading ? <Loading /> :
choices.map((choice, index) => {
return (
<div
key={choice.id}
id={choice?.id}
ref={choice?.id == activeChoice ? myRef : empty}
className={`${choice?.id == activeChoice ? "active" : "inactive"} configurator__choice`}
onClick={() => enableChoice(choice?.id)}
>
{choice?.type === "parent_size" ? <ChoiceSize key={choice.id} number={index + 1} choiceData={choice} availableChoices={availableChoices} activeChoice={activeChoice} onChange={onChange} choices={choices} fetchNewChoiceData={fetchNewChoiceData} onSubmit={onSubmit} /> : <div></div>}
{choice?.type === "parent" ? <ChoiceOption key={choice.id} number={index + 1} choiceData={choice} availableChoices={availableChoices} activeChoice={activeChoice} choiceNameId={choice?.id} choiceId={choice?.choices[0]?.next.id} onClick={onClick} /> : <div></div>}
</div>
)
})
}
So when choice.type is equal to parent_size it loads the ChoiceSize component and adds data as props.
The problem I have is that whenever I am for example on /product/1 and I want to switch to /product/2 my old components (ChoiceSize and/or ChoiceOption) won't go away. It will add new choices with the data from product 2 but the old choices from product 1 are still there.
Does anyone know how I can fix this? Maybe that the whole component gets unmounted and the old choices will get removed and the new choices will get added

Related

I have an <Add /> component that is called in two different pages, but only one of them is receiving the add count prop passed to it in React JS

Please help me understand why this logic is not working on one of the instances of my counter button. My goal is to have my counter display the count like this:
To add items to the cart, I am using this function:
const handleAddProduct = (product) => {
const productExists = cartItems.find((item) => item._id === product._id);
if (productExists) {
setCartItems(
cartItems.map((item) =>
item._id === product._id
? {
...productExists,
customer_qty: productExists.customer_qty + 1,
}
: item
)
);
} else {
setCartItems([...cartItems, { ...product, customer_qty: 1 }]);
}
};
This works well when I view the <Cart /> page because this page displays products in the "cartItems" array that have the newly added "customer_qty" key. The <Add /> component is rendering
<p> product.customer_qty </p>
The cartItems and the handleAddProduct are passed down like this:
- \<App />
- \<Cart />
- \<ShoppingCartCard /> /*Each card here is an item from "cartItems"*/
- \<Add />
However, in the normal shopping page, I cannot get the same product.customer_qty to display on the counter, since the product card instance is rendered for each item in the original array of products, so those products don't have "product.customer_qty"
For the other branch of the site, the cartItems and the handleAddProduct are passed down like this:
- \<App />
- \<Products />
- \<Specials /> /*Specials passes down product data */
- \<Carousel />
- \<ProductCard />
- \<Add />
So what I am missing is a way to have the <Add /> components in both branches, to display the same count number.
I have tried doing a setCount, but if I do it at the top <App /> level, it doesn't count individually for each product (all products get the same count). If I do it at the product card level, then then it is two different counts.
what am I missing here? The count should appear in three places: The shopping cart in the masthead, the Cart page, and at each product card.
I can share my Github repository if you'd like to see it (It's private because this is a course assignment.).
OK, so I realise that I have probably over-complicated my question. But the difficulty lies in the complex way in which data for each product is received and displayed. I have solved my problem simply by understanding the difference between what "product" means in each component (Cart and ProductCard).
in the <Cart /> component, each "product" is a synthetic duplicate of the original (created by using the spread operator "...". And is one object of a small array of products that have been added to "CartItems".
Whereas in the <ProductCard /> component, each "product" an object from a large array from the original dataset .
Originally I wanted my <Add /> component to display the value for the key "customer_qty:" which would have been counting how many items the customer wants to purchase. This displayed well in the <Cart /> component, but not in the <Products /> component.
Remember: While the <Cart /> lists items in that have already been added to the shopping cart, the <ProductCard /> will list items from the original data set.
So astute beginners or more experienced coders may see the problem: "customer_qty" does not exist if it hasn't been added to the cart.
So <Cart /> has access to this value, while <ProductCard /> will not because it is accessing the original object, which does not include "customer_qty:" in it, therefore it is null.
The solution
Create a variable with an anonymous function that checks first if the item has been added to the cart already. If it has, then quantity will be that item's "customer_qty", which exists in the cart items only. If it doesn't exist, then it is 0.
const quantity = (product) => {
let quantity;
cartItems.map((item) => {
if (item._id === product._id) {
quantity = item.customer_qty;
}
});
return quantity;
};
The complete Add component looks like this:
const Add = ({ product, handleRemoveProduct, handleAddProduct, cartItems }) => {
const productExists = cartItems.find((item) => item._id === product._id);
const quantity = (product) => {
let quantity;
cartItems.map((item) => {
if (item._id === product._id) {
quantity = item.customer_qty;
}
});
return quantity;
};
return (
<div className="counter">
{productExists ? (
<div className="show">
<div className="minus-box">
<i
className="icon-minus"
onClick={() => {
handleRemoveProduct(product);
}}
></i>
</div>
<div className="count">{productExists ? quantity(product) : 0}</div>
<div className={"add-box"}>
<i
className="icon-pink-cross"
onClick={() => {
handleAddProduct(product);
}}
></i>
</div>
</div>
) : (
<div className="first-add">
<i
className="icon-pink-cross"
onClick={() => {
handleAddProduct(product);
}}
></i>
</div>
)}
</div>
);
};
export default Add;

How to display a message inside an empty div when a component doesn't render in it?

I have a div inside which I'm rendering dynamic card components(SearchRes):
<div className="search-res">
{games.filter(game => (game.background_image !== null)&&(game.ratings_count>38)).map(game=>
<SearchRes
key={game.name}
title={game.name}
rating={game.metacritic===null?<MdGamepad/>:game.metacritic}
genre={game.genres.map(genre=>genre.name+" ")}
imglnk={(game.background_image===null)?"":game.background_image}
gameid={game.id}
slug={game.slug}
plat={game.parent_platforms}
/>
)}
</div>
When the cards don't render (when they don't pass the filter conditions), I get a blank space. I want to display something like no results found when such thing happens, how can I implement this?
Currently I'm using CSS to implement it:
.search-res:empty::before {
content: "No data";}
but clearly this is not viable as the message is visible when the search is not even initiated:
If I understand your question, you want to conditionally render a "No Data" fallback only after you've fetched data and there is nothing to render.
Add a loading state and conditionally render a loading indicator.
Filter the games and in the render check that there is an array length (i.e. something to map), and conditionally render the array or the fallback text if there is a current search term.
Code
const [searchLoaded, setSearchLoaded] = React.useState(false);
// in search handler
// toggle searchLoaded false when initiating a search
// toggle searchLoaded true when search completes
const results = games.filter(
game => game.background_image !== null && game.ratings_count > 38
);
<div className="search-res">
{results.length
? results.map(game => (
<SearchRes
key={game.name}
title={game.name}
rating={game.metacritic === null ? <MdGamepad/> : game.metacritic}
genre={game.genres.map(({ name }) => name).join(" ")}
imglnk={game.background_image === null ? "" : game.background_image}
gameid={game.id}
slug={game.slug}
plat={game.parent_platforms}
/>
))
: searchLoaded && "No Data"
}
</div>
I would suggest to filter the array first, based on the conditions and then check if the array has length i.e. arr.length !== 0 and if that is the case, then map the results of the array to the card component. And if not then show the message that no data found. You would have to use if/else or ternary operator.
Thanks😊
if you are using a filter only for conditioning then there is no need for a filter instead you can use conditional rendering like this:
<div className="search-res">
{games.map(game=>
{(game.background_image !== null && game.ratings_count>38) ?
<SearchRes
key={game.name}
title={game.name}
rating={game.metacritic===null?<MdGamepad/>:game.metacritic}
genre={game.genres.map(genre=>genre.name+" ")}
imglnk={(game.background_image===null)?"":game.background_image}
gameid={game.id}
slug={game.slug}
plat={game.parent_platforms}
/> : "NO RESULTS FOUND"}
)}
</div>

Can we pass fake keys to children in React.js?

I have created a component and it's running well in local server. But I am getting below warning
Warning: Each child in a list should have a unique "key" prop.
Getting this warning means we need to fix the key index props? as given here.
below is some snippets of my component code..
render() {
return (
<div>
<Container>
<Row>
<Col className="col-12">
{this.state.client.map((val,index)=>{
if(index == this.state.colaborators.length -1)
return <a href={"/users/"+val}>{val}</a>
return <a href={"/users/"+val}>{val} ,</a>
})}
</Col>
</Row>
</Container>
</div>
</div>
</div >
);
}
}
export default App;
I checked some solution from here
As I told my code is working well. Can we use some fake key props? for example
key={fake index}
And we are using will this affect in my working code?
If this.state.client ever changes, don't just use the index (which is sadly common); see this article for why and its demo of what can go wrong. You can only do that with a list that never changes, or only grows/shrinks (and not at the same time), not with one where the order changes (you insert at the beginning, or sort, or...) More in the docs.
I'm guessing val will be unique in the list, so use that as the key:
{this.state.client.map((val, index) => {
const href = "/users/" + val;
const display = index == this.state.colaborators.length - 1 ? val : `${val} ,`;
return <a key={val} href={href} >{display}</a>;
})}
If your lists order is not going to change, simply use:
return <a key={index} href={"/users/"+val}>{val}</a>
return <a key={index} href={"/users/"+val}>{val} ,</a>
It will not affect your code and it will remove the warning.

Render HTML inside a Material-UI Card with React

I have the following < CardHeader /> component inside a < Card />.
<CardHeader
title={card.title}
subheader={`${moment(card.createdAt).startOf('minute').fromNow()}` + ' by ' + <div>ABC</div>}/>
It renders the following way:
18 minutes ago by [object Object].
What is the right way of doing this? Thanks in advance!
You can pass on a node to CardHeader's subHeader props, just don't mix string and html
<CardHeader
title={card.title}
subheader={<div>{moment(card.createdAt).startOf('minute').fromNow()} by ABC</div>}/>
Sample DEMO
Wrap your heading in a fragment
You can separate out part of your logic to its own level. This will make reuse easier, and testing also easier.
const MyHeading = ({card}) => {
return (
<React.Fragment>
{`${moment(card.createdAt).startOf('minute').fromNow()} by`
<div>ABC</div>
</React.Fragment>
)
}
In the above I'm only retrieving card, but you can pass all sorts of data to your heading component.
<CardHeader
title={card.title}
subheader={<MyHeading {...props} />
/>

How to inject a dinamically created element into an existing div in React JSX?

I have a list of objects photos, from a json data file, that I would like to organize into 3 different <div> columns, but I dont know how to achieve that, here is my broken non-optimized code:
<div className="container">
<div ref={leftColRef} className="left-col" />
<div ref={centreColRef} className="centre-col" />
<div ref={rightColRef} className="right-col" />
{Object.keys(photos).forEach((n, i) => {
const id = photos[n].id;
const thumb = photos[n].thumbnailUrl;
const title = photos[n].title;
const element = (
<Thumbnail id={id} title={title} thumb={thumb} />
);
if (i % 3 === 0) {
leftColRef.current.append(element);
} else if (i % 3 === 1) {
centreColRef.current.append(element);
} else {
rightColRef.current.append(element);
}
// this line works, it idsplays the data but is commented as the data needs to go inside its respective columns
// return <Thumbnail key={id} title={title} thumb={thumb} />;
})}
</div>
The idea is to insert some elements into the left-column when i%3 = 0 and others in the centre-column when i%3 = 1 and so on ...
And a link to my codesandbox
Any help/advise will be much appreciated.
Easiest is probably to prepare the data outside the render function and to render the column one by one.
You should not manipulate the DOM like it's done in jQuery using JSX
Example:
const Component = (props) => {
const filterPhotos = (column) => {
return props.photos.filter((photo,index)=> index%3==column);
}
return <>
<MyColumn photos={filterPhotos(0)}/>
<MyColumn photos={filterPhotos(1)}/>
<MyColumn photos={filterPhotos(2)}/>
</>;
}
First, using ref on div to inject stuff on it is wrong. It's the opposite of how react works.
Like charlies said, I would split the photos in 3 different arrays before the render. Then, you'll be able to do something like this :
<div ref={leftColRef} className="left-col" />
{ photosLeft.map(photo => <Thumbnail key={photo.id} {...photo} />)
</div>
when preparing your data, try to use the same object properties and component props name so you can spread it easily ( {...photo} ).
Note: Also, when rendering an array in react, each child must have a unique key props. It will help react to render on that part of dom if your data change.

Categories