Strange behaviour when passing variable down to child component

I am seeing some strange behaviour when I am trying to pass down a variable to a child component in react. When I console.log just before the return statement (so console.log(1)) in the parent component the data is correct, however when I console.log in the child component (so console.log(2)) the data has changed??

I have a suspicion that it relates to the randomSelect() function but again when console logging out this looks to only be called twice (as expected).

I have pasted a ‘playerOneId’ in directly (avoiding using the randomSelect() function) and the data shows correctly in the child component when doing this, hence my suspicion around the randomSelect() function. It could be unrelated but not sure.

A gold medal to anyone can answer this one as it has had me for hours now and I’ve run out of ideas.

PARENT COMPONENT:

const Board = () => {

const [starships, setStarships] = useState([]);
const [playerSelected, setPlayerSelected] = useState(false);
const [result, setResult] = useState('');
const [playerScore, setPlayerScore] = useState(0);
const [computerScore, setComputerScore] = useState(0);

const STARSHIP_QUERY = `{
        allStarships {
            starships {
                id
                name
                starshipClass
                maxAtmospheringSpeed
                costInCredits
                passengers
                filmConnection {
                    films {
                        title
                    }
                }
            }
        }
    }
`

useEffect(() => {
    fetch('https://connectr-swapi.herokuapp.com/', {
        method: "POST",
        headers: {"Content-Type": "application/json"},
        body: JSON.stringify({query: STARSHIP_QUERY})
    })
    .then(response => response.json())
    .then(data => setStarships(data.data.allStarships.starships))
    .catch(error => console.log({'Error': error}))
    
},[])

const randomSelect = () => {
    const random = Math.floor(Math.random() * starShipIds.length);
    const selectedId = starShipIds[random];
    return selectedId;
}

const starShipIds = starships.map(ship => ship.id)    
const valueOneID = randomSelect();
const valueTwoID = randomSelect();
const playerOneId = valueOneID;
const computerId = valueTwoID;
const playerOneShip = starships.filter(ship => ship.id === playerOneId) ;
const computerShip = starships.filter(ship => ship.id === computerId);


const catergorySelect = (key, value) => {

    let computerValue = key === 'filmConnection' ? computerShip[0][key].films.length : computerShip[0][key];

    if (value > computerValue) {
        setResult('You Win!');
        setPlayerScore(playerScore + 1)
    }

    if (value === computerValue) setResult('You Draw!');

    if (value < computerValue) {
        setResult('You Lose!');
        setComputerScore(computerScore + 1)
    }

    setPlayerSelected(true);
}

console.log(1, playerOneShip[0]); // data is showing correctly

return (
    <div className="background">
        <div className="row">
            <div className="col-12 col-sm-4">
                {playerOneShip.length &&
                    <GameCard 
                        ship={playerOneShip[0]} // data passed in
                        player='player-one'
                        select={catergorySelect}
                    />
                }
                {playerSelected &&
                    <Score 
                        score={playerScore}
                        colour="white"
                    />
                }
            </div>                
            <div className="col-12 col-sm-4">
                <div className="row">
                    <h1>{result}</h1>
                </div>
                <div className="row">
                    <DefaultBtn 
                        text="START AGAIN"
                        colour="white"
                    />
                </div>
            </div>                
            <div className="col-12 col-sm-4">
                {playerSelected && 
                    <React.Fragment>
                    <div>
                    {computerShip.length &&
                        <GameCard 
                            ship={computerShip[0]}
                            player='computer'
                            catergorySelect={catergorySelect}
                        />
                    }
                    </div>
                    <div>
                        <Score 
                            score={computerScore}
                            colour="white"
                        />
                    </div>
                    </React.Fragment>
                }
            </div>   
        </div>              
    </div>
)
}

CHILD COMPONENT:

const GameCard = props => {

const [selected, setSelected] = useState(0);
const [disableCategory, setDisableCategory] = useState(false);

const {
    ship,
    player,
    select,
} = props;

console.log(2, ship) // different data is showing

const categories = Object.entries(props.ship).map(([key, value], index) => {
    const choosenCategory = selected === index ? 'selected' : '';
    const disableButton = disableCategory ? 'disable' : '';

    switch (key) {
        case 'maxAtmospheringSpeed':
            return <li className={`card ${player} ${choosenCategory} ${disableButton}`} onClick={(() => { select(key, value); setSelected(index); setDisableCategory(true)})} key={index}>{`Maximum Speed: ${value}`}</li>
        case 'costInCredits':
            return <li className={`card ${player} ${choosenCategory} ${disableButton}`} onClick={(() => { select(key, value); setSelected(index); setDisableCategory(true)})} key={index}>{`Cost In Credits: ${value}`}</li>
        case 'passengers':
            return <li className={`card ${player} ${choosenCategory} ${disableButton}`} onClick={(() => { select(key, value); setSelected(index); setDisableCategory(true)})} key={index}>{`Number Of Passengers: ${value}`}</li>
        case 'filmConnection':
            return <li className={`card ${player} ${choosenCategory} ${disableButton}`} onClick={(() => { select(key, value.films.length); setSelected(index); setDisableCategory(true)})} key={index}>{`Number Of films: ${value.films.length}`}</li>
        default:
            return null
    }
});

return (
    <div className="card">
        <img className="card-image" src="assets/img/starships/2.jpg" />
        <div className="card-body">
            <p className="card-title">{`Ship Name: ${ship.name}`}</p>
            <p className="card-sub-title">{`Class: ${ship.starshipClass}`}</p>
            <ul>
                {categories}
            </ul>
        </div>
    </div>
)

}

Answer

It’s probably a reference issue, the variable passed in props is updated by another render in the parent.

A way of fixing it could be to put all this section of code in a useEffect depending on the loading of the starships:

const starShipIds = starships.map(ship => ship.id)    
const valueOneID = randomSelect();
const valueTwoID = randomSelect();
const playerOneId = valueOneID;
const computerId = valueTwoID;
const playerOneShip = starships.filter(ship => ship.id === playerOneId) ;
const computerShip = starships.filter(ship => ship.id === computerId);

It could look like this:

useEffect(() => {
    const starShipIds = starships.map(ship => ship.id)    
    const valueOneID = randomSelect(starShipIds);
    const valueTwoID = randomSelect(starShipIds);
    const playerOneId = valueOneID;
    const computerId = valueTwoID;
    setPlayerOneShip(starships.filter(ship => ship.id === playerOneId));
    setComputerShip(starships.filter(ship => ship.id === computerId));
    
},[starships])

For this you need to create a state for player ship and computer ship and replace previous usage of these, like in my example above.

Also, you should pass the starship ids to random select as a parameter and not use a const and assume it has the correct value because it is in the scope of the function.