React useState, setState and {state} in return - javascript

I come across the rendering issue with React State.
The problem is that {state} in return get value one beat late.
But the console log in handleChange shows right value.
If the previous value of state is 9, current value of state value is 10 then the console.log({state}) in handleChange shows 10 and the <span>{state}<span> in return shows 9.
It looks different from other state async problem.
I can't understand why this happened.
const [findText, setFindText] = useState("");
const [findCount, setFindCount] = useState(0);
const handleChange = (e) => {
let str = e.target.value;
setFindText(str);
let cnt = 0;
doxDocument.map((docx) => {
cnt += docx.src.split(findText).length - 1;
});
setFindCount(cnt);
console.log({findCount})
};
return(
<div>
<input
type="text"
value={findText}
onChange={handleChange}
/>
<span>{findCount} found <span>
</div>
);

Two problems...
findText will not have been updated to the new value when you use it in split(). Either use str instead or calculate findCount in a memo or effect hook with a dependency on findText.
You're completely misusing filter(). Use reduce() to calculate a computed sum
const [findText, setFindText] = useState("");
const findCount = useMemo(
() =>
findText
? doxDocument.reduce(
(sum, { src }) => sum + src.split(findText).length - 1,
0
)
: 0,
[findText, doxDocument] // may not need doxDocument
);
return (
<div id="App">
<input
type="text"
value={findText}
onChange={(e) => setFindText(e.target.value)}
/>
<span>{findCount} found</span>
</div>
);

Related

React Hool re-render loop test Condition on state

