Proper Way of Searching in React with debounce - reactjs

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.

Related

Debouncing MUI Autocomplete not working properly

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

Rendered fewer hooks than expected. This may be caused by an accidental early return statement in React Hooks

The following component throws me this error message at runtime when I try to render the table as Input value (editable field) with the additional warning given below.
Warning: Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks. You can only call Hooks at the top level of your React function.
This is my code,
const EditableCell = (initialValue: any) => {
const [value, setValue] = React.useState(initialValue);
const onChange = (e: any) => {
setValue(e.target.value);
};
React.useEffect(() => {
setValue(initialValue);
}, [initialValue]);
return <Input type="text" value={value} onChange={onChange} />;
};
const ParameterTable: React.FC<ParameterTableProps> = () => {
const {
decisionMetadataInput: input,
decisionMetadataOutput: output,
paramList
} = testData.data;
const inMeta = React.useMemo(() => input?.metadata ?? [], [input]);
const outMeta = React.useMemo(() => output?.metadata ?? [], [output]);
const allColumn = React.useMemo(
() =>
[...inMeta, ...outMeta].map((meta) => (
<Column
title={
<>
<span>{meta.field}</span>
<br />
<Typography.Text italic>({meta.type})</Typography.Text>
</>
}
key={meta.field}
dataIndex={["data", meta.field]}
render={(dataIndex) => EditableCell(dataIndex)}
/>
)),
[inMeta, outMeta]
);
const datasource = React.useMemo(
() =>
paramList.map((param) => {
const inParam = param?.paramInput?.param ?? [];
const outParam = param?.paramOutput?.param ?? [];
const data = [...inParam, ...outParam].reduce(
(prev, current) => ({
...prev,
[current.field]: current.value
}),
{}
);
return { data, num: param.paramOrder };
}),
[paramList]
);
return (
<Table dataSource={datasource} rowKey="num" pagination={false}>
<Column title="F" dataIndex="num" />
{allColumn}
</Table>
);
};
This is my codesandbox URL - https://codesandbox.io/s/react-typescript-forked-hl179m?file=/src/ParameterTable.tsx
I suspect this happens because I call the function EditableCell inside the useMemo hook, however I'm not sure how to resolve this. Any help/ suggestion would do, thanks.
I'm trying to make the Antd React Table as editable fields, however when I tried to map the dataIndex as value in a seperate function to render in the column I get this 'Uncaught Error: Rendered fewer hooks than expected. This may be caused by an accidental early return statement in React Hooks'
Instead of calling EditableCell as if it were a plain function, wrap it in React.createElement so that React can see that it's a component (and can therefore have hooks called inside it).
Change to
render={(dataIndex) => <EditableCell initialValue={dataIndex} />}
and
const EditableCell = ({ initialValue }: { initialValue: any }) => {
It would also be a good idea to avoid any, which defeats the purpose of using TypeScript - wherever you have an any, identify the actual type that'll be used there, and use that type instead.

How to debounce mui Autocomplete

I need to debounce onInputChange function
export interface AutocompleteProps<V extends FieldValues> {
onInputChange: UseAutocompleteProps<UserOrEmail, true, false, false>['onInputChange'];
}
export default function MyAutocomplete<V extends FieldValues>({
onInputChange
}: AutocompleteProps<V>) {
return (
<Controller
name={name}
control={control}
render={({ field: { onChange, value } }) => (
<Autocomplete<UserOrEmail, true, false, false>
onInputChange={onInputChange} //here
/>
)}
/>
);
I have done it like this(used debounce outside and passing it like a prop) and it works
const [search, setSearch] = useState('');
const searchDelayed = useCallback(
debounce((newValue: string) => setSearch(newValue), 100),
[]
);
//Passing the prop like this
/*
onInputChange={(_, newValue) => {
searchDelayed(newValue);
}}
*/
I want to debounce in MyAutocomplete but I can not call onInputChange in debounce function. If I do not call it then I get an error, and it does not work
const searchsDelayed = useCallback( //does not work
debounce(() => onInputChange, 100),
[onInputChange]
);
Any ideas how it can be done?
[Answer reviewed to avoid confusion.]
The main issue is here:
debounce(() => onInputChange, 100);
you are debouncing a function that returns onInputChange, it doesn't invoke it, and it would be needed to be called twice.
To fix this, just pass onInputChange directly:
debounce(onInputChange, 100);
Anyway, you are trying to use useCallback but you probably would be fine without any optimization anyway, so you could use
const searchDelayed = debounce(onInputChange, 100);
But if you actually need the optimization, in this case you should use useMemo since you need the result of the debounce invocation:
const searchDelayed = useMemo(
() => debounce(onInputChange, 100),
[onInputChange]
);

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}/>
}

Is it safe to use ref.current as useEffect's dependency when ref points to a DOM element?

