Возникли проблемы с реализацией функции истории для крестиков-ноликов с использованием реагирующих крючков

В настоящее время я пытаюсь сделать проект Tic Tac Toe, используя хуки. У меня проблемы с реализацией функциональности "история":

import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";
import "./styles.scss";

const App = props => {
  const [turn, setTurn] = useState("X");
  const [history, setHistory] = useState([Array(9).fill(null)]);
  const [winner, setWinner] = useState(null);

  const latestBoard = history[history.length - 1];

  const handleClick = i => {
    if (latestBoard[i] !== null) {
      return;
    }

    latestBoard.splice(i, 1, turn);
    setHistory([...history, [...latestBoard]]);
    setTurn(turn === "O" ? "X" : "O");
  };

  const handleHistory = index => {
    if (index === 1) {
      setHistory([Array(9).fill(null)]);
      return;
    }

    let newHistory = history.slice(0, index);
    setHistory(newHistory);
    console.info(history);
  };

  const calcWinner = squares => {
    /*...*/
  };

  return (
    <div className = "container">
      <GameBoard onClick = {idx => handleClick(idx)} squares = {latestBoard} />
      <Sidebar
        winner = {winner}
        turn = {turn}
        history = {history}
        goBack = {idx => handleHistory(idx)}
      />
    </div>
  );
};

const GameBoard = ({ squares, onClick }) => {
  const grid = squares.map((item, idx) => {
    return (
      <div key = {idx} onClick = {() => onClick(idx)}>
        {item}
      </div>
    );
  });

  return <div className = "grid-container">{grid}</div>;
};

const Sidebar = ({ winner, turn, history, goBack }) => {
  let historyList = history.map((pos, idx) => {
    return idx >= 1 ? ( 
      <li key = {idx} onClick = {() => goBack(idx)}>
        {idx}
      </li>
    ) : null;
  });

  let info;

  if (winner) {
    info = `The winner is ${winner}`;
  } else {
    info = `${turn} it's your turn`;
  }

  return (
    <div className = "sidebar">
      <div className = "gameInfo">{info}</div>
      <ul className = "history-dropdown"> Go to move {historyList}</ul>
    </div>
  );
};

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);



Каждый раз, когда я щелкаю элемент списка, чтобы вызвать handleHistory, он не вступает в силу до второго щелчка.

У меня есть пример кода: https://codesandbox.io/embed/q75y24qj49?fontsize=14

Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Навигация по приложениям React: Исчерпывающее руководство по React Router
Навигация по приложениям React: Исчерпывающее руководство по React Router
React Router стала незаменимой библиотекой для создания одностраничных приложений с навигацией в React. В этой статье блога мы подробно рассмотрим...
Массив зависимостей в React
Массив зависимостей в React
Все о массиве Dependency и его связи с useEffect.
1
0
151
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Есть две небольшие проблемы, которые необходимо исправить. Вы правильно поняли часть React. Проблемы:

  1. В вашем щелчке дескриптора вам нужно создать новый массив вместо использования старого.
  2. параметры среза должны отличаться в истории обработки, чтобы получить желаемый результат.

Добавили комментарии в ваш код для понимания. Надеюсь, это будет полезно для вас.

const { useState, useEffect } = React


const App = props => {
  const [turn, setTurn] = useState("X");
  const [history, setHistory] = useState([Array(9).fill(null)]);
  const [winner, setWinner] = useState(null);

  const latestBoard = history[history.length - 1];

  const handleClick = i => {
    if (latestBoard[i] !== null) {
      return;
    }
    //latestBoard.splice(i, 1, turn);
    //setHistory([...history, [...latestBoard]]);
    // latestBoard and history are same
    // reference hence you are spreading the same array
    // you need to do something like this below
    const copylatestBoard = [...latestBoard]; // Because array are passed by
    // reference 1st change
    copylatestBoard.splice(i, 1, turn); // 2nd change

    setHistory([...history, [...copylatestBoard]]); //3rd change
    setTurn(turn === "O" ? "X" : "O");
  };

  const handleHistory = index => {
    // if (index === 1) {
    //   setHistory([Array(9).fill(null)]);
    //   return;
    // }
    let newHistory = history.slice(0, index + 1); // need to be index + 1 to slice desired
    // as history at 0 is blank board. I am sure it will be obvious now
    setHistory(newHistory);
  };

  const calcWinner = squares => {
    /*...*/
  };

  return (
    <div className = "container">
      <GameBoard onClick = {idx => handleClick(idx)} squares = {latestBoard} />
      <Sidebar
        turn = {turn}
        history = {history}
        goBack = {idx => handleHistory(idx)}
      />
    </div>
  );
};

const GameBoard = ({ squares, onClick }) => {
  const grid = squares.map((item, idx) => {
    return (
      <div key = {idx} onClick = {() => onClick(idx)}>
        {item}
      </div>
    );
  });

  return <div className = "grid-container">{grid}</div>;
};

const Sidebar = ({ winner, turn, history, goBack }) => {
  let historyList = history.map((pos, idx) => {
    return idx >= 1 ? ( // to start after the first click
      <li key = {idx} onClick = {() => goBack(idx)}>
        {idx}
      </li>
    ) : null;
  });

  let info;

  if (winner) {
    info = `The winner is ${winner}`;
  } else {
    info = `${turn} it's your turn`;
  }

  return (
    <div className = "sidebar">
      <div className = "gameInfo">{info}</div>
      <ul className = "history-dropdown"> Go to move {historyList}</ul>
    </div>
  );
};

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
body {
	 font-family: sans-serif;
	 text-align: center;
	 display: flex;
	 justify-content: center;
}
 .container {
	 border: 1px solid black;
	 width: 25em;
	 height: 90vh;
	 max-height: 18em;
	 display: flex;
	 justify-content: center;
}
 .container > * {
	 border: 1px solid black;
	 margin: 1em;
}
 .grid-container {
	 flex: 5;
	 display: grid;
	 grid-template-columns: 1fr 1fr 1fr;
	 grid-template-rows: 1fr 1fr 1fr;
	 grid-template-areas: ". . ." ". . ." ". . .";
	 position: relative;
	 height: 80%;
}
 .grid-container > * {
	 border: 1px solid black;
}
 .grid-container > *:hover {
	 cursor: pointer;
}
 .sidebar {
	 padding: 1em;
}
 .sidebar .history-dropdown {
	 transition: all 600ms ease-in-out;
}
 .sidebar .history-dropdown > *:hover {
	 cursor: pointer;
	 border: 1px solid black;
}
 
<script src = "https://cdnjs.cloudflare.com/ajax/libs/react/16.8.6/umd/react.production.min.js"></script>
<script src = "https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.6/umd/react-dom.production.min.js"></script>

<div id = "root"></div>

Другие вопросы по теме