React チュートリアル TypeScriptのフック版

Reactのチュートリアルを、TypeScriptでフックを使用して実装コードになります。


目次

React MAIN CONCEPTS の確認

React ドキュメントを読むのに必要最小限の知識

  • We define variables with let and const statements. For the purposes of the React documentation, you can consider them equivalent to var.
    • 変数は、let and constステートメントで定義します。Reactのドキュメントでは、それらをと varと同等と見なすことができます。
  • We use the class keyword to define JavaScript classes. There are two things worth remembering about them. Firstly, unlike with objects, you don’t need to put commas between class method definitions. Secondly, unlike many other languages with classes, in JavaScript the value of this in a method depends on how it is called.
    • classキーワードを使用してJavaScriptクラスを定義します。それらについて覚えておく価値のあることが2つあります。まず、オブジェクトの場合とは異なり、クラスメソッド定義の間にコンマを置く必要はありません。次に、クラスを持つ他の多くの言語とは異なり、JavaScript thisでは、メソッドの値はそれがどのように呼び出されるかに依存します。
  • We sometimes use => to define “arrow functions". They’re like regular functions, but shorter. For example, x => x * 2 is roughly equivalent to function(x) { return x * 2; }. Importantly, arrow functions don’t have their own this value so they’re handy when you want to preserve the this value from an outer method definition.
    • 「矢印関数」の=>を定義に使用することがあります。これらは通常の関数のようですが、短くなっています。たとえば、x => x * 2 は、function(x) { return x * 2; } とほぼ同等です。重要なことに、矢印関数には独自の this値がないため、外部メソッド定義の this値を保持する場合に便利です。

チュートリアル:Reactの導入 (TypeScript版)

TypeScriptのアプリケーション雛形を作成します。

npx create-react-app rewrite-react-tutorial-in-hooks --template typescript

index.cssindex.tsx を、スターターコードに置き換えします。

コンパイルエラー

Parameter 'i' implicitly has an 'any' type. TS7006

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

型を指定してコンパイルエラーを解消します。

renderSquare(i: number) {

データをPropsを経由で渡す

interface SquareProps {
  value: number;
}

class Square extends React.Component<SquareProps> {

インタラクティブなコンポーネントを作る

interface SquareState { 
  value: string | null;
}

class Square extends React.Component<SquareProps, SquareState> {
  constructor(props:any) {
    super(props);
    this.state = {
      value: '',
    };
  }
  render() {
    return (
      <button className="square" onClick={() => this.setState({value: 'X'})}>
        {this.state.value}
      </button>
    );
  }
}

index.tsx(TypeScript版の完成形)

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';

interface SquareProps {
  value: any;
  onClick: () => void;
}

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

interface BoardState {
  squares: ('O' | 'X' | null)[];
  onClick: (i: number) => void;
}


class Board extends React.Component<BoardState, {}> {

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

  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>
    );
  }
}

interface HistoryData {
  squares: ('O' | 'X' | null)[];
}

interface GameState {
  history: HistoryData[];
  xIsNext: boolean;
  stepNumber: number;
}

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

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

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

  render() {
    const history = this.state.history;
    const current = history[this.state.stepNumber];
    const winner = calculateWinner(current.squares);
    
    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>
      )
    });

    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>
    );
  }
}

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

function calculateWinner(squares: any) {
  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;
}

フックで置き換え

import React, { useState } from 'react';
const Game = () => {
  const [history, setHistory] = useState([{
    squares: Array(9).fill(null)
  }])
  const [stepNumber, setStepNumber] = useState(0)
  const [xIsNext, setXIsNext] = useState(true)

  const handleClick = (i: number) => {
    const new_history = history.slice(0, stepNumber + 1);
    const current = new_history[new_history.length - 1];
    let squares = current.squares.slice();
    if (calculateWinner(squares) || squares[i]) {
      return;
    }
    squares[i] = xIsNext ? 'X': 'O';
    setHistory(new_history.concat([{squares}]));
    setStepNumber(new_history.length);
    setXIsNext(!xIsNext);
  }

  const jumpTo = (step: number) => {
    setStepNumber(step)
    setXIsNext((step % 2) === 0)
  }

index.tsx(TypeScriptフック版の完成形)

import React, { useState } from 'react';
import ReactDOM from 'react-dom';
import './index.css';

interface SquareProps {
  value: any;
  onClick: () => void;
}

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

interface BoardState {
  squares: ('O' | 'X' | null)[];
  onClick: (i: number) => void;
}


const Board = (props: BoardState) => {
  const renderSquare = (i: number) => {
    return (
      <Square 
        value={props.squares[i]}
        onClick={() => props.onClick(i)}
      />
    );
  }

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

const Game = () => {
  const [history, setHistory] = useState([{
    squares: Array(9).fill(null)
  }])
  const [stepNumber, setStepNumber] = useState(0)
  const [xIsNext, setXIsNext] = useState(true)

  const handleClick = (i: number) => {
    const new_history = history.slice(0, stepNumber + 1);
    const current = new_history[new_history.length - 1];
    let squares = current.squares.slice();
    if (calculateWinner(squares) || squares[i]) {
      return;
    }
    squares[i] = xIsNext ? 'X': 'O';
    setHistory(new_history.concat([{squares}]));
    setStepNumber(new_history.length);
    setXIsNext(!xIsNext);
  }

  const jumpTo = (step: number) => {
    setStepNumber(step)
    setXIsNext((step % 2) === 0)
  }

  const current = history[stepNumber];
  const winner = calculateWinner(current.squares);

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

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

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

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

function calculateWinner(squares: any) {
  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;
}

その他

Visual Studio Code で React の開発

https://code.visualstudio.com/docs/nodejs/reactjs-tutorial

Webエディタ

参考