Учебник: Введение в React

Spread the love

Учебник не предполагает каких-либо предварительных знаний о React.

Прежде чем мы начнем изучение

Содержимое учебника:

  • Начальная настройка проекта – расскажем о том как создать проект.
  • Обзор – расскажем о базовых концепциях React: компоненты (components), свойства (props), и состояния (state).
  • Создание игры – расскажем о наиболее общих техниках разработки на React.
  • Добавление функционала Time Travel (Перемещение по времени) – расскажем более подробно о преимуществах использования React.

Примечание
По ходу чтения учебника вы можете копировать и вставлять код, но мы рекомендуем вводить его вручную. Это поможет вам развить мышечную память и более глубже понять материал учебника.

Что же мы будем создавать?

В этом уроке будет показано, как создать интерактивную игру в крестики-нолики на React.

Вы можете сразу увидеть, окончательный результат здесь. Если на данном этапе код вам совершенно не понятен, не беспокойтесь! Цель этого руководства – помочь вам научится использовать React и его синтаксис.

Мы рекомендуем вам ознакомиться с окончательной версии игры, прежде чем продолжить обучение. Одна из особенностей, которую вы можете заметить, это то, что справа от игрового поля есть нумерованный список. Этот список содержит историю всех ходов, произошедших в игре, и обновляется по ходу игры.

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

Начальная настройка проекта

Мы предполагаем, что вы хотя бы немного знакомы с HTML и JavaScript. Мы также предполагаем, что вы знакомы с такими понятиями программирования, как функции, объекты, массивы и классы.

Если вам нужно почитать о JavaScript, мы рекомендуем прочитать это руководство. Обратите внимание, что мы также используем некоторые функции ES6 – недавней версии JavaScript.

Есть два способа следовать этому руководству: вы можете либо писать код в своем браузере, либо локально создать проект.

Опция 1: Код в браузере

Это самый быстрый способ начать!

Сначала откройте этот стартовый код в новой вкладке. Новая вкладка должна отображать пустую игровую доску в крестики-нолики и код React.

Если вы выбрали этот вариант то вы можете пропустить второй вариант установки и сразу перейти к следующему разделу “Обзор”.

Опция 2: Создание локального проекта

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

Итак начнем:

  1. Убедитесь что у вас установлена последняя версия Node.js.
  2. Установите через npm Rect командой: npm install -g create-react-app

Далее создайте новый проект:

npx create-react-app my-app
  1. В папке нового проекта удалите все файлы в папке src/ 

Примечание: не удаляйте саму папку src, только файлы внутри нее.

cd my-app
cd src

# If you're using a Mac or Linux:
rm -f *

# Or, if you're on Windows:
del *

# Then, switch back to the project folder
cd ..
  1. Добавьте новый файл index.css с папку src/ со следующим кодом CSS.
  2. Добавьте файл index.js в src/  с этим JS кодом.
  3. Добавьте следующие три линии кода сверху файла index.js в папке src/:
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';

Сейчас если вы запустите  npm start в папке проекта и откроете в браузере http://localhost:3000 , вы должны увидеть пустое поле игры крестики нолики.

Начальный обзор React

Теперь, когда все настроено, давайте рассмотрим что такое React!

Что такое React?

React – это декларативная, эффективная и гибкая библиотека JavaScript для создания пользовательских интерфейсов. Она позволяет вам создавать сложные пользовательские интерфейсы из небольших и изолированных частей кода, называемых «компонентами».

В React есть несколько типов компонентов, мы начнем с подкласса React.Component:

class ShoppingList extends React.Component {
  render() {
    return (
      <div className="shopping-list">
        <h1>Shopping List for {this.props.name}</h1>
        <ul>
          <li>Instagram</li>
          <li>WhatsApp</li>
          <li>Oculus</li>
        </ul>
      </div>
    );
  }
}

// Example usage: <ShoppingList name="Mark" />

Компоненты в React являются строительными кирпичиками из чего строится проект. В примере выше ShoppingList – это класс компонента React. Компонент принимает параметры, называемые props (сокращение от «properties»), и возвращает иерархию представлений, отображаемых с помощью метода рендеринга render.

Метод render возвращает описание того, что вы хотите видеть на экране. React берет описание и отображает результат. В частности, render возвращает React element, который представляет собой упрощенное описание того, что нужно визуализировать. Большинство разработчиков React используют специальный синтаксис под названием «JSX», который облегчает написание этих структур. Так тег <div /> из синтаксиса JSX преобразуется во время сборки в React.createElement(‘div’). То есть пример выше эквивалентен:

return React.createElement('div', {className: 'shopping-list'},
  React.createElement('h1', /* ... h1 children ... */),
  React.createElement('ul', /* ... ul children ... */)
);

