I’m new to React and I find useEffect’s behavior a little confusing. I’ve written a simple hangman game (where the user tries to guess a word or phrase one letter at a time). I made a useEffect hook to update the board with each guess (generated by user input), and count wrong guesses.
Weirdly, the DOM only updates on subsequent updates. For example, say the user guesses ‘T’, then ‘X’. Only after the ‘X’ guess will the board populate with all the ‘Ts’.
Even stranger, when I add a random variable that increases by 1 each time useEffect runs, it works as expected.
For my own learning, I want to understand why this is happening, and how to resolve it in a not hacky way.
Below is my code – let me know if I’m missing any information.
Game.jsx
import Board from './Board'
import Input from './Input'
import { useState, useEffect } from 'react'
export default function Game() {
const answer = ['I', 'F', ' ', 'T', 'H', 'E', 'R', 'E', 'S', ' ', 'A', ' ', 'W', 'I', 'L', 'L', ' ', 'T', 'H', 'E', 'R', 'E', 'S', ' ', 'A', ' ', 'W', 'A', 'Y']
const [board, setBoard] = useState(new Array(answer.length).fill(' '))
const [guess, setGuess] = useState('')
const [count, setCount] = useState(0)
const [state, setState] = useState(0)
let newBoard = board
useEffect(() => {
let miss = true
setState(state + 1) // hack to make useeffect work correctly
answer.map((letter, index) => {
if (guess == letter) {
newBoard[index] = letter
miss = false
}
})
if (miss) { setCount(count + 1) }
setBoard(newBoard)
}, [guess])
return(
<>
<Input guess={guess} setGuess={setGuess} />
<Board board={board} answer={answer} />
</>
);
}
Input.jsx
import { useRef, useState } from "react";
export default function Input(props) {
const inputRef = useRef();
const handleSubmit = (e) => {
e.preventDefault();
props.setGuess(inputRef.current.value.toUpperCase())
}
return (
<form onSubmit={handleSubmit}>
<input
type="text"
ref={inputRef}
placeholder="Make a guess"
maxLength="1"
/>
<button type="submit">Enter</button>
</form>
);
}
Board.jsx
import Box from './Box'
import './assets/Board.css'
export default function Board(props) {
const board = props.board
const answer = props.answer
return (
<div className='board'>
{board.map((letter, index) => <Box key={index} value={letter} isSpace={answer[index] == ' '} />)}
</div>
);
}
Box.jsx
import './assets/Box.css'
export default function Box(props) {
const value = props.value
const isSpace = props.isSpace
return (
<div className={`box ${isSpace && ' is-space'}`}>{value}</div>
)
}
3
Answers
You should not use a
useEffect
to process such a change.You can define a custom function that will call the
set***
when needed.Please see the new
onGuess
function in the below demo:in the scenario you described, you don’t need to useEffect, as setSate(useSate) is enough to update the state.
and also this code
is a very big mistake.
It seems like you are trying to force a re-render by updating the state (setState(state + 1)) before performing the logic inside the useEffect. This is generally not a recommended practice. React will automatically trigger re-renders when state changes, so manually incrementing the state inside the useEffect is unnecessary and could lead to unexpected behavior.
The issue here is that when you are calling setBoard(newBoard) in your useEffect, newBoard contains a reference to your board state. Even when you update a value stored at a particular index, the reference is unchanged, and without a reference to a new value React won’t know that it needs to re-render. The most simple fix for this is just to use the spread operator when setting your state in your use effect:
This way you’re setting the board state to an entirely new array, which will trigger a rerender.
The
setState(state + 1)
call forces your code to work because with that line React does see a state update, and so it understands it needs to rerender.It is true though that you don’t need to use a useEffect here, at least as your code currently stands. Rather than setting a guess state (which it doesn’t seem you’re currently using other than to trigger this useEffect), you could create the following handler in your
Game
component:And then in your Input component, your
handleSubmit
method could look like this: