82
96

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.

PatheeAdvent Calendar 2019

Day 3

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

Last updated at Posted at 2019-12-02

:star: はじめに

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

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

  • 自分も普段からReactを使用しているのですが、日本ではReactに関するイベントも少なく、イマイチ盛り上がりに欠けるような気がしています
    • Vueは Vue Fes Japan を開催していて羨ましい・・・(今年は台風で中止でしたが)
  • 確かに、自分も初めてReactに触れた時はこれまでのJavaScriptと概念も書き方も違いすぎて困惑しましたし、次々と新しい機能がリリースされるのでキャッチアップが大変な面もありました
  • それでも、慣れてしまえばサクサクComponentを作成できますし、パフォーマンスの面でも優れていますし、React Nativeでネイティブアプリも作ることもできます
  • 日本でもっと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/
terminal
brew install yarn

:pencil: create react-appでアプリの基盤を作成

  • react-tutorial という名前のアプリを作成します
  • --typescript のオプションをつけることでTypeScriptでアプリを作成できます
  • https://create-react-app.dev/docs/getting-started/
  • nodeのversionが古いとエラーになるので、その場合は nodebrew などで新しいversionにしておきましょう
terminal
yarn create react-app react-tutorial --typescript
  • これだけでアプリを動かす環境が整いました

:pencil: アプリを起動する

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

localhost_3000_(Laptop with HiDPI screen).png

:pencil: eslint でコードの書き方を統一する

  • 先に eslint でコードの静的チェックをすることで不用意なエラーや書き方のズレを防ぐようにしておくと便利です
  • TypeScript用の tslint もありますが、今後は eslint に統合されていくようなので eslint だけ使用しています
  • package.jsondevDependencies に以下を追記し、 yarn install を実行して必要なライブラリをinstallします
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 としています
.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 のように呼び出せて便利です
package.json
{

  ...

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

  ...

}

.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 オプションが必要です
terminal
yarn add styled-components
yarn add -D @types/styled-components
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;

:star: 新しいルーティングにコンポーネントを追加する

  • react-router を使用することでURLによって表示するコンポーネントを切り替えるルーティングを実装することができます
  • react-routerreact-router-dom に含まれているので、 react-router-dom のみ追加すれば大丈夫です
yarn add react-router-dom
yarn add -D @types/react-router-dom
  • 先に新しいルーティングで表示する仮のOtameshiコンポーネントを作成しておきます
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です
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 で生成しています
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 コンポーネントを拡張してスタイルを当てています
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;
`;

...

:star: React Hooksで状態管理を行う

  • React Hooksを使用するとFunctional Componentでもstate(状態)を管理できるようになります
  • 試しに、入力したテキストを受け取って表示させてみましょう
  • useState を使用する場合は const [state名, stateを変更する関数名] = useState(初期値); のように定義します
  • テキストエリアに入力された時に onChange イベントが発火し、入力されたvalueを changeText に渡して更新しています
  • 公式の説明
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

:star: Layoutコンポーネントを作成する

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

localhost_3000_otameshi(Laptop with HiDPI screen) (1).png

  • 今回はここまでです!

今後の予定

↓次はこちら
React + Redux + TypeScript でモダンなwebアプリを作るチュートリアルを考えてみた②

  • reduxの導入
  • redux hooksでの状態管理
  • redux-sagaでの非同期処理
  • axiosでの通信処理
  • immutable.jsでデータのモデル化
82
96
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
82
96

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?