Why does the handleClick method in my component seem to loop? - reactjs

I am setting out to learn some react, and I am trying to create a grid of clickable buttons. For now all I want is to change the color of the button once it is clicked.
Starting off from this React Tutorial, I wrote my components to create a variable size grid through loops.
I added a handleClick method to my Grid component to change the color of the button clicked. When I click a button though, the entire column changes colour.
From the debugger it seems to pass the correct values of the indeces (i and j) to the handleClick method, so I am at a loss as to why this is happening.
The code for my components is the following:
var height = 3;
var width = 5;
class Cell extends React.Component {
render() {
return (
<button
className={ this.props.value === 'ON' ? "cell cellOn" : "cell
cellOff" }
onClick={() => this.props.onClick()}>
</button>
);
}
}
export class Grid extends Component {
constructor(props) {
super(props);
this.state = {
cells: Array(height).fill(Array(width).fill(null)),
};
}
handleClick(i, j) {
const cells = this.state.cells.slice();
cells[i][j] = ( cells[i][j] === 'ON' ) ? 'OFF' : 'ON' ;
this.setState({cells: cells});
}
renderCell(i, j) {
return(
<Cell value={this.state.cells[i][j]}
onClick={() => this.handleClick(i, j)} />
);
}
renderCellRow(i, w) {
return(
<div>
{Array.from(Array(w), (_,x) => x).map((j) => this.renderCell(i, j)) }
</div>
);
}
render() {
const status = 'Next player: X';
return (
<div>
<div className="status">{status}</div>
<div>
{Array.from(Array(height), (_,x) => x).map((i) => this.renderCellRow(i, width)) }
</div>
</div>
);
}
}
Could somebody offer some suggestions? Is it because of the map calls that I used to loop on height and width?
Thanks in advance!

The problem is in the way of your state initialization:
Array(height).fill(Array(width).fill(null))
I'll suggest to avoid using fill. It fills the array with objects by reference. So when you change one of them you get updates into the other ones.
Here is a codepen with the working version https://codepen.io/krasimir/pen/YYdLQL?editors=0010

Related

Why do I get React error "Cannot read propery"

I am getting the "Cannot read property" error when using refs inside a .map function.
What I am trying to do:
Change the background colour of specific values in the .map, so for example if the customer has autorenew on the background colour is green. If not the background colour is red.
The problem I am facing. I can make the code work on a button onClick function but as soon as I put it inside a componentDidMount I get the error cannot read property "style" of undefined.
My code:
export default class Dash extends React.Component {
constructor(props) {
super(props);
this.accordionContent = [];
}
accordionToggle = (value) => {
this.accordionContent[value].style.backgroundColor = "red";
};
render() {
return (
<>
{this.state.valueToMap.map((index, value) => {
var autoRenew = "on";
if (autoRenew === "on") {
this.accordionToggle(value);
}
return (
<div
ref={(ref) => (this.accordionContent[value] = ref)}
className="mini-lower"
>
BACKGROUND I AM TRYING TO CHANGE
</div>
//other bits of code here
);
})}
</>
);
}
}
I can call
<button onClick={() => this.accordionToggle(value)} >Click</button>
inside the .map and it works fine.
I ended up finding a really clean way to do what I was trying to achieve.
Instead of using refs i just used this below
className={this.state.renewalStatus[value] === 'EXPIRED' ? 'class-one' : 'class-two'}
Wish I had worked it out sooner!

ReactJS Assigning Value inside a class component method to props

I'm trying to access the index of the selected tab in a React component so as to map it to props as follows:
class AttendanceDetail extends React.Component {
handleSelect(key, props) {
console.log(key)
props.index = key;
}
render(){
const {single_class, courses, attendances} = this.props;
// console.log(this.state);
if(single_class) {
return(
<div className='container content-section'>
// Some irrelevant Code
<Tabs defaultActiveKey={0} onSelect={this.handleSelect} id="uncontrolled-tab-example">
{ courses.map((course, index) => {
return (
<Tab eventKey={index} title={course.course + " year " + course.yearofstudy}>
//Other irrelevant code...
</Tab>
)
})}
</Tabs>
</div>
)
} else {
return (
<div className='container content-section'>
Loading Unit details...
</div>
);
}
}
}
So basically the handleSelect method is what determines the index of the selected tab and logs it to the console. The problem is, I'm tring to map that key (index) to props so as to access it else where but to no avail. Could someone help me out? What am I missing?
You're not supposed to set the component's props, only read. You can use state within the component:
export class Wrapper extends React.Component {
constructor() {
this.state = {
index: 0 //initial state
}
}
handleSelect(index, props) {
this.setState({index})
}
render() {
return (
<span>{this.state.index}</span>
)
}
}
you can read more on the official docs.
if i understood the scenario correctly, you need to log index value of the currently active tab. try using onFocus event handler to get the index value of the currently visible tab and set the state that will be used by handleSelect
constructor(props){
super(props);
this.state = {
index:''
}
}
the handler definition
setIndex = (index) => {
this.setState({index})
}
update handleSelect
handleSelect(index) {
console.log(index)
// call event handler of parent component eg: this.props.getIndex(index);
}
update tabs component handler
<Tabs defaultActiveKey={0} onSelect={() => {this.handleSelect(this.state.index)}} id="uncontrolled-tab-example">
call handler on focus of tab
<Tab
onFocus={() => {this.setIndex(index)}}
eventKey={index}
title={course.course + " year " + course.yearofstudy}>
//Other irrelevant code...
</Tab>

React passing Drag Events Up; this.props.onDragStart not a function

I am building a chess game using React and have a piece component which is draggable. When I handle drag events within the component itself I have no problems. However, when I try to pass drag events up to the parent component for handling, I get the error: TypeError: _this6.props.onDragStart is not a function.
The strangest part is that it seems to work until the error is thrown. I click the piece to move and and I get alerted with "starttt" which means that the function in the parent component was called successfully but then I get the error above implying that the function that was just called could not be found. I am very confused by this.
Here is my code for the parent component and draggable component.
class Square extends React.Component {
drag_start(event) {
alert('startttt')
}
renderSquare(color){
var class_name = "dark square"
var url = null;
if (this.props.value) {
url = this.props.value.url;
}
if (color) {
class_name = "light square"
}
if (url){
return <div className={class_name} onClick={() =>
this.props.onClick()} onDragStart = {(event) => this.drag_start(event)}
>
<ReactPiece url = {url}/> </div>
}
else {
return <div className={class_name} onClick={() =>
this.props.onClick()} > </div>
}
}
render() {
var color = this.props.color
return (
<React.Fragment>
{this.renderSquare(color)}
</React.Fragment>
);
}
}
class ReactPiece extends React.Component {
dragEnd = (event) => {
alert('end')
}
drop = (event) => {
alert('drop')
}
render() {
var url = this.props.url
return <img src={url} width="43" height="43" alt ='' draggable="true"
onDragStart={(event) => this.props.onDragStart(event)} onDrop=
{this.drop}
onDragOver={(event) => event.preventDefault()} onDragEnd={this.dragEnd} />;
}
}
You are not passing anything into the onDragStart props of ReactPeice

Attach ToggleClass events to future elements in ReactJS

I have multiple Div elements rendered dynamically through a loop.
Am trying to,
Attach an onClick event to the dynamically rendered elements.
Toggle the class through the onClick function.
The class name has to be toggled only to the span element that is clicked.
But the problem with the below code is, the class gets toggled to all the span elements rendered through this loop.
But I need to toggle the class only to the particular element that is clicked.
Though not the exact/complete code, the basic structure looks like this.,
constructor(props) {
super(props);
this.state = {
classnm: 'collapsed',
};
}
func2 = event => {
let currentClass = this.state.classnm;
if(currentClass == 'collapsed') {
this.setState({classnm: 'expanded'});
}
else {
this.setState({classnm: 'collapsed'});
}
};
render() {
return (
<div>
<Iterate>{this.renderIndexColumn}</Iterate>
</div>
);
}
// The below function gets called at least 10 times, rendering the below span element at least 10 times.
renderIndexColumn = () => {
return (
<div>
<span id={data.RefId} className = {this.state.classnm} onClick={(event) => {this.func2(event)}}>
<i className="material-icons">arrow_right</i></span>
</div>
);
};
If it is JQuery, I would have used ' on', 'this' to target the particular element & toggleClass.
Am not sure how to do the same in React.
Can someone help to achieve this? Thanks.
You don't need to set className to this.state.classnm. Just do this instead:
<span id={data.RefId} className = 'collapsed' ... />
And then your func2 should be something like:
func2 = event => {
event.target.classList.toggle('collapsed')
};

Trigger Re-Render of Child component

I'm new to React and am running into the same problem a few times. In this particular situation, I'm trying to get an option in a select dropdown to update when I update a text input.
I have a parent, App, with the state attribute "directions", which is an array. This gets passed as a property to a child, GridSelector, which creates the text field and dropdown. When the text field is changed, a function triggers to update the parent state. This in turn causes the GridSelector property to update. However, the dropdown values, which are originally generated from that GridSelector property, do not re-render to reflect the new property value.
I'm trying to figure out the most React-ful way to do this and similar manuevers. In the past, I've set a state in the child component, but I think I've also read that is not proper.
My working site is at amaxalaus.bigriverwebdesign.com
Here's the pertinent code from each file:
App.js
class App extends React.Component {
constructor(props){
super(props);
this.state = {
directions: [],
dataRouteDirections: '/wp-json/wp/v2/directions',
currentDirectionsIndex: 0
}
this.addImageToGrid = this.addImageToGrid.bind(this);
this.changeTitle=this.changeTitle.bind(this);
}
componentDidMount(){
fetch(this.state.dataRouteDirections)
.then(data => data=data.json())
.then(data => this.setState({directions:data}));
}
addImageToGrid(image) {
this.refs.grid.onAddItem(image); //passes image add trigger from parent to child
}
createNewDirections(){
var directions= this.state.directions;
var index = directions.length;
var lastDirections = directions[directions.length-1];
var emptyDirections= {"id":0,"acf":{}};
emptyDirections.acf.grid="[]";
emptyDirections.acf.layout="[]";
emptyDirections.title={};
emptyDirections.title.rendered="New Directions";
if (lastDirections.id!==0 ) { ///checks if last entry is already blank
this.setState({
directions: directions.concat(emptyDirections), //adds empty directions to end and updates currentdirections
currentDirectionsIndex: index
});
}
}
changeTitle(newTitle){
var currentDirections = this.state.directions[this.state.currentDirectionsIndex];
currentDirections.title.rendered = newTitle;
}
render() {
var has_loaded; //has_loaded was added to prevent double rendering during loading of data from WP
this.state.directions.length > 0 ? has_loaded = 1 : has_loaded = 0;
if (has_loaded ) {
/* const currentGrid = this.state.directions;*/
return ( //dummy frame helpful for preventing redirect on form submit
<div>
<div className="fullWidth alignCenter container">
<GridSelector
directions={this.state.directions}
currentDirectionsIndex={this.state.currentDirectionsIndex}
changeTitle={this.changeTitle}
/>
</div>
<Grid ref="grid"
currentGrid={this.state.directions[this.state.currentDirectionsIndex]}
/>
<ImageAdd addImageToGrid={this.addImageToGrid}/>
<div className="fullWidth alignCenter container">
<button onClick={this.createNewDirections.bind(this)}> Create New Directions </button>
</div>
</div>
)
} else {
return(
<div></div>
)
}
}
}
GridSelector.js
class GridSelector extends React.Component {
constructor(props) {
super(props);
var currentDirections = this.props.directions[this.props.currentDirectionsIndex];
this.state = {
currentTitle:currentDirections.title.rendered
}
}
createOption(direction) {
if (direction.title) {
return(
<option key={direction.id}>{direction.title.rendered}</option>
)
} else {
return(
<option></option>
)
}
}
handleChangeEvent(val) {
this.props.changeTitle(val); //triggers parent to update state
}
render() {
return(
<div>
<select name='directions_select'>
{this.props.directions.map(direction => this.createOption(direction))}
</select>
<div className="fullWidth" >
<input
onChange={(e)=>this.handleChangeEvent(e.target.value)}
placeholder={this.state.currentTitle}
id="directionsTitle"
/>
</div>
</div>
)
}
}
You made a very common beginner mistake. In React state should be handled as an immutable object. You're changing the state directly, so there's no way for React to know what has changed. You should use this.setState.
Change:
changeTitle(newTitle){
var currentDirections = this.state.directions[this.state.currentDirectionsIndex];
currentDirections.title.rendered = newTitle;
}
To something like:
changeTitle(newTitle){
this.setState(({directions,currentDirectionsIndex}) => ({
directions: directions.map((direction,index)=>
index===currentDirectionsIndex? ({...direction,title:{rendered:newTitle}}):direction
})

Resources