React setState in If Statement - javascript

I'm making a chess game using React and I'm trying to write a function that computes the squares that should be highlighted when a certain chess piece is selected. I have the game as one component and each chess piece as a second component, and the button in the chess component calls a function from the game component when clicked.
I want to highlight the appropriate squares if and only if the selected piece is the correct color, so I had a setState call in an if statement in my handleClick() function. After reading Issue with setState() in if statement I moved the conditional so that the handleClick function is only linked to pieces of the correct color.
The issue now is that the state gets changed as desired, but for some reason the components don't rerender to reflect that change. Can someone please let me know how I could fix this? Here's my code for handling clicks:
handleClick(num){
this.setState(prevState => {
return {
gameBoard: prevState.gameBoard,
turn: prevState.turn,
selected: num
}
})
}
and here's my code for creating the board:
<div>
{
this.state.gameBoard.map((object)=>
<div className = "board-row"> {object.map((object2) =>
<Piece key={object2.at} turn = {this.state.turn} selected = {object2.selected} piece = {object2.piece} identifier = {object2.at} onClick = {() => this.handleClick(object2.at)} color = {(object2.at+Math.floor(object2.at/8))%2 === 0?"white":"black"} />)}
</div>
)
}
</div>

try:
handleClick(num){
this.setState(prevState => {
return {
...prevState,
selected: num
};
})
}

Ths issue here is mostly a question of dependencies. Your rendering is based on the gameboard field of the state, which isn't being modified. The component therefore doesn't rerender since it doesn't know that there are additional dependencies (selected) for it.
The easiest and cleanest approach I would suggest is to simply move your map function inside a useCallback (or useMemo, both should work) with the proper dependencies, then simply use it in your rendering method.
const foobar = useCallback( () => this.state.gameBoard.map((object)=>
<div className = "board-row"> {object.map((object2) =>
<Piece key={object2.at} turn = {this.state.turn} selected = {object2.selected} piece = {object2.piece} identifier = {object2.at} onClick = {() => this.handleClick(object2.at)} color = {(object2.at+Math.floor(object2.at/8))%2 === 0?"white":"black"} />)}
</div>
)
, [this.state] )
render (
<div>
{foobar()}
</div>
)

Related

React component doesn't re-render on first prop-change

