0

Why useEffect in below code run every time I search in below input?

import SearchInput from './searchInput'

const Sidebar = () => {
  const { list } = useList()
  const [searchValue, setSearchValue] = useState<string>('')

  useEffect(() => {
    console.log('list :>> ', list)
  }, [list])

  return (
    <SearchInput searchValue={searchValue} setSearchValue={setSearchValue} />
  )
}

export default React.memo(Sidebar)

useList hook:

export default function useList() {
  let { data } = useQuery(GET_LIST)
  const list = _.cloneDeep(data)?.menuTree?.filter((i) => i.parentId === null) || []

  const otherMenus = data?.menuTree || []

  return { list, otherMenus }
}

SearchInput component:

const SearchInput = ({ searchValue, setSearchValue }) => {
  return (
    <div>
        <Input
          value={searchValue}
          onChange={(e) => setSearchValue(e.target.value)}
        />
    </div>
  )
}

export default React.memo(SearchInput)

I believe every time DOM rerenders, useList hook runs again, I tried memoizing the component and data returned from useQuery, nothing helped. how do I prevent this from happening?

Mir Stephen
  • 1,758
  • 4
  • 23
  • 54
  • 2
    At the end of the `useEffect` you have `[list]`. This is called a dependency array and is used to determine when the `useEffect` should trigger, all values in the array will under go a reference comparison against their previous values each render and if any of them have changed trigger the `useEffect`. `useList` is setting list to `_.cloneDeep(data)` each render which is generating a new reference. Each time you call `setSearchValue` you will trigger a rerender. TLDR: `setSerachValue` causes rerender -> `useList` creates a new `list` reference -> `useEffect` triggered. – Jacob Smit Mar 20 '23 at 02:45
  • 1
    If `data` is memoized you should be able to memoize both `otherMenus` and `list` using `[data]` as the dependency array, ensuring they only update whenever `data` does. This should stop the `useEffect` retriggering more often than expected. If this is not the case we do not have enough information to help. – Jacob Smit Mar 20 '23 at 02:47
  • Does this answer your question? [How to prevent infinite re-rendering with useEffect() in React](https://stackoverflow.com/questions/65856539/how-to-prevent-infinite-re-rendering-with-useeffect-in-react) – Tirtharaj Ghosh Mar 20 '23 at 02:53
  • @JacobSmit I understand the concept, but any idea how do I prevent it. I removed cloneDeep as well, but it didn't help – Mir Stephen Mar 20 '23 at 13:54

1 Answers1

3

Function Components are different to Class Components.

In Class Components the aim is to keep the number of rerenders to a minimum as the component does not have the fine grain control over memoization that is provided with Function Components and hooks.

That's not to say that Function Components should not care about rerenders, but you should not be afraid of rerenders. Using correct state and memoization management in most cases a rerender should be indiscernible to the user.

useList

export default function useList() {
  // useQuery Assumptions:
  //     - internally makes proper use of memoization or
  //       external cache / storage to ensure heavier
  //       calculations are only run when needed.
  //     - data will remain the same reference each render 
  //       unless data has changed.
  let { data } = useQuery(GET_LIST);

  // only recalculate return when data from useQuery
  // has changed reference.
  return useMemo(() => {
    return {
      list: _.cloneDeep(data)?.menuTree?.filter((i) => i.parentId === null) || [],
      otherMenus: data?.menuTree || []
    };
  }, [data]);
}

Sidebar

import SearchInput from './searchInput'

const Sidebar = () => {
  // list should now be only be receiving a new reference whenever
  // data from useQuery receives a new reference.
  const { list } = useList();

  // state value and setter used to store search term.
  const [searchValue, setSearchValue] = useState<string>('');

  // useEffect will run on component mount and whenever
  // the list value from useList receives a new reference
  useEffect(() => {
    console.log('list :>> ', list)
  }, [list]);

  // render out search input.
  return (
    <SearchInput searchValue={searchValue} setSearchValue={setSearchValue} />
  )
}

// Most of the time React.memo is the incorrect tool to use, 
// heavy calculations should be handled with memoization
// hooks such as useMemo, useCallback, etc.
export default Sidebar

SearchInput

const SearchInput = ({ searchValue, setSearchValue }) => {
  // Render controlled search input.
  return (
    <div>
        <Input
          value={searchValue}
          onChange={(e) => setSearchValue(e.target.value)}
        />
    </div>
  )
}

// Most of the time React.memo is the incorrect tool to use, 
// heavy calculations should be handled with memoization
// hooks such as useMemo, useCallback, etc.
export default SearchInput

Anytime that SearchInput inside of Sidebar calls its setSearchValue prop a rerender of the Sidebar component will be triggered due to an update of its state value searchValue.

This in turn will rerun the useList and useQuery hooks, but if my useQuery assumptions are correct this should be either insignificant or required:

  • Insignificant: data from useQuery has not changed since the last render and no heavy calculations will be run within useQuery, as no new reference was given to data the useMemo with dependency array [data] will not be recalculated, avoiding the heavy cloneDeep function.
  • Required: data from useQuery has received a new reference signifying a change in the value stored within and possible recalculation of heavy memoized values. This means that the useMemo with dependency array [data] will be recalculated running the heavy cloneDeep function. This would be necessary to ensure the user is seeing the most relevant data.

Alternative Option

If the list value will have no interaction with the searchValue value you could create a wrapping component to handle the search logic / ui of the Sidebar (lets call this SidebarSearch).

Sidebar

import SidebarSearch from './sidebarSearch'

const Sidebar = () => {
  
  const { list } = useList();

  useEffect(() => {
    console.log('list :>> ', list)
  }, [list]);

  // render out search input.
  return (
    <SidebarSearch />
  )
}

export default Sidebar

SidebarSearch

import SearchInput from './searchInput'

const SidebarSearch = () => {
  // state value and setter used to store search term.
  const [searchValue, setSearchValue] = useState<string>('');

  // render out search input.
  return (
    <SearchInput searchValue={searchValue} setSearchValue={setSearchValue} />
  )
}

export default SidebarSearch

This would mean that whenever SearchInput calls the setSearchValue prop it would trigger a rerender starting from SidebarSearch avoiding useList being rerun.

Jacob Smit
  • 2,206
  • 1
  • 9
  • 19