I'm aware that ref is a mutable container so it should not be listed in useEffect's dependencies, however ref.current could be a changing value.
When a ref is used to store a DOM element like <div ref={ref}>, and when I develop a custom hook that relies on that element, to suppose ref.current can change over time if a component returns conditionally like:
const Foo = ({inline}) => {
const ref = useRef(null);
return inline ? <span ref={ref} /> : <div ref={ref} />;
};
Is it safe that my custom effect receiving a ref object and use ref.current as a dependency?
const useFoo = ref => {
useEffect(
() => {
const element = ref.current;
// Maybe observe the resize of element
},
[ref.current]
);
};
I've read this comment saying ref should be used in useEffect, but I can't figure out any case where ref.current is changed but an effect will not trigger.
As that issue suggested, I should use a callback ref, but a ref as argument is very friendly to integrate multiple hooks:
const ref = useRef(null);
useFoo(ref);
useBar(ref);
While callback refs are harder to use since users are enforced to compose them:
const fooRef = useFoo();
const barRef = useBar();
const ref = element => {
fooRef(element);
barRef(element);
};
<div ref={ref} />
This is why I'm asking whether it is safe to use ref.current in useEffect.
It isn't safe because mutating the reference won't trigger a render, therefore, won't trigger the useEffect.
React Hook useEffect has an unnecessary dependency: 'ref.current'.
Either exclude it or remove the dependency array. Mutable values like
'ref.current' aren't valid dependencies because mutating them doesn't
re-render the component. (react-hooks/exhaustive-deps)
An anti-pattern example:
const Foo = () => {
const [, render] = useReducer(p => !p, false);
const ref = useRef(0);
const onClickRender = () => {
ref.current += 1;
render();
};
const onClickNoRender = () => {
ref.current += 1;
};
useEffect(() => {
console.log('ref changed');
}, [ref.current]);
return (
<>
<button onClick={onClickRender}>Render</button>
<button onClick={onClickNoRender}>No Render</button>
</>
);
};
A real life use case related to this pattern is when we want to have a persistent reference, even when the element unmounts.
Check the next example where we can't persist with element sizing when it unmounts. We will try to use useRef with useEffect combo as above, but it won't work.
// BAD EXAMPLE, SEE SOLUTION BELOW
const Component = () => {
const ref = useRef();
const [isMounted, toggle] = useReducer((p) => !p, true);
const [elementRect, setElementRect] = useState();
useEffect(() => {
console.log(ref.current);
setElementRect(ref.current?.getBoundingClientRect());
}, [ref.current]);
return (
<>
{isMounted && <div ref={ref}>Example</div>}
<button onClick={toggle}>Toggle</button>
<pre>{JSON.stringify(elementRect, null, 2)}</pre>
</>
);
};
Surprisingly, to fix it we need to handle the node directly while memoizing the function with useCallback:
// GOOD EXAMPLE
const Component = () => {
const [isMounted, toggle] = useReducer((p) => !p, true);
const [elementRect, setElementRect] = useState();
const handleRect = useCallback((node) => {
setElementRect(node?.getBoundingClientRect());
}, []);
return (
<>
{isMounted && <div ref={handleRect}>Example</div>}
<button onClick={toggle}>Toggle</button>
<pre>{JSON.stringify(elementRect, null, 2)}</pre>
</>
);
};
See another example in React Docs: How can I measure a DOM node?
Further reading and more examples see uses of useEffect
2021 answer:
This article explains the issue with using refs along with useEffect: Ref objects inside useEffect Hooks:
The useRef hook can be a trap for your custom hook, if you combine it with a useEffect that skips rendering. Your first instinct will be to add ref.current to the second argument of useEffect, so it will update once the ref changes.
But the ref isn’t updated till after your component has rendered — meaning, any useEffect that skips rendering, won’t see any changes to the ref before the next render pass.
Also as mentioned in this article, the official react docs have now been updated with the recommended approach (which is to use a callback instead of a ref + effect). See How can I measure a DOM node?:
function MeasureExample() {
const [height, setHeight] = useState(0);
const measuredRef = useCallback(node => {
if (node !== null) {
setHeight(node.getBoundingClientRect().height);
}
}, []);
return (
<>
<h1 ref={measuredRef}>Hello, world</h1>
<h2>The above header is {Math.round(height)}px tall</h2>
</>
);
}
I faced the same problem and I created a custom hook with Typescript and an official approach with ref callback. Hope that it will be helpful.
export const useRefHeightMeasure = <T extends HTMLElement>() => {
const [height, setHeight] = useState(0)
const refCallback = useCallback((node: T) => {
if (node !== null) {
setHeight(node.getBoundingClientRect().height)
}
}, [])
return { height, refCallback }
}
I faced a similar problem wherein my ESLint complained about ref.current usage inside a useCallback. I added a custom hook to my project to circumvent this eslint warning. It toggles a variable to force re-computation of the useCallback whenever ref object changes.
import { RefObject, useCallback, useRef, useState } from "react";
/**
* This hook can be used when using ref inside useCallbacks
*
* Usage
* ```ts
* const [toggle, refCallback, myRef] = useRefWithCallback<HTMLSpanElement>();
* const onClick = useCallback(() => {
if (myRef.current) {
myRef.current.scrollIntoView({ behavior: "smooth" });
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [toggle]);
return (<span ref={refCallback} />);
```
* #returns
*/
function useRefWithCallback<T extends HTMLSpanElement | HTMLDivElement | HTMLParagraphElement>(): [
boolean,
(node: any) => void,
RefObject<T>
] {
const ref = useRef<T | null>(null);
const [toggle, setToggle] = useState(false);
const refCallback = useCallback(node => {
ref.current = node;
setToggle(val => !val);
}, []);
return [toggle, refCallback, ref];
}
export default useRefWithCallback;
I've stopped using useRef and now just use useState once or twice:
const [myChart, setMyChart] = useState(null)
const [el, setEl] = useState(null)
useEffect(() => {
if (!el) {
return
}
// attach to element
const myChart = echarts.init(el)
setMyChart(myChart)
return () => {
myChart.dispose()
setMyChart(null)
}
}, [el])
useEffect(() => {
if (!myChart) {
return
}
// do things with attached object
myChart.setOption(... data ...)
}, [myChart, data])
return <div key='chart' ref={setEl} style={{ width: '100%', height: 1024 }} />
Useful for charting, auth and other non-react libraries, because it keeps an element ref and the initialized object around and can dispose of it directly as needed.
I'm now not sure why useRef exists in the first place...?

Resources