Если вам интересно, createElement() более подробно описан в API reference, но в этом руководстве мы не будем подробно это описывать. Вместо этого мы просто продолжим использование JSX.

JSX содержит в себе все мощь JavaScript. Вы можете поместить любые выражения в фигурные скобки внутри JSX. Каждый элемент React представляет собой объект JavaScript, который вы можете сохранить в переменной или передать другой переменной.

Компонент из примера выше ShoppingList сейчас рендерит только встроенные компоненты DOM, такие как  <div /> и <li />. Но вы также можете создавать и визуализировать пользовательские компоненты. Например, теперь мы можем ссылаться на весь список покупок, написав <ShoppingList />. Каждый компонент React самодостаточен и может работать независимо; это позволяет создавать сложные пользовательские интерфейсы из простых компонентов.

Проверка начального кода

Если вы собираетесь работать над учебником в своем браузере, откройте этот код в новой вкладке: Начальный код. Если вы собираетесь работать над учебником с локальным проектом откройте в вашем редакторе файл src/index.js в папке вашего проекта.

Этот стартовый код является основой того, что мы строим. И он уже содержит стили CSS, так что вам нужно сосредоточиться только на изучении React и программировании игры в крестики-нолики.

Изучив код, вы заметите, что у нас есть три компонента React:

  • Square
  • Board
  • Game

Компонент Square отображает одну кнопку <button>, а Board отображает 9 squares (квадратов). Компонент Game отображает игровую доску, которые мы изменим позже. В настоящее время в нашем коде нет интерактивных компонентов.

Передача параметров через props

Просто чтобы попробовать, давайте попытаемся передать некоторые данные из нашего компонента Board в наш компонент Square.

В методе renderSquare в Board измените код, чтобы передать значение с именем prop в Square:

В методе renderSquare компонента Board, измените код, чтобы передать через prop значение  value в Square:

class Board extends React.Component {
  renderSquare(i) {
    return <Square value={i} />;
  }

Теперь изменим метод  render компонента Square что бы отобразить переменную value {/* TODO */} на {this.props.value}:

class Square extends React.Component {
  render() {
    return (
      <button className="square">
        {this.props.value}
      </button>
    );
  }
}

До наших изменений:

После: Вы должны увидеть номера в каждой клетке поля.

Увидеть код целиком на данном шаге

Поздравляем! Вы только что передали “prop” из родительского компонента Board в дочерний компонент Square. Передача prop – это то, как информация передается в приложениях React от родительских к дочерним компонентам.

Делам компонент интерактивным

Давайте добавим немного функциональности. Сделаем так чтобы при клике на компонент Square в нем отображалось “X”. Сначала измените тег кнопки, который возвращается из функции render () компонента Square, следующим образом:

class Square extends React.Component {
  render() {
    return (
      <button className="square" onClick={function() { alert('click'); }}>
        {this.props.value}
      </button>
    );
  }
}

Если вы сейчас кликните по компоненту Square, вы увидите сообщение alert.

Примечание
Что бы сохранить типизацию и избежать запутанности с this, мы будем использовать синтаксис стрелочных функций для обработчиков событий здесь и далее ниже:

class Square extends React.Component {
 render() {
   return (
     <button className="square" onClick={() => alert('click')}>
       {this.props.value}
     </button>
   );
 }
}

Обратите внимание на onClick={() => alert('click')}, мы передали функции onClick другую функцию. Она запустится только после клика на компонент. Если забудете о () => и напишете onClick={alert('click')} то функцию запуститься сразу после отображения компонента.

На следующем шаге сделаем что бы компонент Square “запоминал” что по нему был клик, и отображал значение “X”. Чтобы компонент мог что нибудь “запомнить”, нужно использовать такое понятие как state.

React компоненты могут использовать state через переменную this.state в их конструкторах. this.state должно рассматриваться как приватная часть компонента React. Давайте сохраним текущее значение value компонента Square в this.state, и изменим это значение при клике по компоненту.

Для этого добавит конструктор в класс компонента:

class Square extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      value: null,
    };
  }

  render() {
    return (
      <button className="square" onClick={() => alert('click')}>
        {this.props.value}
      </button>
    );
  }
}

Примечание

В классах JavaScript, вам всегда нужно вызывать метод super при определение конструктора подкласса. Поэтому все конструкторы классов React компонентов должны начинаться с вызова super(props).

Сейчас изменим метод render компонента Square что бы отобразить текущее значение value при клике:

  • Заменим this.props.value на this.state.value внутри тега <button>.
  • Заменим обработчик событий () => alert() на () => this.setState({value: 'X'}).
  • Перенесем className и onClick на отдельные линии для удобство чтения.
class Square extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      value: null,
    };
  }

  render() {
    return (
      <button
        className="square"
        onClick={() => this.setState({value: 'X'})}
      >
        {this.state.value}
      </button>
    );
  }
}

