Reputation: 430
I'm trying to build a dynamic component using ReactJS. So, I have a products list and each time the user would like to add a new product, he will click on the + button. this will create a new product and add it to the list. the list had a button named delete all to delete all products.
To delete all products, i used a state in the products list initialized with "created" on the component creation and when the user clicks on delete all i'm changing it to "deleted" toupdate the childs (products) using ComponentWillUpdate callback.
here is some code :
index.js
render() {
return (
<div className="App">
<h1>Dynamic List</h1>
<ProductList />
<hr />
<h1>Static List</h1>
<ProductListStatic />
</div>
);
}
Product.js
class Product extends React.Component {
constructor(props) {
super(props);
this.state = {
state: "created"
};
}
componentDidUpdate(prevProps) {
if (
this.props.parentState === "deleted" &&
this.state.state === "created"
) {
this.setState({ state: "deleted" });
}
}
render() {
if (this.state.state !== "deleted") {
return (
<div style={{ margin: "5px" }}>
Product.....
<button onClick={this.props.addNewProduct}>+</button>
</div>
);
} else {
return null;
}
}
}
my ProductList.js
class ProductList extends React.Component {
constructor(props) {
super(props);
this.state = {
products: [],
state: "created"
};
}
componentDidMount() {
let newProducts = [...this.state.products];
newProducts.push(
<Product
key={0}
addNewProduct={this.addNewProduct}
parentState={this.state.state}
/>
);
this.setState({ products: newProducts });
}
addNewProduct = () => {
let length = this.state.products.length;
const newProducts = [
...this.state.products,
<Product
key={length}
addNewProduct={this.addNewProduct}
parentState={this.state.state}
/>
];
this.setState({ products: newProducts });
};
deleteAll = () => {
this.setState({ state: "deleted" });
};
render() {
return (
<div>
{this.state.products}
<button style={{ marginTop: "20px" }} onClick={this.deleteAll}>
Delete All
</button>
</div>
);
}
}
Well this doesn't update the childs and i don't know why. i think that's because the list is dynamic. I gived it a try using a static list and it works fine (code below).
ProductListStatic.js
class ProductListStatic extends React.Component {
constructor(props) {
super(props);
this.state = {
products: [],
state: "created"
};
}
deleteAll = () => {
console.log("delete all");
this.setState({ state: "deleted" });
};
render() {
let products = [
<Product
key={0}
addNewProduct={this.addNewProduct}
parentState={this.state.state}
/>,
<Product
key={1}
addNewProduct={this.addNewProduct}
parentState={this.state.state}
/>
];
return (
<div>
{products}
<button style={{ marginTop: "20px" }} onClick={this.deleteAll}>
Delete All
</button>
</div>
);
}
}
here is a demo : SandBox live dome
anynoe knows why it doesn't work with dynamic products ?
Upvotes: 1
Views: 704
Reputation: 56855
The problem is that JSX elements in a state array have no idea that they need to be re-rendered when the parent re-renders. In the static version, the children are created directly in the JSX, so behavior for re-renders is normal. Storing the data for the children in the state array then creating them by calling .map
on that state array per render is a reasonable solution.
Beyond this, the overall design seems needlessly complex. I don't think child Product
s should be concerned about when they render or if a product should be added in the parent with a callback from the child. It's much simpler to keep all of this information in the parent. The rendering can be implict--if we want a new item, push it onto the product/items array and if we want to clear all items, set the array to []
. No need for this.state.state
.
Here's a proof-of-concept:
const mockProduct = () => ({
name: [
"apple", "banana", "cherry", "kiwi",
"watermelon", "pineapple", "blueberry"
][~~(Math.random()*7)],
price: ~~(Math.random() * 5) + 0.99
});
const Product = props =>
<div>
<span>{props.name}</span>
<span>{props.price}</span>
</div>
;
class Products extends React.Component {
constructor(props) {
super(props);
this.state = {products: [mockProduct()]};
this.addNewProduct = this.addNewProduct.bind(this);
this.deleteAll = this.deleteAll.bind(this);
}
addNewProduct() {
this.setState(state => ({
products: state.products.concat(mockProduct())
}));
}
deleteAll() {
this.setState({products: []});
}
render() {
return (
<React.Fragment>
<div>
{this.state.products.map((e, i) =>
<div
style={{display: "flex"}}
key={`${i}-${e.name}`}
>
<Product name={e.name} price={e.price} />
<button onClick={this.addNewProduct}>+</button>
</div>
)}
</div>
<div>
<button onClick={this.deleteAll}>delete all</button>
</div>
</React.Fragment>
);
}
}
ReactDOM.createRoot(document.querySelector("#app"))
.render(<Products />);
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<div id="app"></div>
A container for rows might be in order if you plan to add additional buttons and data for each product. Then, callbacks would become necessary for rows to signal to parents when an change should be applied to an item. This is dependent on your design goals and is premature at this point (after removing rendering concerns from Product
, there isn't much left to justify its existence).
Upvotes: 2