Search code examples
reactjsreduxreact-hooksreact-modal

Using state change to trigger modal in React Redux


I'm building a game in React Redux, and I have a condition where, upon being dealt cards, if the cards are over a certain amount I want a modal to put up so that the user can select one to discard.

Im doing this in the game logic by switching a part of the state called "cardHandOverflow" to true, and i want the modal to render on this condition. Im using React Modal library for this.

However im getting the error

react-dom.development.js:14997 Uncaught Error: Too many re-renders. React limits the number of renders to prevent an infinite loop.

Here is the code im using:

export const CardHand: React.FC = () => {
  const cardHandOverflow = useSelector((state: RootState) => state.gameStateReducer.players.filter(player => player.id === Player.id)[0].cardHandOverflow);
  
  const [modalIsOpen, setIsOpen] = useState(false);

  const closeModal = () => {
    setIsOpen(false)
  }

  if (cardHandOverflow) {
    setIsOpen(true)
  }

  return (
    <>
      {modalIsOpen ? 
        <DiscardModal 
          modalIsOpen={modalIsOpen} 
          closeModal={closeModal}
          discardableCards={cards}
        /> : null}
    </>
  )

Its obviously creating some sort of render loop with the useSelector and state change retriggering, however when i switch it out for a button the modal renders fine. How can i get the state change to render the modal once (so its acting like a click event)?

Many thanks :-)


Solution

  • This is caused by setIsOpen(true); when cardHandOverflow is truthy.

    setIsOpen causes a rerender, even when the value does not change, and since it is directly part of your function component logic it will be run ever render while cardHandOverflow is true.

    To avoid these loops you should make use of hooks provided by React, in this case useEffect is most appropriate.

    export const CardHand: React.FC = () => {
        const cardHandOverflow = useSelector((state: RootState) => state.gameStateReducer.players.filter(player => player.id === Player.id)[0].cardHandOverflow);
        
        const [modalIsOpen, setIsOpen] = useState(false);
    
        const closeModal = () => {
            setIsOpen(false)
        }
    
        useEffect(() => {
            if (cardHandOverflow) {
                setIsOpen(true)
            }
        }, [cardHandOverflow, setIsOpen])
    
        return (
            <>
            {modalIsOpen ? 
                <DiscardModal 
                modalIsOpen={modalIsOpen} 
                closeModal={closeModal}
                discardableCards={cards}
                /> : null}
            </>
        )
    }
    

    The useEffect will only run the contained code on component mount and whenever a value in the dependency array [cardHandOverflow, setIsOpen] changes.