Вызывая this.setState внутри обработчика onClick, мы обновляем указанное значение. После обновления значение this.state.value будет 'X', и мы увидите X на игровой доске. Если сейчас вы кликните на любой компонент Square, на нем должен отобразиться X .

Посмотреть код на этом шаге

Инструменты разработчика (Опционально)

React Devtools это расширение для Chrome и Firefox которые позволяет вам контролировать дерево компонентов React в вашем браузере.

После установки React DevTools вы можете щелкнуть правой кнопкой мыши по любому элементу на странице, нажать «Проверить», чтобы открыть инструменты разработчика, и вкладка React появится в качестве последней вкладки справа.

Однако, обратите внимание что нужно сделать несколько дополнительных шагов что бы DevTool заработал с CodePen:

  1. Залогинится или зарегистрироваться и подтвердить свой email (требуется для предотвращения спама).
  2. Нажать на кнопку “Fork”.
  3. Нажать на “Change View” и затем выбрать “Debug mode”.
  4. В новой вкладке которая откроется, должен отобразится вкладка React.

Завершение создание игры

Теперь у нас есть основные строительные блоки для нашей игры в крестики-нолики. Чтобы завершить игру, нам нужно чередовать размещение “X” и “O” на доске, и нам нужен способ определить победителя.

Поднятие state вверх

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

Как вариант мы можем сделать так что бы компонент Board опрашивал каждый компонент Square и получало бы его state. Хотя такой подход возможен в React, он будет не самым эффективным, а код станет трудным для понимания. Вместо этого лучше сохранять состояние (state) игры в родительском компоненте Board, а не в каждом Square. Пусть лучше компонент Board будет указывать каждому Square, что отображать, передавая prop, точно так же, как мы делали, когда передавали число в каждый квадрат.

Что бы собрать данные от нескольких дочерних компонентов или чтобы два дочерних компонента взаимодействовали друг с другом, вам нужно объявить общее состояние (state) в их родительском компоненте . Родительских компонент может передавать state дочерним компонентам используя props; это позволит синхронизировать все дочерние компоненты друг с другом.

Поднятие состояния (state) в родительский компонент является обычной практикой при рефакторинге компонентов React – давайте сделаем то же самое. Мы добавим constructor в Board и установим начальное состояние state, чтобы оно содержало массив с 9 нулями. Эти 9 нулей будут соответствовать 9 клеткам:

class Board extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      squares: Array(9).fill(null),
    };
  }

  renderSquare(i) {
    return <Square value={i} />;
  }

  render() {
    const status = 'Next player: X';

    return (
      <div>
        <div className="status">{status}</div>
        <div className="board-row">
          {this.renderSquare(0)}
          {this.renderSquare(1)}
          {this.renderSquare(2)}
        </div>
        <div className="board-row">
          {this.renderSquare(3)}
          {this.renderSquare(4)}
          {this.renderSquare(5)}
        </div>
        <div className="board-row">
          {this.renderSquare(6)}
          {this.renderSquare(7)}
          {this.renderSquare(8)}
        </div>
      </div>
    );
  }
}

Когда мы начнем заполнять доску, она может выглядеть примерно так:

[
  'O', null, 'X',
  'X', 'X', 'O',
  'O', null, null,
]

Ранее для отображение значений внутри Square, мы передавали value через prop со значениями от 0 до 8 в каждом Square. В предыдущем шаге, мы заменили числа на “X”.

Теперь мы снова используем механизм prop. Мы изменим Board, так чтобы проинформировать каждый отдельный Square о его текущем значении value ('X''O', or null). Мы уже определили массив  squares в конструкторе Board, и теперь изменим метод renderSquare :

  renderSquare(i) {
    return <Square value={this.state.squares[i]} />;
  }

Просмотреть текущее состояния кода на данном шаге 

Теперь каждый Square получит value значение которого будет одним из 'X''O', или null для пустых squares.

Далее, нам нужно определить то что будет происходит при клике на Square . Теперь компонент Board будет определять какое значение будет у squares. Нам нужно создать способ обновления состояния Square при обновление состояния Board. Так же нам будет нужна такая связь что бы при обновление состояния внутри Square обновлялась значение в Board. Но так как state рассматривается как приватная часть компонента мы не можем обновить state Board напрямую из Square.

Чтобы сохранить приватность state Board, мы передадим функцию от Board в Square. Эта функция будет вызываться при нажатии на Square. Для этого мы изменим метод renderSquare  компонента Board:

  renderSquare(i) {
    return (
      <Square
        value={this.state.squares[i]}
        onClick={() => this.handleClick(i)}
      />
    );
  }

Теперь мы передадим два props от Board в Square: value и onClick. prop onClick функция которая вызывается при клики на Square. Далее сделаем следующие изменения в Square:

  • Заменим this.state.value на this.props.value в методе render Square
  • Заменим this.setState() на this.props.onClick() в методе  render Square
  • Удалим constructor из Square потому что Square больше не отслеживаем состояние игры

