20200602のReactに関する記事は6件です。

ReactのCSS

CSS Modules

  • CSS のローカルスコープを JavaScript を用いて自然な形で利用できるようにした ライブラリ
/* style.css */ 
.common {
  font-size: 10px; 
} 

.normal {
  composes: common;
  color: green; 
}
import styles from "./style.css";

const MyAwesomeComponent = () => (
  <div className={styles.normal}>Awesome!</div> 
);
  • このアプリを起動するとこう見える
<div class="style-normal__227xg">Awesome!</div>
  • 実際のクラス名は <ファイル名>-<クラス名>__<ハッシュ> というフォーマットで自動生成され、末 尾に付与されるランダムなハッシュ値によって一意であることが保証される。
  • CSS Modulesの詳しい内容はこちら https://postd.cc/css-modules/

CSS in JS ライブラリ

  • JSX ファイルの中にスタイルを直 接書き込むタイプ

Radium

  • メディアクエリや:hover、:focus、:active といったセレクタが使える
import React, { FC } from 'react'; 
import Radium from 'radium';

const styles = {
  base: { color: '#fff', 
    ':hover': {
      background: '#0074d9', 
      opacity: 0.4, 
    }, 
  }, 
  primary: { 
    background: '#0074D9', 
  }, 
  warning: { 
    background: '#FF4136',
  },
};

const Button: FC<{ kind: 'primary' | 'warning' }> = ({ kind, children }) => (
  <button style={[styles.base, styles[kind]]}> 
    {children} 
  </button> 
);

expot default Radium(Button);

styled-components

  • styled.div で div タグをカスタマイズしたコンポーネントが作れる
  • スタイルが適用されたコンポーネントを作成できる
import React, { FC } from 'react'; 
import styled from 'styled-components';

const Button = styled.button`

  background: transparent; 
  border-radius: 3px; 
  border: 2px solid palevioletred; 
  color: palevioletred; 
  margin: 0.5em 1em; 
  padding: 0.25em 1em;

  ${props => props.primary && css` 
    background: palevioletred; 
    color: white; 
  `}
`;

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

const TwoButtons: FC = () => ( 
  <Container> 
    <Button>Normal Button</Button>
    <Button primary>Primary Button</Button> 
  </Container> );

export default TwoButtons;

Emotion

  • インラインで記載可能
  • フ ァイル サイズ が 小 さ く て 実 行 速 度 が 速 い
/** @jsx jsx */ 
import React, { FC } from 'react'; 
import { css, jsx } from '@emotion/core';

const color = 'darkgreen';

const ColoredText: FC = () => (

  <div 
    css={css`
      background-color: hotpink;
      &:hover {
        color: ${color}; 
      } 
    `}
  >
    This has a hotpink background. 
  </div>
);

export default ColoredText;
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React デバッグ方法

Debugger for Chrome

https://marketplace.visualstudio.com/items?itemName=msjsdiag.debugger-for-chrome

設定方法・手順

launch.json

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Chrome",
      "type": "chrome",
      "request": "launch",
      "url": "http://localhost:3000",
      "webRoot": "${workspaceFolder}/src",
      "sourceMapPathOverrides": {
        "webpack:///src/*": "${webRoot}/*"
      }
    }
  ]
}

ソースコードにブレークポイントを指定

yarn startにてローカル環境を起動

デバッグツールを起動

React Developer Tools

  • componentタブでstateとpropsが確認できる

*参考
https://qiita.com/sh-suzuki0301/items/9c2af4b28ba665cc0744

Redux DevTools

  • Redux-sagaだとデバックが可能(redux-thunkだと動かない)

  • top層のindex.tsxに以下の内容を記載 (src/index.tsx(index.jsx))

    • これを記載しないとRedux DevToolsが動かない
import { applyMiddleware, compose, createStore } from 'redux';

