React: why state is updated only at handleChage of input? - javascript

I have this Component:
export const DashboardPage = () => {
const [mounted, setMounted] = useState(false);
const [currentNumber, setCurrentNumber] = useState(null);
const [insertedNumber, setInsertedNumber] = useState(null);
const [result, setResult] = useState(null);
const [arrayNumbers, setArrayNumbers] = useState([]);
useEffect(() => {
const rnd = randomNumber();
if (!mounted) {
setCurrentNumber(rnd);
setMounted(true);
}
}, [mounted]);
const handleChange = (e) => {
setInsertedNumber(e.target.value);
}
const handleClick = (e) => {
e.preventDefault();
arrayNumbers.push(insertedNumber);
setArrayNumbers(arrayNumbers);
setResult("Oops, ritenta! era " + currentNumber)
if (parseInt(insertedNumber) === parseInt(currentNumber)) {
setResult("EVVIVA hai vinto!");
}
}
const handleOnSubmit = (e) => {
e.preventDefault();
}
return (
<>
<h2>Storico tentativi: {arrayNumbers.map(e => e)}</h2>
<h2>La soluzione è: {currentNumber}</h2>
<Form onSubmit={handleOnSubmit}>
<Form.Group controlId="formGTN">
<Form.Label>Guess the number:</Form.Label>
<Form.Control type="text" onChange={handleChange} />
</Form.Group>
<Button variant="primary" type="submit" onClick={handleClick} label="Is it?" />
</Form>
<h1>{result}</h1>
</>
)
}
export default DashboardPage;
It is a simple "Guess The Number" play.
I have the issue that:
On very first loading of page, I can get into arrayNumbers the only one number I inserted.
From second and other attempts, when I CHANGE the value inside the input, so when I delete previous number, I get state update and I have the previous (and on) numbers. Like if "state" is "back" of one try.
I can see this behavious also in Chrome's Redux plugin. State is update ONLY a step other.
I could move the push into array on onChange, but... If I insert a number of 2 digits I get 2 insert on array (44 will be "4" and "4").
THank you!

You should never mutate the state and/or update the state without setState. In JavaScript, the Array.push() method mutates the original array. So, in your example, you mutate arrayNumbers but you should add a new element to the array without updating the original one.
Also, listening to the onSubmit event is enough, you don't have to add the onClick event to your submit button. You can move everything from handleClick to handleOnSubmit.
export const DashboardPage = () => {
const [mounted, setMounted] = useState(false);
const [currentNumber, setCurrentNumber] = useState(null);
const [insertedNumber, setInsertedNumber] = useState(null);
const [result, setResult] = useState(null);
const [arrayNumbers, setArrayNumbers] = useState([]);
useEffect(() => {
const rnd = randomNumber();
if (!mounted) {
setCurrentNumber(rnd);
setMounted(true);
}
}, [mounted]);
const handleChange = (e) => {
setInsertedNumber(e.target.value);
};
const handleOnSubmit = (e) => {
e.preventDefault();
setArrayNumbers([...arrayNumbers, insertedNumber]);
if (parseInt(insertedNumber, 10) === parseInt(currentNumber, 10)) {
setResult('EVVIVA hai vinto!');
} else {
setResult('Oops, ritenta! era ' + currentNumber);
}
};
return (
<>
<h2>Storico tentativi: {arrayNumbers.map((e) => e)}</h2>
<h2>La soluzione è: {currentNumber}</h2>
<Form onSubmit={handleOnSubmit}>
<Form.Group controlId="formGTN">
<Form.Label>Guess the number:</Form.Label>
<Form.Control type="text" onChange={handleChange} />
</Form.Group>
<Button variant="primary" type="submit" label="Is it?" />
</Form>
<h1>{result}</h1>
</>
);
};

For the above component, you might try these changes in the handleClick function:
const handleClick = (e) => {
e.preventDefault();
let newArr = [...arrayNumbers];
newArr.push(insertedNumber);
setArrayNumbers(newArr);
setResult('Oops, ritenta! era ' + currentNumber);
if (parseInt(insertedNumber) === parseInt(currentNumber)) {
setResult('EVVIVA hai vinto!');
}
};

Related

Can't update text area value

