React state variable changes unpredictably - reactjs

I'm trying to write a front-end using React for the first time.
I stumbled upon a problem trying to give users the option to order a list of React components and later undo the ordering.
So what I tried to do is save the initial list order in a separate state variable skills_initial. This is done by cloning the skills list. If the ordering gets undone, the skills_initial state variable is used to reset it to the initial order.
class Day extends React.Component {
constructor(props) {
super(props);
var skills = [];
var skill;
var xp;
for (var i in skill_names) {
skill = skill_names[i];
xp = this.props.end[skill] - this.props.start[skill];
skills.push(<Skill name={skill} xp={xp} key={skill}/>);
}
var skills_clone = [];
for (var k=0; k < skills.length; k++) {
skills_clone.push(skills[k]);
}
this.state = {skills: skills, skills_initial: skills_clone, descend: false, ascend: false};
this.descend = this.descend.bind(this);
this.ascend = this.ascend.bind(this);
}
descend() {
document.getElementById('ascend').checked = false;
if (this.state.descend) {
document.getElementById('descend').checked = false;
this.setState(prevState => ({
skills: prevState.skills_initial,
descend: false,
ascend: false
}));
} else {
this.setState(prevState => ({
skills: prevState.skills.sort(
function(skill1, skill2) {
return skill2.props.xp - skill1.props.xp;
}),
descend: true,
ascend: false
}));
}
}
ascend() {
document.getElementById('descend').checked = false;
if (this.state.ascend) {
document.getElementById('ascend').checked = false;
this.setState(prevState => ({
skills: prevState.skills_initial,
ascend: false,
descend: false,
}));
} else {
this.setState(prevState => ({
skills: prevState.skills.sort(
function(skill1, skill2) {
return skill1.props.xp - skill2.props.xp;
}),
ascend: true,
descend: false
}));
}
}
render() {
return (
<ol id="skill_list_vertical" style={{listStyleType: "none"}}>
<input id="descend" type="radio" onClick={this.descend}/>descend
<input id="ascend" type="radio" onClick={this.ascend}/>ascend
{this.state.skills}
</ol>
);
}
}
var skill_names = [
"attack",
"defence",
"strength",
"constitution",
"ranged",
"prayer",
"magic",
"cooking",
"woodcutting",
"fletching",
"fishing",
"firemaking",
"crafting",
"smithing",
"mining",
"herblore",
"agility",
"thieving",
"slayer",
"farming",
"runecrafting",
"hunting",
"construction",
"clue_easy",
"clue_medium",
"bounty_rogue",
"bounty_hunter",
"clue_hard",
"LMS",
"clue_elite",
"clue_master"
];
So the first two times the radio button is checked, the list is correctly ordered. Only upon trying to undo the ordering the second time, the list remains ordered. In no place the state variable skills_initial is changed unless in the constructor which I thought to be only called once.
Any further advice on my code structure / react habits is greatly appreciated.

Related

How to not allow the user to not add the same language?

