- 投稿日:2020-06-02T23:36:58+09:00
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;
- 投稿日:2020-06-02T22:50:00+09:00
React デバッグ方法
Debugger for Chrome
https://marketplace.visualstudio.com/items?itemName=msjsdiag.debugger-for-chrome
設定方法・手順
launch.json
- VSCodeにてlaunch.jsonを編集
- create-react-appで開発する場合
- https://create-react-app.dev/docs/setting-up-your-editor/#visual-studio-code
{ "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/9c2af4b28ba665cc0744Redux 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);
- 投稿日:2020-06-02T19:17:58+09:00
[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記事)
- 投稿日:2020-06-02T09:43:14+09:00
React + Redux + Redux-Saga + Express + Mongo DB + TypeScriptでCRUDなWebアプリケーションを作成してHerokuにデプロイしてみる
概要
名前と年齢を入力し、保存・編集・削除ができるCRUDなWebアプリケーションを作成してHerokuにデプロイした備忘録になります(使ってみたい技術スタックが多かったため、長文な記事になってしまいました)。
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 --versionjqのインストール
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-externalsWebpackでビルド環境の構築
Webpackでは、TypeScriptをAirBnbのLintルールに従ってビルドします。
各自、使用したいルールがあれば、そちらを使用して下さい。tsconfig.jsonの作成
TypeScriptのコンパイラーオプションを設定します。
現状だとclient/
配下も範囲に含まれるのでエラーが発生します。
エラーを回避するために"include" : ["src/**/*]"
を記述し、範囲を選択します。$ npx tsc --inittsconfig.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.jsconst 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-reducerspackage.jsonの修正
npm scriptを使用し、コマンドから実行できるようにルートのpackage.jsonを修正します。
Herokuのデプロイ時に必要になる、engines
とheroku-postbuild
コマンドも一緒に追記してあります。
基本的には、yarn start:dev
で開発を行い、Herokuにデプロイする時は、yarn build:prod
でビルドを行います。/** localhost:3000 */ yarn start:dev /** localhost:3001 */ yarn start:prodpackage.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.tsimport 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.tsimport 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 3001POST
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.tsimport 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
というライブラリーを使用します。フォーム
フォームでは、送信に必要な値である
name
とage
のインターフェースを定義し、ステートとして持ちます。
changeName
がディスパッチされるとname
を返し、chagenAge
がディスパッチされるとage
を返します。
postForm
がディスパッチされると非同期処理が走り、成功するとステートが送信されます。client/src/redux/reducers/formReducer.tsimport { 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, }; });キャラクター
キャラクターでは、名前と年齢を編集する時に必要な値である
name
とage
、DBに保存されているjsonのオブジェクトをインターフェースを定義し、ステートとして持ちます。
changeName
とchagenAge
の挙動はフォームと同じです。
getCharacters
がディスパッチされるとpayload
からDBに保存されている値を取得し、characterArray
に代入します。client/src/redux/reducers/charactersReducer.tsimport { 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
と必要な値であるname
とage
を引数として持ちます。client/src/redux/sagas/postFormSaga.tsimport { 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.data
をcharacterArray
の変数に代入し、返します。client/src/redux/sagas/getCharactersSaga.tsimport { 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
と必要な値であるname
とage
を引数として持ちます。client/src/redux/sagas/updateCharactersSaga.tsimport { 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.tsimport { 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.tsimport { 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.tsimport { 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
のライブラリーを使用します。
AddForm
とCharacterList
のコンポーネントでエラーが発生しますが、後からコンポーネントを作成し、エラーを解消します。フォーム
client/src/redux/container/AddFormContainer.tsimport { 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.tsimport { 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.tsximport 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.tsximport 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;先程の
AddForm
とCharacterList
のコンポーネントで発生したエラーが解消されたと思います。コンポーネントを表示する
フォームとリストのコンポーネントを作成したので表示します。
現在、コンテナーでコンポーネントをコネクトしてます。
そのため、コンポーネントではなく、コンテナーをインポートします。client/src/components/App.tsximport 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.tsximport 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 herokuHerokuにログイン
以下のコマンドを叩いてブラウザに遷移するのでログインします。
$ heroku loginHeroku appを作成
Herokuでアプリを作成し、MongoLabのアドオンを追加をします。
アドオンの追加にはHerokuにクレジットカード登録が必要です。
2020年6月2日の現時点、今回、使用するアドオンは無料になります。$ heroku create 任意のアプリ名 $ heroku addons:create mongolabMONGODB_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へのデプロイが完了です。
- 投稿日:2020-06-02T03:37:30+09:00
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の~
配下などで作業をすることで、期待通りの動きをしてくれるはず。
- 投稿日:2020-06-02T02:00:07+09:00
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.jsimport { 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.jsimport 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.jsimport 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されてるコンテンツのみ!
+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