I'm trying to learn typescript, currently creating a note taking app. It's very simple: when you click on adding a new note, you a get an empty textarea, where you can edit your note. I'm able to add notes, but I can't update the value of each textarea. What am I doing wrong?
Here's what I have so far:
const [showSidePanel, setShowSidePanel] = React.useState<boolean>(false);
const [notes, setNotes] = React.useState([{text: '', id: nanoid()}]);
const [noteText, setNoteText] = React.useState<string>('');
const addNote = (): void => {
const newNote = {text: 'hey', id: nanoid()};
setNotes([...notes, newNote])
}
const handleChange = (event: ChangeEvent<HTMLTextAreaElement>) => {
setNoteText(event.target.value)
}
const toggleSidePanel = React.useCallback(() => setShowSidePanel(!showSidePanel), [showSidePanel]);
const wrapperRef = React.useRef<HTMLDivElement>(null);
useClickOutside(wrapperRef, () => setShowSidePanel(false));
return (
<div ref={wrapperRef}>
<GlobalStyle />
<SidePanel showSidePanel={showSidePanel}>
<Button onClick={addNote}>Add note</Button>
{notes.map((n) =>
<Note onChange={() => handleChange} text={noteText} key={n.id}/>
)}
</SidePanel>
<ToggleButton onClick={toggleSidePanel}>Open</ToggleButton>
</div>
);
}
If I understand it correctly, each Note is a text-area and you want to update each one of them independently and noteText state is used for active text-area component, correct?
If that's the case then we can either (1)remove noteText state and update notes array directly for appropriate notes.id or (2)we can preserve noteText state and update the notes array after debouncing.
Latter solution is preferable and coded below:
// a hook for debouncing
export const useDebouncedValue = (value, timeOut=500) => {
const [debouncedValue, setDebouncedValue] = useState(null);
useEffect(() => {
let someTimeout;
someTimeout = setTimeout(() => {
setDebouncedValue(value);
}, timeOut);
return () => clearInterval(someTimeout);
}, [value, timeOut]);
return {
debouncedValue,
};
};
Main logic for updating the notes array logic
const Comp = () => {
const [showSidePanel, setShowSidePanel] = React.useState(false);
const [notes, setNotes] = React.useState([{ text: "", id: nanoid() }]);
const [currNoteText, setCurrNoteText] = React.useState({
text: "",
id: "",
});
// this always holds the debounced value
const updatedNoteText = useDebouncedValue(currNoteText.text, 600);
// effect will get triggered whenever the currNoteText.text is changed and pause of 600ms is taken
useEffect(() => {
const notesIndex = notes.findIndex((ele) => ele.id === currNoteText.id);
if (notesIndex >= 0) {
const updatedNotes = _.cloneDeep(notes);
updatedNotes[notesIndex].text = updatedNoteText;
// updation of notes array
setNotes(updatedNotes);
}
}, [updatedNoteText]);
const addNote = () => {
const newNote = { text: "hey", id: nanoid() };
setNotes([...notes, newNote]);
};
const handleChange = (event, noteId) => {
// setting current changed note in currNoteText Object
setCurrNoteText({ id: noteId, text: event.target.value });
};
const toggleSidePanel = React.useCallback(
() => setShowSidePanel(!showSidePanel),
[showSidePanel]
);
const wrapperRef = React.useRef(null);
useClickOutside(wrapperRef, () => setShowSidePanel(false));
return (
<div ref={wrapperRef}>
<GlobalStyle />
<SidePanel showSidePanel={showSidePanel}>
<Button onClick={addNote}>Add note</Button>
{notes.map((n) => (
<Note
// here along with event we are also passing note-id
onChange={(e) => handleChange(e, n.id)}
text={noteText}
key={n.id}
/>
))}
</SidePanel>
<ToggleButton onClick={toggleSidePanel}>Open</ToggleButton>
</div>
);
};

React, how do I track that the user has stopped typing in the input?

