React + Redux + TypeScript でモダンなwebアプリを作るチュートリアルを考えてみた①

# :star: はじめに

- フロントエンドで利用されているフレームワークでReactがjQueryを抜いて１位になったというアンケート結果が公表されました

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">調査：フロントエンドまわりのアンケート調査結果 2019年版。Sass の利用率77%。JSはReactがjQueryを上回っています。英語ですが要チェック！ <a href="https://t.co/hs4h5TCF2e">https://t.co/hs4h5TCF2e</a></p>&mdash; Webクリエイター ボックス (@webcreatorbox) <a href="https://twitter.com/webcreatorbox/status/1194789563455221760?ref_src=twsrc%5Etfw">November 14, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

**Q. Which JavaScript libraries and/or frameworks do you currently use most frequently on projects?**

<img src="https://ashleynolan.co.uk/assets/img/blog/tooling-survey/2019/q16.jpg" />

- 自分も普段からReactを使用しているのですが、日本ではReactに関するイベントも少なく、イマイチ盛り上がりに欠けるような気がしています
  - Vueは [Vue Fes Japan](https://vuefes.jp/2019/) を開催していて羨ましい・・・（今年は台風で中止でしたが）
- 確かに、自分も初めてReactに触れた時はこれまでのJavaScriptと概念も書き方も違いすぎて困惑しましたし、次々と新しい機能がリリースされるのでキャッチアップが大変な面もありました
- それでも、慣れてしまえばサクサクComponentを作成できますし、パフォーマンスの面でも優れていますし、React Nativeでネイティブアプリも作ることもできます
  - [Vue・React・Angularのパフォーマンス比較検証](https://ics.media/entry/190731/)
  - [Reactを使うとなぜjQueryが要らなくなるのか](https://qiita.com/naruto/items/fdb61bc743395f8d8faf)
  - [Yahoo! JAPAN トップページを Atomic Design と React・Redux・TypeScript で作り変えたお話](https://techblog.yahoo.co.jp/entry/20191203785540/)
- 日本でもっとReactユーザーが増えて欲しい・盛り上がって欲しいという願いも込めて、実践的なチュートリアルっぽいものを作ってみました

# :star: 作るもの
- Google Books のAPIを使って本の検索アプリを作る


# :star: 主な技術・ライブラリなど
- React (Hooks)
- Redux (Hooks)
- TypeScript
- styled-components
- react-router
- immutable.js
- redux-saga
- axios
- eslint
- semantic-ui
- yarn

# :star: 手順

## :pencil: `yarn` のインストール
- パッケージマネージャーについて、 `npm` がデフォルトで入ってますが `yarn`の方が高速で安定性があるので `yarn` を使用しています
- https://yarnpkg.com/lang/ja/docs/install/

```bash:terminal
brew install yarn
```

## :pencil: `create react-app`でアプリの基盤を作成
- `react-tutorial` という名前のアプリを作成します
- `--typescript` のオプションをつけることでTypeScriptでアプリを作成できます
- https://create-react-app.dev/docs/getting-started/
- nodeのversionが古いとエラーになるので、その場合は `nodebrew` などで新しいversionにしておきましょう

```bash:terminal
yarn create react-app react-tutorial --typescript
```

- これだけでアプリを動かす環境が整いました

## :pencil: アプリを起動する
- 作成したアプリのディレクトリに移動して `yarn start` で起動します
- 他のスクリプトについても自動で生成された `README` に説明があるので確認しておきましょう

```bash:terminal
cd react-tutorial
yarn start
```

- デフォルトだと http://localhost:3000 で次のような画面が表示されるはずです

![localhost_3000_(Laptop with HiDPI screen).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/178077/e33cd78f-3e40-c9b7-0d78-91a576ff891a.png)

- この状態が次のコミットになります
  - https://github.com/mikan3rd/react-tutorial/commit/224b68ce6a59a55063e067de8d0bb348fa896739


## :pencil: `eslint` でコードの書き方を統一する
- 先に `eslint` でコードの静的チェックをすることで不用意なエラーや書き方のズレを防ぐようにしておくと便利です
- TypeScript用の `tslint` もありますが、今後は `eslint` に統合されていくようなので `eslint` だけ使用しています
  - https://eslint.org/blog/2019/01/future-typescript-eslint
- `package.json` の `devDependencies` に以下を追記し、 `yarn install` を実行して必要なライブラリをinstallします

```json:package.json
{

  ...

  "devDependencies": {
    "@typescript-eslint/eslint-plugin": "^2.0.0",
    "@typescript-eslint/parser": "^2.0.0",
    "eslint": "^6.1.0",
    "eslint-config-prettier": "^6.0.0",
    "eslint-config-react": "^1.1.7",
    "eslint-import-resolver-webpack": "^0.11.1",
    "eslint-plugin-import": "^2.18.2",
    "eslint-plugin-prettier": "^3.1.0",
    "eslint-plugin-react": "^7.14.3",
    "prettier": "^1.18.2"
  }

  ...

}

```

- `.eslintrc.json` を作成してeslintの設定を記述します
- `.eslintrc.json` は好きなようにカスタマイズできますが、次に自分の例を載せておきます
  - 基本的にはrecommendedの設定をそのまま使用しています
  - React, TypeScript, Prettier を併用できるようにpluginを設定しています
  - また、importのソートも統一したかったので `eslint-plugin-import` も追加しています
  - TypeScriptを使用している場合はPropTypesがほぼ不要になるため、今回は `"ignoreDeclarationSort": true` としています

```json:.eslintrc.json
{
  "extends": [
    "eslint:recommended",
    "plugin:react/recommended",
    "plugin:@typescript-eslint/recommended",
    "prettier/@typescript-eslint",
    "plugin:prettier/recommended",
    "plugin:import/errors",
    "plugin:import/warnings",
    "plugin:import/typescript"
  ],
  "plugins": ["react", "@typescript-eslint", "prettier"],
  "env": {
    "node": true,
    "browser": true,
    "jest": true,
    "es6": true
  },
  "rules": {
    "sort-imports": ["error", { "ignoreDeclarationSort": true }],
    "import/order": ["error", { "newlines-between": "always" }],
    "prettier/prettier": [
      "error",
      {
        "singleQuote": true,
        "semi": true,
        "printWidth": 120,
        "trailingComma": "all",
        "jsxSingleQuote": true
      }
    ]
  },
  "parser": "@typescript-eslint/parser",
  "settings": {
    "react": {
      "version": "detect"
    },
    "react/prop-types": ["error", { "skipUndeclared": true }],
    "import/ignore": ["node_modules"],
    "import/resolver": {
      "node": { "moduleDirectory": ["node_modules", "src"] }
    }
  }
}

```



- さらに、 `scripts` にlint用の記述を追加しておくと `yarn lint:fix` のように呼び出せて便利です

```json:package.json
{

  ...

  "scripts": {
    "lint": "eslint . --ext .js,.jsx,.ts,.tsx",
    "lint:fix": "yarn lint --fix"
  },

  ...

}

```

- lintに対応したのが次のコミットになります
  - https://github.com/mikan3rd/react-tutorial/commit/b3d31cca321932b6f06859e708ac8610371d8e5d

- VSCodeを使用している場合は `.vscode/setting.json` に次のようにワークスペースの設定を追加しておくと捗ります
  - VSCodeデフォルトのフォーマッターとバッティングしてしまうので `"editor.formatOnSave": false` にしておく必要があります

```json:.vscode/setting.json
{
  "editor.rulers": [120],
  "files.trimTrailingWhitespace": true,
  "eslint.enable": true,
  "editor.renderWhitespace": "all",
  "css.lint.ieHack": "warning",
  "javascript.implicitProjectConfig.checkJs": true,
  "typescript.updateImportsOnFileMove.enabled": "always",
  "eslint.validate": [
    "javascript",
    "javascriptreact",
    {
      "language": "typescript",
      "autoFix": true
    },
    {
      "language": "typescriptreact",
      "autoFix": true
    }
  ],
  "editor.formatOnSave": false,
  "eslint.autoFixOnSave": true
}

```

## :star: スタイルを `styled-components` で記述する
- `styled-components` を導入することで、class名でスタイルのマッピングをすることを辞め、コンポーネントに直感的にスタイルを適用することができるようになります
- https://www.styled-components.com/
- ライブラリに型定義ファイルが提供されている場合、 `@types/XXX` をinstallできます
- 型定義ファイルのように開発環境のみで使用するライブラリは `devDependencies` に記述するため、 `-D` オプションが必要です

```bash:terminal
yarn add styled-components
yarn add -D @types/styled-components
```

- 試しに、既存の `.css` ファイルを置き換えてみます
- VSCodeをご利用の方は次のようなextensionもあるのでオススメです
- https://marketplace.visualstudio.com/items?itemName=jpoissonnier.vscode-styled-components
- グローバル（アプリ全体）に適用させるスタイルは `createGlobalStyle` で定義します
- `<>` は `<React.Fragment>` の省略で、複数の要素を返す場合に1つにまとめる時に使用します
- https://ja.reactjs.org/docs/fragments.html

```tsx:App.tsx
import React from 'react';
import styled, { createGlobalStyle } from 'styled-components';

import logo from './logo.svg';

const App: React.FC = () => {
  return (
    <>
      <GlobalStyle />

      <Wrapper>
        <Header>
          <Logo src={logo} className='App-logo' alt='logo' />
          <Text>
            Edit <CodeText>src/App.tsx</CodeText> and save to reload.
          </Text>
          <OfficialLink className='App-link' href='https://reactjs.org' target='_blank' rel='noopener noreferrer'>
            Learn React
          </OfficialLink>
        </Header>
      </Wrapper>
    </>
  );
};

const GlobalStyle = createGlobalStyle`
  body {
    margin: 0;
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
      'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
      sans-serif;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
  }

  code {
    font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
      monospace;
  }
`;

const Wrapper = styled.div`
  text-align: center;
`;

const Header = styled.header`
  background-color: #282c34;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  font-size: calc(10px + 2vmin);
  color: white;
`;

const Logo = styled.img`
  height: 40vmin;
`;

const OfficialLink = styled.a`
  color: #09d3ac;
`;

const Text = styled.p``;

const CodeText = styled.code``;

export default App;
```

- `styled-componsnts` に対応したのが次のコミットになります
  - https://github.com/mikan3rd/react-tutorial/commit/5d1dba1496f10bb5922d88a145711a9b8a917b86

## :star: 新しいルーティングにコンポーネントを追加する
- `react-router` を使用することでURLによって表示するコンポーネントを切り替えるルーティングを実装することができます
- `react-router` は `react-router-dom` に含まれているので、 `react-router-dom` のみ追加すれば大丈夫です

```bash
yarn add react-router-dom
yarn add -D @types/react-router-dom
```

- 先に新しいルーティングで表示する仮のOtameshiコンポーネントを作成しておきます

```tsx:Otameshi.tsx
import React from 'react';
import styled from 'styled-components';

export const Otameshi: React.FC = () => {
  return <Wrapper>Otameshi</Wrapper>;
};

const Wrapper = styled.div``;
```

- `routes.tsx` を作成してルーティングを定義します
- `Switch` を使用するとpathが一番最初に合致した `Route` のみ表示させることができます
- 一番最後に `Redirect` を記述することで、どれにも合致しなかった時に `to` にリダイレクトさせることができます
- `Redirect` の代わりに404ページなどを表示させたい場合は、pathの指定のない `Route` を記述すればOKです

```tsx:routes.tsx
import React from 'react';
import { Switch, Route, Redirect } from 'react-router-dom';

import App from 'App';
import { Otameshi } from 'Otameshi';

export const Path = {
  app: '/',
  otameshi: '/otameshi',
};

const routes = (
  <Switch>
    <Route exact path={Path.app} component={App} />
    <Route exact path={Path.otameshi} component={Otameshi} />
    <Redirect to={Path.app} />
  </Switch>
);

export default routes;

```

- これを `ReactDOM.render` で描画するように書き換えます
- `Router` にはブラウザの履歴を記録する `history` が必要なので、 `createBrowserHistory` で生成しています

```tsx:index.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import { createBrowserHistory } from 'history';
import { Router } from 'react-router-dom';

import routes from 'routes';
import * as serviceWorker from './serviceWorker';

const history = createBrowserHistory();

ReactDOM.render(<Router history={history}>{routes}</Router>, document.getElementById('root'));

serviceWorker.unregister();
```

- これでrouterが適用されたので、 http://localhost:3000/otameshi にアクセスすると仮のOtameshiコンポーネントが表示されます
- http://localhost:3000/otameshiiii など定義していないURLアクセスするとリダイレクトされることも確認できます
- さらに、Appコンポーネントにこのpathへのリンクを作ってみましょう
- styled-componentsでは `styled(XXX)` のようにコンポーネントのスタイルを拡張することができます
- ここでは、`styled(Link)` として `Link` コンポーネントを拡張してスタイルを当てています

```tsx:App.tsx
import React from 'react';
import styled, { createGlobalStyle } from 'styled-components';
import { Link } from 'react-router-dom';

import logo from './logo.svg';
import { Path } from 'routes';

const App: React.FC = () => {
  return (
    <>
      <GlobalStyle />

      <Wrapper>
        <Header>
          <Logo src={logo} className='App-logo' alt='logo' />
          <Text>
            Edit <CodeText>src/App.tsx</CodeText> and save to reload.
          </Text>
          <OfficialLink className='App-link' href='https://reactjs.org' target='_blank' rel='noopener noreferrer'>
            Learn React
          </OfficialLink>
          <OtameshiLink to={Path.otameshi}>おためしページへのリンク</OtameshiLink>
        </Header>
      </Wrapper>
    </>
  );
};



...

const OtameshiLink = styled(Link)`
  color: #fff;
  margin-top: 30px;
`;

...

```

- 上記の対応は次のコミットを参考にしてみてください
  - https://github.com/mikan3rd/react-tutorial/commit/3da1f112048711d410030d6f466a30466e279149


## :star: React Hooksで状態管理を行う
- React Hooksを使用するとFunctional Componentでもstate（状態）を管理できるようになります
- 試しに、入力したテキストを受け取って表示させてみましょう
- `useState` を使用する場合は `const [state名, stateを変更する関数名] = useState(初期値);` のように定義します
- テキストエリアに入力された時に `onChange` イベントが発火し、入力されたvalueを `changeText` に渡して更新しています
- 公式の説明
  - https://ja.reactjs.org/docs/hooks-state.html

```tsx:Otameshi.tsx
import React, { useState } from 'react';
import styled from 'styled-components';

export const Otameshi: React.FC = () => {
  const [text, changeText] = useState('');
  return (
    <Wrapper>
      <Body>
        <Title>Otameshi Component</Title>

        <TextArea placeholder='テキストを入力してね！' onChange={(event): void => changeText(event.target.value)} />

        <TextResult>{text}</TextResult>
      </Body>
    </Wrapper>
  );
};

const Wrapper = styled.div`
  display: flex;
  justify-content: center;
`;

const Body = styled.div``;

const Title = styled.h1`
  text-align: center;
`;

const TextArea = styled.textarea`
  display: block;
  margin: 0 auto;
  box-sizing: border-box;
  width: 200px;
`;

const TextResult = styled.p`
  width: 200px;
  padding: 10px;
  margin: 20px auto;
  border: 1px solid blue;
  white-space: pre-wrap;
  box-sizing: border-box;
`;

```

![localhost_3000_otameshi(Laptop with MDPI screen) (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/178077/c0218ad7-f18d-f1fd-7658-dceaa5de45a0.png)

- 上記の対応は次のコミットを参考にしてみてください
  - https://github.com/mikan3rd/react-tutorial/commit/e58f8897a65bd3ef2659f343e2e22dab5f73e333

## :star: Layoutコンポーネントを作成する
- HeaderやFoooterなど複数のページで表示したい共通コンポーネントがある場合、別々に呼び出すのは面倒です
- そこで、react-routerの仕組みを使って複数のコンポーネントに共通コンポーネントを当てることができます
- ここでは全てのコンポーネントの枠組みとなるLayoutコンポーネントを作成してみましょう
  - Globalスタイルなどもここに移植しましょう
  - childrenはコンポーネントの子要素が渡ってきます


```tsx:Layout.tsx
import React from 'react';
import styled, { createGlobalStyle } from 'styled-components';
import { Reset } from 'styled-reset';

export const Layout: React.FC = ({ children }) => {
  return (
    <>
      <Reset />
      <GlobalStyle />

      <Wrapper>
        <Header>React Tutorial</Header>
        <Body>{children}</Body>
      </Wrapper>
    </>
  );
};

const GlobalStyle = createGlobalStyle`
  body {
    margin: 0;
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
      'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
      sans-serif;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
  }

  code {
    font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
      monospace;
  }
`;

const Wrapper = styled.div`
  height: 100%;
`;

const Header = styled.div`
  display: flex;
  align-items: center;
  height: 60px;
  color: #fff;
  background-color: #09d3ac;
  font-size: 20px;
  font-weight: bold;
  padding: 0 20px;
`;

const Body = styled.div`
  height: calc(100vh - 60px);
`;

```

-  `routes.tsx`で`<Switch>`の親要素にLayoutを適用しましょう

```tsx:routes.tsx
import React from 'react';
import { Redirect, Route, Switch } from 'react-router-dom';

import { Layout } from 'components/Layout';
import { App } from 'components/App';
import { Otameshi } from 'components/Otameshi';

export const Path = {
  app: '/',
  otameshi: '/otameshi',
};

const routes = (
  <Layout>
    <Switch>
      <Route exact path={Path.app} component={App} />
      <Route exact path={Path.otameshi} component={Otameshi} />
      <Redirect to={Path.app} />
    </Switch>
  </Layout>
);

export default routes;

```

- これで必ずLayoutコンポーネントを経由して子要素が呼ばれるようになりました
- App, Otameshi両方のコンポーネントにHeaderが表示されます

![localhost_3000_otameshi(Laptop with HiDPI screen).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/178077/9c6f6e10-aee2-6ea4-8594-5ed80b00f594.png)


![localhost_3000_otameshi(Laptop with HiDPI screen) (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/178077/3ec53dd6-36ac-de97-3985-1762112771e8.png)



- 今回はここまでです！

# 今後の予定

↓次はこちら
[React + Redux + TypeScript でモダンなwebアプリを作るチュートリアルを考えてみた②](https://qiita.com/mikan3rd/items/d8ce24b9e9b819ec6b97)

- reduxの導入
- redux hooksでの状態管理
- redux-sagaでの非同期処理
- axiosでの通信処理
- immutable.jsでデータのモデル化