I'm trying to render dynamically a collection of component using componentDidUpdate.
This is my scenario:
var index = 0;
class myComponent extends Component {
constructor(props) {
super(props);
this.state = {
componentList: [<ComponentToRender key={index} id={index} />]
};
this.addPeriodHandler = this.addPeriodHandler.bind(this);
}
componentDidUpdate = () => {
var container = document.getElementById("container");
this.state.componentList.length !== 0
? ReactDOM.render(this.state.componentList, container)
: ReactDOM.unmountComponentAtNode(container);
};
addHandler = () => {
var array = this.state.componentList;
index++;
array.push(<ComponentToRender key={index} id={index} />);
this.setState = {
componentList: array
};
};
render() {
return (
<div id="Wrapper">
<button id="addPeriod" onClick={this.addHandler}>
Add Component
</button>
<div id="container" />
</div>
);
}
}
The problem is that componentDidUpdate work only one time, but it should work every time that component's state change.
Thank you in advance.
This is not how to use react. With ReactDOM.render() you are creating an entirely new component tree. Usually you only do that once to initially render your app. Everything else will be rendered by the render() functions of your components. If you do it with ReactDOM.render() you are basically throwing away everything react has already rendered every time you update your data and recreate it from scratch when in reality you may only need to add a single node somewhere.
Also what you actually store in the component state should be plain data and not components. Then use this data to render your components in the render() function.
Example for a valid use case:
class MyComponent extends Component{
state = {
periods: []
};
handleAddPeriod = () => {
this.setState(oldState => ({
periods: [
...oldState.periods,
{/*new period data here*/}
],
});
};
render() {
return (
<div id="Wrapper">
<button id="addPeriod" onClick={this.handleAddPeriod}>
Add Component
</button>
<div id="container">
{periods.map((period, index) => (
<ComponentToRender id={index} key={index}>
{/* render period data here */}
</ComponentToRender>
))}
</div>
</div>
);
}
}
}
Also you should not work with global variables like you did with index. If you have data that changes during using your application this is an indicator that is should be component state.
try
addHandler = () =>{
var array = this.state.componentList.slice();
index++;
array.push(<ComponentToRender key={index} id={index}/>);
this.setState=({
componentList: array
});
}
if that works, this is an issue with the state holding an Array reference that isn't changing. When you're calling setState even though you've added to the Array, it still sees the same reference because push doesn't create a new Array. You might be able to get by using the same array if you also implement shouldComponentUpdate and check the array length of the new state in there to see if it's changed.
Related
I have a Grid with 3*3 squares.
When a click on a square , we change the background color to green.
So, I tried to put the all the states in the parent GridContainer.
state = {
gridCells: []
};
This will hold the indices that are clicked.
GridContainer nests Grid and Grid nests Square.
render() {
return (
<div>
<Grid action={this.handler} />
<button>Reset Clicks</button>
</div>
);
}
Here is my current implementation.
Now how do I clear the background cells when I reset clicks and make the background back to white again?
function Square(props) {
const liClickHandler = event => {
event.target.classList.add("highlight");
props.clickAction();
};
return <li onClick={e => liClickHandler(e)} />;
}
function Grid(props) {
const gridHandler = index => {
props.action(index);
};
return (
<ul>
{Array.from(new Array(9)).map((item, index) => (
<Square key={index} clickAction={() => gridHandler(index)} />
))}
</ul>
);
}
class GridContainer extends React.Component {
state = {
gridCells: []
};
handler = index => {
let temp = [...this.state.gridCells];
temp.push(index + 1);
this.setState({
gridCells: temp
});
};
render() {
return (
<div>
<Grid action={this.handler} />
<button>Reset Clicks</button>
</div>
);
}
}
So when I click a Sqaure , it calls a method clickAction that calls handler
that updates the state and we have an array which indices were clicked in order.
How do I implement Reset clicks that updates the background of those Sqaures back to white ? How do I let know my child know.
Am I maintaining the state wrong?
Sandbox link : https://codesandbox.io/s/3-x-3-grids-s0b43?file=/src/index.js:640-1563
I'd advise to rethink the way how your components are structured.
Each component should be independent unit with it's own logic and state (if needed of course). I'm saying if needed for state, cause ideally components should be stateless.
There are several problems with Square class:
It adds class via event.target, which is not react way to go. React works with virtual DOM and has it's own set of methods to interact with html. Working with DOM directly - will bite your later, starting from writing tests for your code in the future.
It does not contain incoming information whether it should be highlighted or not
Both these problems result in fact that you cannot reset presentation of your squares easily.
I've updated your sample: https://codesandbox.io/s/3-x-3-grids-uflhr?file=/src/index.js
It's still not ideal, but you can notice that gridCells is passed from top via props. And then each square gets own props param. This allows state to come through the flow and let squares rerender with updated class.
In react you should think the "react" way:
pass the necessary state down through the props
pass down the callbacks so that children can update the parent state
Here is corrected version of the demo:
import React from "react";
import ReactDOM from "react-dom";
import "./styles.css";
function Square(props) {
return (
<li onClick={props.onClick} className={props.active ? "highlight" : ""} />
);
}
function Grid(props) {
let squares = [];
for (let i = 0; i < 9; i++) {
squares.push(
<Square
key={i}
onClick={() => props.onCellClick(i)}
active={props.cells[i]}
/>
);
}
return <ul>{squares}</ul>;
}
class GridContainer extends React.Component {
state = {
gridCells: []
};
onCellClick = index => {
this.setState(prevState => {
const newCells = [...prevState.gridCells];
newCells[index] = true;
return {
gridCells: newCells
};
});
};
render() {
return (
<div>
<Grid cells={this.state.gridCells} onCellClick={this.onCellClick} />
<button
onClick={() => {
let that = this; //we could bind the callback aswell
that.setState(() => ({ gridCells: [] }));
}}
>
Reset Clicks
</button>
</div>
);
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<GridContainer />, rootElement);
i'm creating a simple react to do list, I'm currently working on a delete button, I have created an array then passed this array into a prop, I then need to splice that item from the prop array when the user clicks the delete button. I was able to store the array number but I cant seem to update the array after its deleted.
CLASS CALL:
<TodoList items={this.state.items} deleteItems={this.deleteItem}/>
SUB-CLASS CODE:
class TodoList extends Component {
constructor(props) {
super(props);
this.removeItem = this.removeItem.bind(this);
}
render() {
return (
<div>
{ this.props.items.map((item, i) => (
<div className={"col-12"} key={item.id}>
<div className={"card text-white"}>
<div className={item.priority}>
<div className={"col-12 card-body"}>
<h1>{item.title}</h1>
<p>{item.text}</p>
<button onClick={() => { this.removeItem(item, i)}} key={i} className={"col-12 btn btn-primary bg-red"}>Delete</button>
</div>
<div/>
</div>
</div>
</div>
))}
</div>
);
}
removeItem(e, i) {
this.props.items.splice(i, '');
console.log(i);
}
}
I have been looking at different stack questions but none of the solutions seem to apply to this, thanks for any constructive feedback :)
I believe <TodoList /> component should have its own state. However, if you can't do so, there's 2 solutions to this problem:
Keep <ToDoList /> component's state and props in sync (In case the parent component modifies the state passed down as items). Then modify the <TodoList /> 's state.
Declare a method that removes the item inside the parent component which has the
state, and pass it down as props (Recommended)
Example code:
class ParentComponent extends Component {
state = {
items: [1, 2, 3]
}
removeItem = index => () => {
this.setState(prevState => ({
items: prevState.items.filter((_, i) => i !== index) //Filter the items
}));
};
render() {
return (
<TodoList items={this.state.items} deleteItems={this.removeItem} />
);
}
}
Important: Always use pure functions to modify the state. Do not use .splice() or .push() (If you haven't cloned the state yet). It's always safer to use .filter(), .map(), .concat(), etc.
I have three files: ShopsContainer.js ShopsComponent.js and ShopsItemComponent.js
ShopsContainer maintains an array of shop items in local state that gets passed down into ShopsComponent as props. ShopsComponent then maps through the items array that is being received as props and renders a ShopsItemComponent for each item in the array.
Within my ShopsContainer file, I have a method that removes a shop item from state using the following code:
removeShop = (shopAccount) => {
this.setState(prevState => ({
items: prevState.items.filter(shop => {
return shop.shopAccount !== shopAccount
})
}));
}
When this happens, the correct item is removed from the items array in state, however, whatever the last ShopItem is that is in the DOM at the time of the removeShop call will get removed no matter if it is the correct item that should be removed or not. In other words, when removeShop gets called and the items array in state gets updated correctly, the wrong ShopItemComponent gets removed from the DOM.
What I would like to happen (or what I think should happen) is when removeShop gets called, that shop gets removed from the items array in state and ShopsContainer re-renders causing ShopsComponent to re-render with the updated props being received. And lastly ShopsComponent would map through the newly updated items array in props displaying a `ShopItemComponent for the correct items. Perhaps the problem has to do with the props being updated?
My code is as follows:
ShopsContainer.js
class ShopsContainer extends Component {
constructor() {
this.state = {
items: null
}
this.getAll();
this.removeShop = this.removeShop.bind(this);
}
getAll = () => {
// API request that fetches items and updates state
}
removeShop = (shopAccount) => {
this.setState(prevState => ({
items: prevState.items.filter(shop => {
return shop.shopAccount !== shopAccount
})
}));
}
render() {
return (
<div>
{this.state.items ? <ShopComponent items={this.state.items} removeShop={this.removeShop} /> : <div><h1>Loading...</h1></div>}
</div>
);
}
}
ShopsComponent.js
class ShopsComponent extends Component {
constructor() {
this.handleRemove = this.handleRemove.bind(this);
}
handleRemove = (shopAccount) => {
this.props.removeShop(shopAccount);
}
render() {
return (
<React.Fragment>
<Header />
{this.props.items.map((shopItem, i) => {
return (<ShopItemComponent key={i} item={shopItem} removeShop={this.handleRemove} />);
})}
</React.Fragment>
);
}
}
Your code is working great, but you only has one mistake , your ShopComponent is assign index as a key for each ShopItemComponent and react is tracking those indexes to update the correct component, so you need to set key as a unique value between items, then I realize that shopAccount should be your id for each item.
The solution code is below.
class ShopsComponent extends Component {
handleRemove = (shopAccount) => {
this.props.removeShop(shopAccount);
}
render() {
return (
<React.Fragment>
<Header />
{this.props.items.map((shopItem) => <ShopItemComponent key={shopItem.shopAccount} item={shopItem} removeShop={this.handleRemove} />)}
</React.Fragment>
);
}
}
I hope you can find useful.
Note, when you are using a arrow function into your class, don't bind that method into the constructor, so remove it, because
handleRemove = (shopAccount) => {
this.props.removeShop(shopAccount);
}
is already binded.
As you can see in the two components below, i want to delete the recipes(in app component) from a button click in the panelcomponent,
i have a method in app to delete the recipe, and a prop(onclick) send to child panelcomponent. Panel then gets the index from the map of recipes, and after the button click it executes the handleDelet method to send the index back to parent. but No this is not working !
class App extends React.Component {
state={
addRecipe:{recipeName:"",ingredients:[]},
recipes:[{recipeName:"Apple",ingredients:["apple","onion","spice"]},
{recipeName:"Apple",ingredients:["apple","onion","spice"]},
{recipeName:"Apple",ingredients:["apple","onion","spice"]}]
}
handleDelete = (index) => {
let recipes = this.state.recipes.slice();
recipes.splice(index,1); //deleting the index value from recipe
this.setState({recipes}) //setting the state to new value
console.log(index,recipes)
}
render() {
return (
<div className="container">
<PanelComponent recipes={this.state.recipes} onClick={()=>this.handleDelete(index)}/>
<ModalComponent />
</div>
);
}
}
class PanelComponent extends React.Component {
handleDelete = (index) => {
this.props.onClick(index); //sending index to parent after click
console.log(index)
}
render() {
return (
<PanelGroup accordion>
{this.props.recipes.map( (recipe,index) => {
return(
<Panel eventKey={index} key={index}>
<Panel.Heading>
<Panel.Title toggle>{recipe.recipeName}</Panel.Title>
</Panel.Heading>
<Panel.Body collapsible>
<ListGroup>
{recipe.ingredients.map((ingredient)=>{
return(<ListGroupItem>{ingredient}</ListGroupItem>);
})}
</ListGroup>
<Button bsStyle="danger" onClick={()=>this.handleDelete(index)}>Delete</Button>
<EditModalComponent />
</Panel.Body>
</Panel>
);
})}
</PanelGroup>
);
}
}
Thea actual error in your code is that while using arrow function in the onClick in parent, you are passing the wrong parameter, instead of {()=>this.handleDelete(index)} what you should write is
{(value)=>this.handleDelete(value)}, However, that also not necessary and you could simple write {this.handleDelete} in App since your handleDelete function is already binded and it received the values from the Child component.
render() {
return (
<div className="container">
<PanelComponent recipes={this.state.recipes} onClick={(value)=>this.handleDelete(value)}/>
<ModalComponent />
</div>
);
}
The difference in writing {()=>this.handleDelete(index)} vs {(value)=>this.handleDelete(value)} is that in the first case, you are explicitly passing the index that you get from the map function in your App component while in the second case, the value passed from the child component when you execute this.props.onClick(value) is being provided to the handleDelete function.
you are sending the function wrongly as props. you are sending the result of the function as props rather than the function itself
class App extends React.Component {
state={
addRecipe:{recipeName:"",ingredients:[]},
recipes:[{recipeName:"Apple",ingredients:["apple","onion","spice"]},
{recipeName:"Apple",ingredients:["apple","onion","spice"]},
{recipeName:"Apple",ingredients:["apple","onion","spice"]}]
}
handleDelete = (index) => {
let recipes = this.state.recipes.slice();
recipes.splice(index,1); //deleting the index value from recipe
this.setState({recipes}) //setting the state to new value
console.log(index,recipes)
}
render() {
return (
<div className="container">
//change here
<PanelComponent recipes={this.state.recipes} onClick={this.handleDelete}/>
<ModalComponent />
</div>
);
}
}
I am trying to append a "Item" component which consists of some array items, in the main "App" Component. But the component is getting replaced with the new array items instead of getting appended. Following is the code snippet:
//the App render function
render: function() {
return (
<div>
{
this.state.productDisplayed.map(function(product, i) {
return (
<Item source = {product.url} prodId = {product.id} key = {product.id} />
)
})
}
</div>
)
}
//The Item render function
render: function(){
return(
<div className = "col-sm-4" >
<img src = {this.props.source} width = "70%" className = "img-responsive"></img>
<div>{this.props.prodId}
</div>
</div>
)
}
"ProductDisplayed" is an array which gets replaced by new items which are then displayed using a "for" loop.
How can i append the items as if I am adding some extra items to the main App component. I am trying to implement infinite scrolling.
In order to append items to your app component, you need to append data to your productDisplayed state array
For this you can do something like
addItem=(item)=>{
var productDisplayed=[...this.state.productDisplayed];
productDisplayed.push(item);
this.setState({productDisplayed});
}
And you can call this function addItem on some event.
You need to append them to the array in your state. I have used the function argument to setState because your nextState is dependent on your previous state.
component ProductList extends React.Component {
constructor() {
super()
this.setState({
productDisplayed: []
});
}
getMoreItems = ( startingId ) => {
Api.getMoreItems(startingId).then(this.addItems);
}
addItems = ( items ) => {
this.setState(( prevState ) => ({
updatedItems: [...prevState.productDisplayed, ...items]
}));
}
render() {
// no changes
// something triggers this.getMoreItems(id)
}
}