Search code examples
reactjsuse-effectuse-state

In React how to prevent too many renders on a map?


I'm doing something wrong in my approach trying to set a className based on the useState from a sub navigation map.

(code stripped):

const [activeLink, setActiveLink] = useState(0)

// removed code

{items.map((item, key) => {
  const { title, link } = item

  return (
    <React.Fragment key={key}>
      <Link
        to={`/${link}`}
        className={activeLink === 1 ? 'active' : ''}
      >
        {title}
      </Link>
    </React.Fragment>
  )
})}

Attempt 1

{items.map((item, key) => {
  const { title, link } = item
  let testLink = null
  testLink = pathname.toString().includes(link)
  if (testLink === true && activeLink === 0) setActiveLink(1)
  if (testLink === false && activeLink === 1) setActiveLink(0)

  return (
    <React.Fragment key={key}>
      <Link
        to={`/${link}`}
        className={activeLink === 1 ? 'active' : ''}
      >
        {title}
      </Link>
    </React.Fragment>
  )
})}

Throws the error of:

Too many re-renders. React limits the number of renders to prevent an infinite loop.

Attempt 2

 const handleactive = link => (pathname.toString().includes(link) ? 1 : 0)
  useEffect(() => {
    if (activeLink === false && handleactive() === 1) return setActiveLink(1)
    return setActiveLink(0)
  }, [activeLink])

Attempt 3

const handleactive = link => {
  if (activeLink === 0 && pathname.toString().includes(link) === true) return setActiveLink(1)
  return setActiveLink(0)
}


{items.map((item, key) => {
  return (
    <React.Fragment key={key}>
      <Link
        to={`/${link}`}
        className={activeLink === 1 ? 'active' : ''}
        handleactive={handleactive(link)}
      >
        {title}
      </Link>
    </React.Fragment>
  )
})}

Research

What am I doing wrong and how can I, in a map, update the state?


Solution

  • You can't call the setState function of useState during a render.

    The map is run on each render therefore each render there is a chance (conditions dependent) that you are calling setActiveLink.

    If you wish to update state per item within a map, you probably want to create an extra component for the Links.

    The links can then either keep their own state or set the parent state via callback functions passed to them from the parent.

    // Map in existing parent component
    {items.map((item, key) => {
    
      return (
        <NavLink 
            key={key} 
            {...item} 
            pathname={pathname} 
        />
      )
    })}
    
    // New component
    const NavLink = ({title, link, pathname}) => {
        const active = useMemo(() => {
            return pathname.toString().includes(link)
        }, [pathname, link]);
    
        return (
            <Link
                to={`/${link}`}
                className={active ? 'active' : ''}
            >
                {title}
            </Link>
        );
    }