How to debounce an input which is bound to a state value? - javascript

I am trying to debounce an input. I have memoized the debounce handler so that it does not change references on each render. I need the input to be bound to a state value as I need to set it elsewhere in my app. The issue is that the input value is never able to be updated as when inside the debounced changeHandler e.target.value always contains previous value, not a new value entered. How can i debounce the input that is bound to a state value?
xport function App() {
const [query, setQuery] = useState("a value");
const changeHandler = (event) => {
console.log(event.target.value); // wrong value
setQuery(event.target.value);
};
const debouncedChangeHandler = useCallback(debounce(changeHandler, 1000), []);
return (
<div>
<input
value={query}
onChange={debouncedChangeHandler}
type="text"
placeholder="Type a query..."
/>
</div>
);
}
sandbox: https://codesandbox.io/s/react-debounce-5pidiy?file=/src/index.js:130-596

I wouldn't try to debounce the keyboard input itself, I'd just wait to validate until the value hadn't changed for while, and then either directly validate it:
const { useState, useEffect, useCallback } = React;
function App() {
const [query, setQuery] = useState("a value");
const changeHandler = (event) => {
setQuery(event.target.value);
};
const validate = useCallback((query) => {
console.log(`Validating "${query}"...`);
});
useEffect(() => {
const timer = setTimeout(() => {
validate(query)
}, 1000);
return () => {
clearTimeout(timer);
}
}, [query]);
return (
<div>
<input
value={query}
onChange={changeHandler}
type="text"
placeholder="Type a query..."
/>
</div>
);
}
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>
...or use a separate state member for it:
const { useState, useEffect, useCallback } = React;
function App() {
const [rawQuery, setRawQuery] = useState("a valid value");
const [query, setQuery] = useState(rawQuery);
const changeHandler = (event) => {
console.log(`raw query: ${event.target.value}`);
setRawQuery(event.target.value);
};
const validate = useCallback((query) => {
console.log(`Validating "${query}"...`);
return query.includes("valid");
});
useEffect(() => {
const timer = setTimeout(() => {
console.log(`validate: ${rawQuery}`);
if (validate(rawQuery)) {
console.log("Valid!");
setQuery(rawQuery);
} else {
console.log("Invalid!");
setRawQuery(query);
}
}, 1000);
return () => {
clearTimeout(timer);
}
}, [rawQuery, query]);
return (
<div>
<input
value={rawQuery}
onChange={changeHandler}
type="text"
placeholder="Type a query..."
/>
<div>Value to validate: {query}</div>
</div>
);
}
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>
That makes it easier to do what you said in a comment you wanted, putting the input back to the previous valid value if an invalid one has provided.
(That updating can be wrapped in a hook for reuse.)

Related

How to stop calling useEffect again

Container
import { InputField } from './InputField';
const sleep = (time: number) => new Promise((res) => setTimeout(res, time, ''));
export const Container = () => {
const [inputValue, setInputValue] = React.useState('');
React.useEffect(() => {
(async () => await sleep(1000))();
async function fetchMyAPI(time, value) {
await sleep(time);
setInputValue(value);
}
fetchMyAPI(1000, 'vbc1');
fetchMyAPI(2000, 'dgi1');
}, []);
const inputChange = (value) => {
setInputValue(value);
};
return <InputField inputValue={inputValue} inputChange={inputChange} />;
};
InputField
export const InputField = ({
inputValue,
inputChange,
}: {
inputValue: string;
inputChange: (value: string) => void;
}) => {
const [value, setValue] = React.useState('');
React.useEffect(() => {
setValue(inputValue.slice(0, -1));
}, [inputValue]);
const handleChange = (event) => {
setValue(event.target.value);
inputChange(event.target.value + '1');
};
return <input value={value} onChange={handleChange} />;
};
inputValue above can change multiple times.
also a local variable in input is used to display , and inputValue is slightly different from it. So when we keep track of InputValue , we pass the cleared data to the local variable. And vice versa, we modify the data to put in the inputValue.
React.useEffect(() => {
setValue(inputValue.slice(0, -1));
}, [inputValue]);
Every time we call handleChange : we do setValue and inputChange. Thus, we change the value variable and the inputValue variable. After the inputValue is changed, useEffect is called which observes the inputValue. And overwrites exactly the same value of the Value variable. This is problem!
What is the correct solution to this problem?
You can create a boolean state effectRan to track whether the effect already ran or not, and only invoke the effect's logic if effectRan == false, then set it to true.
When the effect runs again with it as true, have it set it back to false to prepare to run again in the next change.
I changed the code a bit to highlight the approach:
const {useState, useEffect } = React
const InputField = () => {
const [value, setValue] = React.useState('');
const [effectRan, setEffectRan] = useState(true);
React.useEffect(() => {
if (!effectRan) {
setValue(prev => prev + '-');
setEffectRan(true)
console.log('Effect just ran');
} else {
setEffectRan(false)
}
}, [value]);
const handleChange = (event) => {
setValue(event.target.value);
};
return <input onChange={handleChange} value={value} />;
};
ReactDOM.render(<InputField />, root)
<script crossorigin src="https://unpkg.com/react#18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.production.min.js"></script>
<div id="root"></div>

