ReactJs : how do I set up a simple conditional visible-hidden rule? - reactjs

I have a collection of components with and add and remove button.
I'd like to hide/grey out these buttons when the collection is too small and too big.
This bit of code picked up from here seems like it would do the job
:
render() {
return (
{ this.props.isGreaterThanOne && (
<div>
this only shows up if the checkboxField Field is checked
</div>
) }
);
}
And I could simply update a boolean within my increment and decrement functions like so :
let isGreaterThanOne = true;
this.setState({
collection: this.state.collection.splice(this.state.collection, this.state.collection.length - 1),
});
if (this.state.collection.length > 1) {
isGreaterThanOne = true;
} else {
isGreaterThanOne = false;
}
return { isGreaterThanOne };
Now I'm consious the above is probably as garbage as it gets. All suggestions welcome. I think the variable should be decalred globally but I don't know how to do it.
Basically I just want there to be a variable that is set to false whenever the remove line should be hid (only one line left) and true when there are two lines or more.
Same deal with another variable traking when the collection reaches 15 or something.
Right now even if I just declare isGreaterThanOne as a global const equal to true the div doesn't show up.
As per #Denialos 's demand here is my full component (with his suggestions) :
/* ************************************* */
/* ******** IMPORTS ******** */
/* ************************************* */
import React, { Component } from 'react';
import UUID from 'node-uuid';
import { Card, CardBlock, Button, InputGroup, Input } from 'reactstrap';
import ProviderInfos from '../ProviderInfos/ProviderInfos';
/* ************************************* */
/* ******** VARIABLES ******** */
/* ************************************* */
/* ************************************* */
/* ******** COMPONENT ******** */
/* ************************************* */
export default class PretzelStandComponent extends Component {
constructor(props) {
super(props);
this.state = {
inputPretzel: [],
isGreaterThanOne: true
};
this.incrementPretzel = this.incrementPretzel.bind(this);
this.decrementPretzel = this.decrementPretzel.bind(this);
}
componentDidMount() {
this.incrementPretzel();
}
incrementPretzel() {
const uuid = require('uuid/v1');
uuid();
const inputPretzel = this.state.inputPretzel;
if (this.state.inputPretzel.length > 1) {
this.state.isGreaterThanOne = true;
} else {
this.state.isGreaterThanOne = false;
}
this.setState({
inputPretzel: inputPretzel.concat(<InputGroup>
<Input placeholder="Pretzel" key={uuid} /><ProviderInfos /></InputGroup>),
});
}
decrementPretzel() {
if (this.state.inputPretzel.length > 1) {
this.state.isGreaterThanOne = true;
} else {
this.state.isGreaterThanOne = false;
}
this.setState({
inputPretzel: this.state.inputPretzel.splice(this.state.inputPretzel, this.state.inputPretzel.length - 1),
});
}
render() {
return (
<Card>
<CardBlock className="main-table">
<fieldset>
<legend>Pretzels</legend>
{this.state.inputPretzel}
<button onClick={this.incrementPretzel}>Ajouter un Pretzel</button>
{ this.props.isGreaterThanOne && (
<div>
<button onClick={this.decrementPretzel}>Enlever un Pretzel</button>
</div>
) }
</fieldset>
<Button color="secondary">Options</Button>{' '}
<Button id="btn">Exécuter</Button>
</CardBlock>
</Card>
);
}
}

Problem here is you try to mutate a prop in your component. As already said, Never mutate your state or props this way.
here is a possible solution to your problem :
render() {
const { inputPretzel } = this.state;
const isGreaterThanOne = inputPretzel && inputPretzel.length > 0;
return (
<Card>
<CardBlock className="main-table">
<fieldset>
<legend>Pretzels</legend>
{inputPretzel}
<button onClick={this.incrementPretzel}>Ajouter un Pretzel</button>
{isGreaterThanOne && (
<div>
<button onClick={this.decrementPretzel}>Enlever un Pretzel</button>
</div>
) }
</fieldset>
<Button color="secondary">Options</Button>{' '}
<Button id="btn">Exécuter</Button>
</CardBlock>
</Card>
);
}

