在React中使用Debounce实现搜索的正确方式——解决TypeError: Expected a function报错问题
Hey there, let's break down what's causing that TypeError: Expected a function and walk through the correct way to implement debounced search with React, Material UI, Formik, and Lodash.
Why You're Seeing the Error
The core issue here is that you're passing the result of a function call to Lodash's debounce instead of passing a function itself.
Look at this line in your code:
const debounceLoadData = useCallback( debounce(dispatch(getPersons(10, 1, filter)), 1000), [] );
When you write dispatch(getPersons(10, 1, filter)), you're immediately executing the dispatch call—this returns an action object (or undefined, depending on your Redux setup), not a reusable function. Lodash's debounce expects its first argument to be a function it can wrap and delay, hence the error.
Also, you've declared debounceLoadData twice, which is a syntax error that needs fixing.
Correct Implementation
Let's fix this with a clean, maintainable approach:
1. Wrap Debounce Around a Proper Function
First, we need to pass an actual function to debounce. There are two solid patterns for this, but the first one is more straightforward for search scenarios:
Pattern 1: Pass the Search Value Directly to the Debounced Function
This way, the debounced function uses the exact search value you pass to it, avoiding issues with stale state references:
import { debounce } from 'lodash'; import { useCallback, useState } from 'react'; // ... const [filter, setFilter] = useState(""); // Use useCallback to keep the debounced function reference stable across re-renders const debounceLoadData = useCallback( debounce((searchValue) => { // Skip empty searches if needed if (!searchValue.trim()) { // Clear results or handle empty state here return; } // Now we call dispatch inside the debounced function dispatch(getPersons(10, 1, searchValue)); }, 1000), [dispatch] // Only re-create if dispatch changes (rare in most setups) ); const onSearchVoter = (value) => { setFilter(value); debounceLoadData(value); // Pass the current search value to the debounced function };
Pattern 2: Use useRef to Preserve Debounced Function (For State-Dependent Logic)
If you need the debounced function to rely on the latest filter state (instead of passing the value directly), use a ref to avoid re-creating the debounced function every time filter changes:
import { debounce } from 'lodash'; import { useCallback, useState, useRef } from 'react'; // ... const [filter, setFilter] = useState(""); const debounceRef = useRef(); const debounceLoadData = useCallback(() => { // Initialize the debounced function once if (!debounceRef.current) { debounceRef.current = debounce(() => { dispatch(getPersons(10, 1, filter)); }, 1000); } // Trigger the debounced function debounceRef.current(); }, [dispatch, filter]); const onSearchVoter = (value) => { setFilter(value); debounceLoadData(); };
Pattern 1 is preferred for search because it avoids potential stale state issues and is easier to reason about.
2. Clean Up the Autocomplete Handler
Your onInputChange doesn't need to be async since there's no await inside—simplify it:
onInputChange={(event, value) => { onSearchVoter(value); }}
3. Full Optimized Code Example
Putting it all together, here's what the fixed component might look like:
import { debounce } from 'lodash'; import { useCallback, useState, useEffect } from 'react'; import { Autocomplete, TextField } from '@mui/material'; import { useFormikContext } from 'formik'; const VoterSearch = () => { const { values, setFieldValue, touched, errors, handleBlur } = useFormikContext(); const [filter, setFilter] = useState(""); const [persons, setPersons] = useState([]); const debounceLoadData = useCallback( debounce((searchValue) => { if (!searchValue.trim()) { setPersons([]); return; } // Adjust this based on your Redux async logic (e.g., if getPersons returns a promise) dispatch(getPersons(10, 1, searchValue)) .then((response) => setPersons(response.data)) .catch((err) => console.error('Search failed:', err)); }, 1000), [dispatch] ); // Clean up debounce on unmount to avoid memory leaks useEffect(() => { return () => debounceLoadData.cancel(); }, [debounceLoadData]); const onSearchVoter = (value) => { setFilter(value); debounceLoadData(value); }; return ( <Autocomplete value={values.voter_id} options={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 ?? ""); }} onInputChange={(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 /> )} /> ); }; export default VoterSearch;
Quick Extra Tip
Always clean up your debounced function when the component unmounts (like we did with useEffect and debounceLoadData.cancel())—this prevents unnecessary network requests or state updates after the component is gone.
内容的提问来源于stack exchange,提问作者Joseph

