skip to Main Content

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


  1. 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:

    const { useState, useEffect, useRef } = React;
    
    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
    
        const onGuess = (g) => {
          const boardCopy = [ ...board ];
          
          setState(s => s + 1);
          
          let miss = true;
          answer.forEach((a, i) => {
              if (g === a) {
                  boardCopy[i] = a;
                  miss = false;
              }
          });
          
          if (miss) {
              setCount(c => c + 1);
          }
          
          setBoard(boardCopy);
        }
        
        return(
            <React.Fragment> 
                <Input guess={guess} setGuess={onGuess} />
                <Board board={board} answer={answer} />
            </React.Fragment>
        );
    }
    
    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>
        );
    }
    
    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>
        );
    }
    
    function Box(props) {
        const value = props.value
        const isSpace = props.isSpace
    
        return (
            <div className={`box ${(isSpace) && ' is-space'}`}>{value}</div>
        )
    }
    
    ReactDOM.render(<Game />, document.getElementById("react"));
    .board {
      display: flex;
      margin-top: 10px;
    }
    
    .box { 
        width: 20px;
        height: 20px;
        border: 1px solid black;
        text-align: center;
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
    <div id="react"></div>
    Login or Signup to reply.
  2. in the scenario you described, you don’t need to useEffect, as setSate(useSate) is enough to update the state.
    and also this code

        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])

    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.

    Login or Signup to reply.
  3. 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:

    setBoard([...newBoard]);
    

    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:

    const handleGuess = (guess) => {
      let miss = true;
      answer.forEach((letter, index) => {
        if (guess == letter) {
          newBoard[index] = letter
          miss = false
        }
      })
      if (miss) {
        setCount(count + 1);
      }
      setBoard([...newBoard]);
    }
    
    return (
      <> 
         <Input handleGuess={handleGuess} />
         <Board board={board} answer={answer} />
      </>
    )
    

    And then in your Input component, your handleSubmit method could look like this:

    const handleSubmit = (e) => {
      e.preventDefault();
      handleGuess(inputRef.current.value.toUpperCase())
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search