How to make a custom debounce hook works with a useCallback? - reactjs

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;

Related

Don't update children use react useContext

I have two children Components, when I onChange in first children, then the second children re render, I don't want to the second children re render. Online code example: https://codesandbox.io/s/billowing-glitter-r5gnh3?file=/src/App.js:1287-1297
const EditSceneModalStore = React.createContext(undefined);
const Parent = () => {
const [saveValue, setSaveValue] = React.useState({});
const initValue = {
name: "zhang",
age: 3
};
const onSave = () => {
console.log("===saveValue==", saveValue);
};
const onChangeValue = (key, value) => {
const newValue = {
...saveValue,
[key]: value
};
setSaveValue(newValue);
};
return (
<EditSceneModalStore.Provider
value={{
initValue,
onChangeValue
}}
>
<ChildInput1 />
<ChildInput2 />
<Button onClick={onSave} type="primary">
save
</Button>
</EditSceneModalStore.Provider>
);
};
const ChildInput1 = () => {
const { onChangeValue, initValue } = React.useContext(EditSceneModalStore);
const [value, setValue] = React.useState(initValue.name);
return (
<Input
value={value}
onChange={(v) => {
setValue(v.target.value);
onChangeValue("name", v.target.value);
}}
/>
);
};
const ChildInput2 = () => {
const { initValue, onChangeValue } = React.useContext(EditSceneModalStore);
const [value, setValue] = React.useState(initValue.InputNumber);
console.log("====ChildInput2===");
return (
<InputNumber
value={value}
onChange={(v) => {
setValue(v.target.value);
onChangeValue("age", v.target.value);
}}
/>
);
};
when I onChange in ChildInput1, then ChildInput2 re-render, I don't want to the ChildInput2 re-render. Example image
As Andrey explained, you should fix the following line:
//you have
const [value, setValue] = React.useState(initValue.InputNumber);
// should be
const [value, setValue] = React.useState(initValue.age);
Additionally, initValue gets unnecessarily re-computed on every re-render, so it should be outside the scope of Parent:
const initValue = {
name: "zhang",
age: 3
};
const Parent = () => {...}
Regarding re renderings, it is ok. When a Provider gets the value changed, all their childs wrapped in a consumer rerender. This is natural. This post explains why.
A component calling useContext will always re-render when the context
value changes. If re-rendering the component is expensive, you can
optimize it by using memoization.
In this case, it is not expensive enough to consider memoization.
I Hope it helps
You have a typo in your code:
//you have
const [value, setValue] = React.useState(initValue.InputNumber);
// should be
const [value, setValue] = React.useState(initValue.age);
also update like that
<InputNumber
value={value}
onChange={(value) => {
setValue(value);
onChangeValue("age", value);
}}
/>
and when you fix like that do not worry about re-render as state of ChildInput2 will no be changed

persist state after page refresh in React using local storage