Don't mutate state directly, you should call setState.
NEVER mutate this.state directly, as calling setState() afterwards may
replace the mutation you made. Treat this.state as if it were
immutable.
setState() does not immediately mutate this.state but creates a
pending state transition. Accessing this.state after calling this
method can potentially return the existing value.
There is no guarantee of synchronous operation of calls to setState
and calls may be batched for performance gains. setState() will
always trigger a re-render unless conditional rendering logic is
implemented in shouldComponentUpdate().
If mutable objects are being used and the logic cannot be implemented
in shouldComponentUpdate(), calling setState() only when the new state
differs from the previous state will avoid unnecessary re-renders.
Font: What the difference of this.state and this.setstate in ReactJS?
Possible solution for your problem, Using immutability-helper:
import update from 'immutability-helper';
.....
.....
incrementPretzel() {
const uuid = require('uuid/v1');
uuid();
const inputPretzel = this.state.inputPretzel;
if (this.state.inputPretzel.length > 1) {
this.setState({ isGreaterThanOne: true });
else {
this.setState({ isGreaterThanOne: false });
}
this.setState({
inputPretzel: inputPretzel.concat(<InputGroup>
<Input placeholder="Pretzel" key={uuid} /><ProviderInfos /></InputGroup>),
});
}
decrementPretzel() {
const inputPretzel = this.state.inputPretzel;
if (this.state.inputPretzel.length > 1) {
this.setState({ isGreaterThanOne: true });
} else {
this.setState({ isGreaterThanOne: false });
}
this.setState((prevState) => ({
data: update(prevState.data, {$splice: [[-1, 1]]})
})
}
render() {
return (
<Card>
<CardBlock className="main-table">
<fieldset>
<legend>Pretzels</legend>
<button onClick={() => this.incrementPretzel}>Ajouter un Pretzel</button>
{ this.state.isGreaterThanOne &&
<div>
<button onClick={() => this.decrementPretzel}>Enlever un Pretzel</button>
</div>
}
</fieldset>
<Button color="secondary">Options</Button>
<Button id="btn">Exécuter</Button>
</CardBlock>
</Card>
);
}

Related

Iterating through array one at a time in react jsx

How do I iterate one index at a time in the array 'tracks' in react jsx. I want to be able to iterate and display just one track at a time in the array, and go to the next track (index) on the click of a button that'll be labeled 'Next', also go back to previous index when clicking the button 'Previous'.
constructor(props){
super(props);
this.state=({
year: (props.location.state && props.location.state.year) || '',
all_tracks: {tracks:[]},
currentTrack: 0,
});
}
onClickNext(){ //Next button
//Nothing done here yet
}
onClickPrev(){ //Previous button
//Nothing done here yet
}
render(){
const {
currentTrack,
all_tracks: { tracks }
} = this.state;
return (
window.addEventListener('DOMContentLoaded', (event) => {
this.gettingTracks() //Have tracks load immediately
}),
<div id = "song"> //THIS PART
<iframe id="track" src={tracks[currentTrack]
? "https://open.spotify.com/embed/track/"+tracks[currentTrack].id
: "Song N/A"} ></iframe>
</div>
<div>
<button id="nextBtn"> Next </button>
</div>
<div>
<button id="prevBtn"> Previous </button>
</div>
);
}
Here is where I populate the tracks array
gettingTracks(){
// store the current promise in case we need to abort it
if (prev !== null) {
prev.abort();
}
// store the current promise in case we need to abort it
prev = spotifyApi.searchTracks('genre: pop year:' + this.state.year, {limit: 20, offset:1});
prev.then((data) => {
this.setState({
all_tracks: {
tracks: data.tracks.items
}
})
prev = null;
}, function(err) {
console.error(err);
});
}
You can store the index of the current track as a state variable. And instead of iterating over the tracks to display them, you can just simply display the current track.
Here is a simple example,
import React from "react";
export default class App extends React.Component {
constructor(props) {
super(props);
this.state = {
all_tracks: {
tracks: []
},
currentTrack: 0
};
this.onClickNext = this.onClickNext.bind(this);
this.onClickPrev = this.onClickPrev.bind(this);
}
componentDidMount() {
// fetch the data from the Spotify API and update this.state.all_tracks.tracks
}
onClickNext() {
//Next button
this.setState(prevState => {
if (this.state.currentTrack === this.state.all_tracks.tracks.length - 1) {
return;
}
return {
...prevState,
currentTrack: prevState.currentTrack + 1
};
});
}
onClickPrev() {
//Previous button
this.setState(prevState => {
if (this.state.currentTrack === 0) {
return;
}
return { ...prevState, currentTrack: prevState.currentTrack - 1 };
});
}
render() {
const {
currentTrack,
all_tracks: { tracks }
} = this.state;
// before rendering tracks[currentTrack].name check if tracks[currentTrack] exist
return (
<>
<div>
<h3>Current Track</h3>
<h4>
{
tracks[currentTrack]
? tracks[currentTrack].name
: 'track does not exist'
}
</h4>
</div>
<div>
<button id="nextBtn" onClick={this.onClickNext}>
Next
</button>
</div>
<div>
<button id="prevBtn" onClick={this.onClickPrev}>
Previous
</button>
</div>
</>
);
}
}
Check the codesandbox for demo

All the toasters close when clicked on the close button in react

I have made a toaster component of my own which on multiple clicks render multiple toasters. The problem I am facing is that all the toasters are terminated when the handle close component is clicked or when the settimeout function is called. I am passing messages through another component as props.
This is my toaster component
export default class MyToaster extends React.Component {
constructor(props) {
super(props);
this.state = {
message: props.message,
show: false,
no: 0
};
}
handleclose = () => {
this.setState({
show: false,
no: this.state.no - 1
})
}
handleOpen = () => {
console.log('HANDLE OPEN')
this.setState({
show: true,
no: this.state.no + 1
}, () => {
setTimeout(() => {
this.setState({
show: false,
no: this.state.no - 1
})
}, 3000)
})
}
createtoaster = () => {
if (this.state.show) {
let toastmessage = [];
for (let i = 0; i < this.state.no; i++) {
let tmessage = <div className="snackbar">
<div className="card-header">
<h3 className="card-title">Toast</h3>
</div>
<div className="card-body">
{this.state.message}
</div>
<div className="card-footer"></div>
<button className="btn" onClick={this.handleclose}>x</button>
</div>
toastmessage.push(tmessage);
}
return toastmessage;
} else {
return null;
}
};
render() {
return (
<div className="col-md-2 offset-md-9">
<button className="btn btn-primary" onClick={this.handleOpen}></button>
{this.createtoaster()}
</div>
)
}
}
I have tried managing the state in the parent component but it doesnt seem to work. I do know that the problem is in managing state of my toaster component but dont know the exact problem and the solution.
Any solutions for this also feel free to point out any of my mistakes.
TIA
Handle close is run on the click of any button rather on the instance of one of them by the looks of it.
if (this.state.show) { // this determines whether to render you toasts...
// and close turns all of them off.
You need to change each toast to have it's own show property and for close to toggle that one and remove it from the array of toasts to generate.
Note:
Your props and state should be separate, don't copy props into state as this will introduce bugs and changes will not be reflected.
constructor(props) {
super(props);
// avoid copying props into state
// https://reactjs.org/docs/react-component.html#constructor
this.state = {
message: props.message,
show: false,
no: 0
};
}
There is a different way to this approach.
export default class MyToaster extends React.Component {
constructor(props) {
super(props);
this.state = {
message: props.message,
show: true,
no: 0
};
}
componentDidMount() {
setTimeout(() => {
this.setState({show: false})
}, 4000)
}
handleclose = () => {
this.setState({
show: false,
no: this.state.no - 1
})
}
handleOpen = () => {
this.setState({
no: this.state.no + 1
}, () => {
setTimeout(() => {
this.setState({
show: false,
no: this.state.no - 1
})
}, 3000)
})
}
render() {
return (
<div className="col-md-2 offset-md-9">
{this.state.show
? (
<div className="container snackbar" style={this.props.style}>
<div className="card-header">
<h3 className="card-title">Toast</h3>
</div>
<div className="card-body">
{this.props.message}
</div>
<div className="card-footer"></div>
</div>
)
: null
}
</div>
)
}
}
And from your parent component you can include
this.state = {
toasterCollection: []
}
//make a function
handleToasterClick = () => {
const toaster = <Toaster message={this.message} style={this.style}/>
this.setState({
// toasterCollection: [...this.state.toasterCollection, toaster]
toasterCollection: [...this.state.toasterCollection, toaster]
});
}
//In your Render give a button
<button className="btn btn-primary" onClick={this.handleToasterClick}>
Toast
</button>
//Also render this
{this.state.toasterCollection}
This should get your code to work.

reactjs- callback function still not updating background color upon enter key being pressed

I have read the react documentation and examples here:
When to use React setState callback
I am trying to use a callback to get setState() to update immediately. But my background color, or squaresColor[i] is still not updating.
The console.log(squaresColor[i]) is outputting undefined: green. The green is correct but the key undefined.
I tried to.. setState(squaresColor: squaresColor), but upon pressing the enter key, the background still does not change. For whatever reason, I am using the same setState in a different method, handleClick(). This is being called asynchronously and it is working fine.
So why is the background color not updating in handleKeyPress? Should i not be using state to make this update?
My Code :
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
/*stop clicking ability after clicking a problem (prevent default) and enable it when submit is done.*/
/* let btnStyle = {
backgroundColor: 'green'
} */
/*we also changed onClick={() => props.onClick()} to just onClick={props.onClick},
as passing the function down is enough for our example. Note that onClick={props.onClick()}
would not work because it would call props.onClick immediately instead of passing it down.*/
class Square extends React.Component
{
render()
{
return (
<button className="square" onClick = {this.props.onClick} style = {{backgroundColor: this.props.backgroundColor}}>
{this.props.value}
{this.props.txt}
</button>
);
}
}
class Board extends React.Component
{
constructor(props)
{
super(props);
this.state =
{
squares: Array(25).fill(null),
xIsNext: true,
squaresColor: Array(25).fill('null'),
num1: generateRandomNumber(),
num2: generateRandomNumber(),
ans: function(a,b){return(a * b);},
sqTxt: Array(25).fill(null),
next: true
};
//this.handleKeyPress = this.handleKeyPress.bind(this);
}
handleClick(i)
{
const squares = this.state.squares.slice(); // makes a mutable copy of the array
const squaresColor = this.state.squaresColor.slice(); // makes a mutable copy of the array
const sqTxt = this.state.sqTxt.slice(); // makes a mutable copy of the array
if (!(this.state.next) || squares[i])
{
return;
}
squaresColor[i] = 'blue';
sqTxt[i] = <input onKeyPress = {(e,i) => this.handleKeyPress(e, i)} className = 'answer-input' type = 'text' name = 'answer' />;
this.setState({
squares: squares,
squaresColor: squaresColor,
sqTxt: sqTxt,
next: false
});
console.log(this.state.squaresColor[i]);
squares[i] = this.state.num1 + ' X ' + this.state.num2;
return this.state.next;
}
/*When an event is invoked, it will be passed an event object as it's first argument. You can name evt whatever you like. Common names are e evt and event.*/
handleKeyPress(e, i)
{
const userAnswer = parseInt(e.target.value, 10);
const num1 = this.state.num1;
const num2 = this.state.num2;
const correctAnswer = num1 * num2;
const squaresColor = this.state.squaresColor.slice(); // makes a mutable copy of the array
if(e.key === 'Enter')
{
squaresColor[i] = 'green';
if(userAnswer === correctAnswer)
{
this.setState({
/* squaresColor: */ squaresColor,
next: true,
}/* , function()
{
console.log(this.state.squaresColor);
this.setState(squaresColor: squaresColor);
} */);
//create new numbers for next problem
this.setState({num1: generateRandomNumber(), num2: generateRandomNumber()});
}
else
{
console.log('incorrect');
}
}
}
renderSquare(i)
{
return (
<Square
value={this.state.squares[i]}
onClick = {() => this.handleClick(i)}
backgroundColor = {this.state.squaresColor[i]}
txt = {this.state.sqTxt[i]}
/>
);
}
render()
{
return (
<div className = 'all-rows'>
<div className="board-row">
{this.renderSquare(0)}
{this.renderSquare(1)}
{this.renderSquare(2)}
{this.renderSquare(3)}
{this.renderSquare(4)}
</div>
<div className="board-row">
{this.renderSquare(5)}
{this.renderSquare(6)}
{this.renderSquare(7)}
{this.renderSquare(8)}
{this.renderSquare(9)}
</div>
<div className="board-row">
{this.renderSquare(10)}
{this.renderSquare(11)}
{this.renderSquare(12)}
{this.renderSquare(13)}
{this.renderSquare(14)}
</div>
<div className="board-row">
{this.renderSquare(15)}
{this.renderSquare(16)}
{this.renderSquare(17)}
{this.renderSquare(18)}
{this.renderSquare(19)}
</div>
<div className="board-row">
{this.renderSquare(20)}
{this.renderSquare(21)}
{this.renderSquare(22)}
{this.renderSquare(23)}
{this.renderSquare(24)}
</div>
</div>
);
}
}
class Game extends React.Component
{
render() {
return (
<div className="game">
<div className="gxame-board">
<Board />
</div>
<div className="game-info">
</div>
</div>
);
}
}
ReactDOM.render(
<Game />,
document.getElementById('root')
);
function generateRandomNumber()
{
return Math.floor(Math.random() * 10);
}
You do not need to call setState again. You should rewrite this part like :
if(e.key === 'Enter') {
squaresColor[i] = 'green';
if(userAnswer === correctAnswer) {
this.setState({
squaresColor,
next: true,
num1: generateRandomNumber(),
num2: generateRandomNumber()
}, () => {
console.log(this.state);
});
} else {
console.log('incorrect');
}
}
And do not comment this.handleKeyPress = this.handleKeyPress.bind(this);.

Delete remove specific collection item

Here's my current code :
/* ************************************* */
/* ******** IMPORTS ******** */
/* ************************************* */
import React, { Component } from 'react';
import UUID from 'node-uuid';
import { Card, CardBlock, Button, InputGroup, Input } from 'reactstrap';
import ProviderInfos from '../ProviderInfos/ProviderInfos';
/* ************************************* */
/* ******** VARIABLES ******** */
/* ************************************* */
/* ************************************* */
/* ******** COMPONENT ******** */
/* ************************************* */
export default class PretzelStandComponent extends Component {
constructor(props) {
super(props);
this.state = {
inputPretzel: [],
inputCurry: [],
inputWurst: []
};
this.incrementPretzel = this.incrementPretzel.bind(this);
this.incrementCurry = this.incrementCurry.bind(this);
this.incrementWurst = this.incrementWurst.bind(this);
this.decrementPretzel = this.decrementPretzel.bind(this);
this.decrementCurry = this.decrementCurry.bind(this);
this.decrementWurst = this.decrementWurst.bind(this);
}
componentDidMount() {
this.incrementPretzel();
this.incrementCurry();
this.incrementWurst();
}
incrementPretzel() {
const uuid = require('uuid/v1');
uuid();
const inputPretzel = this.state.inputPretzel;
this.setState({
inputPretzel: inputPretzel.concat(<InputGroup>
<Input placeholder="Pretzel" key={uuid} /><ProviderInfos /></InputGroup>),
});
}
incrementCurry() {
const uuid = require('uuid/v1');
uuid();
const inputCurry = this.state.inputCurry;
this.setState({
inputCurry: inputCurry.concat(<InputGroup>
<Input placeholder="Curry" key={uuid} /><ProviderInfos /></InputGroup>),
});
}
incrementWurst() {
const uuid = require('uuid/v1');
uuid();
const inputWurst = this.state.inputWurst;
this.setState({
inputWurst: inputWurst.concat(<InputGroup>
<Input placeholder="Wurst" key={uuid} /><ProviderInfos /></InputGroup>),
});
}
decrementPretzel() {
this.setState({
inputPretzel: this.state.inputPretzel.splice(this.state.inputPretzel, this.state.inputPretzel.length - 1),
});
}
decrementCurry() {
this.setState({
inputCurry: this.state.inputCurry.splice(this.state.inputCurry, this.state.inputCurry.length - 1),
});
}
decrementWurst() {
this.setState({
inputWurst: this.state.inputWurst.splice(this.state.inputWurst, this.state.inputWurst.length - 1),
});
}
render() {
return (
<Card>
<CardBlock className="main-table">
<fieldset>
<legend>Pretzels</legend>
{this.state.inputPretzel}
<button onClick={this.incrementPretzel}>Add a Pretzel</button>
<button onClick={this.decrementPretzel}>Remove a Pretzel</button>
</fieldset>
<fieldset>
<legend>Curry</legend>
{this.state.inputCurry}
<button onClick={this.incrementCurry}>Add Curry</button>
<button onClick={this.decrementCurry}>Remove Curry</button>
</fieldset>
<fieldset>
<legend>Wurst</legend>
{this.state.inputPretzel}
<button onClick={this.incrementPretzel}>Add Wurst</button>
<button onClick={this.decrementPretzel}>Remove Wurst</button>
</fieldset>
<Button color="secondary">Options</Button>{' '}
<Button id="btn">Exécuter</Button>
</CardBlock>
</Card>
);
}
}
As you can see I have three different elements and collections of these elements :
Pretzels
Currys
and Wursts
I can add them and remove the last one. but I'd like to remove each one.
In the Html code that I'm placing in the setState and adding to each collection I want to append a delete button or somehow have a delete button next to each line wich deletes the right line.
Update
I added some parts to keep track of input state and add a value to each item:
pretzelValue
curryValue
wurstValue
These are the value of the inputs, which are then passed into the increment functions. Also removed input from the FoodType component, if you want to be able to edit them it's a bit trickier.
Original
You can clean it up a bit by just using arrays of objects for the sets of food. Then using another component for the FoodType should make it much cleaner and give good performance for onClick. Each item has it's own uuid, so you can .filter on that to remove the item from the state.
The state and functions to add/remove could be more generic, but this is a decent start.
Something like this:
const uuid = require('uuid/v1');
export default class PretzelStandComponent extends Component {
state = {
pretzels: [],
curries: [],
wursts: [],
pretzelValue: '',
curryValue: '',
wurstValue: ''
}
componentDidMount() {
}
incrementPretzel = () => {
this.setState({
pretzels: this.state.pretzels.concat([{id: uuid(), value: this.state.pretzelValue}]),
pretzelValue: ''
});
}
incrementCurry = () => {
this.setState({
curries: this.state.curries.concat([{id: uuid(), value: this.state.curryValue}]),
curryValue: ''
});
}
incrementWurst = () => {
this.setState({
wursts: this.state.wursts.concat([{id: uuid(), value: this.state.wurstValue}]),
wurstValue: ''
});
}
decrementPretzel = (id) => {
this.setState({
pretzels: this.state.pretzels.filter((pretzel) => pretzel.id !== id)
});
}
decrementCurry = (id) => {
this.setState({
curries: this.state.curries.filter((curry) => curry.id !== id)
});
}
decrementWurst = (id) => {
this.setState({
wursts: this.state.wursts.filter((wurst) => wurst.id !== id)
});
}
onPretzelChange = (event) => {
this.setState({
pretzelValue: event.target.value
});
}
onCurryChange = (event) => {
this.setState({
curryValue: event.target.value
});
}
onWurstChange = (event) => {
this.setState({
wurstValue: event.target.value
});
}
render() {
const {pretzels, curries, wursts} = this.state;
return (
<Card>
<CardBlock className="main-table">
<fieldset>
<legend>Pretzels</legend>
{pretzels.map((pretzel) => (
<FoodType id={pretzel.id} placeholder="Pretzel" onRemove={this.decrementPretzel} value={pretzel.value} />
))}
<input onChange={this.onPretzelChange} value={this.state.pretzelValue} />
<button onClick={this.incrementPretzel}>Add a Pretzel</button>
</fieldset>
<fieldset>
<legend>Curry</legend>
{curries.map((curry) => (
<FoodType id={curry.id} placeholder="Curry" onRemove={this.decrementCurry} value={curry.value} />
))}
<input onChange={this.onCurryChange} value={this.state.curryValue} />
<button onClick={this.incrementCurry}>Add Curry</button>
</fieldset>
<fieldset>
<legend>Wurst</legend>
{wursts.map((wurst) => (
<FoodType id={wurst.id} placeholder="Wurst" onRemove={this.decrementWurst} value={wurst.value} />
))}
<input onChange={this.onWurstChange} value={this.state.wurstValue} />
<button onClick={this.incrementWurst}>Add Wurst</button>
</fieldset>
<Button color="secondary">Options</Button>{' '}
<Button id="btn">Exécuter</Button>
</CardBlock>
</Card>
);
}
}
FoodType component
class FoodType extends Component {
onRemove = () => {
this.props.onRemove(this.props.id);
}
render() {
const {placeholder, id, value} = this.props;
return (
<InputGroup>
<div key={id}>{value}</div>
<ProviderInfos />
<button onClick={this.onRemove}>X</button>
</InputGroup>
);
}
}
Also cleaned up the binds with the property initializer syntax.

Reactjs setState not updating for this one function only

For this application, clicking a listed item once should create a button component underneath this listed item. Clicking the button should cause this listed item to be deleted.
I am currently facing difficulty trying to 'delete' the listed item after the button is clicked. Here is the code that went wrong (this is found in CountdownApp component) :
handleDelete(index) {
console.log('in handleDelete')
console.log(index)
let countdownList = this.state.countdowns.slice()
countdownList.splice(index, 1)
console.log(countdownList) // countdownList array is correct
this.setState({
countdowns: countdownList
}, function() {
console.log('after setState')
console.log(this.state.countdowns) // this.state.countdowns does not match countdownList
console.log(countdownList) // countdownList array is still correct
})
}
In the code above, I removed the item to be deleted from countdownList array with splice and tried to re-render the app with setState. However, the new state countdowns do not reflect this change. In fact, it returns the unedited state.
I have also tried the following:
handleDelete(index) {
this.setState({
countdowns: [] // just using an empty array to check if setState still works
}, function() {
console.log('after setState')
console.log(this.state.countdowns)
})
}
In the code above, I tried setting state to be an empty array. The console log for this.state.countdowns did not print out an empty array. It printed out the unedited state again
This is the only event handler that isn't working and I have no idea why (main question of this post) :/
If I have 'setstate' wrongly, why does the other 'setState' in other parts of my code work?? (I would like to request an in-depth explanation)
This is all my code for this app (its a small app) below:
import React from 'react'
import ReactDOM from 'react-dom'
class DeleteButton extends React.Component {
render() {
return (
<ul>
<button onClick={this.props.onDelete}>
delete
</button>
</ul>
)
}
}
class Countdown extends React.Component {
render () {
//console.log(this.props)
return (
<li
onClick={this.props.onClick}
onDoubleClick={this.props.onDoubleClick}
>
{this.props.title} - {this.props.days}, {this.props.color}
{this.props.showDeleteButton ? <DeleteButton onDelete={this.props.onDelete}/> : null }
</li>
)
}
}
const calculateOffset = date => {
let countdown = new Date(date)
let today = new Date
let timeDiff = countdown.getTime() - today.getTime()
let diffDays = Math.ceil(timeDiff / (1000 * 3600 * 24))
return diffDays
}
class CountdownList extends React.Component {
countdowns() {
let props = this.props
// let onClick = this.props.onClick
// let onDoubleClick = this.props.onDoubleClick
let rows = []
this.props.countdowns.forEach(function(countdown, index) {
rows.push(
<Countdown
key={index}
title={countdown.title}
days={calculateOffset(countdown.date)}
color={countdown.color}
showDeleteButton={countdown.showDeleteButton}
onDelete={() => props.onDelete(index)}
onClick={() => props.onClick(index)}
onDoubleClick={() => props.onDoubleClick(index)}
/>
)
})
return rows
}
render() {
return (
<div>
<ul>
{this.countdowns()}
</ul>
</div>
)
}
}
class InputField extends React.Component {
render() {
return (
<input
type='text'
placeholder={this.props.placeholder}
value={this.props.input}
onChange={this.props.handleInput}
/>
)
}
}
class DatePicker extends React.Component {
render() {
return (
<input
type='date'
value={this.props.date}
onChange={this.props.handleDateInput}
/>
)
}
}
class CountdownForm extends React.Component {
constructor(props) {
super(props)
this.state = {
title: this.props.title || '',
date: this.props.date || '',
color: this.props.color || ''
}
}
componentWillReceiveProps(nextProps) {
this.setState({
title: nextProps.title || '',
date: nextProps.date || '',
color: nextProps.color || ''
})
}
handleSubmit(e) {
e.preventDefault()
this.props.onSubmit(this.state, this.reset())
}
reset() {
this.setState({
title: '',
date: '',
color: ''
})
}
handleTitleInput(e) {
this.setState({
title: e.target.value
})
}
handleDateInput(e) {
this.setState({
date: e.target.value
})
}
handleColorInput(e) {
this.setState({
color: e.target.value
})
}
render() {
return (
<form
onSubmit={(e) => this.handleSubmit(e)}
>
<h3>Countdown </h3>
<InputField
placeholder='title'
input={this.state.title}
handleInput={(e) => this.handleTitleInput(e)}
/>
<DatePicker
date={this.state.date}
handleDateInput={(e) => this.handleDateInput(e)}
/>
<InputField
placeholder='color'
input={this.state.color}
handleInput={(e) => this.handleColorInput(e)}
/>
<button type='submit'>Submit</button>
</form>
)
}
}
class CountdownApp extends React.Component {
constructor() {
super()
this.state = {
countdowns: [
{title: 'My Birthday', date: '2017-07-25', color: '#cddc39', showDeleteButton: false},
{title: 'Driving Practice', date: '2017-07-29', color: '#8bc34a', showDeleteButton: false},
{title: 'Korean BBQ', date: '2017-08-15', color: '#8bc34a', showDeleteButton: false}
]
}
}
handleCountdownForm(data) {
if (this.state.editId) {
const index = this.state.editId
let countdowns = this.state.countdowns.slice()
countdowns[index] = data
this.setState({
title: '',
date: '',
color: '',
editId: null,
countdowns
})
} else {
data.showDeleteButton = false
const history = this.state.countdowns.slice()
this.setState({
countdowns: history.concat(data),
})
}
}
handleDelete(index) {
console.log('in handleDelete')
console.log(index)
let countdownList = this.state.countdowns.slice()
countdownList.splice(index, 1)
console.log(countdownList)
this.setState({
countdowns: countdownList
}, function() {
console.log('after setState')
console.log(this.state.countdowns)
})
}
handleCountdown(index) {
const countdownList = this.state.countdowns.slice()
let countdown = countdownList[index]
countdown.showDeleteButton = !countdown.showDeleteButton
this.setState({
countdowns: countdownList
})
}
handleDblClick(index) {
const countdownList = this.state.countdowns
const countdown = countdownList[index]
this.setState({
title: countdown.title,
date: countdown.date,
color: countdown.color,
editId: index
})
}
render() {
return (
<div>
<CountdownForm
title={this.state.title}
date={this.state.date}
color={this.state.color}
onSubmit={(data) => {this.handleCountdownForm(data)}}
/>
<CountdownList
countdowns={this.state.countdowns}
onDelete={(index) => this.handleDelete(index)}
onClick={(index) => this.handleCountdown(index)}
onDoubleClick={(index) => this.handleDblClick(index)}
/>
</div>
)
}
}
ReactDOM.render(
<CountdownApp />,
document.getElementById('app')
)
I managed to find the answer to my own question!
setState worked as expected. The bug was due to <li> container that wrapped the event handler.
Clicking <li> causes it to call onClick event (which is managed by handleCountdown function in CountdownApp component) which causes it to setState.
As the delete button was wrapped in <li> container, clicking the delete button calls 2 event listeners - handleCountdown and handleDelete. handleCountdown is called twice in this case, once from clicking <li> to expand and the next call when the delete button is clicked.
There is a high chance that the last async setState dispatched from handleCountdown overwrites handleDelete's setState. Hence, the bug.
Here is changes: (I recoded everything again so the names might differ a little but the logic stays the same)
class Countdown extends React.Component {
render () {
return (
<li>
<div onClick={this.props.onClick} > // Add this div wrapper!
{this.props.title} - {this.props.days}, {this.props.color}
</div>
{this.props.toShow ?
<ButtonsGroup
onDelete={this.props.onDelete}
onEdit={this.props.onEdit}
/>
: null}
</li>
)
}
}
So the solution is to separate the clickable area and the buttons. I added a div wrapper over the text in <li> so whenever the text in <li> is clicked, the added <ul> will be out of onClick event handler area.

Resources