Hi @Dave 👋,
You did a great job on this challenge. 👍 I had a great time playing the game too. 😉
I saw you question about the issues you are having with dependency array of React's useEffect
hook. I had a quick look at your code for the Board.js
component and here are a few things that I have noticed which might be why you are facing those issues. I'll give you some rules/guidelines that I use myself when I work with the useEffect
hook and I'll try to keep it simple so that you'll know where to go from here.
Rule 1: Don't ignore the warnings about the dependency array
- We get warnings when React sees we are doing something wrong. You should take a step back and carefully review your implementation of the
useEffect
hook.
Rule 2: Know the data types of the dependencies being passed to the array
- This is where most trouble come from. We should carefully check the data types of the different elements being passed to the dependency array. If the dependencies are primitive data types (i.e: string, number, boolean, null, undefined) we don't have much to worry as they are compared by value. But if they are reference data types (i.e array, object, function, date), the
useEffect
hook will compare them by reference (i.e their pointer in memory) and each time the component renders, we create a new reference to those dependencies and as the useEffect
hook detects they are different, it reruns hence resulting in an infinite loop. 😱 Hopefully, React provides us with tools to work around this problem; they are the useMemo
and useCallback
hooks.
Consider the useEffect
below which was extracted from your Board.js
component:
React.useEffect(() => {
if (gameType !== "CPU") return;
if (marker !== turn) {
renderSquareChoice(computerMove(squares, marker));
}
}, [gameType, turn]);
renderSquareChoice
is a function inside your component and you are using it inside useEffect
, so React prompts you that it needs to be added to the dependency array. Now, you add it as a dependency, you get an infinite loop because renderSquareChoice
is a function meaning it is a reference data type. To fix this, you need to wrap the renderSquareChoice
with useCallback
like so:
const renderSquareChoice = useCallback((square) => {
if (status || squares[square]) return;
const nextSquares = [...squares];
nextSquares[square] = turn;
setSquares(nextSquares);
}, []);
And you will be able to add it to useEffect
as a dependency without causing an infinite loop. NOTE: In the above code, useCallBack
also has a dependency array, you might need to add status
and squares
as dependencies.
- There are cases where we don't need to pass the entire object or array as a dependency to
useEffect
. For example, if my component has a person
state/prop but my effect is only dependent on the age
property of that person
state. All we need to do is to pass this age
property to useEffect
like below:
const [ person, setPerson ] = useState({
name: 'Chris',
age: 36,
role: 'guest'
});
useEffect(() => {
// some side effect that needs to run whenever the age of `person` state is mutated
}, [person.age]);
So we are not passing the entire person
state which an object and therefore a reference data type because each render of the component would create a new reference for the person
state. Instead, we pass only the age
property which is a number and therefore a primitive data type. I illustrated a simple case here but for complex cases the useMemo
hook would be more appropriate. This feedback is already very long, I suggest you to read more about useMemo
.
Rule 3: Use the functional variant of the state setter function inside useEffect
- Whenever
useEffect
is dependent on a state which is mutated inside it, you should use the functional variant of the state setter function. Taking another example extracted from the Board.js
component:
// Update total scores
React.useEffect(() => {
if (status === null) return;
const newScore = { ...score };
newScore[status] += 1;
setScore(newScore);
}, [status]);
This useEffect
should be refactored as follows:
// Update total scores
React.useEffect(() => {
if (status === null) return;
setScore((prevScore) => {
const newScore = { ...prevScore };
newScore[status] += 1;
return newScore;
});
}, [status]);
By doing so, our useEffect
is no more dependent on score
state and hence it isn't needed in the dependency array.
I know that's a lot to read as feedback but I tried to keep it as lean as possible. useEffect
is a big topic in itself. I also got these issues while tackling the Coffeeroasters challenge with React (still in progress 😉). The more you practice, the better you'll get at it.
I hope this helps you as a starting point.
Here are some resources from Jack Herrington that helped me:
Keep it up.