🐷
React 公式チュートリアル (tic tac toe) の作り方にひと手間加える
はじめに
-
React 公式 チュートリアル に下記の要素を追加
- TypeScript への書き換え
- Material-UI の適用
- Storybook の適用
- Storybook と Atomic Design の考え方で、公式チュートリアルのゲーム Web アプリを構築
Before / After
Before
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 への書き換え
- 公式チュートリアルの TypeScript 化は下記の例をはじめ、Web 上に様々な例が公開されている
- 詳細は 最終的なソースコード一式 参照
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
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
-
Game
はBoard
とMoves
の組み合わせで構成されるゲーム画面 - ここで、ゲームの一通りの操作が可能
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
-
Page
はGame
に対し、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