Why is my state counter one value behind what it should be? - javascript

Im new to reactjs. Im trying to create a comment section for some uploaded files, and keeping a counter on the comment buttons attached to each file. However, the counter is returning strange values.
Here is the relevent code:
class ListItem extends React.Component {
constructor(props){
super(props)
this.clicked = false
this.commentButtonRef = React.createRef();
this.state = {clickCounter:0, counterMat:[]}
}
handleClick = () =>{
console.log(this.state.clickCounter)
this.clicked = true;
this.counterMat = []
this.props.onCommentButtonClick(this.props.file, this.clicked)
this.clicked = false;
//update click counter
this.setState({clickCounter:this.state.clickCounter + 1}, this.updateCounterMatrix())
}
updateCounterMatrix = ()=> {
const temp = this.state.counterMat.slice() //copy the array
temp[1] = this.state.clickCounter //execute the manipulations
this.setState({counterMat: temp},console.log(this.state.counterMat, this.state.clickCounter))
}
createCounterMat=(element)=>{
// use ref callback to pass DOM element into setState
this.setState({counterMat:[element,this.state.clickCounter]})
console.log(this.counterMat)
}
render(){
return(
<div className="item">
<i className="large file alternate icon"></i>
<div className="content">
<div className="header">{this.props.file}</div>
<button className='comment-button'
id = {this.props.file}
onClick = {this.handleClick}
key = {this.props.file}
ref = {this.createCounterMat}
clickcounter = {this.state.clickCounter}
> Comment</button>
</div>
</div>
)
}
}
Here are the issues im having:
1) As soon as this page first renders, my use of a reactRef callback function createCounterMat in the button element should console log's undefined, which is unexpected.
2) On the first click of my button, the handleClick function calls correctly. However, the console log's inside both handleClick and updateCounterMatrix both return a value of 0 for this.state.clickCounter. I expected the first to be 0, but the second console.log to be 1 by this stage.
3) On the second click, the clickCounter state seems to correctly increment by 1. However, the console.log(this.state.counterMat, this.state.clickCounter) gives a value of 0 inside this.state.counterMat, and a value of 1 in the case of simply this.state.clickCounter.
Here is a screenshot showing all of this
Can anyone help me work out what's going on?

You're calling console.log before the set state, not after. This:
this.setState(
{counterMat: temp},
console.log(this.state.counterMat, this.state.clickCounter)
)
... means "call console.log, then pass its result along with {counterMat: temp} into this.setState". You probably meant to do:
this.setState(
{counterMat: temp},
() => console.log(this.state.counterMat, this.state.clickCounter)
)

Related

My React function gets called twice, with the second call setting values incorrectly

