Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

: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でデータのモデル化
zenkigen
「テクノロジーを通じて、人と企業が全機現できる社会の創出に貢献する」 『全機現』という言葉は、「人の持つ能力の全てを発揮する」という禅の言葉です。 多くの大人が全機現し、それを見た子供達が、大人になることに希望を持つ社会を次世代に引き渡したい。 その思いが当社の創業精神です。
https://harutaka.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away