What I'm trying to do is to not allow the user to add two same languages.
The user has the possibility to click the button, that generates the input fields, and puts the language.
export default class AddLanguage extends React.Component {
constructor(props) {
super(props);
this.state = {
language: [],
};
}
handleClick() {
var language = this.state.language;
this.setState({
language: language,
});
}
handleLanguageChanged(i, event) {
var language= this.state.language;
language[i] = event.target.value;
this.setState({
language: language,
});
sessionStorage.setItem('items', JSON.stringify({language}));
}
handleLanguageDeleted(i) {
var language= this.state.language;
language.splice(i, 1);
this.setState({
language: language,
});
}
componentDidMount() {
this.userItems = JSON.parse(sessionStorage.getItem('items'));
if (sessionStorage.getItem('items')){
this.setState({
items: this.userItems.items,
})
}else {
this.setState({
items: [],
})
}
}
renderRows() {
var context = this;
How to not allow the user to not add the same language?
Assuming the new language entered by the user is processed using handleLanguageChanged and that it is added/updated at index i (with value being sourced from event.target.value) the below may be helpful to achieve the desired objective:
handleLanguageChanged(i, event) {
// hold the event.target.value for future reference
const updLang = event.target.value;
// use setState with a function
this.setState(prev => {
const currList = prev.language;
// check if currList has updLang except at location "i"
const isDupe = currList.some(
(lang, idx) => (idx !== i && lang.toLowerCase() === updLang)
);
// if it is dupe, simply return "prev" (no actual update)
if (isDupe) return prev;
else {
// update the currList & use it to set the new state
currList[i] = updLang;
return {...prev, language: currList};
}
});
};
Also, kindly reconsider if the below function is necessary as-is:
handleClick() {
var language = this.state.language;
this.setState({
language: language,
});
}

componentDidMount function in React

I am designing a quiz in React, and I am having one final problem with my code.
There are 7 questions in the MongoDB Database which I am pulling data from. The idea is, every time the "Next" button on my quiz is clicked, the next question should load after the user has answered the previous question. Currently, only the first question loads, and I am having trouble loading up the remaining questions. I have a componentDidMount function which I guess would be responsible for loading information from the MongoDB database.
componentDidMount() {
axios.get('/challenge', {
params: {
questionID: "Phish01",
//"Phish0" + this.state.currentQuestion;
},
})
For every question in the database, I have added a field named "questionID" which identifies the question. Since there are 7 questions, each question has an ID of Phish01, Phish02....Phish07.
Obviously looking at the code, only the first question loads up, which is fine. However, I am unsure as to the best method in loading up the remaining questions. I was thinking of using some form of concatenation as seen in the commented out piece of code above, but don't think that's the correct way of going about it.
I also was considering making a new function as seen below, but similar to before, I think I am doing it wrong.
/*setNextQuestion() {
axios.get('/challenge', {
params: {
questionID: "Phish0" + this.state.currentQuestion;
},
})
//questionID: "Phish0" + 1;
if (this.state.currentQuestion !== prevState.currentQuestion) {
this.setState(() => {
return{
disabled: true,
questionID: questionID,
}
});
}}*/
I thought I would also add this code sample in too:
constructor(props) {
super(props);
this.state = {
currentQuestion: 0,
userInput: null, // user has not answered a question yet at the start
points: 0,
options: [],
disabled: true, // Next button is disabled by default
optionEnabled: true, //options should be enabled by default
};
If anyone is able to help or point me towards a resource that would help me in understanding my issue, that would be great!
Thanks!
Added in the rest of the code file:
Class Challenge extends Component {
constructor(props) {
super(props);
this.state = {
currentQuestion: 0,
userInput: null, // user has not answered a question yet at the start
points: 0,
// options : ["Hello"],
options: [],
isLoaded: false,
quizzes: {},
disabled: true, // Next button is disabled by default
optionEnabled: true, //options should be enabled by default
intervalIsSet: false,
gameOver: false,
};
}
// this function below should be responsible for loading the data from the MongoDB database
componentDidMount() {
axios.get('/challenge', {
params: {
questionID: "Phish01",
//"Phish0" + this.state.currentQuestion;
},
})
.then((response) => {
const result = response.data;
console.log(response.data);
this.setState({
isLoaded: true,
quizzes: result,
options: result.options,
answer: result.answer,
answerDetails: result.answerDetails,
wrongAnswer: result.wrongAnswer,
});
}).catch((error) => {
this.setState({
isLoaded: true,
error,
});
});
}
nextQuestion = () => {
const { chosenAnswer, answer, points } = this.state;
alert(this.state.wrongAnswer);
alert(this.state.answerDetails);
if (chosenAnswer === answer) {
this.setState({
points: this.state.points + 1
});
}
this.setState({
currentQuestion: this.state.currentQuestion + 1
});
console.log(this.state.currentQuestion);
};
//this function should be responsible for loading the remaining questions
/*setNextQuestion() {
axios.get('/challenge', {
params: {
questionID: "Phish0" + this.state.currentQuestion;
},
})
//questionID: "Phish0" + 1;
if (this.state.currentQuestion !== prevState.currentQuestion) {
this.setState(() => {
return{
disabled: true,
questionID: questionID,
}
});
}
}*/
lastQuestion = () => {
if (this.state.currentQuestion === 7) { //array starts at 0
this.setState({
gameOver: true
});
}
};
//when user clicks on an option, the "Next" button should then be enabled
checkAnswer(chosenAnswer) {
if (chosenAnswer == this.state.answer) {
alert('You have chosen the correct answer!');
alert('Click Next to continue');
this.setState({
disabled: false,
optionEnabled: false,
});
//alert(this.state.answerDetails);
} else {
alert('You have selected the wrong answer');
alert('Click Next to continue');
this.setState({
disabled: false,
optionEnabled: false,
});
}
}
render() {
const {options, userInput, currentQuestion, gameOver} = this.state;
if (gameOver) {
return (
<div className="result">
<h3>The quiz is over. Final score is {this.state.points} points</h3>
</div>
);
} else {
return (
<div className="quizChallenge">
<h2>{this.state.quizzes.description} </h2>
<table id="t01">
<tbody>
<tr>
{this.state.options.map((option) => {
return (
<td key={option}>
<button
disabled={!this.state.optionEnabled}
onClick={() => this.checkAnswer(this.state.options.indexOf(option))}>
{option}
</button></td>
);
})}
</tr>
</tbody>
</table>
{currentQuestion < 8 &&
<button
className="ui inverted button"
disabled={this.state.disabled}
onClick={this.nextQuestion}
>Next
</button>
}
{currentQuestion === 7 &&
<button
className="ui inverted button"
disabled={this.state.disabled}
onClick={this.lastQuestion}
>Finish Challenge!
</button>
}
</div>
);
}
}
}
export default Challenge;

React checkboxes. State is late when toggling the checkboxes

I have a group of 3 checkboxes and the main checkbox for checking those 3 checkboxes.
When I select all 3 checkboxes I want for main checkbox to become checked.
When I check those 3 checkboxes nothing happens but when I then uncheck one of those trees the main checkbox becomes checked.
Can someone explain to me what actually is happening behind the scenes and help me somehow to solve this mystery of React state? Thanks!
Here is a code snnipet:
state = {
data: [
{ checked: false, id: 1 },
{ checked: false, id: 2 },
{ checked: false, id: 3 }
],
main: false,
}
onCheckboxChange = id => {
const data = [...this.state.data];
data.forEach(item => {
if (item.id === id) {
item.checked = !item.checked;
}
})
const everyCheckBoxIsTrue = checkbox.every(item => item === true);
this.setState({ data: data, main: everyCheckBoxIsTrue });
}
onMainCheckBoxChange = () => {
let data = [...this.state.data];
data.forEach(item => {
!this.state.main ? item.checked = true : item.checked = false
})
this.setState({
this.state.main: !this.state.main,
this.state.data: data,
});
}
render () {
const checkbox = this.state.data.map(item => (
<input
type="checkbox"
checked={item.checked}
onChange={() => this.onCheckboxChange(item.id)}
/>
))
}
return (
<input type="checkbox" name="main" checked={this.state.main} onChange={this.onMainCheckBoxChange} />
{checkbox}
)
I can't make a working code snippet based on the code you provided, one of the issues was:
const everyCheckBoxIsTrue = checkbox.every(item => item === true);
where checkbox is not defined.
However, I think you confused about using the old state vs the new state, it'd be simpler to differentiate if you name it clearly, e.g.:
eventHandler() {
const { data } = this.state; // old state
const newData = data.map(each => ...); // new object, soon-to-be new state
this.setState({ data }); // update state
}
Here's a working example for your reference:
class App extends React.Component {
state = {
data: [
{ checked: false, id: 1 },
{ checked: false, id: 2 },
{ checked: false, id: 3 }
],
main: false,
}
onCheckboxChange(id) {
const { data } = this.state;
const newData = data.map(each => {
if (each.id === id) {
// Toggle the previous checked value
return Object.assign({}, each, { checked: !each.checked });
}
return each;
});
this.setState({
data: newData,
// Check if every checked box is checked
main: newData.every(item => item.checked === true),
});
}
onMainCheckBoxChange() {
const { main, data } = this.state;
// Toggle the previous main value
const newValue = !main;
this.setState({
data: data.map(each => Object.assign({}, each, { checked: newValue })),
main: newValue,
});
}
render () {
const { data, main } = this.state;
return (
<div>
<label>Main</label>
<input
type="checkbox"
name="main"
// TODO this should be automatically checked instead of assigning to the state
checked={main}
onChange={() => this.onMainCheckBoxChange()}
/>
{
data.map(item => (
<div>
<label>{item.id}</label>
<input
type="checkbox"
checked={item.checked}
onChange={() => this.onCheckboxChange(item.id)}
/>
</div>
))
}
</div>
);
}
}
ReactDOM.render(
<App />
, document.querySelector('#app'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="app"></div>
Side note: You might want to consider not to use the main state
You shouldn't be storing state.main to determine whether every checkbox is checked.
You are already storing state that determines if all checkboxes are checked, because all checkboxes must be checked if every object in state.data has checked: true.
You can simply render the main checkbox like this:
<input
type="checkbox"
name="main"
checked={this.state.data.every(v => v.checked)}
onChange={this.onMainCheckBoxChange}
/>;
The line this.state.data.every(v => v.checked) will return true if all of the checkboxes are checked.
And when the main checkbox is toggled, the function can look like this:
onMainCheckBoxChange = () => {
this.setState(prev => {
// If all are checked, then we want to uncheck all checkboxes
if (this.state.data.every(v => v.checked)) {
return {
data: prev.data.map(v => ({ ...v, checked: false })),
};
}
// Else some checkboxes must be unchecked, so we check them all
return {
data: prev.data.map(v => ({ ...v, checked: true })),
};
});
};
It is good practice to only store state that you NEED to store. Any state that can be calculated from other state (for example, "are all checkboxes checked?") should be calculated inside the render function. See here where it says:
What Shouldn’t Go in State? ... Computed data: Don't worry about precomputing values based on state — it's easier to ensure that your UI is consistent if you do all computation within render(). For example, if you have an array of list items in state and you want to render the count as a string, simply render this.state.listItems.length + ' list items' in your render() method rather than storing it on state.

How to Add filter into a todolist application in Reactjs with using .filter

im new to react, trying to make an todolist website, i have the add and delete and displaying functionality done, just trying to add an search function, but i cant seem to get it working, where as it doesn't filter properly.
i basically want to be able to filter the values on the todos.title with the search value. such as if i enter an value of "ta" it should show the todo item of "take out the trash" or any item that matches with that string.
when i try to search, it gives random outputs of items from the filtered, i am wondering if my filtering is wrong or if i am not like displaying it correctly.
ive tried to pass the value into todo.js and display it there but didn't seem that was a viable way as it it should stay within App.js.
class App extends Component {
state = {
todos: [
{
id: uuid.v4(),
title: "take out the trash",
completed: false
},
{
id: uuid.v4(),
title: "Dinner with wife",
completed: true
},
{
id: uuid.v4(),
title: "Meeting with Boss",
completed: false
}
],
filtered: []
};
// checking complete on the state
markComplete = id => {
this.setState({
todos: this.state.filtered.map(todo => {
if (todo.id === id) {
todo.completed = !todo.completed;
}
return todo;
})
});
};
//delete the item
delTodo = id => {
this.setState({
filtered: [...this.state.filtered.filter(filtered => filtered.id !== id)]
});
};
//Add item to the list
addTodo = title => {
const newTodo = {
id: uuid.v4(),
title,
comepleted: false
};
this.setState({ filtered: [...this.state.filtered, newTodo] });
};
// my attempt to do search filter on the value recieved from the search field (search):
search = (search) => {
let currentTodos = [];
let newList = [];
if (search !== "") {
currentTodos = this.state.todos;
newList = currentTodos.filter( todo => {
const lc = todo.title.toLowerCase();
const filter = search.toLowerCase();
return lc.includes(filter);
});
} else {
newList = this.state.todos;
}
this.setState({
filtered: newList
});
console.log(search);
};
componentDidMount() {
this.setState({
filtered: this.state.todos
});
}
componentWillReceiveProps(nextProps) {
this.setState({
filtered: nextProps.todos
});
}
render() {
return (
<div className="App">
<div className="container">
<Header search={this.search} />
<AddTodo addTodo={this.addTodo} />
<Todos
todos={this.state.filtered}
markComplete={this.markComplete}
delTodo={this.delTodo}
/>
</div>
</div>
);
}
}
export default App;
search value comes from the header where the value is passed through as a props. i've checked that and it works fine.
Todos.js
class Todos extends Component {
state = {
searchResults: null
}
render() {
return (
this.props.todos.map((todo) => {
return <TodoItem key={todo.id} todo = {todo}
markComplete={this.props.markComplete}
delTodo={this.props.delTodo}
/>
})
);
}
}
TodoItem.js is just the component that displays the item.
I not sure if this is enough to understand the issue 100%, i can add more if needed.
Thank you
Not sure what is wrong with your script. Looks to me it works fine when I am trying to reconstruct by using most of your logic. Please check working demo here: https://codesandbox.io/s/q9jy17p47j
Just my guess, it could be there is something wrong with your <TodoItem/> component which makes it not rendered correctly. Maybe you could try to use a primitive element such as <li> instead custom element like <TodoItem/>. The problem could be your logic of markComplete() things ( if it is doing hiding element works ).
Please let me know if I am missing something. Thanks.

SetState not working with spread operator

I have a language switcher in a React native app, it has the selected language ticked, which is stored in state. However this is not working, any advice appreciated. The code is as below. The selectedLanguage is being populated so it knows what needs updating but it's not updating the state object.
constructor(props) {
super(props);
this.state = {
languages: [
{
language: 'Dutch',
selected: false,
},
{
language: 'English',
selected: true,
},
{
language: 'French',
selected: false,
},
{
language: 'German',
selected: false,
},
{
language: 'Polish',
selected: false,
}
],
};
};
changeLanguage(selectedLanguage){
this.state.languages.map((language) => {
if(language.language === selectedLanguage){
this.setState(prevState => ([
...prevState.languages, {
language: selectedLanguage,
selected: true,
}
]));
}
});
}
The spread operator isn’t going to deep compare objects in an array. You’ve got to either move from a languages array to a languages object, as in:
languages: {
Dutch: false,
English: true,
...
}
Or, you need to copy and replace:
changeLanguage(selectedLanguage){
this.state.languages.map((language, index) => {
if(language.language === selectedLanguage){
this.setState(prevState => {
const newLangs = [...prevState.languages];
newLangs[index].selected = true;
return newLangs;
});
}
});
}
First need to find out the right object for given selectedLanguage from an array.
Use Array#map to traverse and update the matched object selected prop value with boolean true and rest to false.
const {languages} = this.state;
const updatedLang = languages.map((lang, key) => {
if (lang.language == selectedLanguage) {
lang.selected = true;
} else {
lang.selected = false;
}
return lang;
})
Now do setState.
this.setState(prevState => ({languages: updatedLang}));

Resources