/* eslint-disable no-underscore-dangle, @typescript-eslint/no-explicit-any */
const composeEnhancers =
  process.env.NODE_ENV === 'development' &&
  typeof window === 'object' &&
  (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
    ? (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({})
    : compose;
/* eslint-enable */

const enhancer = composeEnhancers(applyMiddleware(sagaMiddleWare));
const store = createStore(reducer, enhancer);
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[react]任意個数のサブコンポーネントの値をuseStateで管理する方法

「データベースから取得したレコードをもとにinputを複数配置する」など
あらかじめ数が決められない可変個数のinputを配置した際に、
それぞれのinputの入力値の処理がなかなか難しかったので、備忘録を兼ねて記事を書きました。

結論から書くと
コンポーネントの入力値をまとめてJSONオブジェクトで管理するとよいです。

ポイントはsetXxxにて
let newdata = Object.assign({}, datas)
として常に新しいオブジェクトを生成すること。

まるっとコードを置いておきます。

import React, { useState } from 'react'

const ArrayInput = () => {
  const [datas, setDatas] = useState({});

  const change = (e) => {
    const key   = e.target.name;
    const value = e.target.value;
    console.log(key + "/" + value);
    datas[key] = value;
    let newdata = Object.assign({}, datas); // important point!
    console.log(newdata);
    setDatas(newdata);
  }

  const inputs = ["a", "b", "c"].map((n, i) => {
    return <input key={i} name={n} value={datas[n]} onChange={change} />;
  });

  return (
    <div>
      {inputs}
    </div>
  );
}

export default ArrayInput;

参考資料(Qiita記事)

[react]子要素に複数配置したinputのvalueを取得する)

[React]useStateで定義したstateを更新したのに再レンダーされない件(object / 配列)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React + Redux + Redux-Saga + Express + Mongo DB + TypeScriptでCRUDなWebアプリケーションを作成してHerokuにデプロイしてみる

概要

名前と年齢を入力し、保存・編集・削除ができるCRUDなWebアプリケーションを作成してHerokuにデプロイした備忘録になります(使ってみたい技術スタックが多かったため、長文な記事になってしまいました)。

crud.jpg

Webアプリケーションを作成する上で以下の記事を参考にさせていただきました。TypeScript等を導入する差異がありますので以下の記事の流れに沿って改めて説明させていただきます。

前提

node + npmがインストールされており、問題なく使用できる環境であることを前提条件とさせて下さい。
インストールされていない場合は、以下の記事を参考にしてみて下さい。

本記事では、yarnを使用しています。
yarn -vのコマンドを叩いてバージョンが表示されない方は以下のコマンドでインストールして下さい。

$ npm i -g yarn

npmで動作させたい方は適宜、package.jsonの修正をお願いします。

コード

Githubのリポジトリは以下になります。

サーバーサイドの環境構築

サーバーサイドの環境構築では以下の内容を行います。

  • MongoDBのインストール
  • jqのインストール
  • モジュールのインストール
  • Webpackでビルド環境の構築
  • ホットリロード環境の構築

MongDBのインストール

HomebrewでMongoDBをインストールします。

$ brew tap mongodb/brewbrew tap
$ brew install mongodb-community

以下のコマンドを叩いてバージョンが表示されれば成功です。

$ mongod --version

jqのインストール

curlでAPIのリクエストで返却される値を見やすい形に整形してくれます。

$ brew install jq

以下のコマンドを叩いてバージョンが表示されれば成功です。

$ jq --version

モジュールのインストール

サーバーサイドでは、Express、MongoDB、TypeScriptを使用し、Webpackでビルドを行います。

$ mkdir simple-crud
$ cd simple-crud

$ npm i -y
$ yarn add express mongoose nodemon

/** TypeScript用 */
$ yarn add -D @types/express @types/mongoose @types/node typescript

/** Webpack用 */
$ yarn add -D dotenv-webpack ts-loader tslint tslint-config-airbnb tslint-loader webpack webpack-cli webpack-node-externals

Webpackでビルド環境の構築

Webpackでは、TypeScriptをAirBnbのLintルールに従ってビルドします。
各自、使用したいルールがあれば、そちらを使用して下さい。

tsconfig.jsonの作成

TypeScriptのコンパイラーオプションを設定します。
現状だとclient/配下も範囲に含まれるのでエラーが発生します。
エラーを回避するために"include" : ["src/**/*]"を記述し、範囲を選択します。

$ npx tsc --init
tsconfig.json
{
  "compilerOptions": {
    "target": "es5",
    "module": "ES2015",
    "sourceMap": true,
    "outDir": "./dist",
    "strict": true,
    "esModuleInterop": true
  },
  "include": ["src/**/*"]
}

tslint.jsonの作成

AirBnbのLintルールを適用させます。
tsconfig.jsonの作成と同様の理由のため、"exclude": ["./client/src/**/*"]を記述します。

tslint.json
{
  "extends": "tslint-config-airbnb",
  "linterOptions": {
    "exclude": ["./client/src/**/*"]
  }
}

webpack.config.jsの作成

WebpackでTypeScriptのトランスパイル、TSLintによるチェック、dotenvの読み込みを行います。
dotenvは、.envファイルを読み込む時に使用します。
src/server.tsをターゲットにビルドを行い、dist/にビルド結果が吐き出されます。

webpack.config.js
const path = require('path');
const nodeExternals = require('webpack-node-externals');
const dotenv = require('dotenv-webpack');

module.exports = {
  entry: './src/server.ts',
  target: 'node', // Module not found: Error: Can't resolve 'fs' 対策
  externals: [nodeExternals()],
  devtool: 'inline-source-map',
  module: {
    rules: [
      {
        enforce: 'pre',
        loader: 'tslint-loader',
        test: /\.ts$/,
        exclude: [/node_modules/],
        options: {
          emitErrors: true,
        },
      },
      {
        loader: 'ts-loader',
        test: /\.ts$/,
        exclude: [/node_modules/],
        options: {
          configFile: 'tsconfig.json',
        },
      },
    ],
  },
  resolve: {
    extensions: ['.ts', '.js'],
  },
  output: {
    filename: 'server.js',
    path: path.resolve(__dirname, 'dist'),
  },
  plugins: [new dotenv({ systemvars: true })],
};

フロントエンドの環境構築

フロントエンドのの環境構築では以下の内容を行っていきます。

  • create-react-appを使用したReactプロジェクトの作成
  • 不要なファイル・フォルダの削除
  • モジュールのインストール

create-react-appを使用したプロジェクトの作成

creact-react-appコマンドで簡単に環境を構築することができます。
今回は、TypeScriptを使用するのでコマンドの最後に--typescriptを付与します。

/** グローバルにコマンドをインストール */
$ npm i -g create-react-app

/** clientという名前のプロジェクトをTypeScriptで作成 */
$ create-react-app client --typescript

不要なファイル・フォルダの削除

create-react-appのバージョンで異なるかもしれませんが、プロジェクト作成時に今回は使用しない不要なファイル、フォルダが存在するので削除します。

rm .gitignore README.md
cd src
rm App.test.tsx logo.svg *.css serviceWorker.ts setupTests.ts

モジュールのインストール

AxiosとRedux-Sagaによる非同期処理、Redux-loggerによるロガーの出力を行います。
今回、Scssでスタイリングしていますが必須ではないので必要であれば、インストールして下さい。

$ cd client
$ yarn add axios node-sass

/** Redux用 */
$ yarn add redux react-redux redux-saga redux-logger

/** TypeScript用 */
$ yarn add @types/react-redux @types/redux-logger typescript-fsa typescript-fsa-reducers

package.jsonの修正

npm scriptを使用し、コマンドから実行できるようにルートのpackage.jsonを修正します。
Herokuのデプロイ時に必要になる、enginesheroku-postbuildコマンドも一緒に追記してあります。
基本的には、yarn start:devで開発を行い、Herokuにデプロイする時は、yarn build:prodでビルドを行います。

/** localhost:3000 */
yarn start:dev

/** localhost:3001 */
yarn start:prod
package.json
{
  "name": "simple-crud",
  "engines": {
    "yarn": "1.x"
  },
  "main": "dist/server.js",
  "scripts": {
    "start:dev": "yarn watch:dev & yarn watch:react",
    "start:prod": "node dist/server.js",
    "build:dev": "webpack --mode development && yarn build:react",
    "build:prod": "webpack --mode production && yarn build:react",
    "build:react": "cd client && yarn build",
    "watch:dev": "webpack -w --mode development & nodemon dist/server.js",
    "watch:react": "cd client && yarn start",
    "heroku-postbuild": "webpack --mode production"
  },
  "dependencies": {
    "express": "^4.17.1",
    "mongoose": "^5.9.13"
  },
  "devDependencies": {
    "@types/express": "^4.17.6",
    "@types/mongoose": "^5.7.19",
    "@types/node": "^13.13.5",
    "dotenv-webpack": "^1.8.0",
    "nodemon": "^2.0.4",
    "ts-loader": "^7.0.4",
    "tslint": "^6.1.2",
    "tslint-config-airbnb": "^5.11.2",
    "tslint-loader": "^3.5.4",
    "typescript": "^3.8.3",
    "webpack": "4.42.0",
    "webpack-cli": "^3.3.11",
    "webpack-node-externals": "^1.7.2"
  }
}

ディレクトリ構造

現状で以下のディレクトリ構造であれば問題ありません。
サーバーサイドでは、src、フロントでは、client/srcに記述していきます。

simple-crud
├── README.md
├── client
│   ├── README.md
│   ├── package.json
│   ├── src
│   │   ├── components
│   │   │   └── App.tsx
│   │   └── index.tsx
│   ├── tsconfig.json
│   └── yarn.lock
├── package.json
├── src
├── tsconfig.json
├── tslint.json
├── webpack.config.js
└── yarn.lock

サーバーサイドのアプリケーション作成

サーバーサイドのアプリケーション作成では以下の内容を行います。

  • mongooseでスキーマの定義
  • ExpressでRest APIの作成
  • MongoDBにリクエストの送信

mongooseでスキーマの定義

名前と年齢を持ったCharacterというスキーマを定義します。
どちらもString型で必須項目にします。

src/character.ts
import mongoose from 'mongoose';

const characterSchema = new mongoose.Schema({
  name: {
    type: String,
    required: true,
  },
  age: {
    type: String,
    required: true,
  },
});

const character = mongoose.model('Character', characterSchema);

export default character;

ExpressでRest APIの作成

MongoDBに接続し、APIサーバーを立てる準備をします。

src/server.ts
import character from './character';
import express from 'express';
import bodyParser from 'body-parser';
import mongoose from 'mongoose';

const app = express();
const port = process.env.PORT || 3001;
const dbUrl = process.env.MONGODB_URI || 'mongodb://localhost/crud';

app.use(express.static('client/build'));
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());

mongoose.connect(
  dbUrl,
  { useNewUrlParser: true, useUnifiedTopology: true, useFindAndModify: false },
  (dbError) => {
    if (dbError) {
      console.log(dbError);
      throw new Error(`${dbError}`);
    } else {
      console.log('db connected');
    }

    app.listen(port, () => {
      console.log(`listening on port ${port}`);
    });
  },
);

以下のコマンドでMongoDBを起動し、サーバーを立てます。

/** MongoDBを起動する
$ brew services start mongodb-community

/** localhost:3001でサーバーを立てる */
$ yarn watch:dev

/** MongoDBを終了する
$ brew services stop mongodb-community

ターミナルに以下の内容が表示されれば成功です。

db connected
listening on port 3001

POST

DBに値を送信して保存できるようにPOSTから作成します。

src/server.ts
/** 中略 */

mongoose.connect(
  dbUrl,
  { useNewUrlParser: true, useUnifiedTopology: true, useFindAndModify: false },
  (dbError) => {

    /** 入力された名前と年齢をPOSTする */
+   app.post('/api/characters', (request, response) => {
+     const { name, age } = request.body;
+     new character({
+       name,
+       age,
+     }).save((error) => {
+       if (error) {
+         response.send(500).send();
+       } else {
+         character.find({}, (findError, characterArray) => {
+           if (findError) {
+             response.status(500).send();
+          } else {
+             response.status(200).send(characterArray);
+           }
+         });
+       }
+     });
+   });

    app.listen(port, () => {
      console.log(`listening on port ${port}`);
    });
  },
);

実際の動作するか以下のコマンドで確認してみましょう。
名前が「hoge」、年齢が「34」 のデータです。

curl -v -Ss -X POST -d 'name=hoge&age=34' http://localhost:3001/api/characters | jq

以下のようなJSON形式で返却されれば成功です。

[
  {
    "_id": "5ecd381cbdce9b04b809441f", // ユニークID
    "name": "hoge",
    "age": "34",
    "__v": 0
  }
]

GET

POSTで値の保存が出来たのでDBに保存されている値を取得できるようにGETを作成します。

src/server.ts
/** 中略 */

mongoose.connect(
  dbUrl,
  { useNewUrlParser: true, useUnifiedTopology: true, useFindAndModify: false },
  (dbError) => {

    /** 中略 */

    /** DBに保存された名前と年齢をGETする */
+   app.get('/api/characters', (_, response) => {
+     character.find({}, (error, characterArray) => {
+       if (error) {
+         response.status(500).send();
+       } else {
+         response.status(200).send(characterArray);
+       }
+     });
+   });

    app.listen(port, () => {
      console.log(`listening on port ${port}`);
    });
  },
);

実際の動作するか以下のコマンドで確認してみましょう。

curl -v -Ss http://localhost:3001/api/characters | jq

以下のようなJSON形式で返却されれば成功です。

[
  {
    "_id": "5ecd381cbdce9b04b809441f", // ユニークID
    "name": "hoge",
    "age": "34",
    "__v": 0
  }
]

PUT

DBに保存されている値を更新できるようにPUTを作成します。

src/server.ts
/** 中略 */

mongoose.connect(
  dbUrl,
  { useNewUrlParser: true, useUnifiedTopology: true, useFindAndModify: false },
  (dbError) => {

    /** 中略 */

    /** 指定されたユニークIDの名前と年齢をPUTする */
+   app.put('/api/characters', (request, response) => {
+     const { id, name, age } = request.body;
+     /** $setでキーを選択してアップデートする */
+     character.findByIdAndUpdate(id, { $set: { name, age } }, (error) => {
+       if (error) {
+         response.status(500).send();
+       } else {
+         character.find({}, (findError, characterArray) => {
+           if (findError) {
+             response.status(500).send();
+           } else {
+             response.status(200).send(characterArray);
+           }
+         });
+       }
+     });
+   });

    app.listen(port, () => {
      console.log(`listening on port ${port}`);
    });
  },
);

実際の動作するか以下のコマンドで確認してみましょう。
今回は先程、取得したユニークIDである5ecd381cbdce9b04b809441fを更新します。

curl -v -Ss -X PUT -d 'id=5ecd381cbdce9b04b809441f&name=fuga&age=43' http://localhost:3001/api/characters | jq

以下のようなJSON形式で返却されれば成功です。

[
  {
    "_id": "5ed1dad209d18805b763e610",
    "name": "fuga",
    "age": "43",
    "__v": 0
  }
]

DELETE

DBに保存されている値を削除できるようにDELETEを作成します。

src/server.ts
/** 中略 */

mongoose.connect(
  dbUrl,
  { useNewUrlParser: true, useUnifiedTopology: true, useFindAndModify: false },
  (dbError) => {

    /** 中略 */

    /** 指定されたユニークIDの名前と年齢を削除する */
+   app.delete('/api/characters', (request, response) => {
+     const { id } = request.body;
+     character.findByIdAndRemove(id, (error) => {
+       if (error) {
+         response.status(500).send();
+       } else {
+         character.find({}, (findError, characterArray) => {
+           if (findError) {
+             response.status(500).send();
+           } else {
+             response.status(200).send(characterArray);
+           }
+         });
+       }
+     });
+   });

    app.listen(port, () => {
      console.log(`listening on port ${port}`);
    });
  },
);

実際の動作するか以下のコマンドで確認してみましょう。
今回は先程、取得したユニークIDである5ecd381cbdce9b04b809441fを更新します。

curl -v -Ss -X DELETE -d 'id=5ed1dad209d18805b763e610' http://localhost:3001/api/characters | jq

以下のようなJSON形式で返却されれば成功です。

[]

長くなりましたが、以上でサーバーサイドの実装は完了です。

フロントエンドのアプリケーション作成

フロントエンドのアプリケーション作成では以下の内容を行います。

  • Reduxの使用
  • コンポーネントの作成
  • コンポーネントの表示とReactとReduxを繋げる

Reduxの使用

Reduxアーキテクチャに沿って以下の内容を定義していきます。
また、Reduxと切り分けて非同期処理を行うRedux-Sagaの作成、見た目とロジックを分担するためのコンテナーの作成も一緒に行います。

  • Actionの作成
  • Reducerの作成
    • フォーム
    • キャラクター
  • Redux-Sagaで非同期処理
    • POST
    • GET
    • PUT
    • DELETE
  • Storeの作成
  • Containerの作成
    • フォーム
    • キャラクター

Acitonの作成

Actionを作成します。
TypeScriptで実装するため、typescript-fsaというライブラリーを使用します。
import { CharactersState } from './reducers/charactersReducer'でエラーが発生しますが、後からReducerで定義し、エラーを解消します。

client/redux/action.ts
import actionCreatorFactory from 'typescript-fsa';
import { CharactersState } from './reducers/charactersReducer';

const actionCreator = actionCreatorFactory();

export const formActions = {
  changeName: actionCreator<string>('CHANGE_NAME'),
  changeAge: actionCreator<string>('CHANGE_AGE'),
  initializeForm: actionCreator<void>('INITIALIZE_FORM'),
  postForm: actionCreator.async<{}, {}>('POST_FORM'),
};

export const characterActions = {
  editName: actionCreator<string>('EDIT_NAME'),
  editAge: actionCreator<string>('EDIT_AGE'),
  getCharacters: actionCreator.async<{}, CharactersState['characterArray']>(
    'GET_CHARACTERS',
  ),
  updateCharacters: actionCreator.async<{}, CharactersState['characterArray']>(
    'UPDATE_CHARACTERS',
  ),
  deleteCharacters: actionCreator.async<{}, CharactersState['characterArray']>(
    'DELETE_CHARACTERS',
  ),
};

Reducerの作成

Reducerを作成します。
TypeScriptで実装するため、typescript-fsa-reducersというライブラリーを使用します。

フォーム

フォームでは、送信に必要な値であるnameageのインターフェースを定義し、ステートとして持ちます。
changeNameがディスパッチされるとnameを返し、chagenAgeがディスパッチされるとageを返します。
postFormがディスパッチされると非同期処理が走り、成功するとステートが送信されます。

client/src/redux/reducers/formReducer.ts
import { reducerWithInitialState } from 'typescript-fsa-reducers';
import { formActions } from '../actions';

export interface FormState {
  name: string;
  age: string;
}

const initialState: FormState = {
  name: '',
  age: '',
};

export const formReducer = reducerWithInitialState(initialState)
  .case(formActions.changeName, (state, name) => {
    return {
      ...state,
      name,
    };
  })
  .case(formActions.changeAge, (state, age) => {
    return {
      ...state,
      age,
    };
  })
  .case(formActions.initializeForm, (state) => {
    return {
      ...state,
      name: '',
      age: '',
    };
  })
  .case(formActions.postForm.started, (state) => {
    return {
      ...state,
    };
  })
  .case(formActions.postForm.done, (state) => {
    return {
      ...state,
    };
  })
  .case(formActions.postForm.failed, (state) => {
    return {
      ...state,
    };
  });
キャラクター

キャラクターでは、名前と年齢を編集する時に必要な値であるnameage、DBに保存されているjsonのオブジェクトをインターフェースを定義し、ステートとして持ちます。
changeNamechagenAgeの挙動はフォームと同じです。
getCharactersがディスパッチされるとpayloadからDBに保存されている値を取得し、characterArrayに代入します。

client/src/redux/reducers/charactersReducer.ts
import { reducerWithInitialState } from 'typescript-fsa-reducers';
import { characterActions } from '../actions';

export interface CharactersState {
  name: string;
  age: string;
  isFetching: boolean;
  characterArray: {
    _id: string;
    name: string;
    age: string;
    _v: number;
  }[];
}

const initialState: CharactersState = {
  name: '',
  age: '',
  isFetching: false,
  characterArray: [],
};

export const characterReducer = reducerWithInitialState(initialState)
  .case(characterActions.editName, (state, name) => {
    return {
      ...state,
      name,
    };
  })
  .case(characterActions.editAge, (state, age) => {
    return {
      ...state,
      age,
    };
  })
  .case(characterActions.getCharacters.started, (state) => {
    return {
      ...state,
      isFetching: true,
    };
  })
  .case(characterActions.getCharacters.done, (state, payload) => {
    return {
      ...state,
      isFetching: false,
      characterArray: payload.result,
    };
  })
  .case(characterActions.getCharacters.failed, (state) => {
    return {
      ...state,
      isFetching: false,
    };
  })
  .case(characterActions.updateCharacters.started, (state) => {
    return {
      ...state,
    };
  })
  .case(characterActions.updateCharacters.done, (state, payload) => {
    return {
      ...state,
      characterArray: payload.result,
    };
  })
  .case(characterActions.updateCharacters.failed, (state) => {
    return {
      ...state,
    };
  })
  .case(characterActions.deleteCharacters.started, (state) => {
    return {
      ...state,
    };
  })
  .case(characterActions.deleteCharacters.done, (state, payload) => {
    return {
      ...state,
      characterArray: payload.result,
    };
  })
  .case(characterActions.deleteCharacters.failed, (state) => {
    return {
      ...state,
    };
  });

先程のimport { CharactersState } from './reducers/charactersReducer'のエラーは解消されたと思います。

Redux-Sagaで非同期処理

Redux-Sagaでは、以下の内容を行います。

  • POST
  • GET
  • PUT
  • DELETE
  • 各SagaをまとめたrootSagaの作成

また、非同期処理にはAxiosというライブラリーを使用します。
Sagaでは、条件式で取得に成功した時と失敗し時で分岐し、yieldでputします。

POST

POSTでは、preventDefaultするためのeと必要な値であるnameageを引数として持ちます。

client/src/redux/sagas/postFormSaga.ts
import { call, put, takeLatest } from 'redux-saga/effects';
import axios from 'axios';

import { characterActions, formActions } from '../actions';

const postForm = (
  e: React.FormEvent<HTMLFormElement>,
  name: string,
  age: string,
) => {
  e.preventDefault();
  return axios
    .post('/api/characters', {
      name,
      age,
    })
    .then((response) => {
      const characterArray = response.data;
      return { characterArray, e, name, age };
    })
    .catch((error) => {
      return { error };
    });
};

function* runPostForm(action: {
  type: string;
  payload: {
    e: React.FormEvent<HTMLFormElement>;
    name: string;
    age: string;
  };
}) {
  const { characterArray, e, name, age, error } = yield call(
    postForm,
    action.payload.e,
    action.payload.name,
    action.payload.age,
  );

  if (characterArray && e && name && age) {
    yield put(
      formActions.postForm.done({
        params: {},
        result: characterArray,
      }),
    );
    yield put(
      characterActions.getCharacters.done({
        params: {},
        result: characterArray,
      }),
    );
    yield put(formActions.initializeForm());
  } else {
    yield put(
      formActions.postForm.failed({
        params: {},
        error: error,
      }),
    );
  }
}

export const watchPostForm = [
  takeLatest(formActions.postForm.started, runPostForm),
];
GET

GETでは、resuponse.datacharacterArrayの変数に代入し、返します。

client/src/redux/sagas/getCharactersSaga.ts
import { call, put, takeLatest } from 'redux-saga/effects';
import axios from 'axios';

import { characterActions } from '../actions';

const getCharacters = () => {
  return axios
    .get('/api/characters')
    .then((response) => {
      const characterArray = response.data;
      return { characterArray };
    })
    .catch((error) => {
      return { error };
    });
};

function* runGetCharacters() {
  const { characterArray, error } = yield call(getCharacters);

  if (characterArray) {
    yield put(
      characterActions.getCharacters.done({
        params: {},
        result: characterArray,
      }),
    );
  } else {
    yield put(
      characterActions.getCharacters.failed({ params: {}, error: error }),
    );
  }
}

export const watchGetCharacters = [
  takeLatest(characterActions.getCharacters.started, runGetCharacters),
];
PUT

PUTでは、更新対象のidと必要な値であるnameageを引数として持ちます。

client/src/redux/sagas/updateCharactersSaga.ts
import { call, put, takeLatest } from 'redux-saga/effects';
import axios from 'axios';

import { characterActions } from '../actions';

const updateCharacters = (id: string, name: string, age: number) => {
  return axios
    .put('/api/characters', {
      id,
      name,
      age,
    })
    .then((response) => {
      const characterArray = response.data;
      return { characterArray, id, name, age };
    })
    .catch((error) => {
      return { error };
    });
};

function* runUpdateCharacters(action: {
  type: string;
  payload: { id: string; name: string; age: number };
}) {
  const { characterArray, id, name, age, error } = yield call(
    updateCharacters,
    action.payload.id,
    action.payload.name,
    action.payload.age,
  );

  if (characterArray && id && name && age) {
    yield put(
      characterActions.updateCharacters.done({
        params: {},
        result: characterArray,
      }),
    );
  } else {
    yield put(
      characterActions.updateCharacters.failed({ params: {}, error: error }),
    );
  }
}

export const watchUpdateCharacters = [
  takeLatest(characterActions.updateCharacters.started, runUpdateCharacters),
];
DELETE

DELETEでは、削除対象のidを引数として持ちます。

client/src/redux/saga/deleteCharactersSaga.ts
import { call, put, takeLatest } from 'redux-saga/effects';
import axios from 'axios';

import { characterActions } from '../actions';

const deleteCharacters = (id: string) => {
  return axios({
    method: 'delete',
    url: '/api/characters',
    data: {
      id,
    },
  })
    .then((response) => {
      const characterArray = response.data;
      return {
        characterArray,
        id,
      };
    })
    .catch((error) => {
      return { error };
    });
};

function* runDeleteCharacters(action: {
  type: string;
  payload: { id: string };
}) {
  const { characterArray, id, error } = yield call(
    deleteCharacters,
    action.payload.id,
  );

  if (characterArray && id) {
    yield put(
      characterActions.deleteCharacters.done({
        params: {},
        result: characterArray,
      }),
    );
  } else {
    yield put(
      characterActions.deleteCharacters.failed({ params: {}, error: error }),
    );
  }
}

export const watchDeleteCharacters = [
  takeLatest(characterActions.deleteCharacters.started, runDeleteCharacters),
];
各SagaをまとめたrootSagaの咲く英

allを使用して、importした各SagaをrootSagaとしてまとめます。

client/src/redux/sagas/index.ts
import { all } from 'redux-saga/effects';
import { watchPostForm } from './postFormSaga';
import { watchGetCharacters } from './getCharactersSaga';
import { watchUpdateCharacters } from './updateCharactersSaga';
import { watchDeleteCharacters } from './deleteCharactersSaga';

export default function* rootSaga() {
  yield all([
    ...watchPostForm,
    ...watchGetCharacters,
    ...watchUpdateCharacters,
    ...watchDeleteCharacters,
  ]);
}

Store

Storeを作成します。
Storeでは、redux-loggerによるログ出力等のミドルウェアの処理を作成します。

client/src/redux/store.ts
import { createStore, combineReducers, applyMiddleware } from 'redux';
import { formReducer, FormState } from './reducers/formReducer';
import {
  characterReducer,
  CharactersState,
} from './reducers/charactersReducer';
import { createLogger } from 'redux-logger';
import createSagaMiddleware from 'redux-saga';
import rootSaga from './sagas';

const sagaMiddleware = createSagaMiddleware();

export type AppState = {
  form: FormState;
  character: CharactersState;
};

const logger = createLogger({
  diff: true,
  collapsed: true,
});

const store = createStore(
  combineReducers<AppState>({
    form: formReducer,
    character: characterReducer,
  }),
  {},
  applyMiddleware(sagaMiddleware, logger),
);

sagaMiddleware.run(rootSaga);
export default store;

Container

Containerでは、見た目とロジックを分担するために作成します。
Actionの作成同様にtypescript-fsaのライブラリーを使用します。
AddFormCharacterListのコンポーネントでエラーが発生しますが、後からコンポーネントを作成し、エラーを解消します。

フォーム
client/src/redux/container/AddFormContainer.ts
import { Action } from 'typescript-fsa';
import { Dispatch } from 'redux';
import { connect } from 'react-redux';
import { AppState } from '../store';
import { formActions } from '../actions';
import AddForm from '../../components/AddForm';

export interface AddFormActions {
  changeName: (inputValue: string) => Action<string>;
  changeAge: (inputValue: string) => Action<string>;
  initializeForm: () => Action<void>;
  postForm: (
    e: React.FormEvent<HTMLFormElement>,
    name: string,
    age: string,
  ) => Action<{}>;
}

const mapStateToProps = (appState: AppState) => {
  return {
    ...appState.form,
  };
};

const mapDispatchToProps = (dispatch: Dispatch<Action<string | void | {}>>) => {
  return {
    changeName: (inputValue: string) =>
      dispatch(formActions.changeName(inputValue)),
    changeAge: (inputValue: string) =>
      dispatch(formActions.changeAge(inputValue)),
    initializeForm: () => dispatch(formActions.initializeForm()),
    postForm: (
      e: React.FormEvent<HTMLFormElement>,
      name: string,
      age: string,
    ) => dispatch(formActions.postForm.started({ params: {}, e, name, age })),
  };
};

export default connect(mapStateToProps, mapDispatchToProps)(AddForm);
リスト
client/src/redux/container/CharacterListContainer.ts
import { Action } from 'typescript-fsa';
import { Dispatch } from 'redux';
import { connect } from 'react-redux';
import { AppState } from '../store';
import { characterActions } from '../actions';
import CharacterList from '../../components/CharacterList';

export interface CharacterListActions {
  editName: (name: string) => Action<string>;
  editAge: (age: string) => Action<string>;
  getCharacters: () => Action<{}>;
  updateCharacters: (id: string, name: string, age: string) => Action<{}>;
  deleteCharacters: (id: string) => Action<{}>;
}

const mapStateToProps = (appState: AppState) => {
  return {
    ...appState.character,
  };
};

const mapDispatchToProps = (dispatch: Dispatch<Action<{}>>) => {
  return {
    editName: (name: string) => dispatch(characterActions.editName(name)),
    editAge: (age: string) => dispatch(characterActions.editAge(age)),
    getCharacters: () =>
      dispatch(characterActions.getCharacters.started({ params: {} })),
    updateCharacters: (id: string, name: string, age: string) =>
      dispatch(
        characterActions.updateCharacters.started({
          params: {},
          id,
          name,
          age,
        }),
      ),
    deleteCharacters: (id: string) =>
      dispatch(characterActions.deleteCharacters.started({ params: {}, id })),
  };
};

export default connect(mapStateToProps, mapDispatchToProps)(CharacterList);

コンポーネントの作成

名前と年齢を入力するフォームと表示するリストを作成します。
Containerで定義したインターフェースをインポートし、タイプとして定義します。

フォーム

フォームでは、onChangeを使用して名前と年齢の入力を検知し、onSubmitで入力された内容を送信します。

client/src/components/AddForm.tsx
import React from 'react';
import { FormState } from '../redux/reducers/formReducer';
import { AddFormActions } from '../redux/container/AddFormContainer';

type AddFormProps = FormState & AddFormActions;

const AddForm: React.FC<AddFormProps> = (props) => {
  const { name, age } = props;

  return (
    <div className="AddForm">
      <h2 className="AddForm__title">フォーム</h2>
      <form onSubmit={(e) => props.postForm(e, name, age)}>
        <input
          className="AddForm__input"
          placeholder="名前"
          value={name}
          onChange={(e) => props.changeName(e.target.value)}
        />
        <input
          className="AddForm__input"
          placeholder="年齢"
          value={age}
          onChange={(e) => props.changeAge(e.target.value)}
        />
        <button className="AddForm__submit" type="submit">
          送信
        </button>
      </form>
    </div>
  );
};

export default AddForm;

リスト

リストでは、useEffectで初期レンダー時にDBに保存されている値を表示します。
useCallbackとuseStateで選択された名前と年齢を編集します。

client/src/components/CharacterList.tsx
import React, { useCallback, useEffect, useState } from 'react';
import { CharactersState } from '../redux/reducers/charactersReducer';
import { CharacterListActions } from '../redux/container/CharacterListContainer';

import './CharacterList.scss';

type CharacterListProps = CharactersState & CharacterListActions;

const CharacterList: React.FC<CharacterListProps> = (props) => {
  const [edit, setEdit] = useState(
    new Array<boolean>(props.characterArray.length).fill(false),
  );

  useEffect(() => {
    props.getCharacters();
  }, []);

  const editHandler = useCallback(
    (index: number) => {
      const newArray = [...edit];
      newArray[index] = !newArray[index];
      setEdit(newArray);
    },
    [edit],
  );

  return (
    <div className="CharacterList">
      {props.isFetching ? (
        <h2 className="CharacterList__title">Now Loading...</h2>
      ) : (
        <div>
          <h2 className="CharacterList__title">リスト</h2>
          <ul className="CharacterList__list">
            {props.characterArray.map((character, index) => (
              <li key={character._id} className="CharacterList__listItem">
                {edit[index] ? (
                  <React.Fragment>
                    <input
                      className="CharacterList__edit"
                      defaultValue={character.name}
                      onChange={(e) => props.editName(e.target.value)}
                    />
                    <input
                      className="CharacterList__edit"
                      defaultValue={character.age}
                      onChange={(e) => props.editAge(e.target.value)}
                    />
                  </React.Fragment>
                ) : (
                  `${character.name} (${character.age})`
                )}
                <div className="CharacterList__listArea">
                  <button
                    className="CharacterList__listButton"
                    onClick={() => {
                      props.updateCharacters(
                        character._id,
                        props.name ? props.name : character.name,
                        props.age ? props.age : character.age,
                      );
                      editHandler(index);
                    }}
                  >
                    <i className="gg-edit-markup"></i>
                  </button>
                  <button
                    className="CharacterList__listButton"
                    onClick={() => props.deleteCharacters(character._id)}
                  >
                    <i className="gg-trash"></i>
                  </button>
                </div>
              </li>
            ))}
          </ul>
        </div>
      )}
    </div>
  );
};

export default CharacterList;

先程のAddFormCharacterListのコンポーネントで発生したエラーが解消されたと思います。

コンポーネントを表示する

フォームとリストのコンポーネントを作成したので表示します。
現在、コンテナーでコンポーネントをコネクトしてます。
そのため、コンポーネントではなく、コンテナーをインポートします。

client/src/components/App.tsx
import React from 'react';
import AddForm from '../redux/container/AddFormContainer';
import CharacterList from '../redux/container/CharacterListContainer';

import './App.scss';

const App: React.FC = () => {
  return (
    <div className="App">
      <AddForm />
      <CharacterList />
    </div>
  );
};

export default App;

コンポーネントの表示とReactとReduxを繋げる

react-reduxというライブラリーのProviderを使用して、Storeを受け取れるようにします。

client/src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom';
+ import { Provider } from 'react-redux';
+ import Store from './redux/store';
import App from './components/App';

import './index.scss';

ReactDOM.render(
  <React.StrictMode>
+   <Provider store={Store}>
      <App />
+   </Provider>
  </React.StrictMode>,
  document.getElementById('root'),
);

長くなりましたが、以上でフロントエンドの実装は完了です。

Herokuにデプロイ

Herokuのアカウントが無い方は作成して下さい。

Heroku CLIをインストール

コマンドラインから操作するため、CLIをインストールします。

$ brew tap heroku/brew && brew install heroku

Herokuにログイン

以下のコマンドを叩いてブラウザに遷移するのでログインします。

$ heroku login

Heroku appを作成

Herokuでアプリを作成し、MongoLabのアドオンを追加をします。
アドオンの追加にはHerokuにクレジットカード登録が必要です。
2020年6月2日の現時点、今回、使用するアドオンは無料になります。

$ heroku create 任意のアプリ名
$ heroku addons:create mongolab

MONGODB_URIの確認

現状のMongoDBはローカルDBなのでHerokuのMongoDBに変更します。
以下のコマンドを叩いてMONGODB_URIを確認します。

$ heroku config

.envファイルの作成

MONGODB_URI.envファイルに設定し、読み込まれるようにします。

MONGODB_URI=mongodb://heroku...(各自で確認したMONGODB_URI)

Procfileの作成

Procfileは、Herokuにデプロイ時のコマンドを指定できます。
ビルドされたserver.jsをnodeで起動します。

web: node dist/server.js

デプロイ

Herokuにデプロイするためにデプロイ先のURLを追加します。

$ git remote add https://git.heroku.com/任意のアプリ名.git

以下のコマンドでビルドが走り、デプロイされます。

$ git push heroku master

以上でHerokuへのデプロイが完了です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

WSL(WSL2)でビルドが遅くて重くてホットリロードが効かないとき

結論

WSLを使うとき、/mnt/c配下で開発せず、~で開発しなさい。

概要

WSL(WSL2)上でVue(Nuxt)やReact(Next, Gatsby)などの開発をしていて、ビルドすると始まりが遅いしビルド時間もかなり長くなるという方や、ファイル名の変更ができなかったり、保存時にホットリロードが効いていないなどで悩んでいる方用の対処法。

なぜ?

  • WSLでのビルド時間が長くなってしまう原因の一つとして/mnt/c配下で作業していることが挙げられる。 /mntはWSL内からWindows側のファイルへアクセスするためにあるディレクトリで、/mnt/c配下はWindowsのCドライブのディレクトリを指す。つまり、Windows側のC:\は、WSLでは/mnt/cということ。
  • Windows側のファイルシステムを経由してしまうと、ビルドに時間がかかったり、一部の機能が制限されてしまうため、ファイル名の変更ができなかったり、ホットリロードが効かなかったりする
  • 解決策としては/mnt/c配下で作業するのを避け、WSLの~配下などで作業をすることで、期待通りの動きをしてくれるはず。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React Redux基礎の基礎/小さなカウンターアプリを作る流れ

この記事で書くこと

UdemyのReact、Redux講座の基礎編を学んでおり、本当にReduxがややこしいので一旦基礎の基礎をまとめました。
環境構築のあたりや細かい用語の説明や調査は省き、ミニマムにコードの流れをまとめただけの記事です。
ほぼ頭の整理のためなのでいろいろ認識違ってるかもしれないです。。w。

最低限わかっておくべきprops、stateについてはこちらが分かりやすかったです。
+と-ボタンで数字が増える、減るというだけの処理を書いてます。

流れ

【1】まず、アクションのタイプを書く。incrementかdecrementか。これをReducerへ渡す。

action/index.js
//あとでレデューサーでも使うので、constしておく。
export const INCREMENT = 'INCREMENT'
export const DECREMENT = 'DECREMENT'

//それぞれをアクションクリエイターとよぶよ
export const increment = () => ({
        type:INCREMENT
})

export const decrement = () => ({
        type:DECREMENT
})

【2】アクションからReducerへINCREMENT、DECREMENTを渡し、typeによりstateを返す。

reducers/count.js
import { INCREMENT,DECREMENT } from '../actions'

//初期状態のオブジェクトを入れる変数名をinitialStateとする。
const initialState = { value:0 }//初期のカウント

//関数として定義。引数はstate、acitonとして2つ持つ
export default(state =initialState, action) => {
    //actionのtype(INCREMENTかDECREMENTか)はaction.typeという処理で拾える
    switch(action.type){
        case INCREMENT://INCREMENTを拾った場合は値をプラス1する。
            return { value:state.value + 1}
        case DECREMENT://同様に、DECREMENTの場合はマイナス。
            return { value:state.value - 1}
        default: //初期状態は0のまま
            return state
    }
}

【3】count.jsを同じReducerの中のindex.jsに渡し、combineReducersでまとめる(今回は1つだが、複数のreducersをまとめるのに良い)

reducers/index.js
//すべてのreducsersをここでまとめてしまうcombineReducers を取り込む。
import { combineReducers } from 'redux'
//count.jsから取得
import count from './count'

//storeで使うので、Reducerで決めた数値をエクスポートする。
export default combineReducers({ count })
//なお、今回は受け渡す値は「count」一つだが
//いろいろな状態を管理したい場合は
//export default combineReducers({ foo,bar,baz }) みたいな感じ


【4】ReducserからComponent/App.jsへ。ここでconnect関数を使い、

component/App.js
import React, { Component } from 'react';
//コネクト関数を追加
import { connect } from 'react-redux'
import { increment, decrement } from '../actions'


class App extends Component {
  //counterには、レデューサーのカウンター内のオブジェクトの値の値を渡す。propsで受け取ってる。
  render(){
    const props = this.props
      return(
      <React.Fragment>
        <div>counter:{props.value}</div>
        <button onClick={props.increment}> +1</button>
        <button onClick={props.decrement}> -1</button>
      </React.Fragment>
    )
  }
}

//mapStateToPropsは、stateの情報から、componentに必要な情報を受け渡す
const mapStateToProps = state => ({ value:state.count.value})
//アクションが実行された時に、該当のアクションを渡し、状態遷移をさせるのがディスパッチ
const mapDispatchToProps = dispatch => ({
  increment: () => dispatch(increment()),
  decrement: () => dispatch(decrement())
})
//connect関数でstoreと描写側がつながるらしい。
export default connect(mapStateToProps,mapDispatchToProps)(App)


【5】これらをまとめるトップにあるファイルがこれ。

index.js
import React from "react";
import ReactDOM from 'react-dom';
//createStoreはstoreを利用するためのもの
import { createStore } from 'redux'
//Providerはすべての環境でstoreを使うためのもの
import { Provider } from 'react-redux'
import './index.css';
import reducer from './reducers';
import App from './components/App';
import registerServiceWorker from './registerServiceWorker';

//ここで作られるstoreはアプリ内の唯一のものになる
const store = createStore(reducer)

//既存のコンポーネントを、providerコンポートネントで囲み、store属性に、
//上のconstしたstoreを当てはめる。これで、バケツリレーをせずにproviderが使える
//この中の<App />が【4】でrenderされてるもの。
ReactDOM.render(
<Provider store={store}>
    <App />
</Provider>,
 document.getElementById('root')
);

registerServiceWorker();//?

実際の表示。※【4】でrenderされてるコンテンツのみ!

スクリーンショット 2020-06-02 1.42.39.png

+1と-1をクリックすると数字が増えるよ。

むずい

書き始めたときは流れに沿って書けば整理できそう!と思ったけど、まとめてみると全然1つの流れ、というわけではない。し、あまりよくわかってないです。。w

とにかく、
action、action creatorでアクションを作り
ビュー側のクリックとかでそのアクションをディスパッチする。
レデューサーでそのアクションを元に実際にstate(値)を変える。
したらビュー側が変わる、というくらいは分かりました!ほんまにわかってるんかこれ!

qiitaでも色々あったので、もう少し整理するためにいろんな記事読んでみようと思います。。
外部サイトですがこれも図になってて分かりやすかった。

ちなみに

これやってます!まだ基礎編を学んだだけなのでこっからしんどそう!
「フロントエンドエンジニアのためのReact・Redux実践入門」
https://www.udemy.com/course/react-application-development/learn/
ちなみに、gitもpushとclone、commitくらいしかしてなかった(しかもSource tree)のでめっちゃ練習になってます、、、w

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む