3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

React 公式チュートリアル (tic tac toe) の作り方にひと手間加える

Last updated at Posted at 2021-04-04

はじめに

  • React 公式 チュートリアル に下記の要素を追加
    • TypeScript への書き換え
    • Material-UI の適用
    • Storybook の適用
  • Storybook と Atomic Design の考え方で、公式チュートリアルのゲーム Web アプリを構築

Before / After

Before

before

After

after

動作確認環境

sw_vers

ProductName:    Mac OS X
ProductVersion: 10.15.7
BuildVersion:   19H114

node -v

v14.15.4

yarn -v

1.22.10

React 開発テンプレート

  • 下記のプロジェクトをコピーして yarn installで完成(毎度、お世話になっております)

Material-UI インストール

  • これといって理由があるわけではないが v5.0.0-alpha(最新プレビュー版) をインストール

$ npm install @material-ui/core@next @emotion/react @emotion/styled

Storybook インストール

  • React 用の Storybook をインストール

$ npx sb init

最終的なソースコード一式

  • 下記のプロジェクトをクローン or ダウンロードして yarn install コマンドで環境構築可能

TypeScirpt への書き換え

Material-UI の適用

  • マス目が <button> で表現されているため、Material-UI の <Button> で表現
  • その他、全体のテンプレートを整えるため <GenericTemplate> の使用、レイアウトを整えるため <Grid> を使用
  • 詳細は 最終的なソースコード一式 参照

Storybook の適用

  • Atomic Design の考え方で、小さいパーツ・コンポーネントから作り上げていき、完成したパーツ・コンポーネントを統合して、ゲームを完成させる
  • Storybook を用いることで、パーツ・コンポーネントをインタラクティブにテスト可能
  • Atomic Design の考え方は下記参照

atoms

  • マス目のパーツ・コンポーネントは <Button> で表現しているため、atoms ディレクトリに Square.tsx のコードを記述
  • 'O', 'X'が配置されているとき、何も配置されていないときのSquareを定義(Squares.stories.tsx 参照)
  • buttonStyle に マス目のサイズ、外観を指定
  • Storybook 上で書き換えが可能

Square

Squares.stories.tsx
// also exported from '@storybook/react' if you can deal with breaking changes in 6.1
// eslint-disable-next-line import/no-extraneous-dependencies
import { Story, Meta } from '@storybook/react/types-6-0';
import { Square, SquareProps } from 'components/atoms/Square';
import { ButtonStyleType } from 'components/modules/interfaces';

export default {
  title: 'tictactoe/atoms/Square',
  component: Square,
} as Meta;

const Template: Story<SquareProps> = (args) => {
  const { value, onClick, buttonStyle, color } = args;

  return (
    <Square
      value={value}
      onClick={onClick}
      buttonStyle={buttonStyle}
      color={color}
    />
  );
};

const onClick = () => {
  // console.log('onclick');
};

const buttonStyle = {
  maxWidth: '50px',
  maxHeight: '50px',
  minWidth: '50px',
  minHeight: '50px',
  border: '1px solid #999',
  borderRadius: '1px',
} as ButtonStyleType;

export const SquareTestX = Template.bind({});
SquareTestX.args = {
  value: 'X',
  onClick,
  buttonStyle,
  color: 'secondary',
};

export const SquareTestO = Template.bind({});
SquareTestO.args = {
  value: 'O',
  onClick,
  buttonStyle,
  color: 'primary',
};

export const SquareTestNull = Template.bind({});
SquareTestNull.args = {
  value: null,
  onClick,
  buttonStyle,
  color: 'inherit',
};

何も配置されていない

O が配置されたとき

X が配置されたとき

molecules

  • Square の集合体として 3x3 の Board を定義
  • ゲーム開始時の初期配置, 5 手進めた状態をテスト
  • squares の状態を書き換えることで、任意の手数の状態を Storybook 上で再現可能
  • Moves は過去の手を遡る機能
  • history 過去の盤面の状態を表現(この段階では、history のサイズだけ、orderd-list が表示される)

Board

