🐷

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

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で完成(毎度、お世話になっております)

https://github.com/oukayuka/Riakuto-StartingReact-ja3.1/tree/master/06-lint/04-advanced

Material-UI インストール

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

https://next.material-ui.com/

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

Storybook インストール

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

https://storybook.js.org/docs/react/get-started/install

$ npx sb init

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

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

https://github.com/takanassyi/react-tutorial-ts-mui

TypeScirpt への書き換え

https://zenn.dev/roiban/articles/473f9cbf2b793a

Material-UI の適用

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

Storybook の適用

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

https://design.dena.com/design/atomic-design-を分かったつもりになる

atoms

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

Square

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

Storybook 用ソースコード(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

Storybook 用ソースコード(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

Storybook 用ソースコード(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

Storybook 用ソースコード(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 で単体テスト

Discussion