После всех этих изменений компонент Square должен выглядит так:

class Square extends React.Component {
  render() {
    return (
      <button
        className="square"
        onClick={() => this.props.onClick()}
      >
        {this.props.value}
      </button>
    );
  }
}

Далее подробное описание того что мы сделали:

  1. Свойство onClick встроенного в DOM компонента  <button> говорит React установить обработчик клика.
  2. Когда на кнопку кликают, React вызывает обработчик onClick которые определен в методе render() компонента Square.
  3. Этот обработчик вызывает this.props.onClick(). Метод  onClick определенное в Board и передан в Square через props.
  4. Так как Board передает onClick={() => this.handleClick(i)} в Square, то при клике на Square вызывается this.handleClick(i).
  5. Но мы пока еще не определили метод handleClick(), и при попытки запустить наш код мы получим ошибку.

Примечание

Внутри пользовательским компонентов таких как Square, вы можете выбирать любые имена для свойств и методов. Мы можем назвать свойство onClick или метод handleClick любым именем. Однако в React, используется следующие соглашения о именование событий  on[Event] и методов обработчиков handle[Event] .

И так добавим метод handleClick в классе Board:

class Board extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      squares: Array(9).fill(null),
    };
  }

  handleClick(i) {
    const squares = this.state.squares.slice();
    squares[i] = 'X';
    this.setState({squares: squares});
  }

  renderSquare(i) {
    return (
      <Square
        value={this.state.squares[i]}
        onClick={() => this.handleClick(i)}
      />
    );
  }

  render() {
    const status = 'Next player: X';

    return (
      <div>
        <div className="status">{status}</div>
        <div className="board-row">
          {this.renderSquare(0)}
          {this.renderSquare(1)}
          {this.renderSquare(2)}
        </div>
        <div className="board-row">
          {this.renderSquare(3)}
          {this.renderSquare(4)}
          {this.renderSquare(5)}
        </div>
        <div className="board-row">
          {this.renderSquare(6)}
          {this.renderSquare(7)}
          {this.renderSquare(8)}
        </div>
      </div>
    );
  }
}

Посмотреть код на текущем шаге

После этих изменений мы снова можем кликать на Square, чтобы отобразить значения на них. Однако теперь состояние сохраняется в компоненте Board вместо отдельных компонентов Square. При изменении состояния Board компоненты Square автоматически перерисовываются. Сохранение состояния всех квадратов в компоненте Board позволит определить победителя в будущем. В терминах React компоненты Square теперь являются контролируемыми компонентами.

Обратите внимание что в handleClick, мы вызываем .slice() для создания копии массива squares и только потом редактирования в нем а не сразу редактирования исходного массива. Мы объясним почему мы создали копию массива squares в следующей секции.

Почему Immutability (“Неизменяемость”) важна

Существует два подхода к редактированию данных. Первый подход называется mutate то есть данные меняются напрямую. А второй подход называется immutability, он заключается в внесение изменений в копию данных и сохранения исходных данных не тронутыми.

Изменения данных напрямую

var player = {score: 1, name: 'Jeff'};
player.score = 2;
// Now player is {score: 2, name: 'Jeff'}

Измении копии данных

var player = {score: 1, name: 'Jeff'};

var newPlayer = Object.assign({}, player, {score: 2});
// Now player is unchanged, but newPlayer is {score: 2, name: 'Jeff'}

// Or if you are using object spread syntax proposal, you can write:
// var newPlayer = {...player, score: 2};

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

Сложные функции становятся простыми

Неизменность делает сложные функции намного проще для реализации. Позже в этом уроке мы реализуем функцию “time travel” (“путешествие во времени”), которая позволит просмотреть историю игры в крестики-нолики и возможность «вернутся» к предыдущим ходам. Эта функциональность не является специфичной для игр – способность отменять и повторять определенные действия является распространенным требованием в приложениях. Если мы не будет напрямую редактировать данные, мы сможем сохранять предыдущие версии истории игры и использовать их позже.

Обнаружение изменений

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

Обнаружение изменений в неизменяемых объектах значительно проще. Если неизменяемый объект, отличается от предыдущего, то объект изменился.

Определение того, когда нужно перерисовать объект в React

Основное преимущество неизменяемости заключается в том, что она помогает создавать чистые компоненты в React. В неизменяемых данных можно легко определить, были ли внесены в них изменения, что помогает определить, когда компонент требует повторного рендеринга.

Функциональные компоненты

Теперь мы преобразуем Square в функциональный компонент.