I have such a functional component. When a user enters input, I send a message to the server and output to other users what someone writes. Here is the code:
const ChatInput = (props) => {
const [message, setMessage] = useState('');
const typingMessage = () =>{
socket.emit('typing',props.username);
}
return (
<div className>
<Input
value = {message}
onChange = {
(e) => typingMessage(e.target.value)
}
placeholder="Type a message here"
/>
<Button
onClick={sendMessage}
icon={<SendOutlined />
}/>
</div>
);
};
How do I track that the user has stopped writing? If he does not enter anything into the input for more than 10 seconds?
You need deboune function, that will count 10sec (it may be different time, depends on you) after last input (onChange trigger)
function debounce(func, timeout = 10000){
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => { func.apply(this, args); }, timeout);
};
}
function saveInput(){
console.log('User has stopped writting 10 sec ago');
}
const processChange = debounce(() => saveInput());
const typingMessage = () =>{
socket.emit('typing',props.username);
processChange()
}
You can use a combination of onFocus, onBlur, and onMouseLeave, in such a way that when onFocus happens, he is writing, when onBlur happens, he is not writing anymore, and when onMouseLeave happens or when he clicks on send, you trigger onBlur yourself. Like below:
I assumed that your Input component can forward ref. If not make it so by following Forwarding Refs.
const ChatInput = (props) => {
const [message, setMessage] = useState("");
const inputRef = useRef();
const typingMessage = () => {
socket.emit("typing", props.username);
};
const notTypingMessage = () => {
socket.emit("typing", "");
};
return (
<div className>
<Input
ref={inputRef}
value={message}
onChange={(e) => setMessage(e.target.value)}
onFocus={() => typingMessage()}
onBlur={() => notTypingMessage()}
onMouseLeave={() => inputRef?.current.blur()}
placeholder="Type a message here"
/>
<Button onClick={()=>{sendMessage(); inputRef?.current.blur()}} icon={<SendOutlined />} />
</div>
);
};
export default ChatInput;
This is a one solution waiting for a second after finish typing and emits stoppedTyping event.
you may still want to optimize it according to the your applications needs
const emit = (action, data) => {
console.log(action, data)
}
function App() {
const props = { username: 'me' }
const [message, setMessage] = useState('');
const [isTyping, setIsTyping] = useState(false);
useEffect(() => {
if (message && !isTyping) {
emit('stoppedTyping', props.username)
}
}, [isTyping])
const typingMessage = (v) => {
emit('typing', props.username);
setMessage(v)
}
const sendMessage = (e) => {
console.log(e)
}
return (
<div className=''>
<input
value={message}
onKeyDown={() => !isTyping && setIsTyping(true)}
onKeyUp={() => {
setTimeout(() => {
setIsTyping((t) => t ? false : t)
}, 1000)
}}
onChange={
(e) => typingMessage(e.target.value)
}
placeholder="Type a message here"
/>
<button
onClick={sendMessage}
>send</button>
<pre>{
JSON.stringify({ message, isTyping })
}</pre>
</div>
);
}

Refresh problem with onChange event handler

