Walt
Walt

Reputation: 111

React Too many re-renders when using setState hook inside callback function

When I use a setState hook inside a function that I use as a callback for a component, I get the Too many re-renders error from React.

Below is a useless example to demonstrate what I'm trying to do.

I have an array of Person objects, and an array of Pets. Each pet has an owner (=Person). I want to show the name of the Person and the pet he owns.

I loop over the Persons array, and for each person there should be a TestComponent. TestComponent expects the name of a Person and the name of a Pet.

The Pet name is calculated in a callback function getPetByOwnerName which will loop over all Pets and if the given Person name matches the Pet's owner name, it will return that petname.

IF a Person has no pets, it should add an empty Pet object to the Pets list using setPets().

setPets() inside the getPetByOwnerName function is causing the Too many re-renders error.

It might not make any sense of doing it this way in this example but it's to recreate an issue I have in a bigger project.

import React, { useState } from 'react'
import TestComponent from './TestComponent'

const TestPage = () => {

    const [persons, setPersons] = useState([{name: 'Tom', age: 35}, {name: 'Fred', age: 50 }])

    const [pets, setPets] = useState([{owner: 'Tom',name: 'Doggo'}])


    const getPetByOwnerName = (name) => {
        let petName = null
        pets.map((pet, index) => {
            if (pet.owner === name) {
                petName = pet.name
            }
            return pet
        })

        if (!petName) {
            const pet = {
                owner: name,
                name: ''
            }
            const updatedPets = pets
            pets.push(pet)
            setPets(updatedPets)
        }

        return petName
    }

    return (
        <React.Fragment>
            {
                persons.map((person, index) => (
                    <TestComponent key={index} personName={person.name} petName={getPetByOwnerName(person.name)}></TestComponent>
                ))
            }
        </React.Fragment>
    )
}

export default TestPage

TestComponent:

import React from 'react'

const TestComponent = ({ personName, petName }) => {

    return (
        <React.Fragment>
            <p>{personName} owns pet: {petName}</p>
        </React.Fragment>
    )
}

export default TestComponent

Upvotes: 0

Views: 837

Answers (1)

topched
topched

Reputation: 765

Its probably better to handle this sort of functionality inside a use effect. You could watch for changes in persons then update pets accordingly. Something like below.

But your actual problem is in this line if (!petName). When petName is an empty string it gets in here and sets the state forever. You need a check like if(!petName && petName !== '')

    useEffect(() => {
      persons.map(person => {
        const petIndex = pets.findIndex(p => p.owner === person.name);

          if (petIndex === -1) {
            const pet = {
              owner: person.name,
              name: ''
          }
          setPets(prevPets => ([...prevPets, pet]))
        }
      })
    }, [persons]);

Also as a side note when using the previous state to update the state its better to use the callback version (see the updated set pets in the example above)

Since this is just an example of your actual issue its probably best to put a debugger; just before you set your state and see why you are setting it so often.

Upvotes: 1

Related Questions