I recently wanted to design an input component with react hooks.
The component would check validation after entering input in 0.5 second.
my code like
const inputField = ({ name, type, hint, inputValue, setInput }) => {
// if there is default value, set default value to state
const [value, setValue] = useState(inputValue);
// all of validation are true for testing
const validCheck = () => true;
let timeout;
const handleChange = e => {
clearTimeout(timeout);
const v = e.target.value;
setValue(v);
timeout = setTimeout(() => {
// if valid
if (validCheck()) {
// do something...
}
}, 500);
};
return (
<SCinputField>
<input type={type} onChange={handleChange} />
</SCinputField>
);
};
unfortunately, it's not worked, because the timeout variable would renew every time after setValue.
I found react-hooks provide some feature like useRef to store variable.
Should I use it or shouldn't use react-hooks in this case?
Update
add useEffect
const inputField = ({ name, type, hint, inputValue, setInput }) => {
// if there is default value, set default value to state
const [value, setValue] = useState(inputValue);
// all of validation are true for testing
const validCheck = () => true;
let timeout;
const handleChange = e => {
const v = e.target.value;
setValue(v);
};
// handle timeout
useEffect(() => {
let timeout;
if (inputValue !== value) {
timeout = setTimeout(() => {
const valid = validCheck(value);
console.log('fire after a moment');
setInput({
key: name,
valid,
value
});
}, 1000);
}
return () => {
clearTimeout(timeout);
};
});
return (
<SCinputField>
<input type={type} onChange={handleChange} />
</SCinputField>
);
};
It looks worked, but I am not sure about it's a right way to use.
Here's how I would do it:
import React, {useState, useEffect, useRef} from 'react';
function InputField() {
const [value, setValue] = useState(''); // STATE FOR THE INPUT VALUE
const timeoutRef = useRef(null); // REF TO KEEP TRACK OF THE TIMEOUT
function validate() { // VALIDATE FUNCTION
console.log('Validating after 500ms...');
}
useEffect(() => { // EFFECT TO RUN AFTER CHANGE IN VALUE
if (timeoutRef.current !== null) { // IF THERE'S A RUNNING TIMEOUT
clearTimeout(timeoutRef.current); // THEN, CANCEL IT
}
timeoutRef.current = setTimeout(()=> { // SET A TIMEOUT
timeoutRef.current = null; // RESET REF TO NULL WHEN IT RUNS
value !== '' ? validate() : null; // VALIDATE ANY NON-EMPTY VALUE
},500); // AFTER 500ms
},[value]); // RUN EFFECT AFTER CHANGE IN VALUE
return( // SIMPLE TEXT INPUT
<input type='text'
value={value}
onChange={(e) => setValue(e.target.value)}
/>
);
}
WORKING EXAMPLE ON SNIPPET BELOW:
function InputField() {
const [value, setValue] = React.useState('');
const timeoutRef = React.useRef(null);
function validate() {
console.log('Validating after 500ms...');
}
React.useEffect(() => {
if (timeoutRef.current !== null) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = setTimeout(()=> {
timeoutRef.current = null;
value !== '' ? validate() : null;
},500);
},[value]);
return(
<input type='text' value={value} onChange={(e) => setValue(e.target.value)}/>
);
}
ReactDOM.render(<InputField/>, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id="root"/>
You don't need to keep the reference to the timeout between renders. You can just return a function from the useEffect to clear it:
React.useEffect(() => {
const timeout = setTimeout(()=> {
if (value !== '') {
validate();
}
}, 500);
return () => {
clearTimeout(timeout); // this guarantees to run right before the next effect
}
},[value, validate]);
Also, don't forget to pass all the dependencies to the effect, including the validate function.
Ideally, you would pass the value as a parameter to the validate function: validate(value) - this way, the function has fewer dependencies, and could even be pure and moved outside the component.
Alternatively, if you have internal dependencies (like another setState or an onError callback from props), create the validate function with a useCallback() hook :
const validate = useCallback((value) => {
// do something with the `value` state
if ( /* value is NOT valid */ ) {
onError(); // call the props for an error
} else {
onValid();
}
}, [onError, onValid]); // and any other dependencies your function may use
This will keep the same function reference between the renders if the dependencies don't change.
You can move timeout variable inside handleChange method.
const inputField = ({ name, type, hint, inputValue, setInput }) => {
// if there is default value, set default value to state
const [value, setValue] = useState(inputValue);
// all of validation are true for testing
const validCheck = () => true;
const handleChange = e => {
let timeout;
clearTimeout(timeout);
const v = e.target.value;
setValue(v);
timeout = setTimeout(() => {
// if valid
if (validCheck()) {
// do something...
}
}, 500);
};
return (
<SCinputField>
<input type={type} onChange={handleChange} />
</SCinputField>
);
};
Related
Requirement
I have a requirement to get the editor state in JSON format as well as the text content of the editor. In addition, I want to receive these values in the debounced way.
I wanted to get these values (as debounced) because I wanted to send them to my server.
Dependencies
"react": "^18.2.0",
"lexical": "^0.3.8",
"#lexical/react": "^0.3.8",
You don't need to touch any of Lexical's internals for this; a custom hook that reads and "stashes" the editor state into a ref and sets up a debounced callback (via use-debounce here, but you can use whatever implementation you like) is enough.
getEditorState is in charge of converting the editor state into whichever format you want to send over the wire. It's always called within editorState.read().
function useDebouncedLexicalOnChange<T>(
getEditorState: (editorState: EditorState) => T,
callback: (value: T) => void,
delay: number
) {
const lastPayloadRef = React.useRef<T | null>(null);
const callbackRef = React.useRef<(arg: T) => void | null>(callback);
React.useEffect(() => {
callbackRef.current = callback;
}, [callback]);
const callCallbackWithLastPayload = React.useCallback(() => {
if (lastPayloadRef.current) {
callbackRef.current?.(lastPayloadRef.current);
}
}, []);
const call = useDebouncedCallback(callCallbackWithLastPayload, delay);
const onChange = React.useCallback(
(editorState) => {
editorState.read(() => {
lastPayloadRef.current = getEditorState(editorState);
call();
});
},
[call, getEditorState]
);
return onChange;
}
// ...
const getEditorState = (editorState: EditorState) => ({
text: $getRoot().getTextContent(false),
stateJson: JSON.stringify(editorState)
});
function App() {
const debouncedOnChange = React.useCallback((value) => {
console.log(new Date(), value);
// TODO: send to server
}, []);
const onChange = useDebouncedLexicalOnChange(
getEditorState,
debouncedOnChange,
1000
);
// ...
<OnChangePlugin onChange={onChange} />
}
Code
File: onChangeDebouce.tsx
import {$getRoot} from "lexical";
import { useLexicalComposerContext } from "#lexical/react/LexicalComposerContext";
import React from "react";
const CAN_USE_DOM = typeof window !== 'undefined' && typeof window.document !== 'undefined' && typeof window.document.createElement !== 'undefined';
const useLayoutEffectImpl = CAN_USE_DOM ? React.useLayoutEffect : React.useEffect;
var useLayoutEffect = useLayoutEffectImpl;
type onChangeFunction = (editorStateJson: string, editorText: string) => void;
export const OnChangeDebounce: React.FC<{
ignoreInitialChange?: boolean;
ignoreSelectionChange?: boolean;
onChange: onChangeFunction;
wait?: number
}> = ({ ignoreInitialChange= true, ignoreSelectionChange = false, onChange, wait= 167 }) => {
const [editor] = useLexicalComposerContext();
let timerId: NodeJS.Timeout | null = null;
useLayoutEffect(() => {
return editor.registerUpdateListener(({
editorState,
dirtyElements,
dirtyLeaves,
prevEditorState
}) => {
if (ignoreSelectionChange && dirtyElements.size === 0 && dirtyLeaves.size === 0) {
return;
}
if (ignoreInitialChange && prevEditorState.isEmpty()) {
return;
}
if(timerId === null) {
timerId = setTimeout(() => {
editorState.read(() => {
const root = $getRoot();
onChange(JSON.stringify(editorState), root.getTextContent());
})
}, wait);
} else {
clearTimeout(timerId);
timerId = setTimeout(() => {
editorState.read(() => {
const root = $getRoot();
onChange(JSON.stringify(editorState), root.getTextContent());
});
}, wait);
}
});
}, [editor, ignoreInitialChange, ignoreSelectionChange, onChange]);
return null;
}
This is the code for the plugin and it is inspired (or copied) from OnChangePlugin of lexical
Since, lexical is in early development the implementation of OnChangePlugin might change. And in fact, there is one more parameter added as of version 0.3.8. You can check the latest code at github.
The only thing I have added is calling onChange function in timer logic.
ie.
if(timerId === null) {
timerId = setTimeout(() => {
editorState.read(() => {
const root = $getRoot();
onChange(JSON.stringify(editorState), root.getTextContent());
})
}, wait);
} else {
clearTimeout(timerId);
timerId = setTimeout(() => {
editorState.read(() => {
const root = $getRoot();
onChange(JSON.stringify(editorState), root.getTextContent());
});
}, wait);
}
If you are new to lexical, then you have to use declare this plugin as a child of lexical composer, something like this.
File: RichEditor.tsx
<LexicalComposer initialConfig={getRichTextConfig(namespace)}>
<div className="editor-shell lg:m-2" ref={scrollRef}>
<div className="editor-container">
{/* your other plugins */}
<RichTextPlugin
contentEditable={<ContentEditable
className={"ContentEditable__root"} />
}
placeholder={<Placeholder text={placeHolderText} />}
/>
<OnChangeDebounce onChange={onChange} />
</div>
</div>
</LexicalComposer>
In this code, as you can see I have passed the onChange function as a prop and you can also pass wait in milliseconds like this.
<OnChangeDebounce onChange={onChange} wait={1000}/>
Now the last bit is the implementation of onChange function, which is pretty straightforward
const onChange = (editorStateJson:string, editorText:string) => {
console.log("editorStateJson:", editorStateJson);
console.log("editorText:", editorText);
// send data to a server or to your data store (eg. redux)
};
Finally
Thanks to Meta and the lexical team for open sourcing this library. And lastly, the code I have provided works for me, I am no expert, feel free to comment to suggest an improvement.
I did search for those related issues and found some solutions, but most about the lodash debounce. In my case, I create useDebounce as a custom hook and return the value directly.
My current issue is useCallback works with an old debounced value.
Here are my code snips.
//To makes sure that the code is only triggered once per user input and send the request then.
export const useDebounce = (value, delay) => {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timeout = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(timeout);
}, [value, delay]);
return debouncedValue;
};
useDebounce works as expected
export const ShopQuantityCounter = ({ id, qty }) => {
const [value, setValue] = useState(qty);
const debounceInput = useDebounce(value, 300);
const dispatch = useDispatch();
const handleOnInputChange = useCallback((e) => {
setValue(e.target.value);
console.info('Inside OnChange: debounceInput', debounceInput);
// dispatch(updateCartItem({ id: id, quantity: debounceInput }));
},[debounceInput]);
console.info('Outside OnChange: debounceInput', debounceInput);
// To fixed issue that useState set method not reflecting change immediately
useEffect(() => {
setValue(qty);
}, [qty]);
return (
<div className="core-cart__quantity">
<input
className="core-cart__quantity--total"
type="number"
step="1"
min="1"
title="Qty"
value={value}
pattern="^[0-9]*[1-9][0-9]*$"
onChange={handleOnInputChange}
/>
</div>
);
};
export default ShopQuantityCounter;
Here are screenshots with console.info to explain what the issue is.
Current quantity
Updated with onChange
I do appreciate it if you have any solution to fix it, and also welcome to put forward any code that needs updates.
This might help you achieve what you want. You can create a reusable debounce function with the callback like below.
export const useDebounce = (value, delay) => {
const [debouncedValue, setDebouncedValue] = useState(value);
let timeout;
const setDebounce = (newValue) => {
clearTimeout(timeout);
timeout = setTimeout(() => setDebouncedValue(newValue), delay);
};
return [debouncedValue, setDebounce];
};
And use the function on your code like this.
export const ShopQuantityCounter = ({ id, qty }) => {
const [value, setValue] = useState(qty);
const [debounceInput, setDebounceInput] = useDebounce(value, 300);
const dispatch = useDispatch();
const handleOnInputChange = useCallback((e) => {
setDebounceInput(e.target.value);
console.info('Inside OnChange: debounceInput', debounceInput);
// dispatch(updateCartItem({ id: id, quantity: debounceInput }));
},[debounceInput]);
console.info('Outside OnChange: debounceInput', debounceInput);
// To fixed issue that useState set method not reflecting change immediately
useEffect(() => {
setValue(qty);
}, [qty]);
return (
<div className="core-cart__quantity">
<input
className="core-cart__quantity--total"
type="number"
step="1"
min="1"
title="Qty"
value={value}
pattern="^[0-9]*[1-9][0-9]*$"
onChange={handleOnInputChange}
/>
</div>
);
};
export default ShopQuantityCounter;
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 need to use an input filter in React. I have a list of activities and need to filter them like filters on the picture. If the icons are unchecked, actions with these types of activities should not be showed. It works.
The problem that when I use input filter and write letters it works. But when I delete letter by letter nothing changes.
I understand that the problem is that I write the result in the state. And the state is changed.But how to rewrite it correctly.
const [activities, setActivities] = useState(allActivities);
const [value, setValue] = useState(1);
const [checked, setChecked] = useState<string[]>([]);
const switchType = (event: React.ChangeEvent<HTMLInputElement>)=> {
const currentIndex = checked.indexOf(event.target.value);
const newChecked = [...checked];
if (currentIndex === -1) {
newChecked.push(event.target.value);
} else {
newChecked.splice(currentIndex, 1);
}
//function that shows activities if they are checked or unchecked
setChecked(newChecked);
const res = allActivities.filter(({ type }) => !newChecked.includes(type));
setActivities(res);
};
//shows input filter
const inputSearch = (event: React.ChangeEvent<HTMLInputElement>) => {
const foundItems = activities.filter(
(item) => item.activity.indexOf(event.target.value) > -1
);
setActivities(foundItems);
};
//shows participants filter
const countSearch = (event: React.ChangeEvent<HTMLInputElement>) => {
setValue(Number(event.target.value));
const participantsSearch = allActivities.filter(
(item) => item.participants >= event.target.value
);
setActivities(participantsSearch);
};
This is render part
<Input
onChange={inputSearch}
startAdornment={
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
}
/>
<Input
onChange={countSearch}
type="number"
value={props.value}
startAdornment={
<InputAdornment
position="start"
className={classes.participantsTextField}
>
<PersonIcon />
</InputAdornment>
}
/>
The issue here is that you save over the state that you are filtering, so each time a filter is applied the data can only decrease in size or remain the same. It can never reset back to the full, unfiltered data.
Also with the way you've written the checkbox and input callbacks you can't easily mix the two.
Since the filtered data is essentially "derived" state from the allActivities prop, and the value and checked state, it really shouldn't also be stored in state. You can filter allActivities inline when rendering.
const [value, setValue] = useState<sting>('');
const [checked, setChecked] = useState<string[]>([]);
const switchType = (event: React.ChangeEvent<HTMLInputElement>)=> {
const currentIndex = checked.indexOf(event.target.value);
if (currentIndex === -1) {
setChecked(checked => [...checked, event.target.value]);
} else {
setChecked(checked => checked.filter((el, i) => i !== currentIndex);
}
};
const inputSearch = (event: React.ChangeEvent<HTMLInputElement>) => {
setValue(event.target.value.toLowerCase());
};
...
return (
...
{allActivites.filter(({ activity, type }) => {
if (activity || type) {
if (type) {
return checked.includes(type);
}
if (activity) {
return activity.toLowerCase().includes(value);
}
}
return true; // return all
})
.map(.....
The problem lies with your inputSearch code.
Each time you do setActivities(foundItems); you narrow down the state of your activities list. So when you start deleting, you don't see any change because you removed the rest of the activities from the state.
You'll want to take out allActivities into a const, and always filter allActivities in inputSearch, like so:
const allActivities = ['aero', 'aeroba', 'aerona', 'aeronau'];
const [activities, setActivities] = useState(allActivities);
// ...rest of your code
const inputSearch = (event: React.ChangeEvent<HTMLInputElement>) => {
const foundItems = allActivities.filter(
(item) => item.activity.indexOf(event.target.value) > -1
);
setActivities(foundItems);
};
// ...rest of your code
I have an React Functional Component which recieves a prop with a variable from useState(). This works fine but if I use it in an EventListener it does not get updated. I tried the following but still it does not work.
Maybe someone can explain why.
Thanks
I would expect x to be the updated number but it always has the value of the initial setup of the EventHandler.
import React, { useState, useEffect } from "react";
import ReactDom from "react-dom";
const App = () => {
const [num, setNum] = useState(50);
return (
<div>
<button onClick={() => setNum(prev => prev + 1)}>Test</button>
<div>{num}</div>
<Child num={num} />
</div>
);
};
const Child = ({ num }) => {
let x = 0;
const md = () => {
console.log(num, x);
};
useEffect(() => {
x = num;
}, [num]);
useEffect(() => {
document.getElementById("box").addEventListener("mousedown", md);
return () => {
document.removeEventListener("mousedown", md);
};
}, []);
return <div id="box">click {num}</div>;
};
ReactDom.render(<App />, document.getElementById("app"));
Each render of your Child will get a new x, a new props object, etc. However you are binding your event listener only once and so capturing only the initial props.num value.
Two ways to fix:
Rebind event listener when num changes, by passing num as a dependency to your effect to bind the event listener:
const Child = ({ num }) => {
useEffect(() => {
// no need to define this in main function since it is only
// used inside this effect
const md = () => { console.log(num); };
document.getElementById("box").addEventListener("mousedown", md);
return () => {
document.removeEventListener("mousedown", md);
};
}, [num]);
return <div id="box">click {num}</div>;
};
Or use a ref to hold the value of num and bind your event listener to the ref. This gives you a level of indirection to handle the change:
const Child = ({ num }) => {
const numRef = useRef(); // will be same object each render
numRef.current = num; // assign new num value each render
useEffect(() => {
// no need to define this in main function since it is only
// used inside this effect
// binds to same ref object, and reaches in to get current num value
const md = () => { console.log(numRef.current); };
document.getElementById("box").addEventListener("mousedown", md);
return () => {
document.removeEventListener("mousedown", md);
};
}, []);
return <div id="box">click {num}</div>;
};