I want to update and re-render for loop test condition on hook state update. After I update state hook the state is rerendered but I want to the loop to be rerendered too. I make a form. The length of the form depends on what number I enter on input.
Here is what I tried:
const [formLength, setFormLength] = useState(0);
let eachForm = () => {
for (let x = 0; x < formLength; x++) {
return <div>{x+1} inpute</div>;
}
};
useEffect(() => {
eachForm();
}, [formLength]);
{formLength <= 0 ? (<>Zeroo</>) : (<><eachForm /></>)}
There is no need for the useEffect hook here, it's not doing anything for you. You can render directly the JSX from the formLength state. eachForm also isn't a valid React component, so it can't be used like <eachForm />. The for-loop will also return during the first iteration and not return an array of div elements like I suspect you are expecting.
Example:
const [formLength, setFormLength] = useState(10);
return (
<>
{formLength
? Array.from({ length: formLength }).map((_, i) => (
<div key={i}>{i+1} input</div>
))
: <>Zero</>
</>
);
When the state changes your component is rerendered so there is no need for useEffect. Try something like this:
const [formFields, setFormFields] = useState([]);
return (
<div className='App'>
<input type='button' onClick={()=>
setFormFields([...formFields,<p key={formFields.length}>{formFields.length}</p>])}
value="+"/>
<form>
{formFields}
</form>
</div>
);

Delete last item from array with useState

I'm new to JS, React and TypeScript. I did a tutorial to add a todo-list. To get some practice, I decided to add the delete button and the "remove last item" button.
Deleting the complete list worked out fine (I'm proud of myself, hah!) but the "delete last item" is not working, I tried different things (e.g. just todos.pop()).
function App() {
const [todos, setTodos] = useState([])
const [input, setInput] = useState("")
// prevents default, adds input to "todos" array and removes the input from form
const addTodo = (e) => {
e.preventDefault()
setTodos([...todos, input])
setInput("")
}
// deletes the todo-list
const clearTodo = (e) => {
e.preventDefault()
setTodos([])
}
// deletes the last entry from the todo-list
const clearLastTodo = (e) => {
e.preventDefault()
setTodos(todos.pop())
}
return (
<div className="App">
<h1>ToDo Liste</h1>
<form>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
type="text"
/>
<button type="submit" onClick={addTodo}>
Hinzufügen
</button>
</form>
<div>
<h2>Bisherige ToDo Liste:</h2>
<ul>
{todos.map((todo) => (
<li>{todo}</li>
))}
</ul>
</div>
<div>
<form action="submit">
<button type="submit" onClick={clearLastTodo}>
Letzten Eintrag löschen
</button>
<button type="submit" onClick={clearTodo}>
Liste löschen
</button>
</form>
</div>
</div>
);
}
export default App;
Am I missing something (clearly I am, otherwise it would work)? But what? :D
Thank you in advance!
There are 3 possible solutions to your problem.
Solution 1:
Slice the array from the first element to the -1 (1 before the last) element.
setTodos(todos.slice(0, -1)));
Or
setTodos((previousArr) => (previousArr.slice(0, -1)));
Solution 2:
Create a copy array and splice it with the value of -1. Then set the array from the first element to the -1 element.
const copyArr = [...todos];
copyArr.splice(-1);
setTodos(copyArr);
Solution 3:
Create a copy list with the ..., pop the copy and set the new value of the array to the copy.
const copyArr = [...todos];
copyArr.pop();
setTodos(copyArr);
You could do it as below. The spread operator makes sure you don't give the same reference to setTodos:
const clearLastTodo = (e) => {
e.preventDefault();
let copy = [...todos]; // makes sure you don't give the same reference to setTodos
copy.pop()
setTodos(copy);
}
A note about state in React:
Always treat state variables as if they were immutable. It's obvious for for primitive values like Number, Boolean... But for Objects and Arrays changing their content doesn't make them different, it should a new memory reference.
There are couple way to remove the last item from an array in javascript
const arr = [1, 2, 3, 4, 5]
arr.splice(-1) // -> arr = [1,2,3,4]
// or
arr.pop() // -> arr = [1,2,3,4]
// or
const [last, ...rest] = arr.reverse()
const removedLast = rest.reverse()
Following the guide updating objects and arrays in state
const onRemoveLastTodo = () => {
// Try with each way above
setTodos(todos.splice(-1))
}

Handle onChange on input type=number [duplicate]

This question already has answers here:
HTML input type="number" still returning a string when accessed from javascript
(9 answers)
Closed 1 year ago.
The default state is a number, adding and subtracting from the default state works as expected. When the input value is changed by typing a number in the input field the state changes to a string. What's wrong here?
const [value, setValue] = useState(1);
const handleChange = e => {
setValue(e.target.value)
}
const handleAdd = () => {
setValue(value+1)
}
const handleSubtract = () => {
setValue(value-1)
}
return (
<input type='number' value={value} onChange={handleChange} />
<button label='+' onClick={handleAdd} />
<button label='-' onClick={handleSubtract} />
)
Based on this:
The purpose of the number type is that mobile browsers use this for showing the right keyboards and some browsers use this for validation purposes.
To fix this, there is some approaches. But I suggest to you to parseInt the value when onChange. So:
const [value, setValue] = useState(1);
const handleChange = (e) => {
setValue(parseInt(e.target.value));
};
const handleAdd = () => {
setValue((prev) => prev + 1);
};
const handleSubtract = () => {
setValue((prev) => prev - 1);
};
return (
<div>
<input type="number" value={value} onChange={handleChange} />
<button label="+" onClick={handleAdd} />
<button label="-" onClick={handleSubtract} />
</div>
);
What's wrong here?
e.target.value is a string, even if you type a number, you will be given a string in your event
setValue(e.target.value)
Your state now has a string inside it
setValue((prev) => prev - 1);
Your add and subtract methods now are working with a string and a number, you are now victim of coercion
https://www.freecodecamp.org/news/js-type-coercion-explained-27ba3d9a2839/
'2' + 1 = 21, so after your state becomes a string, your code won't work as you intended
You need to use parseInt as written in another answer, or parse in any way you want, but always make sure your state has the type you intended
const handleChange = e => {
setValue(Number(e.target.value) || 0)
}
That is another way, note that when parsing a string that cannot become a number you get NaN, the above code will make it become 0 instead, but you can handle it your way

Why setState doesn't stack like the one before?

I have a problem. I try to convert my string dynamically, but for some reason, it only converts the first letter of my initial string. What can I do to solve this?
Ex:
input: Mike
String_1 = 'Mike'
String_2 = 13 (I want it to be 139115, 13 for M, 9 for I, 11 for k and 5 for e).
This is my code:
import './App.css';
import React, {useState} from 'react';
import emojies from './emojies';
function App() {
let [String_1, setString_1] = useState( '' );
let [String_2, setString_2] = useState( '' );
let [Index, setIndex] = useState();
return (
<div>
<div>
<input
type="text"
value={String_1}
placeholder="Enter a message"
onChange={e => {
const val = e.target.value;
setString_1(val);
setString_2(val.toLowerCase().charCodeAt(0)-96);
}
}
/>
</div>
<div>
<p>{String_1}</p>
<p>{String_2}</p>
</div>
</div>
);
}
export default App;
This has nothing to do with React or setState.
Your issue is the logic around generating that String_2.
As you can see from the below snippet, your val.toLowerCase().charCodeAt(0)-96 only returns 13, so setState is acting correctly by passing on that value.
const val = 'Mike'
const String_2 = val.toLowerCase().charCodeAt(0) - 96
console.log(String_2)
const correct = parseInt(val.toLowerCase().split('').map(x => x.charCodeAt() - 96).join(''))
console.log(correct)
The new logic splits the string up into chars, maps over each of them to create a list of chars, then joins them together and converts into a int.
You make a mistake on setting the value for String_2.
Try this.
setString_2(val.split("").map(c => {
return c.toLowerCase().charCodeAt(0) - 96;
}).join(""));
Pass your string value from input box to this function. It iterates over all the alphabets from the string and convert them to their idx + 1 and join everything.
const convertStringToCharIdxString = (str) => {
const calcCharIdx = (char) => char.charCodeAt(0) - 97 + 1; // + 1, because you are considering a as 1
return str.split('').map(calcCharIdx).join('');
}

React: Transitions are cut after element re-render further in the elements array

While creating a swap list in React, I encountered a strange transition bug. The way the list works is simple: you click on an element (A), its background transitions to a lighter color, then on another (B), they swap places, and element A, now on a new position, transitions back to the original color.
At least that's what's happens when element A has a higher index then element B. When it's the other way round, the transition after the swap is cut.
I managed to find a workaround using window.requestAnimationFrame, but it's not perfect. The state of the transition isn't preserved, which means that it transitions back always from the full light color. It doesn't matter that much here, but it's an issue in my other project. Sometimes the transitions are also cut anyway.
The code is simple. The thing to note is that elements preserve their keys after the swap. I created a code sandbox for you to play around.
import React, { useState } from "react";
const Item = props => {
let classes = "item";
if (props.selected) {
classes += " selected";
}
return (
<div className={classes} onClick={props.onClick}>
{props.value}
</div>
);
};
const App = () => {
const [list, setList] = useState([1, 2, 3]);
const [selected, setSelected] = useState(-1);
const select = index => {
setSelected(index);
};
const swap = index => {
const newList = [...list];
[newList[index], newList[selected]] = [newList[selected], newList[index]];
setList(newList);
setSelected(-1);
};
// The workaround that kind of works, but is not perfect.
// const swap = index => {
// const newList = [...list];
// [newList[index], newList[selected]] = [newList[selected], newList[index]];
// setList(newList);
// window.requestAnimationFrame(() => {
// setSelected(index);
// window.requestAnimationFrame(() => {
// setSelected(-1);
// });
// });
// };
const onClick = selected < 0 ? select : swap;
const items = list.map((value, index) => (
<Item
key={value}
value={value}
selected={selected === index}
onClick={onClick.bind(this, index)}
/>
));
return <div className="list">{items}</div>;
}
Here are the key css rules:
.item {
background: #0b7189; // darker
transition: background-color 1s;
}
.item.selected {
background-color: #228cdb; //lighter
}
I'm looking for a solution that is more reliable than my workaround.
All help will be appreciated! :)
I've made three changes in your code, so upon swapping the highlighted box remains the same one, then turns off:
Added the line setSelected(index); inside the swap function.
Added a setTimeout in order to delay the colour change.
Changed the key inside the map loop to the index value, as it must be unique and therefore it's just a better practice to use the index.
function App() {
const [list, setList] = useState([1, 2, 3]);
const [selected, setSelected] = useState(-1);
const select = index => {
setSelected(index);
};
const swap = index => {
const newList = [...list];
[newList[index], newList[selected]] = [newList[selected], newList[index]];
setList(newList);
// This was added in order to keep the highlight after swap.
// Note that it takes a second to take place
setSelected(index);
// And this was added in order to allow the colour to lighten,
// before returning to the original colour
setTimeout(() => setSelected(-1), 1000);
};
const onClick = selected < 0 ? select : swap;
const items = list.map((value, index) => (
<Item
key={index}
value={value}
selected={selected === index}
onClick={onClick.bind(this, index)}
/>
));
return <div className="list">{items}</div>;
}

Categories

Resources