В React, функциональные компоненты это такие компоненты которые содержать только метод render и не содержит их собственное состояние state. Вместо того что бы создавать класс на основе React.Component, мы можем написать функцию которая принимает props в качестве аргументов и возвращает то что должно быть визуализировано методом render. Функциональные компоненты как правило более простые чем классы.

Заменим класс Square на функцию:

function Square(props) {
  return (
    <button className="square" onClick={props.onClick}>
      {props.value}
    </button>
  );
}

Просмотреть код полностью на данном шаге

Примечание

Когда мы изменили Square на функциональный компонент, мы так же изменили onClick={() => this.props.onClick()} на более короткую запись onClick={props.onClick} (обратите внимание на отсутствие скобок с обеих сторон). В классе, мы использовали стрелочную функцию для доступа к значению this, но в функциональном компоненте нам не нужно использовать this.

Переход хода

Теперь нам нужно исправить очевидный дефект в нашей игре: мы не может поставить букву “O” на доску.

По умолчанию первый ход за «X». Добавим в конструктор класса Board новую переменную xIsNext отвечающую за переход хода:

class Board extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      squares: Array(9).fill(null),
      xIsNext: true,
    };
  }

Каждый раз когда игрок ходит, xIsNext (a boolean) будет переключать свое состояние для того что бы определить какой игрок делает следующий ход. Мы обновим функцию handleClick так что бы переключать значение xIsNext сразу после окончания хода:

  handleClick(i) {
    const squares = this.state.squares.slice();
    squares[i] = this.state.xIsNext ? 'X' : 'O';
    this.setState({
      squares: squares,
      xIsNext: !this.state.xIsNext,
    });
  }

Теперь каждый раз при клике будет сменяться ход. Давайте также изменим текст “status” в методе render  Board, чтобы он отображал, какой игрок должен сделать следующий ход:

  render() {
    const status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');

    return (
      // the rest has not changed

После применения этих изменений Board должен выглядить следующим образом:

class Board extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      squares: Array(9).fill(null),
      xIsNext: true,
    };
  }

  handleClick(i) {
    const squares = this.state.squares.slice();
    squares[i] = this.state.xIsNext ? 'X' : 'O';
    this.setState({
      squares: squares,
      xIsNext: !this.state.xIsNext,
    });
  }

  renderSquare(i) {
    return (
      <Square
        value={this.state.squares[i]}
        onClick={() => this.handleClick(i)}
      />
    );
  }

  render() {
    const status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');

    return (
      <div>
        <div className="status">{status}</div>
        <div className="board-row">
          {this.renderSquare(0)}
          {this.renderSquare(1)}
          {this.renderSquare(2)}
        </div>
        <div className="board-row">
          {this.renderSquare(3)}
          {this.renderSquare(4)}
          {this.renderSquare(5)}
        </div>
        <div className="board-row">
          {this.renderSquare(6)}
          {this.renderSquare(7)}
          {this.renderSquare(8)}
        </div>
      </div>
    );
  }
}

Просмотр кода на текущем шаге

Определение победителя

Теперь, когда мы показываем, какой ход игрока следующий, мы должны также определить состояние, когда игра выиграна, и больше нет ходов. Так же нам нужно определить кто именно победитель, поэтому добавим вспомогательную функцию в конец файла:

function calculateWinner(squares) {
  const lines = [
    [0, 1, 2],
    [3, 4, 5],
    [6, 7, 8],
    [0, 3, 6],
    [1, 4, 7],
    [2, 5, 8],
    [0, 4, 8],
    [2, 4, 6],
  ];
  for (let i = 0; i < lines.length; i++) {
    const [a, b, c] = lines[i];
    if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
      return squares[a];
    }
  }
  return null;
}