How to get each user's keystroke when he pressed a certain key?

I need to get each user's keystroke when he pressed a certain key("#") and stop getting his keystroke when he pressed other key(space(" ")). For example: a user enters the text "I wanna go to #shop", I need to save his input and the tag inside it. How can I do it? I wrote some code to do it but I don't know how to make it completely
onKeyDown = (e) => {
let value = e.target.value, tags = [], currentTag = "";
if (e.key == "Enter") {
this.setState((state) => {
const item = this.createNote(value, tags);
return { notes: [...state.notes, item] };
});
}
if (e.key == "#") {}
};
You can make use of regex /#[^\s]+/g
Live Demo
export default function App() {
const [value, setValue] = useState("");
const [tags, setTags] = useState([]);
function onInputChange(e) {
const value = e.target.value;
setValue(value);
const tags = value.match(/#[^\s]+/g) ?? [];
setTags(tags);
}
return (
<>
<input type="text" name="" value={value} onChange={onInputChange} />
<ul>
{tags.map((tag) => {
return <li key={tag}> {tag} </li>;
})}
</ul>
</>
);
}
EDITED: You can make use of useMemo hook as
Thanks to 3limin4t0r
Live Demo
export default function App() {
const [value, setValue] = useState("");
const tags = useMemo(() => value.match(/#\S+/g) || [], [value]);
function onInputChange(e) {
const value = e.target.value;
setValue(value);
}
return (
<>
<input type="text" name="" value={value} onChange={onInputChange} />
<ul>
{tags.map((tag) => {
return <li key={tag}> {tag} </li>;
})}
</ul>
</>
);
}
Instead of parsing individual key values, you can use a function like this to parse your input field on changes and return an array of hashtags (without the leading #):
TS Playground link
function parseTags (input: string): string[] {
return (input.match(/(?:^#|[\s]#)[^\s]+/gu) ?? []).map(s => s.trim().slice(1));
}
Here's a working example in a functional component which incorporates the function:
<script src="https://unpkg.com/react#17.0.2/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#17.0.2/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/#babel/standalone#7.16.4/babel.min.js"></script>
<div id="root"></div>
<script type="text/babel" data-type="module" data-presets="react">
const {useState} = React;
function parseTags (input) {
return (input.match(/(?:^#|[\s]#)[^\s]+/gu) ?? []).map(s => s.trim().slice(1));
}
function Example () {
const [value, setValue] = useState('');
const [tags, setTags] = useState([]);
const handleChange = (ev) => {
const {value} = ev.target;
setValue(value);
setTags(parseTags(value));
};
return (
<div>
<input
type="text"
onChange={handleChange}
placeholder="Type here"
value={value}
/>
<div>Parsed tags:</div>
<ol>
{tags.map((str, index) => <li key={`${index}.${str}`}>{str}</li>)}
</ol>
</div>
);
}
ReactDOM.render(<Example />, document.getElementById('root'));
</script>
Something like this should work for you; You can adapt if you don't have access to hooks:
const RecorderInput = ({ onChange }) => {
const [isRecording, setIsRecording] = useState(false);
const toggleRecording = (e) => {
const character = String.fromCharCode(e.charCode);
if (character === '#') {
setIsRecording(true);
}
if (character === ' ') {
setIsRecording(false);
}
}
const handleChange = (e) => {
if (isRecording) onChange(e);
toggleRecording(e);
}
<input type="text" onChange={handleChange} />
}
As other suggested your onChange can also use regex groups to capture hashes as the user types. Thinking about this now, it would probably be a lot cleaner to do it this way but regex is well documented so I won't go through the hassle

How to debounce a function?

I am trying to set a timer for onChange but I don't understand why it doesn't work. I tried several methods, but none of them worked. What can I change to make it work?
import React, {useState} from 'react'
import { debounce } from "lodash";
const Search = ({getQuery}) => {
const [text, setText] = useState('')
const onChange = (q) => {
setText(q)
getQuery(q)
}
const handleDebounce = () => {
debounce(onChange, 3000);
};
return (
<section className='search'>
<form>
<input
type='text'
className='form-control'
placeholder='Search characters'
value={text}
onChange={(e) => handleDebounce( onChange(e.target.value), 3000)}
autoFocus
/>
</form>
</section>
)}
export default Search
removce handleDebounce completely and call your own onChange at the input onChange
onChange={onChange}
then adjust your onChange implementation as:
const onChange = (e) => {
const query = e.target.value;
setText(query);
debounce(() => getQuery(query), 3000);
}

How do I guarantee that reset is called before add using useState

I created a custom hook
const useMessageStorage = () => {
const [messages, setMessages] = useState([]);
const addMessages = (newMessage) => {
setMessages(oldMessages => [...oldMessages, newMessage)]);
}
const clear = () => {
setMessages([]);
}
return { clear, addMessages }
}
Inside the consumer component, I want to do something like
const { clear, addMessages } = useMessageStorage();
...
clear() // clear before adding new messages
addMessage('new message')
The above doesn't work because both calls are asynchronous. But I want to clear all messages before adding a new message in some specific scenario. How to tackle this problem? I considered using useRef but I'm afraid of using it because of race condition.
React state updates are asynchronously processed, this is true, but they are also processed in the order they are enqueued in. Since your addMessages callback uses a functional state update it will update from the previous state, not the state of the render cycle it was enqueued in.
// clear()
setMessages([]); // "draft" state: []
// addMessage(newMessage)
setMessages((oldMessages) => [ // updated from "draft" state: [newMessage]
...oldMessages,
newMessage,
]);
Your code does work.
const useMessageStorage = () => {
const [messages, setMessages] = React.useState([]);
const addMessages = (newMessage) => {
setMessages((oldMessages) => [...oldMessages, newMessage]);
};
const clear = () => {
setMessages([]);
};
return { clear, messages, addMessages };
};
function App() {
const [message, setMessage] = React.useState("");
const { clear, messages, addMessages } = useMessageStorage();
const addMessage = () => {
if (message) {
addMessages(message);
setMessage("");
}
};
const clearAndAddMessage = () => {
if (message) {
clear();
addMessages(message);
setMessage("");
}
};
return (
<div className="App">
<label>
Message:
<input
type="text"
id="message"
onChange={(e) => setMessage(e.target.value)}
value={message}
/>
</label>
<button type="button" onClick={addMessage}>
Add Message
</button>
<button type="button" onClick={clearAndAddMessage}>
Clear & Add Message
</button>
<ul>
{messages.map((message, i) => (
<li key={i}>{message}</li>
))}
</ul>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(
<App />,
rootElement
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>
<div id="root" />

How to debounce Formik Field ReactJS

I want to debounce Formik <Field/> but when I type in the field seems debounce does not work. Also I have tried lodash.debounce, throttle-debounce and the same result. How to solve this?
CodeSandbox - https://codesandbox.io/s/priceless-nobel-7p6nt
Snippet:
import ReactDOM from "react-dom";
import { withFormik, Field, Form } from "formik";
const App = ({ setFieldValue }) => {
let timeout;
const [text, setText] = useState("");
const onChange = text => {
if (timeout) clearTimeout(timeout);
timeout = setTimeout(() => setText(text), 750);
};
return (
<Form>
<Field
type="text"
name="textField"
placeholder="Type something..."
onChange={e => {
onChange(e.target.value);
setFieldValue("textField", e.target.value);
}}
style={{ width: "100%" }}
/>
<br />
<br />
<div>output: {text}</div>
</Form>
);
};
const Enhanced = withFormik({
mapPropsToValues: () => ({
textField: ""
}),
handleSubmit: (values, { setSubmitting }) => {
setSubmitting(false);
return false;
}
})(App);
ReactDOM.render(<Enhanced />, document.getElementById("root"));
const [text, setText] = useState("");
const [t, setT] = useState(null);
const onChange = text => {
if (t) clearTimeout(t);
setT(setTimeout(() => setText(text), 750));
};
I would like to suggest to move the call inside of timeout function.
const App = ({ setFieldValue }) => {
let timeout;
const [text, setText] = useState("");
const onChange = text => {
if (timeout) clearTimeout(timeout);
timeout = setTimeout(() => {
setText(text);
//changing value in container
setFieldValue("textField", text);
}, 750);
};
return (
<Form>
<Field
type="text"
name="textField"
placeholder="Type something..."
onChange={e => {
onChange(e.target.value);
}}
style={{ width: "100%" }}
/>
<br />
<br />
<div>output: {text}</div>
</Form>
);
};
Using Custom Hooks
This is abstracted from the answer provided by #Skyrocker
If you find yourself using this pattern a lot you can abstract it out to a custom hook.
hooks/useDebouncedInput.js
const useDebouncedInput = ({ defaultText = '', debounceTime = 750 }) => {
const [text, setText] = useState(defaultText)
const [t, setT] = useState(null)
const onChange = (text) => {
if (t) clearTimeout(t)
setT(setTimeout(() => setText(text), debounceTime))
}
return [text, onChange]
}
export default useDebouncedInput
components/my-component.js
const MyComponent = () => {
const [text, setTextDebounced] = useDebouncedInput({ debounceTime: 200 })
return (
<Form>
<Field
type="text"
name="textField"
placeholder="Type something..."
onChange={(e) => setTextDebounced(e.target.value)}
/>
<div>output: {text}</div>
</Form>
)
}
An Example Using Redux, Fetching, and Validation
Here's a partial example of using a custom hook for a debounced field validator.
Note: I did notice that Field validation seems to not validate onChange but you can expect it onBlur when you leave the field after your debounced update has executed (I did not try racing it or with a long debounce to see what happens). This is likely a bug that should be opened (I'm in the process of opening a ticket).
hooks/use-debounced-validate-access-code.js
const useDebouncedValidateAccessCode = () => {
const [accessCodeLookUpValidation, setAccessCodeLookUpValidation] = useState()
const [debounceAccessCodeLookup, setDebounceAccessCodeLookup] = useState()
const dispatch = useDispatch()
const debouncedValidateAccessCode = (accessCodeKey, debounceTime = 500) => {
if (debounceAccessCodeLookup) clearTimeout(debounceAccessCodeLookup)
setDebounceAccessCodeLookup(
setTimeout(
() =>
setAccessCodeLookUpValidation(
dispatch(getAccessCode(accessCodeKey)) // fetch
.then(() => undefined) // async validation requires undefined for no errors
.catch(() => 'Invalid Access Code'), // async validation expects a string for an error
),
debounceTime,
),
)
return accessCodeLookUpValidation || Promise.resolve(undefined)
}
return debouncedValidateAccessCode
}
some-component.js
const SomeComponent = () => {
const debouncedValidateAccessCode = useDebouncedValidateAccessCode()
return (
<Field
type="text"
name="accessCode"
validate={debouncedValidateAccessCode}
/>
)
}

Categories