Search code examples
javascriptreactjsreact-hooksuse-effect

React.js | Infinite render if I pass setState as a callback function, even after destructuring props


Issue

I have a child component that gets some button id-name configs as props, renders selectable HTML buttons according to those configs and returns the selected button's value(id) to the callback function under a useEffect hook. However it causes an infinite render loop because I need to pass the props as a dependency array. Note that React.js wants me to destructure props, but it still causes an infinite render loop even if I do that.

Child Component

import React, {createRef, useState, useEffect} from "react";

const OptionButton = ({ buttons, buttonClass, callback }) => {

    const [value, setValue] = useState()
    const refArray = []
    const buttonWidth = ((100 - (Object.keys(buttons).length - 1)) - ((100 - (Object.keys(buttons).length - 1)) % Object.keys(buttons).length)) / Object.keys(buttons).length

    useEffect(() => {
        if (callback) {
            callback(value);
        }
    }, [value, callback])

    const select = (event) => {
        event.target.style.backgroundColor = "#10CB81"
        refArray.forEach((currentRef) => {
            if (currentRef.current.id !== event.target.id) {
                currentRef.current.style.backgroundColor = "#F5475D"
            }
        })
        setValue(event.target.id)
    }

    return(
        <span style={{display: "flex", justifyContent: "space-between"}}>
            {Object.entries(buttons).map((keyvalue) => {
                const newRef = createRef()
                refArray.push(newRef)
                return <button ref={newRef} id={keyvalue[0]} key={keyvalue[0]} className={buttonClass} onClick={select} style={{width: `${buttonWidth}%`}}>{keyvalue[1]}</button>
            })}
        </span>
    )
}

export default OptionButton

So as you can see here my child component gets button configs as key-value (button value-button name) pairs, renders these buttons and when user clicks one of these buttons it gets the id of that button, sets it to 'value' constant using useState hook and then passes that value to parent component callback.

Parent Component

return(
    <OptionButton buttons={{"ButtonValue": "ButtonName", "Button2Value": "Button2Name"}} callback={(value) => this.setState({buttonState: value})} buttonClass="buttonclass"/>
)

It's just fine if I don't use this.setState at the callback function. For example if I just do

(value) => console.log(value)

there is no infinite loop. As far as I can tell it only happens if I try to use setState.

What am I doing wrong? How can I fix this? Thanks in advance.


Solution

  • The reason why there is infinite loop is because you have callback as dependency in useEffect. And what you are passing to the component is you pass a new callback function on each new render, hence it always enters useEffect because of that. Since you are using classes consider passing instance method as callback instead of arrow function as here.


    Also I think you are overusing refs. You could also achieve what you are doing by just storing say the id of clicked button, and then dynamically styling all buttons, e.g. inside map if id of current button is same as the one stored, use #10CB81 bg color in style object, otherwise different one.

    Also there are better ways to check which btn was clicked, see here.