Debouncing MUI Autocomplete not working properly - reactjs

I'm trying to implement debounce for mui autocomplete,
but it's not working well.
I want to send a debounced request to server when inputValue change.
Am I miss something?
It looks loadData fired every input change. Only first load debounce works.
https://codesandbox.io/s/debounce-not-working-j4ixgg?file=/src/App.js
Here's the code from the sandbox:
import { useState, useCallback } from "react";
import { Autocomplete, TextField } from "#mui/material";
import { debounce } from "lodash";
import topFilms from "./topFilms";
export default function App() {
const [value, setValue] = useState(null);
const [inputValue, setInputValue] = useState("");
const [options, setOptions] = useState([]);
const loadData = () => {
// sleep(1000)
const filteredOptions = topFilms.filter((f) =>
f.title.includes(inputValue)
);
// This log statement added by Ryan Cogswell to show why it isn't working.
console.log(
`loadData with ${filteredOptions.length} options based on "${inputValue}"`
);
setOptions(filteredOptions);
};
const debouncedLoadData = useCallback(debounce(loadData, 1000), []);
const handleInputChange = (e, v) => {
setInputValue(v);
debouncedLoadData();
};
const handleChange = (e, v) => {
setValue(v);
};
return (
<div className="App">
<Autocomplete
value={value}
inputValue={inputValue}
onChange={handleChange}
onInputChange={handleInputChange}
disablePortal
options={options}
getOptionLabel={(option) => option.title}
isOptionEqualToValue={(option, value) => option.title === value.title}
id="combo-box-demo"
renderInput={(params) => <TextField {...params} label="Movie" />}
/>
</div>
);
}

tldr: codesandbox link
You can accomplish this with a few hooks working together. First lets look at a hook for debouncing state:
function useDebounce(value, delay, initialValue) {
const [state, setState] = useState(initialValue);
useEffect(() => {
console.log("delaying", value);
const timer = setTimeout(() => setState(value), delay);
// clear timeout should the value change while already debouncing
return () => {
clearTimeout(timer);
};
}, [value, delay]);
return state;
}
The above takes in a value and returns the debounced value from the hook later.
The callback at the end of the useEffect prevents a bunch of timers triggering one after the other.
Then your component can be reduced to:
export default function App() {
const [value, setValue] = useState(null);
const [inputValue, setInputValue] = useState("");
const [options, setOptions] = useState([]);
const debouncedValue = useDebounce(inputValue, 1000);
// fetch data from server
useEffect(() => {
console.log("fetching", debouncedValue);
const filteredOptions = topFilms.filter((f) =>
f.title.includes(debouncedValue)
);
setOptions(filteredOptions);
}, [debouncedValue]);
const handleInputChange = (e, v) => {
setInputValue(v);
};
const handleChange = (e, v) => {
setValue(v);
};
return (
<div className="App">
<Autocomplete
// props
/>
</div>
);
}
The useEffect here is run when the dependency debouncedValue is changed via react state business.
When typing in the TextField your console should look like:
delaying s
delaying sp
delaying spi
fetching spi
delaying spir
fetching spir
This useEffect in App leaves you with a good place to do your fetch to a server like you mentioned you'll need.
The useEffect could easily be replaced with something like useQuery

In your sandbox, loadData was being successfully debounced, however it was always executed with inputValue === "" which then matched all of the options. All of the actual filtering was then being immediately done by the Autocomplete component just the same as if you had provided the full set of options statically. Due to the behavior of JavaScript closures, inputValue is always the empty string because that is its initial value as you create the debounced version of loadData.
Whenever creating debounced functions, I recommend declaring and defining the original function and the debounced version at the top level rather than inside your component. Any local variables and state that your debounced function is dependent on can be passed in as arguments to the function (e.g. in my version of your code, the input value and the setOptions function are passed to debouncedLoadData). This avoids accidentally using stale values due to closure behavior and removes the need for useCallback.
Below is a reworked version of your code that moves loadData and debouncedLoadData to the top level. I also changed loadData to provide no data (rather than all data) when it is passed an empty string so that the debounce behavior is easier to see -- otherwise whenever the Autocomplete has the full set of options, the filtering will occur immediately based on the options that the Autocomplete already has. This also more closely simulates what real code would likely do (i.e. avoid calling the back end for empty string).
import { useState } from "react";
import { Autocomplete, TextField } from "#mui/material";
import { debounce } from "lodash";
import topFilms from "./topFilms";
const loadData = (inputValue, setOptions) => {
if (inputValue.length === 0) {
console.log("no options");
setOptions([]);
return;
}
const filteredOptions = topFilms.filter((f) =>
f.title.toLowerCase().includes(inputValue.toLowerCase())
);
console.log(
`loadData with ${filteredOptions.length} options based on "${inputValue}"`
);
setOptions(filteredOptions);
};
const debouncedLoadData = debounce(loadData, 1000);
export default function App() {
const [value, setValue] = useState(null);
const [inputValue, setInputValue] = useState("");
const [options, setOptions] = useState([]);
const handleInputChange = (e, v) => {
setInputValue(v);
debouncedLoadData(v, setOptions);
};
const handleChange = (e, v) => {
setValue(v);
};
return (
<div className="App">
<Autocomplete
value={value}
inputValue={inputValue}
onChange={handleChange}
onInputChange={handleInputChange}
disablePortal
options={options}
getOptionLabel={(option) => option.title}
isOptionEqualToValue={(option, value) => option.title === value.title}
id="combo-box-demo"
renderInput={(params) => <TextField {...params} label="Movie" />}
/>
</div>
);
}

