I am learning React while doing a new project with it.
I want to create an input component with a event that is only triggered if the user exits the input or hit enter. (here is the link to codesandbox)
const MyInput = ({ value, onValueChange }) => {
const [myCurrentTypingValue, setMyCurrentTypingValue] = useState(value);
useEffect(() => setMyCurrentTypingValue(value), [value]);
const commitChanges = () => {
const numericValue = Number(myCurrentTypingValue);
setMyCurrentTypingValue(numericValue);
if (value !== numericValue) onValueChange(numericValue);
};
const handleOnChange = (e) => setMyCurrentTypingValue(e.target.value);
const handleOnBlur = () => commitChanges();
const handleOnKeyPress = (e) => {
if (e.charCode === 13) commitChanges();
};
return (
<input
type="number"
value={myCurrentTypingValue}
onChange={handleOnChange}
onBlur={handleOnBlur}
onKeyPress={handleOnKeyPress}
/>
);
};
As you can see, when I type something, the input is correctly displayed when I press enter or exit, but when I type a number that starts with 0 (e.g. 000023), although the numerical value is correctly converted, it is still displayed with all zeros in front of it.
Because I change the state of my component, I expect my input box value property to be refreshed, and it is not:
Why is this happening? I checked with the debugger tool, my state is correct?
How can I have the input box reflect the new state with the good numeric formatted value?
When it's time to "commit" your changes, you should stringify the parsed number value and update the input value (which is always a string). Here's a self-contained example:
TS Playground
body { font-family: sans-serif; }
input[type="number"], pre { font-size: 1rem; padding: 0.5rem; }
<div id="root"></div><script src="https://unpkg.com/react#18.1.0/umd/react.development.js"></script><script src="https://unpkg.com/react-dom#18.1.0/umd/react-dom.development.js"></script><script src="https://unpkg.com/#babel/standalone#7.17.10/babel.min.js"></script>
<script type="text/babel" data-type="module" data-presets="env,react">
// import * as ReactDOM from 'react-dom/client';
// import {useEffect, useState} from 'react';
// This Stack Overflow snippet demo uses UMD modules instead of the above import statments
const {useEffect, useState} = React;
// Parse a string as a number, but if the string is not a valid number then return 0
function parseNumber (str) {
const n = Number(str);
return Number.isNaN(n) ? 0 : n;
}
function NumberInput ({value, setValue}) {
const [rawValue, setRawValue] = useState(String(value));
useEffect(() => setRawValue(String(value)), [value]);
const commit = () => setValue(parseNumber(rawValue));
const handleChange = (ev) => setRawValue(ev.target.value);
const handleKeyUp = (ev) => {
if (ev.key === 'Enter') commit();
};
return (
<input
type="number"
onBlur={commit}
onChange={handleChange}
onKeyUp={handleKeyUp}
value={rawValue}
/>
);
}
function App () {
const [value, setValue] = useState(0);
return (
<div>
<h1>Number input management</h1>
<NumberInput {...{value, setValue}} />
<pre><code>{JSON.stringify({value}, null, 2)}</code></pre>
</div>
);
}
const reactRoot = ReactDOM.createRoot(document.getElementById('root'));
reactRoot.render(<App />);
</script>
Related
I am just trying to figure out how to do my to-do list and currently am just experimenting with adding elements containing text given a text input element.
The issue is presented in this clip: https://imgur.com/a/DDTyv1I
import { useState } from "react"
function App() {
const [inputVal, setInputVal] = useState('')
const [tasks, setTasks] = useState([])
console.log(inputVal);
return <>
<Input valor = {setInputVal} inputVal={inputVal} tasks={tasks} setTasks={setTasks}/>
{
tasks.map(e=>(
<Display text={e.text}/>
))
}
</>
}
const Input = ({valor, inputVal, tasks, setTasks}) =>{
const keyPressed = (val) =>{
if(val.key === 'Enter'){
valor(val.target.value)
setTasks([
...tasks, {text: inputVal, key: Math.random()*2000}
])
}
}
return <>
<input type="text" onKeyUp={keyPressed}/>
</>
}
const Display = ({text}) => {
return <>
<h1>{text}</h1>
</>
}
export default App;
I believe this is happening because you are not using onChange on your input so your state is going stale and you are always one value behind.
I have tidied up the code and added some missing pieces (like the value attribute in the input element).Then I split the function that takes care of the submission to 2 functions - one function that is handling changing the input value and one that submits the value as a new entry to your tasks list
import { useState } from "react"
const Input = ({ input, handleChange, tasks, setTasks }) => {
const onSubmit = (e) => {
if (e.key === 'Enter') {
setTasks([
...tasks,
{ text: input, key: Math.random()*2000 }
])
setInput('');
}
}
const handleChange = (e) => {
setInput(e.target.value)
}
return <input type="text" onKeyUp={onSubmit} onChange={handleChange} value={input}/>
}
const Display = ({ text }) => <h1>{text}</h1>
const App = () => {
const [input, setInput] = useState('')
const [tasks, setTasks] = useState([])
return <>
<Input input={input} setInput={setInput} tasks={tasks} setTasks={setTasks}/>
{tasks.map((task) => (
<Display text={task.text}/>
))
}
</>
}
When keyPressed is called, your code calls setInputVal (valor) and setTasks. The setTask is being called before setInputVal actually has time to update the state, so it sets the ”old” value. This is because state setting is asynchronous and the code does not wait for the inputVal to be set before setting the task.
I've been stuck on this error for a long time, so I would appreciate some help. Here is a minimally reproducible exmaple:
import "./App.css";
import React, { useState } from "react";
import $ from "jquery";
function App() {
const [value, setValue] = useState("");
const focusHandler = () => {
$("input").on("keypress", (e) => {
let copy = value;
if (e.key === ".") {
e.preventDefault();
copy += " ";
setValue(copy);
}
});
};
const blurHandler = (event) => {
$("input").off("keypress");
setValue(event.target.value);
};
const changeHandler = (event) => {
setValue(event.target.value);
};
return (
<div>
<input
value={value}
onFocus={focusHandler}
onBlur={blurHandler}
onChange={changeHandler}
/>
</div>
);
}
export default App;
On input focus, I'm adding an event listener to look for a . keypress and append a tab (4 spaces) to the input if it is pressed. But when I press . multiple times, the input gets stuck at the first tab, and doesn't move any further (e.g. input permanenetly shows 4 spaces). Using console.log shows me that the value state doesn't seem to be updating in focusHandler and reverts to the original value ("").
An important note is that switching to a class-based component with this.state makes it work. Any insight as to why this is happening?
As mentioned in the comments, jQuery is the wrong tool for the job. Bringing in jQuery is the same as calling DOM methods directly: it's circumventing React and adding additional listeners on top of the ones React already gives you. You can expect misbehavior if you're setting state from multiple handlers unrelated to React. Once you're in React, use the tools it gives you (refs, effects, handlers) to solve the problem.
Worst case scenario is when an approach appears to work, then fails in production, on other people's machines/browsers, when refactoring from classes to hooks or vice-versa, in different versions of React, or for 1 out of 1000 users. Staying well within the API React gives you better guarantees that your app will behave correctly.
Controlled component
For manipulating the input value, you can use both onKeyDown and onChange listeners. onKeyDown fires first. Calling event.preventDefault() inside of onKeyDown will block the change event and ensure only one call to setState for the controlled input value occurs per keystroke.
The problem with this the input cursor moves to the end after the component updates (see relevant GitHub issue). One way to deal with this is to manually adjust the cursor position when you've made an invasive change to the string by adding state to keep track of the cursor and using a ref and useEffect to set selectionStart and selectionEnd properties on the input element.
This causes a brief blinking effect due to asynchrony after the render, so this isn't a great solution. If you're always appending to the end of the value, you assume the user won't move the cursor as other answers do, or you want the cursor to finish at the end, then this is a non-issue, but this assumption doesn't hold in the general case.
One solution is to use useLayoutEffect which runs synchronously before the repaint, eliminating the blink.
With useEffect:
const {useEffect, useRef, useState} = React;
const App = () => {
const [value, setValue] = useState("");
const [cursor, setCursor] = useState(-1);
const inputRef = useRef();
const pad = ". ";
const onKeyDown = event => {
if (event.code === "Period") {
event.preventDefault();
const {selectionStart: start} = event.target;
const {selectionEnd: end} = event.target;
const v = value.slice(0, start) + pad + value.slice(end);
setValue(v);
setCursor(start + pad.length);
}
};
const onChange = event => {
setValue(event.target.value);
setCursor(-1);
};
useEffect(() => {
if (cursor >= 0) {
inputRef.current.selectionStart = cursor;
inputRef.current.selectionEnd = cursor;
}
}, [cursor]);
return (
<div>
<p>press `.` to add 4 spaces:</p>
<input
ref={inputRef}
value={value}
onChange={onChange}
onKeyDown={onKeyDown}
/>
</div>
);
};
ReactDOM.createRoot(document.querySelector("#app"))
.render(<App />);
input {
width: 100%;
}
<script crossorigin src="https://unpkg.com/react#18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.development.js"></script>
<div id="app"></div>
With useLayoutEffect:
const {useLayoutEffect, useRef, useState} = React;
const App = () => {
const [value, setValue] = useState("");
const [cursor, setCursor] = useState(-1);
const inputRef = useRef();
const pad = ". ";
const onKeyDown = event => {
if (event.code === "Period") {
event.preventDefault();
const {selectionStart: start} = event.target;
const {selectionEnd: end} = event.target;
const v = value.slice(0, start) + pad + value.slice(end);
setValue(v);
setCursor(start + pad.length);
}
};
const onChange = event => {
setValue(event.target.value);
setCursor(-1);
};
useLayoutEffect(() => {
if (cursor >= 0) {
inputRef.current.selectionStart = cursor;
inputRef.current.selectionEnd = cursor;
}
}, [cursor]);
return (
<div>
<p>press `.` to add 4 spaces:</p>
<input
ref={inputRef}
value={value}
onChange={onChange}
onKeyDown={onKeyDown}
/>
</div>
);
};
ReactDOM.createRoot(document.querySelector("#app"))
.render(<App />);
input {
width: 100%;
}
<script crossorigin src="https://unpkg.com/react#18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.development.js"></script>
<div id="app"></div>
Uncontrolled component
Here's another attempt using an uncontrolled component. This doesn't have the blinking problem because the DOM element's .value property is synchronously set at the same time as the .selectionStart property and is rendered in the same repaint.
const App = () => {
const pad = ". ";
const onKeyDown = event => {
if (event.code === "Period") {
event.preventDefault();
const {target} = event;
const {
value, selectionStart: start, selectionEnd: end,
} = target;
target.value = value.slice(0, start) +
pad + value.slice(end);
target.selectionStart = start + pad.length;
target.selectionEnd = start + pad.length;
}
};
return (
<div>
<p>press `.` to add 4 spaces:</p>
<input
onKeyDown={onKeyDown}
/>
</div>
);
};
ReactDOM.createRoot(document.querySelector("#app"))
.render(<App />);
input {
width: 100%;
}
<script crossorigin src="https://unpkg.com/react#18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.development.js"></script>
<div id="app"></div>
Don't mix direct DOM manipulation, whether that's vanilla JavaScript or jQuery, with React. There is no need to add an event handler with jQuery here, because your methods are already event handlers. Just use them directly:
const focusHandler = (e) => {
// handle the event here!
}
My solution:
const changeHandler = (event) => {
const key = event.nativeEvent.data;
if (key === ".") {
event.preventDefault();
const initialValue = event.target.value.split(".")[0];
console.log(initialValue);
setValue(initialValue + " ");
} else {
setValue(event.target.value);
}
};
I am building a search component where the user types in the value and it filters through some dummy data and renders the result for the user to see. The problem is when I type in one character in the search field i get the entire array of data on every character I type. So for example there are 4 data strings in the array so if I type in two characters in the search bar then my result is 8.
Here is the code and the problem duplicated: Any help is appreciated.
The answer is below, basically, the code was loading in an extra map from CompanyInfoList component. I also had a useContext that was not needed in the same component, so i removed it and replaced results.map from useContext setState hook to filtered.map. filtered was the prop that needed to be passed down from CompSearch to CompanyInfoList. The last change I made was to delete the RenderList component and remove return RenderList in the serachList component to CompanyInfoList
CompSearch.js
import React, {useContext, useState} from "react";
import CompanyInfoList from "./CompanyInfoList";
import { CompListContext } from "./CompListContext";
const CompSerach = () => {
// const [input, setInput] = useState('');
const [results, setResults] = useContext(CompListContext);
const [searchField, setSearchField] = useState("");
const [searchShow, setSearchShow] = useState(false);
const filtered = results.filter((res) => {
return (
res.name.toLowerCase().includes(searchField.toLowerCase()) ||
res.employee.toLowerCase().includes(searchField.toLowerCase()) ||
res.date.toLowerCase().includes(searchField.toLowerCase()) ||
res.amount.toLowerCase().includes(searchField.toLowerCase())
);
});
const handleChange = (e) => {
setSearchField(e.target.value);
if (e.target.value === "") {
setSearchShow(false);
} else {
setSearchShow(true);
}
};
function searchList() {
if (searchShow) {
return <CompanyInfoList filtered={filtered} />;
}
}
return (
<>
<div>
<input
type="search"
placeholder="search Company Title"
// input="input"
// value={input}
onChange={handleChange}
// onChange={handleChange}
/>
</div>
{searchList()}
</>
);
};
export default CompSerach;
CompanyInfoList.js
import Results from "./Results";
const CompanyInfoList = ({ filtered }) => {
const fltr = filtered.map((result) => (
<Results
key={result.id}
name={result.name}
employee={result.employee}
date={result.date}
amount={result.amount}
/>
));
return <div>{fltr}</div>;
};
export default CompanyInfoList;
Here is the where I am having the problem,
const handleCLick = () => {
const parsedId = getYouTubeID(videoLink);
console.log(parsedId);
setVideoId(parsedId);
console.log(videoId);
}
Here when I am trying to log the 'parsedId' it logs the data correctly
ioNng23DkIM
And after using the setVideoId() function when I try to log the value it returns undefined
undefined
Here is a snap shot of the log output.
Home.js code:
import React, { useRef, useState } from "react";
import { Link } from "react-router-dom";
import getYouTubeID from 'get-youtube-id';
function Home(props) {
const [videoLink, setVideoLink] = useState();
const [isBool, setBool] = useState(false);
const [videoId, setVideoId] = useState();
const urlRef = useRef();
const handleChange = (event) => {
setVideoLink(event.target.value);
if (urlRef.current.value === '') {
alert('Please enter a URL');
setBool(true);
} else {
setBool(false);
}
}
const handleCLick = () => {
const parsedId = getYouTubeID(videoLink);
console.log(parsedId);
setVideoId(parsedId);
console.log(videoId);
}
return (
<section className="homeLayout">
<div className="logo-display">
<img className="logo-img" alt="logo" src="./logo.png" />
<h1>WatchIt</h1>
</div>
<div className="searchlayer">
<form>
<input ref={urlRef} id="videoLink" placeholder="Enter the youtube video URL:" onBlur={handleChange} required />
<Link style={{ pointerEvents: isBool ? 'none' : 'initial' }} to={`/play?=${videoId}`} onClick={handleCLick}>Play</Link>
</form>
</div>
</section>
);
}
export default Home;
You can use useEffect to solve your problem.
Use effect will listen to you state change n then you can perform logic in there.
The problem you're facing is because setState will set the value eventually, not immediately (Usually this means the update will be visible when the component is rendered again). If you want to do something after the value is set, you need to use useEffect.
Splitting your handleClick we get,
const handleCLick = () => {
const parsedId = getYouTubeID(videoLink);
console.log(parsedId);
setVideoId(parsedId); // Queue the change for `videoId`
}
useEffect(() => {
console.log(videoId);
}, [videoId]); // Call this function when the value of `videoId` changes
I am trying to make the component that the focus moves to the next input when each letter inputted.
I think I need multiple ref like an array but I don't know about it.
It's a sample code for the question.
function PIN({length, onChange, value}){
const inputEl = React.useRef(null);
function handleChange(e){
onChange(e);
inputEl.current.focus();
}
return (
<div>
{
new Array(length).fill(0).map((i)=>(
<input type="text" ref={inputEl} onChange={handleChange} />
))
}
</div>
)
}
You can create multiple refs
function PIN({length, onChange, value}){
const inputRefs = useMemo(() => Array(length).fill(0).map(i=> React.createRef()), []);
const handleChange = index => (e) => {
//onChange(e); // don't know about the logic of this onChange if you have multiple inputs
if (inputRefs[index + 1]) inputRefs[index + 1].current.focus();
}
return (
<div>
{
new Array(length).fill(0).map((inp, index)=>(
<input type="text" ref={inputRefs[index]} onChange={handleChange(index)} />
))
}
</div>
)
}
The ref on input is equivalent to a callback function. You can pass a method to him. The parameter received by this method is the input dom element, which you can store in an array.
import React from "react";
import "./styles.css";
export default function App() {
const inputEl = React.useRef([]);
function handleChange(i){
inputEl.current[i+1].focus();
}
return (
<div>
{
new Array(3).fill(0).map((n,i)=>(
<input
key={i}
type="text"
ref={ref=>inputEl.current.push(ref)}
onChange={()=>handleChange(i)}
/>
))
}
</div>
)
}
In your inputs, you can pass a function to the ref parameter, this will allow you to store all of your refs in an array:
let myRefs = [];
const saveThisRef = (element) => {
myRefs.push(element);
}
Then you can pass your function to each input you render:
<input type="text" ref={saveThisRef} onChange={handleChange} />
Then you can advance to the next input in the onChange handler:
// Find the index of the next element
const index = myRefs.indexOf(element) + 1;
// Focus it
if (index < myRefs.length) myRefs[index].focus();
Re-rendering the component that holds the dynamic Refs list with a different number of refs raises an exception ("Rendered more hooks than during the previous render"), as you can see in this example:
https://codesandbox.io/s/intelligent-shannon-u3yo6?file=/src/App.js
You can create a new component that renders a single and holds it's own single ref, and use the parent element to manage the current focused input, and pass this data to you'r new component, for example.
Here is an example that would actually work:
const { useState, useCallback, useEffect, useRef } = React;
const Pin = ({ length, onChange, value }) => {
const [val, setVal] = useState(value.split(''));
const [index, setIndex] = useState(0);
const arr = [...new Array(length)].map(
(_, index) => index
);
const myRefs = useRef(arr);
const saveThisRef = (index) => (element) => {
myRefs.current[index] = element;
};
function handleChange(e) {
const newVal = [...val];
newVal[index] = e.target.value;
if (index < length - 1) {
setIndex(index + 1);
}
setVal(newVal);
onChange(newVal.join(''));
}
const onFocus = (index) => () => {
const newVal = [...val];
newVal[index] = '';
setIndex(index);
setVal(newVal);
onChange(newVal.join(''));
};
useEffect(() => {
if (index < myRefs.current.length) {
myRefs.current[index].focus();
}
}, [index, length, myRefs]);
return arr.map((index) => (
<input
type="text"
ref={saveThisRef(index)}
onChange={handleChange}
onFocus={onFocus(index)}
value={val[index] || ''}
maxLength="1"
key={index}
/>
));
};
const App = () => {
const [value, setValue] = useState('');
const onChange = useCallback(
(value) => setValue(value),
[]
);
console.log('value:', value);
return (
<Pin
length={5}
value={value}
onChange={onChange}
/>
);
};
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
All answers will shift focus to next input when you correct an already set value. The requirement is that focus should shift when a letter is inputted, not when you remove a value.