user3903448
user3903448

Reputation: 330

Infinite calls to the callback from useEffect when the state changes for the first time

I have a parent component that passes a callback function to child component so that whenever the state of an array changes in child component, the callback will be called. For some reason, whenever the first time I change the state in child component, it leads to an infinite loop where the callback keeps getting called forever.

Here is the real code below.

I have defined a mutation to set the list of similarBooks when user adds a new book in the list. Child component keeps track of the list. Initial value of similarBooks is provided by parent component. Child component has a form to add books into the similarBooks list. Whenever the state of that list changes, useEffect in child component gets called which in turn calls the callback function provided by the parent function. The callback function executes the setSimilarBooks mutation.

One interesting thing is that if I just move onChangeSimilarBooks callback into the Child component, then the issue doesn't exist anymore. Or if I keep the callback in parent, but don't call the setSimilarBooks mutation, then also issue disappears.

Parent component:

const [setSimilarBooks, { data: mutationSimilarBooksData, 
      loading: mutationSimilarBooksLoading, error: mutationSimilarBooksError }] 
         = useMutation(setSimilarBooksMutation);
const onChangeSimilarBooks = (values) => {
    console.log(values);
    if (!mutationSimilarBooksLoading) {
        setSimilarBooks({
            variables: {
                bookId: bookId,
                similarBooksIds: values.map(value => value.id),
            },
        });
    }
}

Parent has a child component:

<ChipsComponent
                key="{bookDetails.book.name}_similarbooks"
                initialValues={bookDetails.book.similarBooks}
                suggestions={booksList.books}
                allowNew={false}
                onChangeCallback={onChangeSimilarBooks}
                userFriendlyValue={userFriendlyValueOfBook}
            />

Child Component:

const [values, setValues] = useState(initialValues);
useEffect(() => {
    onChangeCallback(values);
}, [values]);

...


...

return (
    <div className="container">
        {values.map((book, index) => (
            <div className="books" key={index}>
                {userFriendlyValue(book)}
                <button onClick={() => deleteTag(index)}>x</button>
            </div>
        ))}
        <div className="newTag">
            <input
                value={input}
                placeholder="Enter a tag"
                onKeyDown={onKeyDown}
                onKeyUp={onKeyUp}
                onChange={onChange}
            />
            {suggestionsActive && <Autocomplete />}
        </div>

    </div>);

I would like for the callback to get called only once every time the state really changes.

Upvotes: 2

Views: 587

Answers (2)

Drew Reese
Drew Reese

Reputation: 202751

It's not overtly clear why moving the callback and useMutation hook into the child and calling setSimilarBooks directly, or keeping the callback in the parent and not calling setSimilarBooks would trigger this. The only possibility I see from the code is that the onChangeSimilarBooks callback isn't a stable reference. You could wrap it in a useCallback hook to memoize it.

Example:

const onChangeSimilarBooks = useCallback((values) => {
  console.log(values);
  if (!mutationSimilarBooksLoading) {
    setSimilarBooks({
      variables: {
        bookId,
        similarBooksIds: values.map(value => value.id),
      },
    });
  }
}, [mutationSimilarBooksLoading, setSimilarBooks]);

Upvotes: 0

Kirill Skomarovskiy
Kirill Skomarovskiy

Reputation: 1565

Reference to the function onChangeSimilarBook changes every render. You have somehow make it stable by reference. You could try to use useEvent.

useevent.md

Upvotes: 1

Related Questions