Board.stories.tsx
// also exported from '@storybook/react' if you can deal with breaking changes in 6.1
// eslint-disable-next-line import/no-extraneous-dependencies
import { Story, Meta } from '@storybook/react/types-6-0';
import { Board, BoardProps } from 'components/molecules/Board';
import { ButtonStyleType, SquareType } from 'components/modules/interfaces';

export default {
  title: 'tictactoe/molecules/Board',
  component: Board,
} as Meta;

const Template: Story<BoardProps> = (args) => {
  const { squares, onClick, buttonStyle } = args;

  return (
    <Board squares={squares} onClick={onClick} buttonStyle={buttonStyle} />
  );
};

const onClick = () => {
  // console.log('onclick');
};

const buttonStyle = {
  maxWidth: '50px',
  maxHeight: '50px',
  minWidth: '50px',
  minHeight: '50px',
  border: '1px solid #999',
  borderRadius: '1px',
} as ButtonStyleType;

const squaresInitial: SquareType[] = [
  null,
  null,
  null,
  null,
  null,
  null,
  null,
  null,
  null,
];

export const BoardTestInitial = Template.bind({});
BoardTestInitial.args = {
  squares: squaresInitial,
  onClick,
  buttonStyle,
};

const squares: SquareType[] = ['X', null, 'O', null, 'O', null, 'X', null, 'O'];

export const BoardTest = Template.bind({});
BoardTest.args = {
  squares,
  onClick,
  buttonStyle,
};

初期配置

5 手進めた例

Moves

Moves.stories.tsx
// also exported from '@storybook/react' if you can deal with breaking changes in 6.1
// eslint-disable-next-line import/no-extraneous-dependencies
import { Story, Meta } from '@storybook/react/types-6-0';
import { Moves, MovesProps } from 'components/molecules/Moves';
import { HistoryType } from 'components/modules/interfaces';

export default {
  title: 'tictactoe/molecules/Moves',
  component: Moves,
} as Meta;

const Template: Story<MovesProps> = (args) => {
  const { history, jumpTo } = args;

  return <Moves history={history} jumpTo={jumpTo} />;
};

const jumpTo = (): void => {
  // console.log("jumpTo");
};

const history = ([
  [null, null, null, null, null, null, null, null, null],
  ['X', null, null, null, null, null, null, null, null],
  ['X', 'O', null, null, null, null, null, null, null],
] as unknown) as HistoryType;

export const MovesTest = Template.bind({});
MovesTest.args = {
  jumpTo,
  history,
};

3 手進めた例

organisms

  • GameBoardMovesの組み合わせで構成されるゲーム画面
  • ここで、ゲームの一通りの操作が可能

Game

Game.stories.tsx
// also exported from '@storybook/react' if you can deal with breaking changes in 6.1
// eslint-disable-next-line import/no-extraneous-dependencies
import { Story, Meta } from '@storybook/react/types-6-0';
import { Game } from 'components/organisms/Game';

export default {
  title: 'tictactoe/organisms/Game',
  component: Game,
} as Meta;

const Template: Story = () => <Game />;

export const GameTest = Template.bind({});

pages

  • PageGameに対し、GenericTemplateを適用した画面
  • 最終的な見栄えを確認

Page

Page.stories.tsx
// also exported from '@storybook/react' if you can deal with breaking changes in 6.1
// eslint-disable-next-line import/no-extraneous-dependencies
import { Story, Meta } from '@storybook/react/types-6-0';
import { Page } from 'components/pages/Page';

export default {
  title: 'tictactoe/pages/Page',
  component: Page,
} as Meta;

const Template: Story = () => <Page />;

export const PageTest = Template.bind({});

表示デバイス 切り替え

  • Storybook では PC, スマホ、タブレットなどデバイスごとの見え方の違いを確認できる
  • 下記は Large Mobile(L) 896x414 において、縦横を入れ替えたときのシミュレーション例

Large Mobile(L) 896x414 (横)

Large Mobile(L) 414x896 (縦)

おわりに

  • Storybook と Atomic Design の考え方で、パーツ・コンポーネント単位で開発し、出来上がったパーツ・コンポーネントを組み合わせて、公式チュートリアルのゲーム Web アプリを構築
  • その他のひと手間として下記の例
    • redux の適用
    • ゲームのロジック部分を jest で単体テスト
3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?