Как повторно отобразить только компонент JSX в функции?

#javascript #reactjs #react-hooks #components

Вопрос:

Я учусь реагировать, создавая игру в морской бой. Моя сетка выглядит так:

 const [board, setBoard] = 
   useState([{ship: false, beenHit: false}, {ship: true, beenHit: false},
               {ship: true, beenHit: false},{ship: false, beenHit: false},
               {ship: false, beenHit: false},{ship: true, beenHit: false}
              ])
 

Каждое поле представляет собой объект с 2 свойствами, подразумевающими, есть ли на поле корабль, и если игрок уже нажал на него. Я назначил функцию receiveHit() каждому полю, поэтому, когда игрок нажимает на поле, он помечает объект в массиве как {ship: true, beenHit: true}. Это работает нормально, но проблема в том, что сетка не перерисовывается, поэтому цвет поля не изменится.

Функция для определения имени класса (и, следовательно, цвета поля):

 function decideColor(field){
     if(field.beenHit amp;amp; field.ship){
        return 'red'
     } else{
         return 'blue'
       }
}
 

Повторный JSX:

 return ({
 board.map((field, i)=> <div onClick = {() => board.receiveHit(i)} className = {decideColor(field)}> 
 </div> )
})
 

Это внутри функционального компонента. Я не могу повторно отобразить весь компонент, так как положение кораблей изменится (они размещаются на доске случайным образом).

Ответ №1:

Я предполагаю, что вы устанавливаете поле непосредственно в состоянии. Таким образом, объект платы останется прежним и не вызовет повторной визуализации. Вы должны переназначить состояние и, таким образом, создать изменение состояния, на которое будет реагировать react. Дело в том, что вы не перерисовываете весь компонент целиком. Ваша функция возвращает новое состояние, да. Но внутренний механизм React решает, что нужно повторно отобразить в DOM, а что нет (реквизит и ключи играют здесь определенную роль, обязательно используйте их). Возьмите мой пример, откройте DevTools и посмотрите, какие изменения вносятся, когда вы нажимаете на поле. Вы увидите, что только нажатый div был отображен в DOM. Поэтому разумно сделать много меньших компонентов, чтобы сделать повторную визуализацию более эффективной.

 .board {
  display:flex; 
  width: calc(20em   20px); /* 10 x 10 field with borders */
  flex-wrap:wrap;
}

.field { 
  display:inline-block;
  width: 2em;
  height: 2em;
  background-color: lightgray;
  border: thin solid black;
}

.field.ship {
  background-color: blue;
}

.field.hit {
  background-color: grey;
}

.field.ship.hit {
  background-color: red;
}
 
 
const Field = ({field, ...props}) => {
  return <div {...props}/>
}

const Board = () => {
  // init 10 x 10 board
  const [board, setBoard] = React.useState([...new Array(100)].map((elem, index) => {return {}}));
  const onClick = (event, i) => {
    // This takes the existing board, replaces the element with the index i 
    // with hit set to true. And sets the newly created *copy* as the new 
    // version of the board. It will trigger a re-render but as mentioned
    // before, only the truly changed (in this case className changes) will 
    // rendered to the DOM.
    setBoard(Object.assign([...board], {
        [i]: {
            ...board[i],
            hit: true
        }
    }));
  }  
  
  const decideClassName = (i) => {
    // color is based on the keys and the resulting class combination,
    // field, field ship, field ship hit, and field hit (see CSS)
    return `field ${Object.keys(board[i]).join(" ")}`;
  }
  
  const setShip = (board, x, y, length, type = "horizontal") => {
    const index = y * 10   x;
    
    for (let i = 0; i < length; i  ) {
      board[index   i * (type == "horizontal" ? 1 : 10)].ship = true;  
    }
  }
  
  // init ships
  React.useEffect(() => {
    const copy = [...board];
    setShip(copy, 5,5,4);
    setShip(copy, 2,2,3, "vertical");
    setBoard(copy);
  }, [])
  
  return (
    <div className="board">
      {
        board.map((field, i) => <Field key={`field-${i}`} field={field} onClick={(event) => onClick(event, i)} className={decideClassName(i)}/>)
      }
    </div>

  )
}
    
ReactDOM.render(
    <Board/>,
  document.getElementById('root')
);