Для определение победы вызовим  calculateWinner(squares) в методе render класса Board. Если игрок выиграл, мы отобразим текст “Winner: X” или “Winner: O”. Так же заменим объявление status в методе render следующим кодом:

  render() {
    const winner = calculateWinner(this.state.squares);
    let status;
    if (winner) {
      status = 'Winner: ' + winner;
    } else {
      status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
    }

    return (
      // the rest has not changed

Сейчас изменим функцию handleClick так что бы при определение победы или при выборе состояния она игнорировала бы дальнейшие клики по Square:

  handleClick(i) {
    const squares = this.state.squares.slice();
    if (calculateWinner(squares) || squares[i]) {
      return;
    }
    squares[i] = this.state.xIsNext ? 'X' : 'O';
    this.setState({
      squares: squares,
      xIsNext: !this.state.xIsNext,
    });
  }

Посмотреть код на данном шаге

Поздравляем! Теперь у вас есть рабочая игра в крестики-нолики. И вы только что изучили основы React. Так что именно вы стали настоящим победителем в этой игре.

Добавление перемещение по времени

В качестве последнего упражнения, давайте сделаем возможным “вернуться назад во времени” к предыдущим ходам в игре.

Сохранение истории ходов

Если бы мы меняли бы массив squares, то бы наша реализация перемещение по времени была бы достаточной сложной.

Однако, мы использовали slice() для создания новой копии массива  squares после каждого хода что значит что он остается неизменным. Мы сохраним прошлую версию массива  squares в другом массиве history. Массив history будет хранить все прошедшие состояние игры в следующем виде:

history = [
  // Before first move
  {
    squares: [
      null, null, null,
      null, null, null,
      null, null, null,
    ]
  },
  // After first move
  {
    squares: [
      null, null, null,
      null, 'X', null,
      null, null, null,
    ]
  },
  // After second move
  {
    squares: [
      null, null, null,
      null, 'X', null,
      null, null, 'O',
    ]
  },
  // ...
]

Сейчас нам надо решить в каком компоненте будет находиться массив  history.

Снова поднятие вверх

Мы хотим, чтобы компонент Game верхнего уровня отображал список прошлых ходов. Для этого потребуется доступ к истории, поэтому мы поместим  history в компонент Game верхнего уровня. Это так же позволит нам удалить squares из его дочерних компонентов. Подобно тому, как мы “подняли состояние” из компонента Square в Board, сейчас мы поднимим переменную из Board в Game. Это даст компоненту Game полный контроль на данными в Board, и позволит Board рендерить предыдущие хода из history.

Для начало, создадим конструктор в компоненте Game и зададим его начальный state:

class Game extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      history: [{
        squares: Array(9).fill(null),
      }],
      xIsNext: true,
    };
  }

  render() {
    return (
      <div className="game">
        <div className="game-board">
          <Board />
        </div>
        <div className="game-info">
          <div>{/* status */}</div>
          <ol>{/* TODO */}</ol>
        </div>
      </div>
    );
  }
}

Далее сделаем так что бы компонент Board получал squares и свойство onClick от компонента Game. Так как теперь у нас будет один обработчик клика в Board для всех Squares, нам будет нужно передавать расположение каждого Square в обработчик onClick чтобы понимать на каком Square был клик. Внесем следующие изменения в Board:

  • Удалим constructor из Board.
  • Заменим this.state.squares[i] на this.props.squares[i] в renderSquare.
  • Заменим this.handleClick(i) на this.props.onClick(i) в renderSquare.

Компонент Board должен теперь выглядеть следующим образом:

class Board extends React.Component {
  handleClick(i) {
    const squares = this.state.squares.slice();
    if (calculateWinner(squares) || squares[i]) {
      return;
    }
    squares[i] = this.state.xIsNext ? 'X' : 'O';
    this.setState({
      squares: squares,
      xIsNext: !this.state.xIsNext,
    });
  }

  renderSquare(i) {
    return (
      <Square
        value={this.props.squares[i]}
        onClick={() => this.props.onClick(i)}
      />
    );
  }

  render() {
    const winner = calculateWinner(this.state.squares);
    let status;
    if (winner) {
      status = 'Winner: ' + winner;
    } else {
      status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
    }

    return (
      <div>
        <div className="status">{status}</div>
        <div className="board-row">
          {this.renderSquare(0)}
          {this.renderSquare(1)}
          {this.renderSquare(2)}
        </div>
        <div className="board-row">
          {this.renderSquare(3)}
          {this.renderSquare(4)}
          {this.renderSquare(5)}
        </div>
        <div className="board-row">
          {this.renderSquare(6)}
          {this.renderSquare(7)}
          {this.renderSquare(8)}
        </div>
      </div>
    );
  }
}

Обновим функцию render в компоненте Game что бы задействовать самую последнюю запись в истории ходов для определения и отображения статуса игры:

  render() {
    const history = this.state.history;
    const current = history[history.length - 1];
    const winner = calculateWinner(current.squares);

    let status;
    if (winner) {
      status = 'Winner: ' + winner;
    } else {
      status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
    }

    return (
      <div className="game">
        <div className="game-board">
          <Board
            squares={current.squares}
            onClick={(i) => this.handleClick(i)}
          />
        </div>
        <div className="game-info">
          <div>{status}</div>
          <ol>{/* TODO */}</ol>
        </div>
      </div>
    );
  }

Так как компонент Game сейчас отображает статус игры мы можем удалить соотвествующий код из метода render компонента Board. После рефакторинга метод render должен выглядеть так:

  render() {
    return (
      <div>
        <div className="board-row">
          {this.renderSquare(0)}
          {this.renderSquare(1)}
          {this.renderSquare(2)}
        </div>
        <div className="board-row">
          {this.renderSquare(3)}
          {this.renderSquare(4)}
          {this.renderSquare(5)}
        </div>
        <div className="board-row">
          {this.renderSquare(6)}
          {this.renderSquare(7)}
          {this.renderSquare(8)}
        </div>
      </div>
    );
  }

