'useEffect' hook only fires once?

I am working of a Guessing Game for ‘React Native’ where the user enters a number and the phone tries to guess it. Each time the phone generates a guess the user can click Greater/Lower. When the user entered number and the computer made guess equal each other we are taken to the game over screen.

The game over screen is not rendering. The logic to render the game over screen is placed inside of a useEffect()

Problem

useEffect is only fired once during the mounting phase and never again?

  const { userSelectedNumber, onGameOver } = props;
  useEffect(() => {
    console.log(currentGuess, userSelectedNumber);
    if (currentGuess === userSelectedNumber) {
      onGameOver(rounds);
    }
  }, [userSelectedNumber, onGameOver]);*emphasized text*

(./screens/GameScreen.js)

We should exit the GameScreen when currentGuess === userSelectedNumber but this code is only run once.

Full code for GameScreen below:

import React, { useState, useRef, useEffect } from "react";
import { View, StyleSheet, Button, Text, Alert } from "react-native";

import NumberContainer from "../components/NumberContainer";
import Card from "../components/Card";

const randNumberGeneratorBetween = (min, max, exclude) => {
  min = Math.ceil(min);
  max = Math.floor(max);

  const randNum = Math.floor(Math.random() * (max - min)) + min;


  if (randNum === exclude) {
    return randNumberGeneratorBetween(1, 100, exclude);
  } else {
    return randNum;
  }
};

const GameScreen = props => {
  const [currentGuess, setCurrentGuess] = useState(
    randNumberGeneratorBetween(1, 100, props.userSelectedNumber)
  );
  const [rounds, setRounds] = useState(0);

  const currentLow = useRef(1);
  const currentHigh = useRef(100);

  const { userSelectedNumber, onGameOver } = props;

  useEffect(() => {
    console.log(currentGuess, userSelectedNumber);
    if (currentGuess === userSelectedNumber) {
      onGameOver(rounds);
    }
  }, [userSelectedNumber, onGameOver]);

  const nextGuessHandler = direction => {
    if (
      (direction === "lower" && currentGuess < props.userSelectedNumber) ||
      (direction === "greater" && currentGuess > props.userSelectedNumber)
    ) {
      Alert.alert("Don't Lie", "You know this is wrong", [
        { text: "Sorry", style: "cancel" }
      ]);
    }

    if (direction === "lower") {
      currentHigh.current = currentGuess;
    } else {
      currentLow.current = currentGuess;
    }
    const nextNumber = randNumberGeneratorBetween(
      currentLow.current,
      currentHigh.current,
      currentGuess
    );
    console.log('nextNumber',nextNumber);
    setCurrentGuess(nextNumber);

    setRounds(currRounds => currRounds + 1);
    console.log('currRound',rounds);

  };

  return (
    <View style={styles.screen}>
      <Text>Opponents Guess</Text>
      <NumberContainer>{currentGuess}</NumberContainer>
      <Card style={styles.buttonContainer}>
        <Button
          title="Lower"
          onPress={nextGuessHandler.bind(this, "lower")}
        ></Button>
        <Button
          title="Greater"
          onPress={nextGuessHandler.bind(this, "greater")}
        ></Button>
      </Card>
    </View>
  );
};

const styles = StyleSheet.create({
  screen: {
    flex: 1,
    padding: 10,
    alignItems: "center"
  },
  buttonContainer: {
    flexDirection: "row",
    justifyContent: "space-between",
    marginTop: 20,
    width: 300,
    maxWidth: "80%"
  }
});

export default GameScreen;

Project can be found here:
https://codesandbox.io/s/github/SMasood1/guessingGame?file=/screens/GameScreen.js:852-1039

Here is Solutions:

We have many solutions to this problem, But we recommend you to use the first solution because it is tested & true solution that will 100% work for you.

Solution 1

You need to add rounds and currentGuess to the dependencies array in the useEffect hook

 useEffect(() => {
    console.log(currentGuess, userSelectedNumber);
    if (currentGuess === userSelectedNumber) {
      onGameOver(rounds);
    }
  }, [userSelectedNumber, onGameOver,currentGuess,rounds]);

Also it is considered a anti-pattern to use props to initialize a state, so I would recommend to add an other useEffect hook:

useEffect(()=>{
    setCurrentGuess(randNumberGeneratorBetween(1, 100, props.userSelectedNumber))

},[props.userSelectedNumber]);

Solution 2

The useEffect hook causes the component to update whenever any of the values of the dependency array changes. Make sure the values you use to trigger that hook are in fact changing.

Solution 3

You can elegantly trigger useEffect by supplying a timestamp on you navigation.navigate call

e.g.

// someComponent.tsx

navigation.navigate('Home', {
  showSubscriptionModal: true
})
// HomeScreen.tsx
const showSubscriptionModal = props.route.params?.showSubscriptionModal ?? false

useEffect(() => {
  if(showSubscriptionModal) setIsShowingModal(true)
},[showSubscriptionModal])

will only fire once, while

// someComponent.tsx

navigation.navigate('Home', {
  showSubscriptionModal: true,
  updateTs: new Date()
})
// HomeScreen.tsx
const showSubscriptionModal = props.route.params?.showSubscriptionModal ?? false

useEffect(() => {
  if(props.route.params?.showSubscriptionModal) setIsShowingModal(true)
},[showSubscriptionModal, props.route.params?.updateTs])

will fire every time you re-navigate to your screen via navigation.navigate()

Note: Use and implement solution 1 because this method fully tested our system.
Thank you 🙂

All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0

Leave a Reply