Related

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

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;

Element obtaining value from input only updating after the second time I press enter. Why is this happening? - ReactJS

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.

Proper Way of Searching in React with debounce

I have a problem searching using debounce in lodash. I'm using React, Material Ui and formik.
It says TypeError: Expected a function in
const debounceLoadData = useCallback( debounce(dispatch(getPersons(10, 1, filter)), 1000), [] );
const [filter, setFilter] = useState("");
const debounceLoadData = useCallback(
debounce(dispatch(getPersons(10, 1, filter)), 1000),
[]
);
const onSearchVoter = (value) => {
setFilter(value);
debounceLoadData(value);
};
<Autocomplete
value={values.voter_id}
options={persons ? persons : []}
getOptionSelected={(option, value) => option === value}
getOptionLabel={(person) =>
person
? [person?.fname, person?.mname, person?.lname].filter(Boolean).join(" ")
: ""
}
onChange={(e, value) => {
setFieldValue("voter_id", value ? value : "");
}}
onInputChange={async (event, value) => {
onSearchVoter(value);
}}
renderInput={(params) => (
<TextField
{...params}
name="voter_id"
label="Voter"
variant="outlined"
onBlur={handleBlur}
helperText={touched.voter_id ? errors.voter_id : ""}
error={touched.voter_id && Boolean(errors.voter_id)}
fullWidth
/>
)}
/>;
The _.debounce() function expects another function, and not the result of calling dispatch. You should wrap the dispatch call in an arrow function, and pass the filter value via the parameter value, and not as a dependency. In addition, add getPerson and dispatch as dependencies to the useCallback.
Note: the getPerson function should also be memoized (via useMemo or useCallback).
const [filter, setFilter] = useState('');
const debounceLoadData = useCallback(
debounce(value => dispatch(getPersons(10, 1, value)), 1000),
[getPersons, dispatch]
);
I do not find useCallback useful in debounce case. Instead use useMemo to create a fixed instance for dispatch (with fixed/variable input arguments).
const debouceLoadData = useMemo(() => {
return debounce(f => {
dispatch(getPersons(10, 1, f))
}, 1000)
}, [dispatch])
Now you can even do debounceLoadData(f) if you want to.
NOTE: the reason why useCallback doesn't work is that, practically you want to setState(v), the v is a variable. And also you don't want to invoke dispatch or getPersons when you are constructing the debounce. You only want to construct the instance, such as () => {} which is what useMemo covers.
This is a subtlety. But you can try useCallback first.

How to pass arguments to useCallback

How in this example from the Shopify Polaris library (ReactJS)
function TextFieldExample() {
const [value, setValue] = useState('Jaded Pixel');
const handleChange = useCallback((newValue) => setValue(newValue), []);
return <TextField label="Store name" value={value} onChange={handleChange} />;
}
is the argument newValue being passed to the useCallback?
I tried to lookup the source of the Polaris library but I couldn't come to a practical conclusion.
Edit: Maybe it would help to understand why this is necessary in React:
const useForceUpdate = ()
=> {
[value, setValue] = useState(0);
return () => setValue(value => value + 1);
}
To call this inside a component, I have to
const forceUpdate = useForceUpdate();
forceUpdate();
instead of just
useForceUpdate();
useCallback returns a function that will be used in onChange prop. Inside TextField component props.onChange will be called when value will be changed. Somehing like this (just an example):
//TextField
function TextFiled(props) {
const handleChange = (event) => {
props.onChange(event.target.value); // here you function that was returned by useCallback
}
return <input onChange={handleChange}/>
}

React hook not setting the state

Hey all trying to use a useState react hook to set a state but it does not work, I gone through the official documentation
Seems like i have followed it correctly but still cannot get the hook to set the state:
const [search, setSearch] = useState('');
const { films } = props;
const matchMovieSearch = (films) => {
return films.forEach(item => {
return item.find(({ title }) => title === search);
});
}
const handleSearch = (e) => {
setSearch(e.target.value);
matchMovieSearch(films);
}
<Form.Control
type="text"
placeholder="Search Film"
onChange={(e) => {handleSearch(e)}}
/>
Search var in useState is allways empty even when i debug and can see that e.target.value has to correct data inputed from the html field
setSearch is an async call, you won't be able to get the search immediately after setting the state.
useEffect is here for rescue.
useEffect(() => {
// your action
}, [search]);
Are you sure you are using the hooks inside a component, hooks can only be used in a Functional React Component.
If that is not the case, there must be something wrong with the Form.Control component, possibly like that component did not implement the onChanged parameter properly.
This is the one I tested with the html input element, and it is working fine. I used the useEffect hook to track the changes on the search variable, and the you can see that the variable is being properly updated.
https://codesandbox.io/s/bitter-browser-c4nrg
export default function App() {
const [search, setSearch] = useState("");
useEffect(() => {
console.log(`search was changed to ${search}`);
}, [search]);
const handleSearch = e => {
setSearch(e.target.value);
};
return (
<input
type="text"
onChange={e => {
handleSearch(e);
}}
/>
);
}

Resources