И в завершение, нам нужно переместить метод handleClick из компонента Board в компонент Game. Нам так же нужно модифицировать handleClick потому что теперь изменилась структура состояний в Game. В методе handleClick, теперь мы добавляем сделанные ход в  history.

  handleClick(i) {
    const history = this.state.history;
    const current = history[history.length - 1];
    const squares = current.squares.slice();
    if (calculateWinner(squares) || squares[i]) {
      return;
    }
    squares[i] = this.state.xIsNext ? 'X' : 'O';
    this.setState({
      history: history.concat([{
        squares: squares,
      }]),
      xIsNext: !this.state.xIsNext,
    });
  }

Примечание

В данном коде мы предпочли используем метод concat(), который в отличие метода push()  не изменяет начальный массив а создает новый.

На данном шаге компонет Board нуждается только в методах renderSquare и render. Состояние игры и метод handleClick должны быть в компоненты Game.

Просмотреть код на текущем шаге

Отображение прошедших ходов

Поскольку мы сохраняем историю игры, теперь мы можем отобразить ее игроку в виде списка прошлых ходов.

В JavaScript, для работы с массивами есть метод map() который обычно используется для наложения одних данных на другие. Например:

const numbers = [1, 2, 3];
const doubled = numbers.map(x => x * 2); // [2, 4, 6]

Используя метод map, мы можем сопоставить нашу историю ходов с элементами представляющими кнопки на экране, и отобразить список кнопок, чтобы “перейти” к прошлым ходам.

 Внесем изменения в метод render копонента Game:

  render() {
    const history = this.state.history;
    const current = history[history.length - 1];
    const winner = calculateWinner(current.squares);

    const moves = history.map((step, move) => {
      const desc = move ?
        'Go to move #' + move :
        'Go to game start';
      return (
        <li>
          <button onClick={() => this.jumpTo(move)}>{desc}</button>
        </li>
      );
    });

    let status;
    if (winner) {
      status = 'Winner: ' + winner;
    } else {
      status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
    }

    return (
      <div className="game">
        <div className="game-board">
          <Board
            squares={current.squares}
            onClick={(i) => this.handleClick(i)}
          />
        </div>
        <div className="game-info">
          <div>{status}</div>
          <ol>{moves}</ol>
        </div>
      </div>
    );
  }

Просмотреть код полностью

Для каждого хода в истории игры мы создаем элемент списка , который содержит кнопку . Кнопка имеет обработчик onClick, который вызывает метод this.jumpTo(). Мы еще не реализовали метод jumpTo(). Пока что мы должны увидеть список ходов, которые произошли в игре, и предупреждение в консоли инструментов разработчика, которое гласит:

Для каждого хода в истории игры, мы создадим элмент списка  <li> в котором будет кнопка  <button>. На кнопке будет обработчик клика onClick который будет вызывать метод this.jumpTo(). Мы пока еще не реализовали метод jumpTo(). Сейчас в браузере мы должны увидеть список ходов, которые произошли в игре, и предупреждение в консоле: Warning: Each child in an array or iterator should have a unique “key” prop. (Каждый дочерний элемент в массиве или итераторе должен иметь уникальное свойство “ключ”.)

Выбор ключа

Когда мы отображаем список, React сохраняет некоторую информацию о каждом отображаемом элементе списка. Когда мы обновляем список, React должен определить, что изменилось. Мы можем добавлять, удалять, переупорядочивать или обновлять элементы списка.

Представьте себе такое изменение

было

<li>Alexa: 7 tasks left</li>
<li>Ben: 5 tasks left</li>

стало

<li>Ben: 9 tasks left</li>
<li>Claudia: 8 tasks left</li>
<li>Alexa: 5 tasks left</li>

В дополнение к обновленным подсчетам, человек, читающий это, вероятно, сказал бы, что мы поменяли местами Алексу и Бена и вставили Клаудию между Алекса и Бена. Однако React – это компьютерная программа, которая не знает, что мы собираемся сделать. Поскольку React не может знать наши намерения, нам необходимо указать ключ для каждого элемента списка, чтобы отличать каждый элемент списка от других элементов. Одним из вариантов было бы использовать строки alexa, ben, claudia. Если бы мы отображали данные из базы данных, в качестве ключей могли бы использоваться идентификаторы ID элементов Alexa, Ben и Claudia.

<li key={user.id}>{user.name}: {user.taskCount} tasks left</li>

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

Ключ key  – это специальное, зарезервированное свойство в React (такое же как например ref). Когда элемент создан, React извлекает свойство key  и сохраняет непосредственно в возвращаемом элементе. React автоматически использует ключ, чтобы определить, какие компоненты обновлять. Но сам компонент не может получить доступ к своему key . Даже если key может выглядеть так, как будто он принадлежит props, на него нельзя ссылаться, используя this.props.key.