i have a function that calculates pages and page buttons based on an api. Inside the buttons get rendered and they have an onClick function. When i click the button, this is supposed to happen:
sets the current page number and writes it into state
calls the api which gets text elements to display according to current page
evaluates page buttons and numbers based on api and marks the current page with a css class
event handler:
handleClick(event) {
let currentPage = Number(event.target.id)
localStorage.setItem("currentPage", currentPage)
this.setState ({
currentPage: currentPage
})
this.fetchApi()
}
then i'm returning the component that deals with pages:
return(
<div>
<Paging
data = {this}
currentPage = {this.state.currentPage}
state = {this.state}
lastPage = {this.state.lastPage}
handleClick = {this.handleClick}
/>
</div>
)
and the component looks like this:
function Paging(props) {
const apiPaging = props.state.apiPaging
const apiPagingSliced = apiPaging.slice(1, -1)
const renderPageNumbers = apiPagingSliced.map((links, index) => {
return <button key={index} id={links.label}
onClick={(index)=>props.handleClick(index)}
className={(links.active ? "mark-page" : "")}
>{links.label} {console.log(links.label) }
</button>
})
return (
<div id = "page-nums">
{renderPageNumbers}
</div>
)
So what happens is that Paging() function gets called twice. There is a handy value inside the api called "active" (links.active) which is a boolean, and if set to true, means that the page is the current page. i then add a class "mark-page" on to highlight that i'm currently on that page. If i {console.log(links.label)} i see that it's invoked twice, first being the correct values and second being the previously clicked values. So it works correctly only if i reload the page again.
i.e if i click page 2,it stays on page 1 and marks page 1. if i then click page 3, it marks page 2. and (afaik) Paging() gets only invoked once, at the end of my only class (Body).
I've been at it yesterday and today and have no idea anymore.
change your handleClick function to this.
handleClick(event) {
window.scrollTo({
top: 0,
behavior: 'smooth',
});
if (event.target.id >= 1) {
let currentPage = Number(event.target.id);
localStorage.setItem('currentPage', currentPage);
this.setState({
currentPage: JSON.parse(localStorage.getItem('currentPage')),
},()=>{
this.fetchApi();
});
}
}
in your fetchApi function you reference currentPage as below.
const apiQuery = JSON.parse(this.state.currentPage);
But it hasn't updated yet.
see https://reactjs.org/docs/react-component.html#setstate

Binding setState() to a global function only affects one instance of a component

Note: I have edited the question after the changes I have made according to Nicholas Tower's answer.
I have a global function which bound to a component and changes it's state.
I want to build a form builder system. There is a global function named setLabel which is bound to a component named InputBox and changes it's state. This global function is triggered via another component named ModalPanel which controls the editable properties on the bound component InputBox. I have simplified the function and component class for simplicity of this question.
Here is the global function:
function setLabel(postID, attributeName ){
var text = 'example';
if(text !== ''){
this.setState({ [attributeName] : text});
}
}
And here is the component which is bound to the setLabel function. Notice how setLabel function is passed from parent InputBox component to child ModalPanel component function as a property.
class InputBox extends Component{
constructor(props){
super(props);
this.state = {
placeholder : '',
maxlength: '',
type: '',
}
this.setLabel = setLabel.bind(this); // Binding this to the global function.
}
render(){
let elementId = "inputElement-" + this.props.idCounter;
let mainElement = <Form.Control
id = {elementId}
type = {this.state.type}
placeholder = {this.state.placeholder}
maxLength = {this.state.maxlength}
/>
return <Row>
<ModalPanel
handleHide = {this.props.handleHide}
handleShow = {this.props.handleShow}
setLabel = {this.setLabel}
/>
</Row>
}
}
Lastly, below is the ModalPanel component function where the setLabel function is triggered.
function ModalPanel(props){
return(
......................
......................
......................
<Button variant="primary" onClick = {() => props.setLabel()}>Save changes</Button>
......................
......................
......................)
}
setLabel function which is aimed to set the state of InputBox must be triggered when a button is clicked in the ModalPanel component. The problem is, there are multiple rendered <InputBox /> components on the window and when I try to use this functionality, "the state change" only affect the first instance of <InputBox /> component. What I want to do is that, every instance should have their own internal state and setLabel() function should be bound to the specific component from where it is called. So that, this function can be able to set the state of different component instances. How could I do that?
Addition:
Please check the link below to see a gif image showing how my system works wrong. As you can see, even though I choose the third input box element to edit it's properties (in this case, set it's placeholder text), the change is being made to the first one.
Go to gif
Add a this. to the beginning, as in:
this.setLabel = setLabel.bind(this);
Now you're setting a property on the instance of the InputBox. Make sure to refer to it using this.setLabel when you reference it later in the component.
Is setLabel acting on a specific postID? Is the problem that <Button /> of every <ModalPanel /> acting on the same postID? Because you aren't using setLabel correctly inside <ModalPanel />. setLabel takes in 2 arguments and right now your implementation isn't using any. This is your click handler.
onClick = {() => props.setLabel()}
Try console.logging inside setLabel and see what values you're getting when you click on each button
function setLabel(postID, attributeName){
console.log(postID, attributeName)
var text = 'example';
if(text !== ''){
this.setState({ [attributeName] : text});
}
}
Since the React components only updated from props or state changes, you need to pair the global state with a local state to update the component. See the code below in a sandbox environment.
let value = 0;
function updateStuff () {
console.log("this from update", this.name);
value++;
this.setState({name: "Hakan " + value});
}
class Test extends React.Component {
constructor(props){
super(props);
this.state = {
name: 'notchanged',
counter: 1
}
this.localFunc = this.localFunc.bind(this)
updateStuff = updateStuff.bind(this)
}
localFunc(){
let {counter} = this.state;
this.setState({counter: counter + 1});
updateStuff();
}
render () {
return (
<div>
<div>Test 2</div>;
<div>Counter: {this.state.counter}</div>
<div>Name: {this.state.name}</div>
<button onClick={this.localFunc}>Increment</button>
</div>
);
}
}
ReactDOM.render(
<Test/>,
document.getElementById('root')
);
Think, you are using React in incorrect way
The preferred way for me looks like:
Have a dumb/presentational InputBox which accepts label as a property (in props, not in state)
Have a smart/container component which contains state of multiple InputBoxes and passes the correct label into InputBox
If you are trying to implement InputBox PropertyEditor as a separate component - consider adding event bus, shared between them for example via React Context (or even use full flux/redux concept)
Add this to your function calls after binding them or use arrow functions !

