Учебник: Введение в React
Учебник не предполагает каких-либо предварительных знаний о React.
Прежде чем мы начнем изучение
Содержимое учебника:
- Начальная настройка проекта — расскажем о том как создать проект.
- Обзор — расскажем о базовых концепциях React: компоненты (components), свойства (props), и состояния (state).
- Создание игры — расскажем о наиболее общих техниках разработки на React.
- Добавление функционала Time Travel (Перемещение по времени) — расскажем более подробно о преимуществах использования React.
Примечание
По ходу чтения учебника вы можете копировать и вставлять код, но мы рекомендуем вводить его вручную. Это поможет вам развить мышечную память и более глубже понять материал учебника.
Что же мы будем создавать?
В этом уроке будет показано, как создать интерактивную игру в крестики-нолики на React.
Вы можете сразу увидеть, окончательный результат здесь. Если на данном этапе код вам совершенно не понятен, не беспокойтесь! Цель этого руководства — помочь вам научится использовать React и его синтаксис.
Мы рекомендуем вам ознакомиться с окончательной версии игры, прежде чем продолжить обучение. Одна из особенностей, которую вы можете заметить, это то, что справа от игрового поля есть нумерованный список. Этот список содержит историю всех ходов, произошедших в игре, и обновляется по ходу игры.
Когда вы ознакомитесь с игрой, вы можете закрыть игру, она нам пока не понадобится. Наш следующий шаг будет — настроить окружение так, чтобы можно было бы приступить к созданию игру.
Начальная настройка проекта
Мы предполагаем, что вы хотя бы немного знакомы с HTML и JavaScript. Мы также предполагаем, что вы знакомы с такими понятиями программирования, как функции, объекты, массивы и классы.
Если вам нужно почитать о JavaScript, мы рекомендуем прочитать это руководство. Обратите внимание, что мы также используем некоторые функции ES6 — недавней версии JavaScript.
Есть два способа следовать этому руководству: вы можете либо писать код в своем браузере, либо локально создать проект.
Опция 1: Код в браузере
Это самый быстрый способ начать!
Сначала откройте этот стартовый код в новой вкладке. Новая вкладка должна отображать пустую игровую доску в крестики-нолики и код React.
Если вы выбрали этот вариант то вы можете пропустить второй вариант установки и сразу перейти к следующему разделу «Обзор».
Опция 2: Создание локального проекта
Этот выбор потребует больше времени, но позволяет вам более глубже освоить учебник и использовать локальный редактор по вашему выбору.
Итак начнем:
- Убедитесь что у вас установлена последняя версия Node.js.
- Установите через npm Rect командой:
npm install -g create-react-app
Далее создайте новый проект:
npx create-react-app my-app
- В папке нового проекта удалите все файлы в папке
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 ..
- Добавьте новый файл
index.css
с папкуsrc/
со следующим кодом CSS. - Добавьте файл
index.js
вsrc/
с этим JS кодом. - Добавьте следующие три линии кода сверху файла
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:
- Залогинится или зарегистрироваться и подтвердить свой email (требуется для предотвращения спама).
- Нажать на кнопку “Fork”.
- Нажать на “Change View” и затем выбрать “Debug mode”.
- В новой вкладке которая откроется, должен отобразится вкладка 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> ); } }
Далее подробное описание того что мы сделали:
- Свойство
onClick
встроенного в DOM компонента<button>
говорит React установить обработчик клика. - Когда на кнопку кликают, React вызывает обработчик
onClick
которые определен в методеrender()
компонента Square. - Этот обработчик вызывает
this.props.onClick()
. МетодonClick
определенное в Board и передан в Square через props. - Так как Board передает
onClick={() => this.handleClick(i)}
в Square, то при клике на Square вызываетсяthis.handleClick(i)
. - Но мы пока еще не определили метод
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, вот несколько идей по улучшению, которые вы можете внести в игру, которые перечислены в порядке возрастания сложности:
- Отобразите местоположение каждого перемещения в формате (столбец, строка) в списке истории перемещений.
- Выделите жирным текущий выбранный элемент в списке перемещения.
- Перепишите компонент Board, так чтобы использовать два цикла для создания квадратов вместо их жесткого кодирования.
- Добавьте кнопку переключения (toggle), которая позволяет сортировать ходы в порядке возрастания или убывания.
- Когда кто-то выигрывает, выделите три поля, которые оказались выигрышные.
- Когда никто не выигрывает, выведите сообщение о ничье.
В этом уроке мы затронули базовые концепции React, такие как элементы, компоненты, реквизиты и состояние. Для более подробного объяснения каждой из этих тем ознакомьтесь с остальной документацией.