Can't update state in React via key event - javascript

I'm making a calculator with React, when I click the divs they trigger a function to update the state which goes into the display output of the calculator, I have no problem at all doing this with clicking the divs. This is the code that is working as expected:
const Calculator = () => {
const initialState = {
val: "0",
}
const [value, setValue] = useState(initialState);
const sendCharacter = (number) =>{
let str = number.toString();
if (value.val != "0") {
const conc = value.val.concat(str);
setValue({
val: conc
});
} else {
setValue({
val: str
});
}
};
return (
<div id='calculator'>
<div id='display'>{value.val}</div>
<div id='one' className='numbers keys' onClick={() => sendCharacter(1)}>1</div>
</div>
);
Now, I want to do the same but instead of clicking div id="one" to add "ones" to my state I want those ones added to the state when I press the key 1. I added an eventsListener via useEffect() that loads only once after initial render. When I click 1 it calls the exact same function with the same exact argument that is called when clicking which is sendCharacter(1), here is my code:
useEffect(() => {
console.log("useEffect");
document.addEventListener("keyup", detectKey, true);
}, []);
const detectKey = (e) => {
if (e.key == "1") {
sendCharacter(1)
}
};
However when I press the i key I'm only able to update the state only the first time, if I try to add the second digit to my number it acts as if my actual state was "0" instead of "1" which doesn't allow it to enter the if statement.
To be clear and short: If if click my div id="one" I can enter the first condition of my if statement and update val via my conc variable, if i do it pressing the key i it always enters the else statement.
if (value.val != "0") {
const conc = value.val.concat(str);
setValue({
val: conc
});
} else {
setValue({
val: str
});
}
I tried logging messages everywhere and to see if my argument "1" is the same via clicking or via pressing my key, they send the exact same value, I'm totally lost in here and frustrated because I've got more than 1 hour trying to figure out what's going on. Also, I checked to see if the component was rendering before pressing the key, which was not the case.
BTW sorry for my English, not my first language, if someone can help me and needs clarification on one part I will answer asap, thanks in advance.

It seems you are falling into closures trap. Try updating your sendCharacter code to following.
const sendCharacter = (number) =>{
let str = number.toString();
setValue(value => {
if (value.val != "0") {
const conc = value.val.concat(str);
return {
val: conc
};
} else {
return {
val: str
};
}
});
};

Related

Remove item from localStorage after onClick and don't show again anymore after browser refresh in React app

In my React app I am showing a banner yes or no, based on React state and some values set in localStorage.
After close button is clicked, it's state showBanner is saved to localStorage and doesn't show the banner anymore
After 2 times using a page url in the React app with query param redirect=my-site it doesn't show the banner anymore:
import queryString from 'query-string';
const location = useLocation();
const queryParams = queryString.parse(location.search);
const [showBanner, setShowBanner] = useState(true);
const handleClick = () => {
setShowBanner(false);
localStorage.removeItem('redirect');
};
const hasQp = queryString
.stringify(queryParams)
.includes('redirect=my-site');
const initialCount = () => {
if (typeof window !== 'undefined' && hasQp) {
return Number(localStorage.getItem('redirect')) || 0;
}
return null;
};
const [count, setCount] = useState(initialCount);
const show = showBanner && hasQp && count! < 3;
useEffect(() => {
const data = localStorage.getItem('my-banner');
if (data !== null) {
setShowBanner(JSON.parse(data));
}
}, []);
useEffect(() => {
localStorage.setItem('my-banner', JSON.stringify(showBanner));
}, [showBanner]);
useEffect(() => {
let pageView = count;
if (pageView === 0) {
pageView = 1;
} else {
pageView = Number(pageView) + 1;
}
if (hasQp && showBanner === true) {
localStorage.setItem('redirect', String(pageView));
setCount(pageView);
}
}, []);
This is working fine (when you see some good code improvements let me know :) ).
But as soon the user clicks the close button I don't want the localStorage item redirect no longer appears. Now after refreshing the page it appears again.
How do i get this to work?
If this is executing when the page loads:
localStorage.setItem('redirect', String(pageView));
Then that means this is true:
if (hasQp && showBanner === true)
The hasQp value is true, which means this is true:
const hasQp = queryString
.stringify(queryParams)
.includes('redirect=my-site');
And showBanner is true because it's always initialized to true:
const [showBanner, setShowBanner] = useState(true);
It's not 100% clear to me why you need this state value, but you could try initializing it to false by default:
const [showBanner, setShowBanner] = useState(false);
But more to the point, I don't think you need this state value at all. It's basically a duplicate of data that's in localStorage. But since both state and localStorage are always available to you, I don't see a reason to duplicate data between them.
Remove that state value entirely and just use localStorage. An example of checking the value directly might be:
if (hasQp && JSON.parse(localStorage.getItem('my-banner')))
Which of course could be refactored to reduce code. For example, consider a helper function to get the value:
const showBanner = () => {
const data = localStorage.getItem('my-banner') ?? false;
if (data) {
return JSON.parse(data);
}
};
Then the check could be:
if (hasQp && showBanner())
There are likely a variety of ways to refactor the code, but overall the point is to not duplicate data. In thie case a value is being stored in localStorage instead of React state because it needs to persist across page loads. Just keep that value in localStorage and use it from there.