How to return an HTML <div> tag from a javascript function in React?

I am working on a React application where I am trying to render text on the screen when a button is clicked. I have defined a function onButtonClick which gets triggered whenever the button is clicked. However, the HTML that I am returning from the function is not rendered on the screen. I am in the learning stages of React so please excuse me if the question seems silly.
class App extends Component {
constructor() {
super();
this.state = {
blockno:0
}
}
OnButtonClick = () => {
this.setState({blockno: this.state.blockno + 1})
return(
<div>
<h3>Some text</h3>
</div>
);
}
render() {
return(
<div>
<Button onButtonClick={this.OnButtonClick}/>
</div>
);
}
}
The value is being returned, but the framework/browser/etc. has no reason to do anything with that value.
Try thinking about this a different way, a "more React way". You don't want to return the value to be rendered, you want to update state. Something like this:
constructor() {
super();
this.state = {
blockno:0,
showDiv: false // <-- note the new property in state
}
}
OnButtonClick = () => {
this.setState({blockno: this.state.blockno + 1, showDiv: true})
}
Now you're not returning anything, but rather updating the state of the component. Then in your render method you conditionally render the UI based on the current state:
render() {
return(
<div>
<Button onButtonClick={this.OnButtonClick}/>
{
this.state.showDiv
?
<div>
<h3>Some text</h3>
</div>
: ''
}
</div>
);
}
The click handler doesn't modify the page, it just modifies the state of the component you're writing. The render method is responsible for rendering the UI based on that state. Any time state changes, render will be called again to re-render the output.
(Note: It's not 100% clear if this is exactly the functionality you're looking for in the UI, since it's not really clear what you're trying to build. But the point here is to illustrate how to update state and render output in React. Your logic can be tweaked as needed from there.)
You have to make a render based on your state. Please check the tutorial at the react docs to learn more about how React works. It's really good
Here is a version of your code that works. Hope it helps
class App extends Component {
constructor() {
super();
this.state = {
blockno: 0
};
}
OnButtonClick = () => {
//updates the states
this.setState({ blockno: this.state.blockno + 1 });
};
//remember: every time there is an update to the state the render functions re-runs
render() {
//variable holding the blocks in an array
let blocks = []
//if blockno is greater than 0, it checks everytime that there is a state change
if (this.state.blockno > 0) {
//for every block added
for (let index = 0; index < this.state.blockno; index++) {
//We`re going to add to the array of blocks a new div with the block number
blocks.push(
<div>
<h3>My block number is {index}</h3>
</div>
);
}
}
return (
<div>
<div>
{/**button that updates the state on every click */}
<button onClick={this.OnButtonClick}>
Click me to add a new div!
</button>
</div>
{/**This render the blocks variable that holds the divs */}
{blocks}
</div>
);
}
}
What I see is that you are trying to build a counter. The value that you're returning from the click handler function can't be rendered, instead you need to manage it in the render function as follow:
class App extends Component {
constructor() {
super();
this.state = {
blockno: 0
}
}
OnButtonClick = () => {
this.setState(prevState => ({ blockno: prevState.blockno + 1 }));
}
render() {
return(
<div>
{this.state.blockno > 0 && <div>some text {this.state.blockno}</div>}
<Button onButtonClick={this.OnButtonClick} />
</div>
);
}
}
Also note that the setState method is asynchronous, please read the documentation https://reactjs.org/docs/react-component.html#setstate

React state is out of sync between render method and what is actually displayed on page