I have an input with an onChange event handler that is refreshing the screen every time I try to type some character inside. I would like to know if any of you guys could give me a hand to solve it.
Here is my code:
const HeaderUser: React.FC<TabWrapper> = () => {
const [drawerEmpreendimentosVisible, setDrawerEmpreendimentosVisible] = useState(false);
const intl = useIntl().formatMessage;
const [currentEmpreendimento] = useLocalStorage().createState('currentEmpreendimento');
const [displayRoadmapScreen, setDisplayRoadmapScreen] = useState(false)
const [displayRegisterScreen, setDisplayRegisterScreen] = useState(false);
const [inspector, setInspector] = useState('');
const [roadmap, setRoadmap] = useState('');
const RegisterScreen = () => {
const handleOk = () => {
setDisplayRegisterScreen(false);
};
const handleCancel = () => {
setDisplayRegisterScreen(false);
};
return (
<div>
<Modal
title="Cadastrar novo roteiro"
visible={displayRegisterScreen}
onOk={handleOk}
onCancel={handleCancel}>
<b>Roteiro:</b>
<br />
//Refreshing issue <Input type="text" placeholder="Roteiro" onChange={e =>
setRoadmap(e.target.value)} /><br />
<b>Inspetor:</b>
<br />
<Input type="text" name="inspector" placeholder="Inspetor" onChange={(e) => console.log(e.target.value)} />
</Modal>
</div>
)
};
Could it be because you have defined your state outside the RegisterScreen component?
Try defining it within the component.
const RegisterScreen = () => {
const [roadmap, setRoadmap] = useState('');
//rest of code...

Local storage not updating React

I'm btrying to save an array of objects in local storage, each time a user clicks a button, i add the username and email fron input fields
but it keeps updating the local storage instead of appending new object to the array
Below is my code
const app = () => {
const [allusers,setAllusers] = useState([JSON.parse(localStorage.getItem('users')) || '']);
const [id,setId] = useState(0);
const [newuser,setNewuser] = useState({
'id':id
'name':'David',
'email':'david#gmail.com'
})
const handleChange = () =>{
setNewuser({...newuser,[e.target.name] : e.target.value});
}
const add = ()=>{
setAllusers([newuser])
localStorage.setItem('users',JSON.stringify(allusers))
setID(id+1); // increase id by 1
}
return(
<div>
<form>
<input type="text" name="user" onChange={handleChange}>
<input type="text" name="email" onChange={handleChange}>
<button onclick={()=>save}>Save</button>
</form>
</div>
)
}
export default app;
There were a lot of syntactical errors and use of functions like save which was never declared and still used.
I rewrote the whole example and made it a bit modular so that you can comprehend it better.
Here is the working example:
Final Output:
Full Source code:
import React, { useState, useEffect } from "react";
import "./style.css";
const App = () => {
const [allusers, setAllusers] = useState([]);
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const handleName = e => {
setName(e.target.value);
};
const handleEmail = e => {
setEmail(e.target.value);
};
const save = e => {
e.preventDefault();
let newUsers = {
id: Math.floor(Math.random() * 100000),
name: name,
email: email
};
localStorage.setItem("users", JSON.stringify([...allusers, newUsers]));
setAllusers(allusers.concat(newUsers));
console.log("Localstorage:", JSON.parse(localStorage.getItem("users")));
};
useEffect(() => {
console.log("Localstorage:", JSON.parse(localStorage.getItem("users")));
if (localStorage.getItem("users")) {
setAllusers(JSON.parse(localStorage.getItem("users")));
}
}, []);
return (
<div>
<form>
<input type="text" name="user" onChange={handleName} />
<input type="text" name="email" onChange={handleEmail} />
<button onClick={save}>Save</button>
<p>{JSON.stringify(allusers)}</p>
</form>
</div>
);
};
export default App;
As You inquired in the comment section, here is how you can implement the Update functionality:
Final Output:
Full source code:
import React, { useState, useEffect } from "react";
import "./style.css";
const App = () => {
const [allusers, setAllusers] = useState([]);
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [id, setId] = useState(null);
const handleName = e => {
setName(e.target.value);
};
const handleEmail = e => {
setEmail(e.target.value);
};
const save = e => {
e.preventDefault();
let newUsers = {
id: Math.floor(Math.random() * 100000),
name: name,
email: email
};
localStorage.setItem("users", JSON.stringify([...allusers, newUsers]));
setAllusers(allusers.concat(newUsers));
console.log("Localstorage:", JSON.parse(localStorage.getItem("users")));
};
const setForUpdate = user => {
setName(user.name);
setEmail(user.email);
setId(user.id);
};
const update = e => {
e.preventDefault();
let modifiedData = allusers.map(user => {
if (user.id === id) {
return { ...user, name: name, email: email };
}
return user;
});
setAllusers(modifiedData);
localStorage.setItem("users", JSON.stringify(modifiedData));
setId(null);
};
useEffect(() => {
console.log("Localstorage:", JSON.parse(localStorage.getItem("users")));
if (localStorage.getItem("users")) {
setAllusers(JSON.parse(localStorage.getItem("users")));
}
}, []);
return (
<div>
<form>
<input value={name} type="text" name="user" onChange={handleName} />
<input value={email} type="text" name="email" onChange={handleEmail} />
<button disabled={!(id == null)} onClick={save}>
Save
</button>
<button disabled={id == null} onClick={update}>
Update
</button>
</form>
{allusers &&
allusers.map(user => (
<div className="userInfo">
<p>{user.name}</p>
<p>{user.email}</p>
<button onClick={() => setForUpdate(user)}>
select for update
</button>
</div>
))}
</div>
);
};
export default App;
You can find the working example here: Stackblitz
You are trying to save allusers to the localStorage right after setAllUsers() but setState is asynchronous. The value does not have to be updated on the next line. You can read more about it at reactjs.org, Why is setState giving me the wrong value?.
I would recommend to use useEffect.
const add=()=> {
setAllusers([... allusers ,newuser])
}
useEffect(()=>{
// this is called only if the variable `allusers` changes
// because I've specified it in second argument of useEffect
localStorage.setItem('users',JSON.stringify(allusers))
}, [allusers]);
()=>handleChange is a function that takes no arguments and returns the handleChange function. You probably want () => handleChange(), which would take no arguments and INVOKE handleChange.
you are adding only one new user while clicking on add button. You need to copy previous data also when setting all users.
Second thing setting state is async and hence your localStorage and allusers may have different value and to avoid this one you need to use useEffect to set the value.
const add = ()=>{
setAllusers([...allusers ,newuser])
setID(id+1); // increase id by 1
}
useEffect(() => {
localStorage.setItem('users',JSON.stringify(allusers))
},[allusers])

React Checkbox to trigger regex expression

Attaching my code - built using create-react-app.
Working on a small component for searching inputs. Highlight word if match and continue to display other text if not matched.
Current solution displays mark on all text once called.
import React, { useState } from "react";
const app = (props) => {
const [value, setValue] = useState("");
const [searchValue, setSearchValue] = useState("");
const [checked, setChecked] = useState(false);
const [sensitive, setSensitive] = useState("i");
let highlight = null;
const handleChange = (event) => {
setValue(event.target.value);
};
console.log(value);
const handleSearchChange = (event) => {
setSearchValue(event.target.value);
};
console.log(searchValue);
const getHighlightedText = (value, searchValue) => {
let regex = new RegExp(`(${searchValue})`, `g${sensitive}`);
console.log(regex);
const parts = value.split(regex);
//console.log(parts);
highlight = <span> { parts.map((part, i) =>
<span key={i} style={part === searchValue ? { backgroundColor: 'Yellow' } : {} }>
{ part }
</span>)
} </span>;
}
const checkedTest = () => {
if(checked === true) {
setSensitive(" ") // makes it case sensitive
console.log(sensitive);
setChecked(true);
} else {
setSensitive("i")
console.log(sensitive);
setChecked(false);
}
}
return (
<>
<form className="text-search-form">
<textarea className="source-text" value={value} onChange={handleChange}/>
<input className="search-term" value={searchValue} onChange={handleSearchChange} onKeyPress={getHighlightedText(value, searchValue)} />
<label htmlFor="caseSensitive">case sensitive?
<input
type="checkbox"
className="case-sensitive"
name="caseSensitive"
defaultChecked={checked}
onClick={getHighlightedText(value, searchValue)}
onChange={checkedTest}
/>
</label>
</form>
<div className="result">{highlight}</div>
</>
);
};
export default Highlighter;
Adjusted the code to display my proper component name.
This should work as expected
Removed all manual invocation of getHighlightedText and instead moved it to JSX and removed its parameters (May not be a good idea). The reason is that the values getHighlightedText depends on are all in the function state and change of any would trigger a rerender which would automatically call this function.
Secondly, fixed your checkedTest, this is more cleaner.
Thirdly, the comparison you were doing in the iteration of parts wasn't case insensitive, so even if your regex was case insensitive, === of string isn't so, moved it to a seperate function and handled both cases. The insensitive case was handled by converting both part and searchvalue to a common case, in this case, lowercase.
As always, this can be improved so much more, but I think it solves your issues.
Demo:
codesandbox
import React, { useState } from "react";
const App = props => {
const [value, setValue] = useState("");
const [searchValue, setSearchValue] = useState("");
const [checked, setChecked] = useState(false);
const [sensitive, setSensitive] = useState("i");
const handleChange = event => {
setValue(event.target.value);
};
const handleSearchChange = event => {
setSearchValue(event.target.value);
};
const getHighlightColor = part => {
let isEqual = false;
if (checked) {
isEqual = part === searchValue;
} else {
isEqual = part.toLowerCase() === searchValue.toLowerCase();
}
return isEqual ? "Yellow" : "transperant";
};
const getHighlightedText = () => {
let regex = new RegExp(`(${searchValue})`, `g${sensitive}`);
const parts = value.split(regex);
if (searchValue)
return (
<span>
{parts.map((part, i) => (
<span key={i} style={{ backgroundColor: getHighlightColor(part) }}>
{part}
</span>
))}
</span>
);
};
const checkedTest = event => {
setChecked(event.target.checked);
setSensitive(event.target.checked ? "" : "i");
};
return (
<>
<form className="text-search-form">
<textarea
className="source-text"
value={value}
onChange={handleChange}
/>
<input
className="search-term"
value={searchValue}
onChange={handleSearchChange}
/>
<label htmlFor="caseSensitive">
case sensitive?
<input
type="checkbox"
className="case-sensitive"
name="caseSensitive"
defaultChecked={checked}
onChange={checkedTest}
/>
</label>
</form>
<div className="result">{getHighlightedText()}</div>
</>
);
};
export default App;
If you change following component and callback, it should work.
1) onChange callback, get the user selected option (true/false)
2) In the callback, call the search function.
Hope this helps.
<input
type="checkbox"
className="case-sensitive"
name="caseSensitive"
defaultChecked={checked}
onChange={event => checkedTest(event.target.checked)}
/>;
const checkedTest = isChecked => {
setChecked(isChecked);
setSensitive(isChecked ? " " : "i");
// call the getHighlightedText
getHighlightedText(value, searchValue);
};

Categories

Resources