React: Handling focus using array of useRef

I have created a basic Todo app but having a hard time in implementing proper focus into it. Following are the requirements for focus:
Focus should be on the newly created input field when "Add" is clicked so the user can begin typing right away.
On deleting an item "Button X", focus will move to the input field in the row which replaced the deleted row, nowhere if there are no fields left, or on the new last row if the last row was deleted.
On moving an item up, focus should be placed on the newly moved field, and all associated buttons should move alongside the element. If a field is already at the top of a list, no reordering should occur, but focus should be transferred to the topmost field nonetheless.
On moving an item down, same principle should apply here (focus should be placed on the field that is moved down). If its the last field, focus it nonetheless.
Here is my implementation.
App.js:
import React, { useState, useRef } from "https://cdn.skypack.dev/react#17.0.1";
import ReactDOM from "https://cdn.skypack.dev/react-dom#17.0.1";
const App = () => {
const [myRows, setMyRows] = useState([]);
const focusInput = useRef([]);
const onAddRow = () => {
setMyRows((prevRows) => {
return [
...prevRows,
{ id: prevRows.length, text: "", up: "↑", down: "↓", delete: "X" },
];
});
focusInput.current[myRows.length - 1].focus();
};
const onMoveUp = (index) => (event) => {
const currentState = [...myRows];
if (index !== 0) {
const prevObject = currentState[index - 1];
const nextObject = currentState[index];
currentState[index - 1] = nextObject;
currentState[index] = prevObject;
setMyRows(currentState);
}
};
const onMoveDown = (index) => (event) => {
const currentState = [...myRows];
if (index !== myRows.length - 1) {
const currObject = currentState[index];
const nextObject = currentState[index + 1];
currentState[index] = nextObject;
currentState[index + 1] = currObject;
setMyRows(currentState);
}
};
const onDelete = (index) => (event) => {
const currentState = [...myRows];
currentState.splice(index, 1);
setMyRows(currentState);
};
const onTextUpdate = (id) => (event) => {
setMyRows((prevState) => {
const data = [...prevState];
data[id] = {
...data[id],
text: event.target.value,
};
return data;
});
};
return (
<div className="container">
<button onClick={onAddRow}>Add</button>
<br />
{myRows?.map((row, index) => {
return (
<div key={row.id}>
<input
ref={(el) => (focusInput.current[index] = el)}
onChange={onTextUpdate(index)}
value={row.text}
type="text"></input>
<button onClick={onMoveUp(index)}>{row.up}</button>
<button onClick={onMoveDown(index)}>{row.down}</button>
<button onClick={onDelete(index)}>{row.delete}</button>
</div>
);
})}
</div>
);
}
ReactDOM.render(<App />,
document.getElementById("root"))
In my implementation when I click Add, focus changes but in unexpected way (First click doesn't do anything and subsequent click moves the focus but with the lag, i.e. focus should be on the next item, but it is at the prior item.
Would really appreciate if someone can assist me in this. Thanks!
The strange behaviour that you observe is caused by the fact that state updates are asynchronous. When you, in onAddRow, do this:
focusInput.current[myRows.length - 1].focus()
then myRows.length - 1 is still the last index of the previous set of rows, corresponding to the penultimate row of what you're actually seeing. This explains exactly the behaviour you're describing - the new focus is always "one behind" where it should be, if all you're doing is adding rows.
Given that description, you might think you could fix this by just replacing myRows.length - 1 with myRows.length in the above statement. But it isn't so simple. Doing this will work even less well, because at the point this code runs, right when the Add button is clicked, focusInput hasn't yet been adjusted to the new length, and nor in fact has the new row even been rendered in the DOM yet. That all happens a little bit later (although appears instantaneous to the human eye), after React has realised there has been a state change and done its thing.
Given that you are manipulating the focus in a number of different ways as described in your requirements, I believe the easiest way to fix this is to make the index you want to focus its own piece of state. That makes it quite easy to manage focus in any way you want, just by calling the appropriate state-updating function.
This is implemented in the code below, which I got working by testing it out on your Codepen link. I've tried to make it a snippet here on Stack Overflow, but for some reason couldn't get it to run without errors, despite including React and enabling Babel to transform the JSX - but if you paste the below into the JS of your Codepen, I think you'll find it working to your satisfaction. (Or, if I've misinterpreted some requirements, hopefully it gets you at least a lot closer than you were.)
Rather than just leaving you to study the code yourself though, I'll explain the key parts, which are:
the introduction of that new state variable I just mentioned, which I've called focusIndex
as mentioned, the calling of setFocusIndex with an appropriate value whenever rows are added, removed or moved. (I've been trying to follow your requirements here and it seems to work well to me, but as I said, I may have misunderstood.)
the key is the useEffect which runs whenever focusIndex updates, and does the actual focusing in the DOM. Without this, of course, the focus will never be updated on calling setFocusIndex, but with it, calling that function will "always" have the desired effect.
one last subtlety is that the "always" I put above is not strictly true. The useEffect only runs when focusIndex actually changes, but when moving rows there are some situations where it is set to the same value it had before, but where you still want to move focus. I found this happening when clicking outside the inputs, then moving the first field up or the last one down - nothing happened, when we want the first/last input to be focused. This was happening because focusIndex was being set to the value it already had, so the useEffect didn't run, but we still wanted it to in order to set the focus. The solution I came up with was to add an onBlur handler to each input to ensure that the focus index is set to some "impossible" value (I chose -1, but something like null or undefined would have worked fine as well) when focus is lost - this may seem artificial but actually better represents the fact that when the focus is on no inputs, you don't want to have a "sensible" focusIndex, otherwise the React state is saying one of the inputs is focused, when none are. Note that I also used -1 for the initial state, for much the same reason - if it starts at 0 then adding the first row doesn't cause focus to change.
I hope this helps and my explanations are clear enough - if you're confused by anything, or notice anything going wrong with this implementation (I confess I have not exactly tested it to destruction), please let me know!
import React, { useState, useRef, useEffect } from "https://cdn.skypack.dev/react#17.0.1";
import ReactDOM from "https://cdn.skypack.dev/react-dom#17.0.1";
const App = () => {
const [myRows, setMyRows] = useState([]);
const focusInput = useRef([]);
const [focusIndex, setFocusIndex] = useState(-1);
const onAddRow = () => {
setMyRows((prevRows) => {
return [
...prevRows,
{ id: prevRows.length, text: "", up: "↑", down: "↓", delete: "X" },
];
});
setFocusIndex(myRows.length);
};
useEffect(() => {
console.log(`focusing index ${focusIndex} in`, focusInput.current);
focusInput.current[focusIndex]?.focus();
}, [focusIndex]);
const onMoveUp = (index) => (event) => {
const currentState = [...myRows];
if (index !== 0) {
const prevObject = currentState[index - 1];
const nextObject = currentState[index];
currentState[index - 1] = nextObject;
currentState[index] = prevObject;
setMyRows(currentState);
setFocusIndex(index - 1);
} else {
setFocusIndex(0);
}
};
const onMoveDown = (index) => (event) => {
const currentState = [...myRows];
if (index !== myRows.length - 1) {
const currObject = currentState[index];
const nextObject = currentState[index + 1];
currentState[index] = nextObject;
currentState[index + 1] = currObject;
setMyRows(currentState);
setFocusIndex(index + 1);
} else {
setFocusIndex(myRows.length - 1);
}
};
const onDelete = (index) => (event) => {
const currentState = [...myRows];
currentState.splice(index, 1);
setMyRows(currentState);
const newFocusIndex = index < currentState.length
? index
: currentState.length - 1;
setFocusIndex(newFocusIndex);
};
const onTextUpdate = (id) => (event) => {
setMyRows((prevState) => {
const data = [...prevState];
data[id] = {
...data[id],
text: event.target.value,
};
return data;
});
};
return (
<div className="container">
<button onClick={onAddRow}>Add</button>
<br />
{myRows?.map((row, index) => {
return (
<div key={row.id}>
<input
ref={(el) => (focusInput.current[index] = el)}
onChange={onTextUpdate(index)}
onBlur={() => setFocusIndex(-1)}
value={row.text}
type="text"></input>
<button onClick={onMoveUp(index)}>{row.up}</button>
<button onClick={onMoveDown(index)}>{row.down}</button>
<button onClick={onDelete(index)}>{row.delete}</button>
</div>
);
})}
</div>
);
}
ReactDOM.render(<App />,
document.getElementById("root"))

Getting Blank Array on First Click?

I am stuck with a Reactjs problem. The problem is mentioned in App.js file. Please look into this. Initially all buttons are White and not clickable, how can I change there color after some time. Should I use useEffect hook or some other way. I am not getting the index of button randomly to change their color and isDisable.
Three Type of Button
White: initially all buttons are white and not clickable.
Red: Randomly from the white button, One button will be red and clickable.
Blue: Clicked Button. After clicking on the red button color will change to blue and not clickable.
When all white buttons become blue, give custom popup in the center of the screen with the icon "You won game…!!!"
I am trying below code to change button color randomly, but fails.
const changeColorHandle = () => {
const rand = Math.floor(Math.random() * 4);
btnArray.map((i, k) => {
if (i.index === rand) {
return ([...btnArray, { color: 'red', isDisable: false }])
}
return [...btnArray]
})
console.log(rand)
};
useEffect(() => {
if (btnArray.length > 0) {
setTimeout(() => {
changeColorHandle()
}, 2000);
}
}, [btnArray])
Can anybody please solve this problem Here. And show me where I am doing it wrong.
Thanks.
One thing you don't want to really mess with the new react hooks is that they are not really applying changes synchronously, that way sometimes you can have a little delay.
I don't think it's the issue here but you can have a more efficient code that also solve the problem :)
Code Sandbox is here
Raw snippet with lines changed is here:
import React, { useState } from "react";
import Button from "./Button";
const Game = () => {
const [buttonValue, setButtonValue] = useState(0);
const [btnArray, setBtnArray] = useState([]);
const handleChange = e => {
setButtonValue(parseInt(e.target.value, 10));
};
const onClickHandle = () => {
const tmpArray = [];
for (let i = 0; i < buttonValue; i++) {
tmpArray.push({ color: "white", isDisable: true });
}
setBtnArray(tmpArray);
};
const changeColorHandle = () => {
alert("clicked");
};
(rest is ok 👍)
So as you can see, buttonValue is removed, and the job of the onClick is just to generate a new array according to the actual value of the input.
You don't get any issue with the user playing with the input value as the btnArray will only be rendered once he has clicked the button :)
(rendering triggered by the state change done with setBtnArray()
Edit: One nice sweet addition could be getting rid of buttonValue as a string as you may use math operations on it rather than using string conversion back and forth
Essentially, you're setting multiple states very quickly in succession. By the time you get to your loop, your buttonCount state is behind and thus the loop where you're pushing values onto btnArray doesn't really run (ends immediately).
Quick aside: You're attempting to mutate btnArray directly inside the loop, don't do that if possible. Create a new Array, push values onto that and then setBtnArray to the new Array.
This works:
const onClickHandle = () => {
if (btnArray.length > 0) {
setBtnArray([]);
}
let newBtnArray = [];
for (let i = 0; i < parseInt(buttonValue, 10); i++) {
newBtnArray.push({ color: "blue", isDisable: true });
}
setBtnArray(newBtnArray); };
I threw out btnCount here, but if you need it for something else, you can set it seperately.
I have figured out your issue, you are setting the button count after clicking so it is running one step behind the array values, and button color change function is also added, check this: CodeSandBox: https://codesandbox.io/s/dreamy-ganguly-e805g?file=/src/components/Game.js
import React, { useState, useEffect } from "react";
import Button from "./Button";
const Game = () => {
const [buttonValue, setButtonValue] = useState("");
const [buttonCount, setButtonCount] = useState(0);
const [btnArray, setBtnArray] = useState([]);
const handleChange = e => {
//you are not using button value any where
//setButtonValue(e.target.value);
setButtonCount(parseInt(e.target.value, 10));
};
const onClickHandle = () => {
if (btnArray.length > 0) {
setBtnArray([]);
}
// setButtonCount(parseInt(buttonValue, 10)); //actually you were setting the count very late so it is one step behind
setButtonValue("");
console.log(buttonCount);
let tempArr = [];
for (let i = 0; i < buttonCount; i++) {
tempArr.push({ index: i, color: "white", isDisable: false });
setBtnArray(tempArr);
}
console.log(btnArray);
};
const changeColorHandle = btnIndex => {
console.log(btnIndex);
//change that button color and save btn array again
let newBtnArray = btnArray.map(btn => {
if (btn.index === btnIndex) {
btn["color"] = "blue";
} else {
//else change all buttons color back to white
btn["color"] = "white";
}
return btn;
});
setBtnArray(newBtnArray); // set new array with modified colors
};
return (
<>
<div className="container">
<div className="row">
<div className="col">
<input type="text" onChange={handleChange} />
<button onClick={onClickHandle}>Enter</button>
</div>
</div>
<br />
<div className="row">
{btnArray.map((i, k) => (
<Button
key={k}
color={i.color}
isDisable={i.isDisable}
onClick={() => changeColorHandle(i.index)}
/>
))}
</div>
</div>
</>
);
};
export default Game;

React autocomplete in a textarea like in an IDE (e.g. VS Code, Atom)

I try to code an auto completion feature like in the gif in React.
So suggestions appear while writing text.
However all packages I could find so far work
a) only in the beginning of the input/textarea (e.g. react-autosuggest)
b) or need a trigger character (like # or #) to open (e.g. react-textarea-autocomplete)
Do I miss some React limitation? Any hints / packages?
We ended up using the fantastic editor Slate.js.
The Mentions example can easily be changed so that the suggestions are triggered by any character (not only '#'). There you go: perfect auto suggest.
I'm actually having the same problem in regards to needing a textarea implementation, but I can help with autocomplete triggering behavior.
We have an implementation of template variables that look like this {{person.name}} which get resolved into whatever the actual value is.
In regards to the autocompletion being triggered only on the first word, you can get around that with a couple modifications to the required functions.
For instance my required functions look like this. (not a completely working example, but all the important bits)
const templateVars = Object.values(TemplateVarMap);
const variables = templateVars.map((templateVar) => {
return {
name: templateVar,
};
});
//This func, onChange, and onSuggestionSelected/Highlight are the important
//parts. We essentially grab the full input string, then slice down to our
//autocomplete token and do the same for the search so it filters as you type
const getSuggestions = (value) => {
const sliceIndex = value
.trim()
.toLowerCase()
.lastIndexOf('{{'); //activate autocomplete token
const inputValue = value
.trim()
.toLowerCase()
.slice(sliceIndex + 2); //+2 to skip over the {{
const inputLength = inputValue.length;
//show every template variable option once '{{' is typed, then filter as
//they continue to type
return inputLength === 0
? variables
: variables.filter(
(variable) => variable.name.toLowerCase().slice(0, inputValue.length) === inputValue
);
};
const getSuggestionValue = (suggestion) => suggestion.name;
const renderSuggestion = (suggestion) => <div>{suggestion.name}</div>;
onSuggestionsFetchRequested = ({ value }) => {
this.setState({
suggestions: getSuggestions(value),
});
};
onSuggestionsClearRequested = () => {
this.setState({
suggestions: [],
});
};
onChange = (event, { newValue }) => {
//onChange fires on highlight / selection and tries to wipe
//the entire input to the suggested variable, so if our value
//is exactly a template variable, don't wipe it
if (templateVars.includes(newValue)) {
return;
}
this.setState({
value: newValue,
});
};
//These both need to do similar things because one is a click selection
//and the other is selection using the arrow keys + enter, we are essentially
//manually going through the input and only putting the variable into the
//string instead of replacing it outright.
onSuggestionHighlighted = ({ suggestion }) => {
if (!suggestion) {
return;
}
const { value } = this.state;
const sliceIndex = value.lastIndexOf('{{') + 2;
const currentVal = value.slice(0, sliceIndex);
const newValue = currentVal.concat(suggestion.name) + '}}';
this.setState({ value: newValue });
};
onSuggestionSelected = (event, { suggestionValue }) => {
const { value } = this.state;
const sliceIndex = value.lastIndexOf('{{') + 2;
const currentVal = value.slice(0, sliceIndex);
const newValue = currentVal.concat(suggestionValue) + '}}';
this.setState({ value: newValue });
};
const inputProps = {
value: this.state.value,
onChange: this.onChange,
};
render() {
return (
<Autosuggest
suggestions={this.state.suggestions}
onSuggestionSelected={this.onSubjectSuggestionSelected}
onSuggestionHighlighted={this.onSubjectSuggestionHighlighted}
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
getSuggestionValue={getSuggestionValue}
renderSuggestion={renderSuggestion}
inputProps={inputProps}
/>
)
}
This lets me type something like This is some text with a {{ and have autocomplete pop up, upon choosing a selection it should go to This is some text with a {{person.name}}.
The only problem here is that it requires the final two characters in the input to be {{ (or whatever your token is) for the autocomplete box to come up. I'm still playing with cursor movement and slicing the string around in different ways so if I edit a template thats not at the end the box still pops up.
Hopefully this helps.
You can try react-predictive-text

Prevent state from updating if input string has spaces

What's wrong with my code below? basically I don't want to update my state when user enter extra space at the beginning or at the end.
handleSearchQuery = (e) = {
if(e.target.value.trim() != "") {
this.setState({
q: e.target.value
});
}
}
The first error seems to be that you forgot a > in your arrow function. Change the first line to:
handleSearchQuery = (e) => {
Anyway, this is how I would write the whole function:
handleSearchQuery = (e) => {
let str = e.target.value.trim();
if(str != this.state.q) {
this.setState({
q: str
});
}
}
This compares the trimmed input to the existing state of q. If they are equal, nothing happens. Otherwise, update the state.
I store the trimmed result of the string in a variable as I would otherwise need to trim() twice... for whatever that's worth.
handleSearchQuery = (e) => {
if(e.target.value.trim() != this.state.q) {
this.setState({
q: e.target.value
});
}
}

Categories

Resources