I need to access DOM elements outside of my React app, which may load slower than my app. Then I need to update my state to render a few different things. To do that I am polling for the DOM elements with a recursive function that gets kicked off from componentDidMount(). I'm seeing a weird issue where once the element is found and I've updated the state, things get out of sync. In the render function, my console.log() shows the updated state value, in React Developer Tools I see the updated state value, but on the actual rendered page I see still see the old state value.
Code:
// initially doesn't exist. Added to the DOM after 3 seconds
let slowElement = document.querySelector('.external-dom-element')
class App extends React.Component {
constructor (props) {
super(props)
this.state = {
showFoundSlowElementMessage: false,
slowElementCheckMaxAttempts: 5,
slowElementCheckCount: 0,
}
this.checkForSlowElement = this.checkForSlowElement.bind(this)
}
componentDidMount () {
this.checkForSlowElement()
}
checkForSlowElement () {
slowElement = document.querySelector('.external-dom-element')
if (slowElement !== null) {
console.log('found') // element found, show message
this.setState({
showFoundSlowElementMessage: true
})
} else {
console.log('not found') // element not found, increment count and check again after delay
this.setState({
slowElementCheckCount: this.state.slowElementCheckCount + 1
}, () => {
if (this.state.slowElementCheckCount < this.state.slowElementCheckMaxAttempts) {
window.setTimeout(this.checkForSlowElement, 1000)
}
})
}
}
render() {
const foundSlowElement = this.state.showFoundSlowElementMessage
? <p>Found slow element</p>
: <p>No sign of slow element, checked {this.state.slowElementCheckCount} times</p>
// null until it is added to the page
console.log(foundSlowElement)
return (
<div>
<h1>Hello</h1>
{foundSlowElement}
</div>
);
}
}
}
ReactDOM.render(<App />, document.getElementById('react-target'));
// Simulate slow element by adding it to the DOM after 3 seconds
window.setTimeout(() => {
const root = document.getElementById('root');
const newElement = '<div class="external-dom-element">slow element</div>';
root.innerHTML += newElement;
}, 3000)
Working example on codepen
I figured this out myself. It has nothing to do with my component, it's the demo itself that is breaking it. When I simulate the slow element by appending the root element's inner html:
root.innerHTML += newElement;
It re-parses the entire element and React loses all of the event handlers, etc. that it had previously set up.
This thread helped me out

Function for changing numbers with React doesn't work properly

I'm trying to change number stored in a variable by clicking a button but the first time I click the button, it doesn't change the value of the variable but the second one does. I change the number in increments of 1, so when I click the button its currentNumber += 1 and I run a console.log after it to see if it changes. The first time I click it, it prints the default value, and the second time that I click it is when it actually changes, and it's messing up the intended functionality of my code. I'm using React for this.
constructor(props) {
super(props);
this.state = {
currentSize: parseInt(this.props.size),
min: 4,
max: 40,
}
};
increaseSize(){
this.setState({currentSize: this.state.currentSize + 1}, function(){
if(this.state.currentSize >= this.state.max){
this.state.currentSize = this.state.max;
this.state.isRed = true;
} else if(this.state.currentSize < this.state.max){
this.state.isRed = false;
}
});
console.log(this.state.currentSize);
};
render() {
var isBold = this.state.bold ? 'normal' : 'bold';
var currentSize = this.state.currentSize;
var textColor = this.state.isRed ? 'red' : 'black';
return(
<div>
<button id="decreaseButton" hidden='true' onClick={this.decreaseSize.bind(this)}>-</button>
<span id="fontSizeSpan" hidden='true' style={{color: textColor}}>{currentSize}</span>
<button id="increaseButton" hidden='true' onClick={this.increaseSize.bind(this)}>+</button>
<span id="textSpan" style={{fontWeight: isBold, fontSize: currentSize}} onClick={this.showElements.bind(this)}>{this.props.text}</span>
</div>
);
}
The number in the variable is then displayed but the one being displayed has a different value to the one inside the variable
As you can see in the picture, the number displayed is 26 but in the variable its 25.
Additionally, you can see that I set a min and max value for the counter. When it reaches either value, it goes 1 further in the display, but not in the console. So in the display it stops at 3 and 41 but in the console it stops at 4 and 40.
What am I doing wrong?
edit: the default value is 16, and that's whats printed to the console the first time I click the button, which is why its not working properly.
Use the functional version of setState() in order to get a hold of prev state values -- because React handles state changes asynchronously, you can't guarantee their values when you set them; This is also the cause where you are using the console.log. Instead go for something like:
increaseSize(){
const aboveMax = this.state.currentSize >= this.state.max;
this.setState( prevState => ({
currentSize: aboveMax ? prevState.max : prevState.currentSize + 1,
isRed: aboveMax
})
, () => console.log(this.state.currentSize) );
};
Or, move the console statement to the render() method if you don't want to the setState() callback function.
See https://reactjs.org/docs/react-component.html#setstate
Don't forget to set isRed in your constructor as well :)

Categories