Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React: Is it okay if useCallback returns a value, or is this a bad pattern?

I have a function called filterContactsByValue. It is curried and takes in a value and a list of contacts and then filters the list based on the value and returns the (new) filtered list.

Since the list is often large (10.000+ entries), the web app should run on smartphones and the filter takes into account many values, I want to optimize the computing resources. Therefore I use useDebounce to not compute unnecessarily.

I also used useCallback like this to memoize the computation of the filteredContacts:

function FilteredContacts({contacts}) {
  const [filterParam, setFilterParam] = useState('');
  const [value] = useDebounce(filterParam, 800);
  const filterContacts = filterContactsByValue(value.toLowerCase());

  // Is this okay? 🤔 ...
  const getFilteredContacts = useCallback(() => filterContacts(contacts), [
    value
  ]);

  return (
    <div className="main">
      <SearchBar
        value={filterParam}
        onChangeText={setFilterParam}
      />
      // ... and then this? 🧐
      <ContactList contacts={getFilteredContacts()} />
    </div>
  );
}

I was wondering whether this is okay, or if returning values like this is bad practice. If it is bad, why and how would you improve it?

Edit: The filterContactsByValue function:

import { any, filter, includes, map, pick, pipe, toLower, values } from 'ramda';
import { stringFields } from './config/constants';

const contactIncludesValue = value =>
  pipe(
    pick(stringFields),
    map(toLower),
    values,
    any(includes(value))
  );

const filterContactsByValue = pipe(
  contactIncludesValue,
  filter
);
like image 939
J. Hesters Avatar asked Mar 11 '19 23:03

J. Hesters


People also ask

Can useCallback return a value?

The useCallback hook will return a memoized version of the callback, and it'll only be changed if one of the dependencies has changed. You can also pass an empty array of dependencies. This will execute the function only once. If you don't pass an array, this will return a new value on every call.

When should you not use useCallback?

Feel free to remove all useMemo and useCallbacks from the code if: they passed as attributes, directly or through a chain of dependencies, to DOM elements. they passed as props, directly or through a chain of dependencies, to a component that is not memoized.

Does useCallback hurt performance?

React's useCallback hook offers a performance gain when passing the generated (memoized) function to a child component in order to avoid unnecessary re-renders.

Does useCallback cause re render?

You can use the useCallback Hook to preserve a function across re-renders. This will prevent unnecessary re-renders when a parent component recreates a function. By the end of this step, you'll be able to prevent re-renders using the useCallback Hook.


2 Answers

Short answer: use useMemo instead of useCallback, like so:

const filteredContacts = useMemo(() => filterContacts(contacts), [
    value
  ]);

...
<ContactList contacts={filteredContacts} />

Why ? useCallback memoizes the creation of a function. Meaning, the reference of the function will be the same, if the diffing parameters are the same. It will STILL be called everytime tho, and in your case, won't prevent any compute.

What you want is to only filter your contacts if value changes. useMemo remembers the last return value of your function, and will only re-run when the diffing parameters change. And they won't change more than once every 800ms, because you debounce it well.

PS: you could use useCallback to prevent filterContacts from being re-computed for no reason like so:

 const filterContacts = useCallback(() => filterContactsByValue(value.toLowerCase(), [value]);

Even tho in your case, the performance gain is tiny.

like image 77
Bear-Foot Avatar answered Nov 03 '22 09:11

Bear-Foot


According to https://github.com/xnimorz/use-debounce you already have useDebouncedCallback hook.

const getFilteredContacts = useDebouncedCallback(
    () => filterContactsByValue(value.toLowerCase()),
    800,
    [value]
  );

You can also use lodash's debounce or throttle (when you have lodash in your project), but as @skyboyer mentioned you may end with out-of-date callback version(s) will be run after appropriate delay

export {debounce} from 'lodash'; 

const getFilteredContacts = useCallback(
    debounce(() => filterContactsByValue(value.toLowerCase()), 1000),
    [value]
);

but useMemo will be better option, because you don't really want function execution in your render method

const FilteredContacts = ({contacts}) => {
    const [filterParam, setFilterParam] = useState('');
    const [value] = useDebounce(filterParam, 800);
    const contactsFilter = useMemo(
        () => filterContactsByValue(value.toLowerCase()),
        [value]
    );
    const filteredContacts = useMemo(
        () => contactsFilter(contacts), 
        [value, contacts]
    );

    return (
        <div className="main">
            <SearchBar
                value={filterParam}
                onChangeText={setFilterParam}
            />
            <ContactList contacts={filteredContacts} />
        </div>
    );
}
like image 26
r g Avatar answered Nov 03 '22 08:11

r g