I am new to React and I am trying to build a hangman game.
At the moment I am using a hardcoded list of words that the program can choose from. So far everything worked great, but now I am trying to reset the game and the react component that should rerender upon one click only re-renders after two clicks on the reset button and I don't know why
these are the states that I am using :
function App() {
const [numberInList, setNumberInList] = useState(0)
const randomWordsList = ["comfort", "calm", "relax", "coffee", "cozy"];
const [generatedWord, setGeneratedWord] = useState(
randomWordsList[numberInList]
);
const [generatedWordLetters, setGeneratedWordLetters] = useState(
randomWordsList[numberInList].split("").map((letter) => {
return { letter: letter.toUpperCase(), matched: false };
})
);
function resetGame(){
setNumberInList(prev => prev + 1)
setGeneratedWord(randomWordsList[numberInList])
setGeneratedWordLetters(
generatedWord.split("").map((letter) => {
return { letter: letter.toUpperCase(), matched: false };
})
);
setFalseTries(0)
}
this is the reset function I am using
within teh function every state gets updated correctly apart from the generatedWordLetters state, which only gets updated upon clicking the reset button two times.
I can't seem to solve this problem on my own, so any help is appreciated!
Please check useEffect on React. You can use boolean flag as state, put the useEffect parameters like below
React.useEffect(() => {
// here your code works
},[flag])
flag is your boolean state when it changes on reset function, your component re render

cannot change usestate to change the state of the components

This is code of my AddBlog Component
import React, { useState } from "react";
function AddBlogs() {
const[state, setState] = useState({blogs:[]})
const AddAnother = () => {
return (
<div>
<label>Topic</label>
<input></input>
<button onClick={addblog}>Add another topic</button>
</div>
);
}
const addblog = () =>{
setState({
blogs:[...state.blogs,<AddAnother></AddAnother>]
});
console.log("Hello");
}
return (
<div>
<form>
<div>
<label>Topic</label>
<input></input>
<button onClick={addblog}>Add another topic</button>
</div>
<div>
{state.blogs}
</div>
</form>
</div>
);
}
export default AddBlogs;
When I click that Add another topic button AddAnother components blinks for just 0.5 second or less. Any solution for this?
I see a couple things that will cause problems. First, when you update state, you shouldn't use the current state. Instead, you should use the setState that accepts a function with the old state as the first parameter, such as the following:
const addblog = () => {
setState((oldState) => {
return { blogs: [...oldState.blogs, <AddAnother />] };
});
console.log("Hello");
};
This won't solve your problem, though. The issue you're seeing is due to you not having a key in your array of components. So try this:
const addblog = () => {
setState((oldState) => {
return { blogs: [...oldState.blogs, <AddAnother key={oldState.blogs.length} />] };
});
console.log("Hello");
};
As David pointed out, the form is also posting, which is causing part of the problem as well.
Because the <form> is posting. If you don't need this to be an actual <form> then remove that element entirely. But if for some reason you want to keep that element, change the button type(s):
<button type="button" onClick={addblog}>Add another topic</button>
By default a <button> is of type submit unless otherwise specified.
Edit: Additionally, as answered here, you need to change the way you're setting state to use the callback overload:
setState((oldState) => {
return { blogs: [...oldState.blogs, <AddAnother key={oldState.blogs.length} />] };
});
In most cases it makes little difference whether you set the state directly or use the callback which sets based on previous state. The latter is often used when queueing up multiple state updates in a loop, for example. But you have an edge case here.
Your AddAnother component internally references a "stale version" of addblog which always references the original state. Changing to the callback version of setState gets around this.
Other ways around this would be to restructure your code to remove the circular dependency, refactor components into their own discrete code, make use of tools like useCallback to define functions which have dependencies on state values, etc.

setState in functional component don't keep value

I have a functional component that keeps a state. This state I try to manipulate using an onClick event in an SVG. The SVG is in another component and has the addAndRemoveSelectedCabin method passed to it via props. I loop through the elements in an useEffect and add an eventListener. This doesn't work. The useEffect with the selectedCabins dependency logs the new number only. It seems the state returns to the initial state after every stateChange.
This is the state and method in the parent component.
const [selectedCabins, setSelectedCabins] = useState([0]);
const addRemoveSelectedCabin = id => {
const newArr = [...selectedCabins, id];
setSelectedCabins(newArr);
}
useEffect(() => {
console.log(selectedCabins);
}, [selectedCabins])
This is how I call the method. [UPDATE]
useEffect(() =>
{
const cabins = document.querySelectorAll(".cabin");
cabins.forEach(cabin =>
{
const id = cabin.getAttributeNS(null, "id").substring(1, 5);
const found = cabinsData.find(el => el.id === id)
if (found && found.status === "available")
{
cabin.classList.add("green")
cabin.addEventListener('click', () => addRemoveSelectedCabin(id));
} else if (found && found.status === "booked")
{
cabin.classList.add("gray")
}
})
}, [])
Console:
[0]
(2) [0, "1105"]
(2) [0, "1101"]
This works if I put the onClick directly in the SVG element. Does anyone know why this is?
<rect
id="C1105"
x="749.4"
y="58.3"
className="cabin"
width="36.4"
height="19.9"
onClick={() => addRemoveSelectedCabin(1105)}
>
<title>1105</title>
</rect>
As I said in my comment, you are binding addRemoveSelectedCabin in the first render. useEffect is only executed once since you pass an empty dependency list. addRemoveSelectedCabin closes over selectedCabins which at that point in time has the value [0].
Why am I seeing stale props or state inside my function? from the React documentation has more information about this.
The solution in your case is simple: Pass a function to the setter to get the "current" state value. Don't reference the state value in the component:
const addRemoveSelectedCabin = id => {
setSelectedCabins(selectedCabins => [...selectedCabins, id]);
}
Having said that, this is still an odd thing to do in React world. You should reevaluate your assumptions that make you think you have to do it that way.
It's not all the elements that should have a click listener.
Depending on how you actually render the elements, that's easy to do. JSX/React is just JavaScript. Whether you have a condition that adds the event handler or not or whether you have a condition that sets onClick or not is basically the same.
But without a more complete example there is not much we can suggest.

How to change button text in ReactJS

How to change the button text when someone click on?
Code:
<Button disabled={this.state.disabled}
type="primary"
htmlType="submit"
style={{
background: '#ff9700',
fontWeight: 'bold',
border: 'none',
float: 'center',
}}
loading={this.state.loading}
onClick={this.enterLoading}
value="Next"
id="buttontext"
onClick="changeText()"
>
Next
</Button>
Mayank is correct.
Create a variable called "text" (or whatever you choose) and put that instead of "Next".
state = {
text: "Next"
}
changeText = (text) => {
this.setState({ text });
}
render() {
const { text } = this.state //destucture state
return (
<Button
onClick={ () => { this.changeText("newtext")} }> {text} </Button> )...etc
Note: this method will always change the text to "newtext" when you click. You can pass a variable there as well to make it more dynamic.
Hope this helps.
Update: Just saw Mayank comment. That code is essentially what I have. Just a tip you no longer need a constructor and you don't have to bind your methods anymore.
Updated: React Hooks
Same thing but with the useState hook. Instead of calling the state variable text, I am using buttonText to be more explicit. The updated version would look like:
import { useState } from 'React';
const [buttonText, setButtonText] = useState("Next"); //same as creating your state variable where "Next" is the default value for buttonText and setButtonText is the setter function for your state variable instead of setState
const changeText = (text) => setButtonText(text);
return (
<Button onClick={() => changeText("newText")}>{buttonText}</Button>
)
You can omit the changeText function all together and have this:
return (
<Button onClick={() => setButtonText("newText")}>{buttonText}</Button>
)
Updated: How to Add Set Timeout
Adding an update to answer a question in the comments: "If I wanted to use a setTimout to bring the button back to the previous text after 1 second where would I add that in?"
There are two ways that comes to mind: add the setTimeout to the changeText function or create an effect that depends on the buttonText.
change text
You can just pop the setTimeout right in this function.
Goes from this
const changeText = (text) => setButtonText(text);
to this
const initialState = "Next";
const [buttonText, setButtonText] = useState(initialState); //same as creating your state variable where "Next" is the default value for buttonText and setButtonText is the setter function for your state variable instead of setState
const changeText = (text) => {
setButtonText(text);
setTimeout(() => setButtonText(initialState), [1000])
}
We add the initialState variable as a const to keep track of the "previous text". Since, it should never change we could define it in all caps snake case like const INITIAL_STATE meh your choice.
useEffect
We still need to define that initialState variable, again so we can keep track of the original. Then we can create a useEffect which is a React hook that allows you to "hook" into changes of a variable (that's only a part of useEffect, just enough to get us going here).
We can break the effect down into two essential parts: the body or callback of the effect, what do we want to do when the effect runs and the dependency or what triggers the effect to run. In this case, our callback will be setTimeout and set the button text inside that timeout and our buttonText will trigger the effect.
Here's the effect:
useEffect(() => {
if(buttonText !== initialState){
setTimeout(() => setButtonText(initialState), [1000])
}
}, [buttonText])
Anytime the state variable buttonText changes this effect will run.
We start at
buttonText = initialState // "Next"
the effect runs and checks the if. Since buttonText equals the initialState the conditions evaluates to false and we terminate the callback and the effect.
When the user clicks the button, changeText executes and sets the buttonText state which updates the variable triggering the effect. Now we run that if check again and this time it passes so we execute the setTimeout.
Inside the timeout we are setting state so the effect runs again and this time it fails because we just changed the state back to initialState.
I recommend throwing a debugger in there or some logs to follow the trail
Long winded explanation. Here's what the whole component would look like using the effect approach.
import React, { useState, useEffect } from "react";
const FancyButton = () => {
const initialState = "Next";
const [buttonText, setButtonText] = useState("Next"); //same as creating your state variable where "Next" is the default value for buttonText and setButtonText is the setter function for your state variable instead of setState
// the effect
useEffect(() => {
if(buttonText !== initialState){
setTimeout(() => setButtonText(initialState), [1000])
}
}, [buttonText])
const changeText = (text) => setButtonText(text);
return (
<button type="button" onClick={() => changeText("newText")}>{buttonText}</button>
)
};
I added the type on the button because that's a good practice. And changed "Button" to "button". You can certainly have any component there you want, this works better for copying and pasting
Where you wrote "Next", the button text, do this instead:
{this.state.disabled ? 'Disabled...' : 'Next'}
I this will display "Disabled..." when the state.disabled == true, and 'Next' when state.disabled == false.

Rendering an array of html elements

I want to render an array of html elements in my component. The reason for storing the data/html in an array is because I want to be able to dynamically load a new element depending on a button-click.
This is how I want to display my array:
<div>
{this.state.steps}
</div>
This is how I initiate my component and array:
componentDidMount() {
this.createProcessStep().then(step => {
this.setState({steps: this.state.steps.concat(step)});
});
}
export function createProcessStep() {
this.setState({processStepCounter: this.state.processStepCounter += 1});
return this.addStepToArray().then(d => {
return this.reallyCreateProcessStep()
});
}
addStepToArray = () => {
const step = {
...Some variables...
};
return new Promise(resolve => {
this.setState({
stepsData: this.state.stepsData.concat(step)
}, resolve)
});
};
"stepsData" is another array that holds data (variables) belonging to each step. "steps" on the other hand, should only hold the html.
This is how one step/element looks like:
<div>
...Some Content...
<button label="+" onClick={ () => {
this.createProcessStep().then(step => {
this.setState({
steps: this.state.steps.concat(step)
});
})
}}/>
...other content...
</div>
This button within each step is responsible for loading/adding yet another step to the array, which actually works. My component displays each step properly, however react doesn't properly render changes to the element/step, which is
to say that, whenever e.g. I change a value of an input field, react doesn't render those changes. So I can actually click on the "+"-button that will render the new html element but whenever a change to this element occurs,
react simply ignores the phenotype of said change. Keeping in mind that the changeHandlers for those steps/elements still work. I can change inputfields, radioButtons, checkboxes etc. which will do exactly what it's
supposed to, however the "re-rendering" (or whatever it is) doesn't work.
Any ideas of what I'm doing wrong here? Thanks!
While you could certainly beat your approach into working, I would advise that you take more common react approach.
You make your components to correctly display themselves from the state . ie as many steps are in the state, your component will display. Than make your add button add necessary information (information, not formated html) to the state.
Here is an example how to use component N times:
const MyRepeatedlyOccuringComponent = (n) => (<p key={n}>There goes Camel {n}</p>)
const App = () => {
const camels = [1,22,333,4444,55555]
const caravan = camels.map((n) => MyRepeatedlyOccuringComponent(n))
return(<div>{caravan}</div>
}

Categories