What I would like to happen is when displayBtn() is clicked for the items in localStorage to display.
In useEffect() there is localStorage.setItem("localValue", JSON.stringify(myLeads)) MyLeads is an array which holds leads const const [myLeads, setMyLeads] = useState([]); myLeads state is changed when the saveBtn() is clicked setMyLeads((prev) => [...prev, leadValue.inputVal]);
In DevTools > Applications, localStorage is being updated but when the page is refreshed localStorage is empty []. How do you make localStorage persist state after refresh? I came across this article and have applied the logic but it hasn't solved the issue. I know it is something I have done incorrectly.
import List from './components/List'
import { SaveBtn } from './components/Buttons';
function App() {
const [myLeads, setMyLeads] = useState([]);
const [leadValue, setLeadValue] = useState({
inputVal: "",
});
const [display, setDisplay] = useState(false);
const handleChange = (event) => {
const { name, value } = event.target;
setLeadValue((prev) => {
return {
...prev,
[name]: value,
};
});
};
const localStoredValue = JSON.parse(localStorage.getItem("localValue")) ;
const [localItems] = useState(localStoredValue || []);
useEffect(() => {
localStorage.setItem("localValue", JSON.stringify(myLeads));
}, [myLeads]);
const saveBtn = () => {
setMyLeads((prev) => [...prev, leadValue.inputVal]);
// setLocalItems((prevItems) => [...prevItems, leadValue.inputVal]);
setDisplay(false);
};
const displayBtn = () => {
setDisplay(true);
};
const displayLocalItems = localItems.map((item) => {
return <List key={item} val={item} />;
});
return (
<main>
<input
name="inputVal"
value={leadValue.inputVal}
type="text"
onChange={handleChange}
required
/>
<SaveBtn saveBtn={saveBtn} />
<button onClick={displayBtn}>Display Leads</button>
{display && <ul>{displayLocalItems}</ul>}
</main>
);
}
export default App;```
You've fallen into a classic React Hooks trap - because using useState() is so easy, you're actually overusing it.
If localStorage is your storage mechanism, then you don't need useState() for that AT ALL. You'll end up having a fight at some point between your two sources about what is "the right state".
All you need for your use-case is something to hold the text that feeds your controlled input component (I've called it leadText), and something to hold your display boolean:
const [leadText, setLeadText] = useState('')
const [display, setDisplay] = useState(false)
const localStoredValues = JSON.parse(window.localStorage.getItem('localValue') || '[]')
const handleChange = (event) => {
const { name, value } = event.target
setLeadText(value)
}
const saveBtn = () => {
const updatedArray = [...localStoredValues, leadText]
localStorage.setItem('localValue', JSON.stringify(updatedArray))
setDisplay(false)
}
const displayBtn = () => {
setDisplay(true)
}
const displayLocalItems = localStoredValues.map((item) => {
return <li key={item}>{item}</li>
})
return (
<main>
<input name="inputVal" value={leadText} type="text" onChange={handleChange} required />
<button onClick={saveBtn}> Save </button>
<button onClick={displayBtn}>Display Leads</button>
{display && <ul>{displayLocalItems}</ul>}
</main>
)

How to rewrite state correctly in filters React

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

How to prevent ReactSelect from clearing the input?

The ReactSelect component in my app is clearing what I have typed after I make a selection. I don't want it to do that.
I want it to leave it as is. When the user has not entered anything, I want it to show the Placeholder text.
I'm using this component as a search box. Selecting an item from the list accomplishes the task. There's no reason to set the component's value to the selection.
react-select does not support autocompletion out of the box so it requires a bit of extra work and hacks to get what you want.
First off, you need to control the inputValue state, some operations like menu-close or set-value will clear the input afterward.
const [input, setInput] = React.useState("");
const handleInputChange = (e, meta) => {
if (meta.action === "input-change") {
setInput(e);
}
};
return (
<Select
value={selected}
onChange={handleChange}
inputValue={input}
isSearchable
{...}
/>
);
react-select also hides the input after an option is selected so you also need to override that behavior as well.
const [selected, setSelected] = React.useState([]);
const handleChange = (s) => {
setSelected({ ...s });
};
React.useLayoutEffect(() => {
const inputEl = document.getElementById("myInput");
if (!inputEl) return;
// prevent input from being hidden after selecting
inputEl.style.opacity = "1";
}, [selected]);
return (
<Select
value={selected}
inputId="myInput"
onChange={handleChange}
{...}
/>
);
Last but not least, you may want to update your input accordingly after a successful selection, here is a basic example, the code below will append the new option value to your current input after a selection.
Also make sure to override the default filterOption to only taking into account the last word when filtering or nothing will match with the options after the first few words.
const [selected, setSelected] = React.useState([]);
const [input, setInput] = React.useState("");
const handleChange = (s) => {
setSelected({ ...s });
setInput((input) => removeLastWord(input) + s.value);
};
const customFilter = () => {
return (config, rawInput) => {
const filter = createFilter(null);
return filter(config, getLastWord(rawInput));
};
};
return (
<Select
value={selected}
filterOption={customFilter()}
onChange={handleChange}
inputValue={input}
components={{
SingleValue: () => null
}}
{...}
/>
);
Where removeLastWord and getLastWord are just some utility functions.
function getLastWord(str: string) {
return str.split(" ").slice(-1).pop();
}
function removeLastWord(str: string) {
var lastWhiteSpaceIndex = str.lastIndexOf(" ");
return str.substring(0, lastWhiteSpaceIndex + 1);
}
Here is a complete example after combining all of the above.
import React from "react";
import Select, { components, createFilter } from "react-select";
import options from "./options";
function getLastWord(str: string) {
return str.split(" ").slice(-1).pop();
}
function removeLastWord(str: string) {
var lastWhiteSpaceIndex = str.lastIndexOf(" ");
return str.substring(0, lastWhiteSpaceIndex + 1);
}
export default function MySelect() {
const [selected, setSelected] = React.useState([]);
const [input, setInput] = React.useState("");
const handleChange = (s) => {
setSelected({ ...s });
setInput((input) => removeLastWord(input) + s.value);
};
const handleInputChange = (e, meta) => {
if (meta.action === "input-change") {
setInput(e);
}
};
React.useLayoutEffect(() => {
const inputEl = document.getElementById("myInput");
if (!inputEl) return;
// prevent input from being hidden after selecting
inputEl.style.opacity = "1";
}, [selected]);
const customFilter = () => {
return (config, rawInput) => {
const filter = createFilter(null);
return filter(config, getLastWord(rawInput));
};
};
return (
<Select
value={selected}
filterOption={customFilter()}
inputId="myInput"
onChange={handleChange}
blurInputOnSelect={false}
inputValue={input}
onInputChange={handleInputChange}
options={options}
isSearchable
hideSelectedOptions={false}
components={{
SingleValue: () => null
}}
/>
);
}
Live Example
You can also override the input styles using the styles props. There is a whole discussion about this issue on this thread
{
input: (provided) => ({
...provided,
input: {
opacity: "1 !important",
},
}),
}

react hooks setTimeout after setState

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>
);
};

Resources