Настоятельно рекомендуется назначать правильные ключи при создании динамических списков. Если вы не можете подобрать подходящий ключ, возможно вам нужно задумать о реструктуризации данных.

Если ключ не указан, React выдаст предупреждение и по умолчанию будет использовать индекс массива в качестве ключа. Использование индекса массива в качестве ключа может быть проблематичным так как при попытке изменить порядок элементов списка или при вставке/удалении элементов списка может возникнуть конфликт. Явное задание key={i} отключит предупреждение, но имеет те же проблемы, что и индексы массива, и поэтому в большинстве случаев не рекомендуется.

Реализация перемещение по времени

В истории игры в крестики-нолики с каждым прошедшим ходом связан уникальный идентификатор: это порядковый номер хода. Ходы никогда не переупорядочиваются, не удаляются и не вставляются в середину, поэтому можно использовать индекс движения в качестве ключа.

В методе render компонента Game, добавим ключ <li key={move}>:

    const moves = history.map((step, move) => {
      const desc = move ?
        'Go to move #' + move :
        'Go to game start';
      return (
        <li key={move}>
          <button onClick={() => this.jumpTo(move)}>{desc}</button>
        </li>
      );
    });

Посмотреть код полностью

Нажатие любой кнопок элемента списка прошедших ходов приводит к ошибке, потому что мы пока не создали метод jumpTo . Но прежде чем мы перейдем к реализации jumpTo, добавим stepNumber в состояние компонента Game, чтобы указать, какой шаг мы сейчас просматриваем.

Добавим stepNumber: 0 в начальное состояние конструктора Game:

class Game extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      history: [{
        squares: Array(9).fill(null),
      }],
      stepNumber: 0,
      xIsNext: true,
    };
  }

Далее, определим метод jumpTo в Game для обновления stepNumber. Так же назначим xIsNext  true если значение в stepNumber четное:

  handleClick(i) {
    // this method has not changed
  }

  jumpTo(step) {
    this.setState({
      stepNumber: step,
      xIsNext: (step % 2) === 0,
    });
  }

  render() {
    // this method has not changed
  }

Далее внесем несколько изменений в метод handleClick.

Добавленно нами переменная stepNumber отображает текущий ход пользователя После того как мы сделаем следующий ход, нам нужно обновить stepNumber добавив stepNumber: history.length в качестве аргумента this.setState.

Так же нужно заменить this.state.history на this.state.history.slice(0, this.state.stepNumber + 1). Это гарантирует что если мы “вернемся назад” а затем сделаем новый ход, мы отбросим все “будущие” состояния в истории потому что они теперь будут не нужны.

  handleClick(i) {
    const history = this.state.history.slice(0, this.state.stepNumber + 1);
    const current = history[history.length - 1];
    const squares = current.squares.slice();
    if (calculateWinner(squares) || squares[i]) {
      return;
    }
    squares[i] = this.state.xIsNext ? 'X' : 'O';
    this.setState({
      history: history.concat([{
        squares: squares
      }]),
      stepNumber: history.length,
      xIsNext: !this.state.xIsNext,
    });
  }

Наконец, изменим метод рендеринга компонента Game, чтобы рендеринг последнего хода всегда выполнялся в соответствии с  stepNumber:

  render() {
    const history = this.state.history;
    const current = history[this.state.stepNumber];
    const winner = calculateWinner(current.squares);

    // the rest has not changed

Посмотреть код полностью

Завершение

Поздравляем! Вы только что создали игру крестики нолики которая:

  • Позволяет вам играть в крестики-нолики,
  • Определяет состояние, когда игрок выиграл игру,
  • Сохраняет историю игры по ходу игры,
  • Позволяет игрокам просматривать историю игры и просматривать предыдущие версии игрового поля.

Хорошая работа! Мы надеемся, что теперь у вас есть начальные знания того как работать с React.

 Финальный результат.

Если у вас есть время и желание попрактиковаться с React, вот несколько идей по улучшению, которые вы можете внести в игру, которые перечислены в порядке возрастания сложности:

  1. Отобразите местоположение каждого перемещения в формате (столбец, строка) в списке истории перемещений.
  2. Выделите жирным текущий выбранный элемент в списке перемещения.
  3. Перепишите компонент Board, так чтобы использовать два цикла для создания квадратов вместо их жесткого кодирования.
  4. Добавьте кнопку переключения (toggle), которая позволяет сортировать ходы в порядке возрастания или убывания.
  5. Когда кто-то выигрывает, выделите три поля, которые оказались выигрышные.
  6. Когда никто не выигрывает, выведите сообщение о ничье.

В этом уроке мы затронули базовые концепции React, такие как элементы, компоненты, реквизиты и состояние. Для более подробного объяснения каждой из этих тем ознакомьтесь с остальной документацией.

Оригинал


Spread the love

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *