- 投稿日:2019-07-01T23:02:42+09:00
redux-form (3) - Field-Level Validation Example
redux-form (1) - Simple Form Example
redux-form (2) - Synchronous Validation Example
redux-form (3) - Field-Level Validation Example
redux-form (4) - Submit Validation Example
ReactでForm componentを作るときに、とても便利なredux-formの説明です。
redux-formの概説についてはまず以下の記事を参考にしてください。
redux-form (1) - Simple Form Example
Field-Level Validation Example
redux-form (2) - Synchronous Validation Exampleでは、Redux state 全体のvaluesを入力として、errorのobjectを返す、validate関数でのチェックの方法を見ました。1個のvalidate関数で全Formのvalidateを行いました。
今回は、各Fieldを個別に validate する方法を紹介します。各Field毎に、validate propとしてvalidate関数を指定します。validate関数はFieldのvalueを入力にとり、value が valid であればundefineを返し、value が invalid であればエラーメッセージ(文字列)を返します。 小さく再利用可能なvalidate関数を作ることで、同じようなコードを何回も書くことを避けることができるメリットがあります。
Field-Level Validation Example - Getting Started With redux-form
以下のソースコードを見てください。
最初に小さなvalidate関数を多数定義しています。
それをField componentの validate prop に指定します。
例えばrequiredは複数のField componentで指定されています。一度定義すれば、再利用可能なので同じコードを繰り返す必要がなくなります。src/FieldLevelValidationForm.jsimport React from 'react' import { Field, reduxForm } from 'redux-form' const required = value => (value || typeof value === 'number' ? undefined : 'Required') const maxLength = max => value => value && value.length > max ? `Must be ${max} characters or less` : undefined const maxLength15 = maxLength(15) export const minLength = min => value => value && value.length < min ? `Must be ${min} characters or more` : undefined export const minLength2 = minLength(2) const number = value => value && isNaN(Number(value)) ? 'Must be a number' : undefined const minValue = min => value => value && value < min ? `Must be at least ${min}` : undefined const minValue13 = minValue(13) const email = value => value && !/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(value) ? 'Invalid email address' : undefined const tooYoung = value => value && value < 13 ? 'You do not meet the minimum age requirement!' : undefined const aol = value => value && /.+@aol\.com/.test(value) ? 'Really? You still use AOL for your email?' : undefined const alphaNumeric = value => value && /[^a-zA-Z0-9 ]/i.test(value) ? 'Only alphanumeric characters' : undefined export const phoneNumber = value => value && !/^(0|[1-9][0-9]{9})$/i.test(value) ? 'Invalid phone number, must be 10 digits' : undefined const renderField = ({ input, label, type, meta: { touched, error, warning } }) => ( <div> <label>{label}</label> <div> <input {...input} placeholder={label} type={type} /> {touched && ((error && <span>{error}</span>) || (warning && <span>{warning}</span>))} </div> </div> ) const FieldLevelValidationForm = props => { const { handleSubmit, pristine, reset, submitting } = props return ( <form onSubmit={handleSubmit}> <Field name="username" type="text" component={renderField} label="Username" validate={[required, maxLength15, minLength2]} warn={alphaNumeric} /> <Field name="email" type="email" component={renderField} label="Email" validate={email} warn={aol} /> <Field name="age" type="number" component={renderField} label="Age" validate={[required, number, minValue13]} warn={tooYoung} /> <Field name="phone" type="number" component={renderField} label="Phone number" validate={[required, phoneNumber]} /> <div> <button type="submit" disabled={submitting}> Submit </button> <button type="button" disabled={pristine || submitting} onClick={reset}> Clear Values </button> </div> </form> ) } export default reduxForm({ form: 'fieldLevelValidation' // a unique identifier for this form })(FieldLevelValidationForm)以下は、オリジナルなものを最小化したindex.jsです。
src/index.jsimport React from 'react' import ReactDOM from 'react-dom' import { Provider } from 'react-redux' import { createStore, combineReducers } from 'redux' import { reducer as reduxFormReducer } from 'redux-form' const dest = document.getElementById('content') const reducer = combineReducers({ form: reduxFormReducer // mounted under "form" }) const store = createStore(reducer) const showResults = values => new Promise(resolve => { setTimeout(() => { // simulate server latency window.alert(`You submitted:\n\n${JSON.stringify(values, null, 2)}`) resolve() }, 500) }) let render = () => { const FieldLevelValidationForm = require('./FieldLevelValidationForm').default ReactDOM.hydrate( <Provider store={store}> <h2>Form</h2> <FieldLevelValidationForm onSubmit={showResults} /> </Provider>, dest ) } render()実行画面
エラー表示
今回は以上です。
- 投稿日:2019-07-01T16:41:58+09:00
【備忘録】React & TypeScript & Webpack4 & Babel7 & dev-server の最小構成ボイラープレートの作成
WebpackとBabelの復習の題材として、最近流行りのReact & TypeScript で最小構成のボイラープレートを作成したので、作成手順と解説を残しておく。
環境
Nodeとnpmは以下の通り。
Node version : 10.15.2
npm version : 6.9.0使用するnpmモジュール
今回使用するモジュールは以下の通り。(使用するモジュールは全てlatest)
npmモジュール名 バージョン 説明 react 16.8.6 - react-dom 16.8.6 - webpack 4.35.0 モジュールバンドラ本体 webpack-cli 3.3.5 webpackをコマンドで実行できるようにする webpack-dev-server 3.7.2 webpackでの開発サーバーの立ち上げ用 @babel/core 7.4.5 トランスパイラ本体 @babel/preset-env 7.4.5 トランスパイラ本体 @babel/preset-react 7.0.0 React用 @babel/preset-typescript 7.3.3 TypeScript用 babel-loader 8.0.6 webpackで使用するできるようにする @types/react 16.8.22 React用の型定義モジュール @types/react-dom 16.8.4 ReactDOM用の型定義モジュール プロジェクト基盤の作成
各種ディレクトリとファイルを作成する。
以下の構造を持つプロジェクト基盤を作成する。. ├── src │ └── index.tsx ├── index.html ├── package.json ├── .babelrc └── webpack.config.jspackage.jsonファイルに設定追加とnpmモジュールの追加
使用するnpmモジュールをpackage.jsonに設定する。
ついでにscriptsにwebpack-dev-server起動用コマンドと、ビルド用コマンドを設定する。{ "name": "react-ts-webpack", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "start": "webpack-dev-server --open", "build": "webpack", "build-prod": "webpack --mode=production" }, "keywords": [], "dependencies": { "@babel/core": "^7.4.5", "@babel/preset-env": "^7.4.5", "@babel/preset-react": "^7.0.0", "@babel/preset-typescript": "^7.3.3", "@types/react": "^16.8.22", "@types/react-dom": "^16.8.4", "babel-loader": "^8.0.6", "react": "^16.8.6", "react-dom": "^16.8.6", "webpack": "^4.35.0", "webpack-cli": "^3.3.5" }, "devDependencies": { "webpack-dev-server": "^3.7.2" } }package.json作成後、以下のコマンドを実行してnpmモジュールをインストールする。
$ npm install.babelrcファイルに設定追加
.babelrcファイルにトランスパイラの設定を追加する。
ES6~をES5に変換する @babel/preset-env と、今回対象となる React、TypeScript を用のプリセットを設定する。{ // プリセットを使用して、ES6 ~ を ES5 に変換 // 今回は React と TypeScript が対象のため、専用のプリセットも追加 "presets": ["@babel/preset-env", "@babel/react", "@babel/typescript"] }webpack.config.jsファイルに設定追加
webpack.config.jsファイルに各種設定を追加する。
const path = require('path'); const rules = [{ // 対象とする拡張子を指定 test: /\.tsx?/, // 対象から外すディレクトリを指定 exclude: /node_modules/, // babelを使用する loader: 'babel-loader', }]; module.exports = { // ブラウザ環境で使用するためwebをtargetとする target: 'web', // モード値を production に設定すると最適化された状態で、 // development に設定するとソースマップ有効でJSファイルが出力される mode: 'development', // 起点となるTSXファイル(エントリーポイント) entry: './src/index.tsx', // ビルド後の出力先設定 output: { // 出力先パス path: path.resolve(__dirname, 'build'), // ファイル名 filename: 'bundle.js', }, module: { // ビルド時に使用するルール(上で設定)を設定 rules }, resolve: { // 対象とする拡張子を指定 extensions: ['.ts', '.tsx', '.js'] }, // webpack-dev-serverの設定 devServer: { // 起点となるパス contentBase: './', // ポート番号 port: 5000, }, };ここまでの設定で使用する準備は完了。
動作確認
動作を確認するためにindex.htmlとindex.tsxの中身を実装する。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>TypeScript App</title> </head> <body> <div id="app-root"></div> <script src="bundle.js"></script> </body> </html>import React from 'react'; import ReactDOM from 'react-dom'; function App(): JSX.Element { const sum = (a: number, b: number): number => a + b; return ( <div> <h1>React & TypeScript!</h1> <p>Test: {sum(15, 15)} </p> </div> ); } export default App; const root = document.getElementById('app-root'); ReactDOM.render(<App />, root);実装が完了したら、以下のコマンドを実行して開発用サーバー起動することを確認する。
$ npm start > react-ts-webpack@1.0.0 start /Users/kento/Programing/VScodeProjects/TypeScriptWithModernReact/react-simple-todo-list > webpack-dev-server --open ℹ 「wds」: Project is running at http://localhost:5000/ ℹ 「wds」: webpack output is served from / ℹ 「wds」: Content not from webpack is served from ./ ℹ 「wdm」: wait until bundle finished: / ℹ 「wdm」: Hash: 530304f8ddadb1675c93 Version: webpack 4.35.0 Time: 1623ms Built at: 2019-07-01 16:29:25 Asset Size Chunks Chunk Names bundle.js 1.23 MiB main [emitted] main Entrypoint main = bundle.js [0] multi (webpack)-dev-server/client?http://localhost:5000 ./src/index.tsx 40 bytes {main} [built] [./node_modules/ansi-html/index.js] 4.16 KiB {main} [built] [./node_modules/html-entities/index.js] 231 bytes {main} [built] [./node_modules/react-dom/index.js] 1.33 KiB {main} [built] [./node_modules/react/index.js] 190 bytes {main} [built] [./node_modules/webpack-dev-server/client/index.js?http://localhost:5000] (webpack)-dev-server/client?http://localhost:5000 4.29 KiB {main} [built] [./node_modules/webpack-dev-server/client/overlay.js] (webpack)-dev-server/client/overlay.js 3.51 KiB {main} [built] [./node_modules/webpack-dev-server/client/socket.js] (webpack)-dev-server/client/socket.js 1.53 KiB {main} [built] [./node_modules/webpack-dev-server/client/utils/createSocketUrl.js] (webpack)-dev-server/client/utils/createSocketUrl.js 2.77 KiB {main} [built] [./node_modules/webpack-dev-server/client/utils/log.js] (webpack)-dev-server/client/utils/log.js 964 bytes {main} [built] [./node_modules/webpack-dev-server/client/utils/reloadApp.js] (webpack)-dev-server/client/utils/reloadApp.js 1.63 KiB {main} [built] [./node_modules/webpack-dev-server/client/utils/sendMessage.js] (webpack)-dev-server/client/utils/sendMessage.js 402 bytes {main} [built] [./node_modules/webpack-dev-server/node_modules/strip-ansi/index.js] (webpack)-dev-server/node_modules/strip-ansi/index.js 161 bytes {main} [built] [./node_modules/webpack/hot sync ^\.\/log$] (webpack)/hot sync nonrecursive ^\.\/log$ 170 bytes {main} [built] [./src/index.tsx] 420 bytes {main} [built] + 29 hidden modules ℹ 「wdm」: Compiled successfully.次は、ビルドが実行できるかの確認をする。以下のコマンドを実行する。
$ npm run build > react-ts-webpack@1.0.0 build /Users/kento/Programing/VScodeProjects/TypeScriptWithModernReact/react-simple-todo-list > webpack Hash: 7061f3fb5f989d0481ae Version: webpack 4.35.0 Time: 942ms Built at: 2019-07-01 16:27:49 Asset Size Chunks Chunk Names bundle.js 907 KiB main [emitted] main Entrypoint main = bundle.js [./node_modules/webpack/buildin/global.js] (webpack)/buildin/global.js 472 bytes {main} [built] [./src/index.tsx] 420 bytes {main} [built] + 11 hidden modules実行後、プロジェクト内を確認すると、build ディレクトリが作成されていることが確認できる。
また、作成されたディレクトリ内にbundle.jsというファイルが作成される。
本番環境用コマンドの動作も確認。
builde.jsのサイズが開発環境用とくらべて小さくなっていることが確認できる。$ npm run build-prod > react-ts-webpack@1.0.0 build-prod /Users/kento/Programing/VScodeProjects/ts-react-simple-boiler > webpack --mode=production Hash: a86a1e1ae0c669a0f858 Version: webpack 4.35.0 Time: 739ms Built at: 2019-07-03 20:06:49 Asset Size Chunks Chunk Names bundle.js 117 KiB 0 [emitted] main Entrypoint main = bundle.js [3] ./src/index.tsx 420 bytes {0} [built] [8] (webpack)/buildin/global.js 472 bytes {0} [built] + 7 hidden modules以上で確認完了。
作成したボイラープレートは こちら
- 投稿日:2019-07-01T16:17:21+09:00
はじめてのreact
作業ログ
$ npx create-react-app my-app $ cd my-app $ npm startChromeブラウザが立ち上がって地球のまわりに衛生が回る軌道みたいなページが表示されたらOK
アクセスしているURL:http://localhost:3000/
下記ファイルを書き換えて保存すると高速で反映される(くそはやい)
IDEによってはJSXの表示に切り替える事をしてあげたほうが見やすい。
my-app/src/App.js
import React from 'react'; import logo from './logo.svg'; import './App.css'; function App() { return ( <div className="App"> <header className="App-header"> <img src={logo} className="App-logo" alt="logo" /> <p> Edit <code>src/App.js</code> and save to reload. </p> <a className="App-link" href="https://reactjs.org" target="_blank" rel="noopener noreferrer" > Learn React </a> </header> </div> ); } export default App;非reactからreactに移行する
rendarに既存のHTMLを突っ込んでエラーになってる箇所を順番に解決してみる
React Developer Toolsこのツールが必須。置き換える場合の注意点
- imgタグは閉じタグ(/img)が必要
- <!-- コメントアウトは削除 -->
- inputタグにも閉じタグが必要
- brタグにも閉じタグが必要
- target="_blank"になってるリンクは rel="noreferrer noopener"をつけてあげないとエラーになる
- style="float:left" は駄目で style={{float:left}}にしないといけない
- etc... etc... etc...
道のりは長い
参考
- 投稿日:2019-07-01T15:42:32+09:00
[勉強用] React hooksで非同期処理を書く (ステップ5)
はじめに
前ステップから続き、勉強用のuseFetchを書いていきます。今回のテーマはキャンセル処理です。
課題
URLを変更すると、クリーンアップ処理が走り、新しいデータ取得が走りますが、ブラウザが処理をしている前のデータ取得の処理を止めることができたわけではありません。例えば、長いデータを転送中だったり、サーバからの応答を待っていたり、サーバの名前解決をしていたりする場合は、データ取得の処理は継続しています。fetchはpromiseベースのAPIですが、AbortControllerを使うことで処理をキャンセル(abort)することができます。
ステップ5: キャンセル処理
const useFetch = url => { const [result, setResult] = useState({}); useEffect(() => { let cleanedUp = false; const abortController = new AbortController(); const fetchData = async () => { try { const response = await fetch(url, { signal: abortController.signal }); if (!response.ok) throw new Error(`status: ${response.status}`); const data = await response.json(); if (!cleanedUp) { setResult({ data }); } } catch (error) { if (!cleanedUp) { setResult({ error }); } } }; setResult({ loading: true }); fetchData(); const cleanup = () => { cleanedUp = true; abortController.abort(); setResult({}); }; return cleanup; }, [url]); return result; };念のため、cleanupでresultも初期化するようにしました。(一瞬以前のresultで描画されるを防ぐため)
動作確認
実際に動くコードはこちらです。codesandbox
キャンセルの動作はUIで直接は分かりませんので、Chrome DevToolsでNetworkタブを確認してください。このように、ローディング中にURLを変更すると、canceledとなっていることを確認できると思います。
おわりに
本コードは勉強用ですので、そのままでは使わないでください。(ちゃんとした実装はこちら)
さらなる課題と解決は次のステップへ。
- 投稿日:2019-07-01T14:59:26+09:00
【ReactNative+Redux】ActionとReducerの記述を改善?した話
はじめに
Typescript+ReactNative+Reduxを勉強中なのですが、
下記のような記述が個人的にはすごく気持ち悪い。HogeAction.tsexport const HOGE_ACTION_1: string = 'HOGE_ACTION_1'; export const HOGE_ACTION_2: string = 'HOGE_ACTION_2'; export class HogeAction1 { type : string = HOGE_ACTION_1; data1: string = ''; } export class HogeAction2 { type : string = HOGE_ACTION_2; data2: boolean = false; data3: number = 0; }Reducer側ではtypeを参照してどのActionか判定しているけど、Actionはちゃんと型として定義しているのだからそんなことする必要はないのでは??
と思ったらReducerではActionをanyで受けているのか...
HogeReducer.tsimport { HOGE_ACTION_1, HOGE_ACTION_2, HogeAction1, HogeAction2 } from '../HogeAction'; import HogeState from '../HogeState'; // Stateを初期化 const initialState: HogeState = new HogeState(); export default function HogeReducer(state = initialState, action : any){ if(!action){ return state; } switch(action.type){ case HOGE_ACTION_1: // HogeAction1処理... case HOGE_ACTION_2: // HogeAction2処理... default: return state; } }修正後
それならActionBaseというActionの基底クラスを作って、そこで各Actionのインスタンスの区別がつくようにしてあげればいいのでは(言語化すると何言ってるのか分からんので、下記コードを見てください)
ActionBase.tsexport default class ActionBase { // コンストラクタ constructor(){ } // クラス名 public type : string = this.getClassName(); public getClassName() { if(!this || !this.constructor){ return ''; } // コンストラクタからクラス名を取得 const constructorRegex: RegExp =/function (.{1,})\(/; const results = (constructorRegex).exec((<any> this).constructor.toString()); return (results && results.length > 1) ? results[1] : ''; } }getClassName()でインスタンス化した各Actionのコンストラクタ文字列を呼び出してActionを特定している。
typeはActionBaseに持たせておけばよいので、各Actionクラスは純粋に操作するプロパティだけを持たせる。
HogeAction.tsimport ActionBase from './ActionBase'; //export const HOGE_ACTION_1: string = 'HOGE_ACTION_1'; //export const HOGE_ACTION_2: string = 'HOGE_ACTION_2'; export class HogeAction1 extends ActionBase{ //type : string = HOGE_ACTION_1; data1: string = ''; } export class HogeAction2 extends ActionBase{ //type : string = HOGE_ACTION_2; data2: boolean = false; data3: number = 0; }Reducerはaction.typeと各Actionクラスの.nameを突合させて判定している。
HogeReducer.tsimport { HogeAction1, HogeAction2 } from '../HogeAction'; import HogeState from '../HogeState'; import ActionBase from '../ActionBase'; // Stateを初期化 const initialState: HogeState = new HogeState(); export default function HogeReducer(state = initialState, action : ActionBase){ if(!action){ return state; } switch(action.type){ case HogeAction1.name: // HogeAction1処理... case HogeAction2.name: // HogeAction2処理... default: return state; } }これで少しはモヤっとする書き方をせずに済むかも。
もっと良い書き方があれば教えていただければ幸いです。
- 投稿日:2019-07-01T14:16:27+09:00
なる早でTypescript + Next + MobX + mobx-react-lite で非同期処理をサクッと扱う
はじめに
この記事ではTypescript(.tsx)でNext.js@canaryのサーバーを立て、MobXの動くものをちょいと作ります。
また、axiosでモックアダプターを作る方法を少し説明します。
そして、GitHubのNoopsChalangeのAPIに接続します。
- これはとりあえず動くものを素早く作るためのチュートリアルです
- Typescriptを手っ取り早く設定するために、執筆時点ではNext.jsのcanaryバージョンを使います
想定読者
以下のいずれか
- TypescriptとReactが読める
- これを読んだ人:なる早でTypescript + Next + MobX + mobx-react-lite でグリグリ動く「買い物リスト」みたいなのを作る
今日作るもの
できたやつ: https://github.com/NanimonoDemonai/noopTest/tree/master
プロジェクトの準備
パッケージインストール
yarn add react react-dom next@canary mobx mobx-react-lite axios yarn add -D typescript @types/react @types/react-dom @types/node babel-preset-mobx/*TODO: Next.js 8.1.1がリリースされたら@canaryを消す*/
執筆当時のpackage.json
package.json{ "dependencies": { "axios": "^0.19.0", "mobx": "^5.10.1", "mobx-react-lite": "^1.4.1", "next": "^8.1.1-canary.63", "react": "^16.8.6", "react-dom": "^16.8.6" }, "devDependencies": { "@types/node": "^12.0.10", "@types/react": "^16.8.22", "@types/react-dom": "^16.8.4", "babel-preset-mobx": "^2.0.0", "typescript": "^3.5.2" } }各種configの作成
以下のファイルを
package.json
と同階層に作成してください。.babelrc
あとあと、クラスデコレータを使うので".babelrc"を作成して、編集しておく、
preset:"next/babel"
はnext@canary
に付いてくる。.babelrc{ "presets": [ "next/babel", "mobx" ] }indexページの作成
pages/index.tsx
を作成する。pages/index.tsxexport default () => ( <h1>It Works!</h1> );tsconfig.json
"compilerOptions": {}
のメンバに"experimentalDecorators": true,
があれば良い。Next.jsのGitHubに書かれている
tsconfig.json
にこのオプションを挿した例はこれ。tsconfig.json{ "compilerOptions": { "allowJs": true, /* Allow JavaScript files to be type checked. */ "alwaysStrict": true, /* Parse in strict mode. */ "esModuleInterop": true, /* matches compilation setting */ "isolatedModules": true, /* to match webpack loader */ "jsx": "preserve", /* Preserves jsx outside of Next.js. */ "lib": ["dom", "es2017"], /* List of library files to be included in the type checking. */ "module": "esnext", /* Specifies the type of module to type check. */ "moduleResolution": "node", /* Determine how modules get resolved. */ "noEmit": true, /* Do not emit outputs. Makes sure tsc only does type checking. */ "experimentalDecorators": true, /* ここに挿した */ /* Strict Type-Checking Options, optional, but recommended. */ "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ "noUnusedLocals": true, /* Report errors on unused locals. */ "noUnusedParameters": true, /* Report errors on unused parameters. */ "strict": true /* Enable all strict type-checking options. */, "target": "esnext" /* The type checking input. */ } }今回のファイル構成
. ├── .next/ ⇦ 勝手にできる ├── node_modules/ ⇦ 勝手に色々入っている ├── pages │ └── index.tsx ⇦ さっき作った ├── store │ ├── api.ts ⇦ これから作る │ └── colorStore.ts ⇦ これから作る ├── next-env.d.ts ⇦ 勝手にできる ├── package.json ⇦ 勝手にできる ├── .babelrc ⇦ さっき作った ├── tsconfig.json ⇦ 自分で作るか、勝手にできたのをいじる └── yarn.lock ⇦ 勝手にできる開発用サーバの起動
以下のコマンドを実行すると
yarn next以下にサーバが立つ
http://localhost:3000Hexbotと接続用のAPIを作る
今回は、GitHubが公開しているのNoopsChalangeの中のHexbotを使ってみようと思います。
これは至極単純なAPIで、
GET https://api.noopschallenge.com/hexbotを叩くと
{ "colors": [ {"value": "#52a351"} ] }このような形式でランダムなRGB値が帰ってきます。
では早速、
axios
でAPIをを使ってみましょう。まずは型から入ります。
store/api.tsexport interface ColorType { "value": string | null; } export interface ColorResponse { "colors": ColorType[] } export const noopURI = "https://api.noopschallenge.com"; export const hexBotEndPoint = "/hexbot";接続するための
axios
インスタンスを作ります。store/api.tsimport axios from "axios"; //追記 //型中略 export const noopAPI = axios.create({ baseURL: noopURI, headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }, responseType: 'json' });あとはモックアダプタを作りましょう。
以下は
MockData
型とwatiTime:number
を受け取って、watiTime
ミリ秒待ってMockData
型のレスポンスをするアダプタを作る関数です。store/api.tsimport axios, {AxiosAdapter, AxiosResponse} from "axios"; //追記 //中略 export interface MockData { color: string | null; waitTime?: number; } export const mockAdapterCreator: (data: MockData) => AxiosAdapter = data => async config => { const initializer: Required<MockData> = { ...{ color: null, waitTime: 0 }, ...data }; //sleep タイマー await new Promise(resolve => setTimeout(resolve, initializer.waitTime)); const mockData: ColorResponse = { colors: [{ value: initializer.color }] }; const response: AxiosResponse = { data: mockData, status: 200, statusText: "", headers: "", config: config, }; return response; };モックAPIは
axios
のアダプタで作ります。axios
のアダプタはAxiosAdapter
型です。
AxiosAdapter
はconfig: AxiosRequestConfig
を受け取って、Promise<AxiosResponse <any>>;
を返す関数の型です。また
AxiosResponse<T>
の型定義は以下の通りです。export interface AxiosResponse<T = any> { data: T; status: number; statusText: string; headers: any; config: AxiosRequestConfig; request?: any; }これらの使い方は、次節で説明します。
非同期処理を行うMobXのstoreを作る
MobXで非同期処理を扱う方法は何点かありますが、今回は
configure({enforceActions: "observed"});
の下で、非同期処理をする方法を3点紹介します。まずは、3点の方法の共通部分である非同期処理を必要としない部分を書いてしまいます。store/colorStore.tsimport {observable, configure, flow, action, runInAction, computed} from "mobx"; import {ColorResponse, ColorType, hexBotEndPoint, mockAdapterCreator, MockData, noopAPI} from "./api"; import {AxiosAdapter, AxiosResponse} from "axios"; configure({enforceActions: "observed"}); export enum ColorTypeStatus { init = "初期値", fetching = "フェッチ中", fetched = "フェッチ完了" } export abstract class AbstractColorAPI { @observable protected _color: ColorType = {value: "null"}; @observable protected _status: ColorTypeStatus = ColorTypeStatus.init; private _adapter: AxiosAdapter | null; constructor(data?: MockData) { this._color = {value: "null"}; this._status = ColorTypeStatus.init; this._adapter = data ? mockAdapterCreator(data) : null; } @computed get color() { return this._color; } @computed get status() { return this._status; } get isMock() { return this._adapter != null; } setMock(data: MockData) { this._adapter = mockAdapterCreator(data); } unSetMock() { this._adapter = null; } protected async get(): Promise<AxiosResponse<ColorResponse>> { return this._adapter != null ? noopAPI.get(hexBotEndPoint, {adapter: this._adapter}) : noopAPI.get(hexBotEndPoint); } //ここを実装する abstract fetchColor(): void; @action.bound refetch() { this._status = ColorTypeStatus.init; this.fetchColor(); } }先ほど実装したモックアダプタは
get()
で使われています。これでaxios
インスタンスは実際のapiの返答の代わりに、MockData
を返します。では、非同期処理の
abstract fetchColor(): void;
を実装するクラスを書いていきます。runInAction(fn)を使う
内容を見ていきましょう。
まず、
MobX
はasyncを普通に書いても動きます。また、configure({enforceActions: "observed"});
がない場合ならば、以下のように書くことができます。もちろん、そのconfigure
を使わないならば、@action
すら不要です。//configure({enforceActions: "observed"});がないとき export class ColorAPI extends AbstractColorAPI { async fetchColor() { if (this._status != ColorTypeStatus.init) { throw new Error("フェッチ中か、フェッチ終わっとるわ"); } this._status = ColorTypeStatus.fetching; const response = await this.get(); this._status = ColorTypeStatus.fetched; this._color = {value: response.data.colors[0].value} }; }しかし、
configure({enforceActions: "observed"});
があるときは、上記の例に単に@action.bound
するだけでは不足です。以下のように書かなければなりません。store/colorStore.tsexport class ColorAPI1 extends AbstractColorAPI { @action.bound async fetchColor() { if (this._status != ColorTypeStatus.init) { throw new Error("フェッチ中か、フェッチ終わっとるわ"); } this._status = ColorTypeStatus.fetching; const response = await this.get(); runInAction(() => { this._status = ColorTypeStatus.fetched; this._color = {value: response.data.colors[0].value} }) }; }最初の
await
の以降で@observable
な値を触ると、たとえ@action.bound
の中にあっても、@action
の外からの変更としてMobX
から怒られてしまいます。なので、インラインでアクションとして実行する
runInAction(fn)
を使わなければなりません。runInAction(() => { /*ここでの処理はActionとみなされ実行される即座に*/ })新しく
@action
を作ってしまう二つ目のやり方を見ていきましょう。これは単純です。
store/colorStore.tsexport class ColorAPI2 extends AbstractColorAPI { @action.bound async fetchColor() { if (this.status != ColorTypeStatus.init) { throw new Error("フェッチ中か、フェッチ終わっとるわ"); } this.setStatus(ColorTypeStatus.fetching); const response = await this.get(); this.setStatus(ColorTypeStatus.fetched); this.setColor({value: response.data.colors[0].value}); }; @action.bound private setColor(color: ColorType) { this._color = color; } @action.bound private setStatus(status: ColorTypeStatus) { this._status = status; } }
await
の次に、@observable
な値を触ることを見越して、@action
を作っておけば、それで機能します。ただし、@action
を追うようなデバッグをした時に、ログが見づらくなるかもしれません。ただしそれでも、runInAction(fn)
よりはマシな気がします。flow(*fn)を使う
3つ目のやり方はジェネレータ関数を使っており、bindとか出てきていますが、やっていることはかなりシンプルです。
store/colorStore.tsexport class ColorAPI3 extends AbstractColorAPI { fetchColor = flow(function* (this: ColorAPI3) { if (this._status != ColorTypeStatus.init) { throw new Error("フェッチ中か、フェッチ終わっとるわ"); } this._status = ColorTypeStatus.fetching; const response = yield this.get(); this._status = ColorTypeStatus.fetched; this._color = {value: response.data.colors[0].value} }).bind(this); }簡単に言えば
@action.bound async fetchColor() { /*ここに処理を書く*/ }を
fetchColor = flow(function* fetchColor(this: クラス名) { /*ここに処理を書く*/ }).bind(this);に書き換え
await
をyield
に書き換えるだけのことです。ただしJavascript
由来のやたら挙動が不気味なthis
の片鱗が見え隠れして、見た目が少し気持ち悪いです。
参考: async actions & flows | MobXとにかく、この気持ち悪さはジェネレーター関数にアロー関数表記がない為であり、あきらめましょう。やっていることはシンプルです。
動かす
以上をReactで使ってみるとこうなります。
pages/index.tsximport * as React from 'react' import {Observer} from "mobx-react-lite"; import {AbstractColorAPI, ColorAPI1, ColorAPI2, ColorAPI3, ColorTypeStatus} from "../store/colorStore"; import {FC} from "react"; const api1 = new ColorAPI1(); const api2 = new ColorAPI2(); const api3 = new ColorAPI3(); const mockData1 = {color: "#000000", waitTime: 1000}; const mockData2 = {color: "#DA291C", waitTime: 2000}; const mockData3 = {color: "#E6C414", waitTime: 500}; const mockApi1 = new ColorAPI1(mockData1); const mockApi2 = new ColorAPI2(mockData2); const mockApi3 = new ColorAPI3(mockData3); const APIViewer: FC<{ controller: AbstractColorAPI; }> = props => ( <Observer>{() => <div> <p style={{ color: props.controller.color.value ? props.controller.color.value : "transparent" }} >██████ {props.controller.color.value} ██████</p> <p>{props.controller.status}</p> <button disabled={props.controller.status != ColorTypeStatus.init} onClick={props.controller.fetchColor}> おす </button> <button disabled={props.controller.status != ColorTypeStatus.fetched} onClick={props.controller.refetch}> 再取得 </button> </div> }</Observer> ); const Index = () => ( <div> <h2>モック</h2> <p>データ:{JSON.stringify(mockData1)}</p> <APIViewer controller={mockApi1}/> <p>データ:{JSON.stringify(mockData2)}</p> <APIViewer controller={mockApi2}/> <p>データ:{JSON.stringify(mockData3)}</p> <APIViewer controller={mockApi3}/> <hr/> <h2>ほんまもの</h2> <APIViewer controller={api1}/> <APIViewer controller={api2}/> <APIViewer controller={api3}/> </div> ); export default Index;まとめ
axios
のアダプターをTypescript
で型をつけながらか書くのは若干めんどい- MobXは非同期処理をサクッと描ける
- ただし、
configure({enforceActions: "observed"});
をつけて非同期処理を行おうとすると少し気持ち悪い
- 投稿日:2019-07-01T11:25:03+09:00
Hooksの解説
ReactHooks
Hooks は既にあなたが知っている React のコンセプトを置き換えるものではありません。むしろ、フックはあなたが既に知っている props、state、コンテクスト、ref、ライフサイクルといったコンセプトに対してより直接的な API を提供するものです。後でお見せするように、フックによって、これらを組み合わせるパワフルな手段も得ることができます。
React からクラスを削除する予定はありません。
公式抜粋
hooksのルール
- フックは関数のトップレベルのみで呼び出してください。ループや条件分岐やネストした関数の中でフックを呼び出さないでください。
- フックは React の関数コンポーネントの内部のみで呼び出してください。通常の JavaScript 関数内では呼び出さないでください
- カスタムフックの場合は例外
公式抜粋
個人的な意見
- ページ全体の制御をする場合は従来のクラスコンポーネントを使用するべき
- 込み入った事をしようとするとちょこちょこハマる
- 便利ではあるが多用するとわけわからない事になるので、SFC( Stateless Functional Component )とコンポーネントの、間に位置する存在
- アニメーションとかはこれのおかげで多少強くなった気がする。
useState
const [age, setAge] = useState(42); const [fruit, setFruit] = useState('banana'); const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);useEffect
類似hooks
- useLayoutEffect (コレに関してはほぼほぼ同じ)
- useMemo
- useCallback
ComponentDidMount
useEffect(() => { console.log('component did mount'); }, []);ComponentWillUnMount
useEffect(() => { return () => { console.log('component will unmount'); }; }, []);ノンブロッキングな命令であるため正確にはDidMountやWillMountとは挙動が異なる
useEffect系の解説
- 第一引数に実行したい関数を渡す
return
に記述したものは、コンポーネントが破棄される際に呼ばれる
- コネクションを明示的に切る必要のあるソケットだけの処理を外に記述できるメリットがある
- 第二引数に変更がかかったさいに変更したい関数を渡す
useEffectとuseLayoutEffectの違い
上記で解説した通りノンブロッキングな命令に対し
useLayoutEffect
は同期的に実行されるため、アニメーションなどに向いている(らしい)useMemoとは
const memoHoge = useMemo(() => { return props.hoge }[props.hoge]);
props.hoge
の値が変わると、コールバックが使用され帰り値が変更される。vueのcomputedのようなもの
useCallbackとは
const memorizedCallback = useCallback(() => { doSomething(a, b); }, [a, b]);中の値のcallbackを返却します。
const memorizedCallback = useMemo(() => { return () => { doSomething(a, b); } }, [a, b]);と同じようなもの
ハマりどころ
- useStateと一緒に使用する際下記のように記述しないと、依存ステータスが更新されない
const [count, setCount] = React.useState(0); const sampleCallback = React.useCallback( (event) => { console.log(event.target); setCount(count + 1); }, [count] // ここがないとカウントが1以上進まなくなる );useRef
ref要素を取得するための箱を提供する。
愚直にグローバルに書いても問題無いとは思うが、こちらのほうがスマートな印象がある。
const TextInputWithFocusButton = () => { const inputEl = useRef(null); const onButtonClick = useCallback((envet) => { inputEl.current.focus(); // currentでrefの参照先が取れる }, [inputEl]); return ( <> <input ref={inputEl} type="text" /> <button onClick={onButtonClick}>テキストを入力</button> </> ); }useReducer
- useStateの代替品
- 普通にReact-Reduxを簡単にしただけのもの
- Stateが氾濫した際に有用
const initialState = {count: 0}; function reducer(state, action) { switch (action.type) { case 'increment': return {count: state.count + 1}; case 'decrement': return {count: state.count - 1}; default: throw new Error(); } } function Counter() { const [state, dispatch] = useReducer(reducer, initialState); // const [state, dispatch] = useReducer(reducer, initialState, initialAction) return ( <> Count: {state.count} <button onClick={() => dispatch({type: 'increment'})}>+</button> <button onClick={() => dispatch({type: 'decrement'})}>-</button> </> ); }useContext
- ここでStoreが簡単に作れるようになる
const value = useContext(MyContext);
- React.createContext からの戻り値を受け取り、そのコンテクストの現在値をを返却する。
- コンテクストの現在値は、ツリー内でこのフックを呼んだコンポーネントの直近にある の value の値によって決定されます。
Storeとかも簡単に作れるようになる
import ReactDOM from 'react-dom'; import React, { useReducer, useContext } from 'react'; // createContextを使用する const context = React.createContext(); interface IState { count: number; user: { name: string; }; } const initialState: IState = { count: 0, user: { name: 'araki' } }; const reducer = (state: IState, action: any) => { switch (action.type) { case 'INCREMENT': return { ...state, count: state.count + 1 }; default: return state; } }; // contextへ stateとdispatchをバインドする const Provider = (props: { children: any }) => { const { children } = props; const [state, dispatch] = useReducer(reducer, initialState); return <context.Provider value={{ state, dispatch }}>{children}</context.Provider>; }; const DispatchSample = () => { const { state, dispatch } = useContext(context); return ( <div> <div>{state.count}</div> <button onClick={() => dispatch({ type: 'INCLEMENT' })}> increment </button> </div> ) } const App = () => { return ( <Provider> <DispatchSample /> </Provider> ); }; ReactDOM.render(<App />, document.querySelector('#app'));
useContext
は非常に強いなと感じましたが
ClassComponent
で従来通りのStoreを使用したい際は、Connect
のHOC
を自作すれば簡単に実現可能になります。余談
面白そうなHooks製ライブラリ
react-vim
rhysd/react-vim-wasm: Vim editor empowers your React web application
Example of react-vim-wasm
- Web上でVIMが使用できるらしい
react-use
streamich/react-use: React Hooks — ?
スター数が結構多いHookライブラリ
- かゆいところに手が届く感じの機能が多い
センサー系のHooks
- ジオロケーション
useGeolocation
- モーションセンサー
useMotion
- ネットワークの状態
useNetwork
- デバイスの画面の向きの状態
useOrientation
UI系のHooks
- オーディオの再生とコントロール
useAudio
- ビデオのコントロール
useVideo
- ビデオのフルスクリーン
useFullscreen
- UI用の待機系命令
useWait
- 音声入力
useSpeech
その他
- 非同期依存注入
useAsync
- 通信のリトライ
useRetry
- クリップボードへコピー
useCopyToClipboard
- ユーザーがページをリロードや閉じる際に警告を促す
useBeforeUnload
- bodyのスクロールをロックする(Modalとかのやつ)
useLockBodyScroll
- sessionStorageの管理
useSessionStorage
the-platform
palmerhq/the-platform: Web. Components. ?
- インタラクティブなデバイスの情報取得系命令が詰まったHooks
- 回転度?とか色々取れるみたい
useDeviceMotion
- ジャイロスコープ?の情報取得
useDeviceOrientation
- スクリプトの依存注入
useScript
※自分がパフォーアンスチューニングでやってたやつ- CSSの依存注入
useStylesheet
上記のCSS版
- 投稿日:2019-07-01T11:25:03+09:00
React Hooksの解説
ReactHooks
Hooks は既にあなたが知っている React のコンセプトを置き換えるものではありません。むしろ、フックはあなたが既に知っている props、state、コンテクスト、ref、ライフサイクルといったコンセプトに対してより直接的な API を提供するものです。後でお見せするように、フックによって、これらを組み合わせるパワフルな手段も得ることができます。
React からクラスを削除する予定はありません。
公式抜粋
hooksのルール
- フックは関数のトップレベルのみで呼び出してください。ループや条件分岐やネストした関数の中でフックを呼び出さないでください。
- フックは React の関数コンポーネントの内部のみで呼び出してください。通常の JavaScript 関数内では呼び出さないでください
- カスタムフックの場合は例外
公式抜粋
個人的な意見
- ページ全体の制御をする場合は従来のクラスコンポーネントを使用するべき
- 込み入った事をしようとするとちょこちょこハマる
- 便利ではあるが多用するとわけわからない事になるので、SFC( Stateless Functional Component )とコンポーネントの、間に位置する存在
- アニメーションとかはこれのおかげで多少強くなった気がする。
useState
const [age, setAge] = useState(42); const [fruit, setFruit] = useState('banana'); const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);useEffect
類似hooks
- useLayoutEffect (コレに関してはほぼほぼ同じ)
- useMemo
- useCallback
ComponentDidMount
useEffect(() => { console.log('component did mount'); }, []);ComponentWillUnMount
useEffect(() => { return () => { console.log('component will unmount'); }; }, []);ノンブロッキングな命令であるため正確にはDidMountやWillMountとは挙動が異なる
useEffect系の解説
- 第一引数に実行したい関数を渡す
return
に記述したものは、コンポーネントが破棄される際に呼ばれる
- コネクションを明示的に切る必要のあるソケットだけの処理を外に記述できるメリットがある
- 第二引数に変更がかかったさいに変更したい関数を渡す
useEffectとuseLayoutEffectの違い
上記で解説した通りノンブロッキングな命令に対し
useLayoutEffect
は同期的に実行されるため、アニメーションなどに向いている(らしい)useMemoとは
const memoHoge = useMemo(() => { return props.hoge }[props.hoge]);
props.hoge
の値が変わると、コールバックが使用され帰り値が変更される。vueのcomputedのようなもの
useCallbackとは
const memorizedCallback = useCallback(() => { doSomething(a, b); }, [a, b]);中の値のcallbackを返却します。
const memorizedCallback = useMemo(() => { return () => { doSomething(a, b); } }, [a, b]);と同じようなもの
ハマりどころ
- useStateと一緒に使用する際下記のように記述しないと、依存ステータスが更新されない
const [count, setCount] = React.useState(0); const sampleCallback = React.useCallback( (event) => { console.log(event.target); setCount(count + 1); }, [count] // ここがないとカウントが1以上進まなくなる );useRef
ref要素を取得するための箱を提供する。
愚直にグローバルに書いても問題無いとは思うが、こちらのほうがスマートな印象がある。
const TextInputWithFocusButton = () => { const inputEl = useRef(null); const onButtonClick = useCallback((envet) => { inputEl.current.focus(); // currentでrefの参照先が取れる }, [inputEl]); return ( <> <input ref={inputEl} type="text" /> <button onClick={onButtonClick}>テキストを入力</button> </> ); }useReducer
- useStateの代替品
- 普通にReact-Reduxを簡単にしただけのもの
- Stateが氾濫した際に有用
const initialState = {count: 0}; function reducer(state, action) { switch (action.type) { case 'increment': return {count: state.count + 1}; case 'decrement': return {count: state.count - 1}; default: throw new Error(); } } function Counter() { const [state, dispatch] = useReducer(reducer, initialState); // const [state, dispatch] = useReducer(reducer, initialState, initialAction) return ( <> Count: {state.count} <button onClick={() => dispatch({type: 'increment'})}>+</button> <button onClick={() => dispatch({type: 'decrement'})}>-</button> </> ); }useContext
- ここでStoreが簡単に作れるようになる
const value = useContext(MyContext);
- React.createContext からの戻り値を受け取り、そのコンテクストの現在値をを返却する。
- コンテクストの現在値は、ツリー内でこのフックを呼んだコンポーネントの直近にある の value の値によって決定されます。
Storeとかも簡単に作れるようになる
import ReactDOM from 'react-dom'; import React, { useReducer, useContext } from 'react'; // createContextを使用する const context = React.createContext(); interface IState { count: number; user: { name: string; }; } const initialState: IState = { count: 0, user: { name: 'araki' } }; const reducer = (state: IState, action: any) => { switch (action.type) { case 'INCREMENT': return { ...state, count: state.count + 1 }; default: return state; } }; // contextへ stateとdispatchをバインドする const Provider = (props: { children: any }) => { const { children } = props; const [state, dispatch] = useReducer(reducer, initialState); return <context.Provider value={{ state, dispatch }}>{children}</context.Provider>; }; const DispatchSample = () => { const { state, dispatch } = useContext(context); return ( <div> <div>{state.count}</div> <button onClick={() => dispatch({ type: 'INCLEMENT' })}> increment </button> </div> ) } const App = () => { return ( <Provider> <DispatchSample /> </Provider> ); }; ReactDOM.render(<App />, document.querySelector('#app'));
useContext
は非常に強いなと感じましたが
ClassComponent
で従来通りのStoreを使用したい際は、Connect
のHOC
を自作すれば簡単に実現可能になります。余談
面白そうなHooks製ライブラリ
react-vim
rhysd/react-vim-wasm: Vim editor empowers your React web application
Example of react-vim-wasm
- Web上でVIMが使用できるらしい
react-use
streamich/react-use: React Hooks — ?
スター数が結構多いHookライブラリ
- かゆいところに手が届く感じの機能が多い
センサー系のHooks
- ジオロケーション
useGeolocation
- モーションセンサー
useMotion
- ネットワークの状態
useNetwork
- デバイスの画面の向きの状態
useOrientation
UI系のHooks
- オーディオの再生とコントロール
useAudio
- ビデオのコントロール
useVideo
- ビデオのフルスクリーン
useFullscreen
- UI用の待機系命令
useWait
- 音声入力
useSpeech
その他
- 非同期依存注入
useAsync
- 通信のリトライ
useRetry
- クリップボードへコピー
useCopyToClipboard
- ユーザーがページをリロードや閉じる際に警告を促す
useBeforeUnload
- bodyのスクロールをロックする(Modalとかのやつ)
useLockBodyScroll
- sessionStorageの管理
useSessionStorage
the-platform
palmerhq/the-platform: Web. Components. ?
- インタラクティブなデバイスの情報取得系命令が詰まったHooks
- 回転度?とか色々取れるみたい
useDeviceMotion
- ジャイロスコープ?の情報取得
useDeviceOrientation
- スクリプトの依存注入
useScript
※自分がパフォーアンスチューニングでやってたやつ- CSSの依存注入
useStylesheet
上記のCSS版
- 投稿日:2019-07-01T06:56:32+09:00
Gatsby.jsでWordPressと連携。記事一覧、詳細を表示するまで
WorpdressからGatsby.jsに移行するにあったって、wordpressの記事情報の取得・表示までの連携をメモします。
以下内容はCLIにて
gatsby new
で作成したプロジェクトを基本にしています。Wordpressプラグインの追加
wrodpressとの連携は
gatsby-source-wordpress
にて行うのでパッケージをダウンロードします。$ yarn add gatsby-source-wordpress
そして
gatsby-config.js
にプラグインの設定を追記しますgatsby-config.jsplugins: [ ... { resolve: 'gatsby-source-wordpress', options: { baseUrl: 'makoto-acu.com', protocol: 'https', hostingWPCOM: false, // wordpress.comにホスティングしている場合はtrue useACF: false, // Advanced Custom Fieldsを使っている場合はtrue includedRoutes: [ "**/categories", "**/posts", "**/pages", "**/media", "**/tags", "**/taxonomies", "**/users", ], } } ]一覧ページの表示
一覧ページの表示は、とても簡単です。
pages/index.js
を以下のように修正しました。
ポイントはquery
部分で、こちらにてビルド時にGraphQLでWordpressのデータを取得しています。pages/index.jsconst IndexPage = ({data}) => ( <Layout> <SEO title="Home"/> <h1>Hello Gatsby</h1> <h1>Posts</h1> {data.allWordpressPost.edges.map(({node}) => ( <div key={node.slug}> <Link to={node.slug} css={{textDecoration: `none`}}> <h3>{node.title}</h3> </Link> <div dangerouslySetInnerHTML={{__html: node.excerpt}}/> </div> ))} </Layout> ) export default IndexPage export const query = graphql` query { allWordpressPost(sort: { fields: [date] }) { edges { node { title excerpt slug } } } } `詳細ページの表示
詳細ページの表示は少し複雑で、APIから取得したデータを元にページを動的に生成します。
まず、gatsby-node.js
に以下を記述します。
こちらのcreatePage()
関数にてAPIの取得結果から後述するtemplateを元に動的にページを生成しています。gatsby-node.jsconst path = require('path') const slash = require('slash') exports.createPages = async ({ graphql, actions }) => { const { createPage } = actions const result = await graphql(` { allWordpressPost { edges { node { id title status } } } } `) if (result.errors) { throw new Error(result.errors) } const { allWordpressPost } = result.data const postTemplate = path.resolve('./src/templates/post.js') // テンプレートのパス allWordpressPost.edges.forEach(edge => { createPage({ path: `/${edge.node.title}/`, // ページを紐付けるURL component: slash(postTemplate), // もととなるページテンプレートを指定 context: { id: edge.node.id, // templateにわたす変数 }, }) }) }続いて、生成ページの基礎となるtemplateを作成します。
templates
ディレクトリを切りpost.js
を以下内容で新たに追加します。
ポイントはGraphQL部分でgatsby-node.js
のcreatepage()
で受け取った変数(ページID)を使って問い合わせを行っている点です。
これでページごとの情報を取得して表示しています。templates/post.jsimport React, { Component } from "react" import { graphql } from "gatsby" import PropTypes from "prop-types" import Layout from "../components/layout" class PostTemplate extends Component { render() { const post = this.props.data.wordpressPost return ( <Layout> <h1 dangerouslySetInnerHTML={{ __html: post.title }} /> <div dangerouslySetInnerHTML={{ __html: post.content }} /> </Layout> ) } } PostTemplate.propTypes = { data: PropTypes.object.isRequired, edges: PropTypes.array, } export default PostTemplate export const pageQuery = graphql` query($id: String!) { wordpressPost(id: { eq: $id }) { title content } } `終わりに
以上、Gatsby.jsでWordPressと連携。記事を一覧、詳細を表示するまででした。
このように簡単にwordpressのリソースを利用できるので、wordpressからの移行にgatsby.jsはとてもオススメだと思います。
色々開発進める中でのTipsを別途纏めていきたいと思います。おまけ
拙著ですがIntelliJでGraphQL扱うなら、このPluginがオススメです。
エンドポイントからschema.jsを自動生成してくれて、流れるようにQuery書けます。
intelliJでgraphQLを書くときに便利なプラグイン
- 投稿日:2019-07-01T02:54:24+09:00
なる早でTypescript + Next + MobX + mobx-react-lite でグリグリ動く「買い物リスト」みたいなのを作る
はじめに
この記事ではTypescript(.tsx)でNext.js@canaryのサーバーを立て、MobXの動くものをちょいと作ります。
- これはとりあえず動くものを素早く作るためのチュートリアルです
- Typescriptを手っ取り早く設定するために、執筆時点ではNext.jsのcanaryバージョンを使います
想定読者
以下のいずれか
- TypescriptとReactが読める
- これを読んだ人:なる早でTypescriptで(勉強用の)Next.jsサーバーを立ち上げる
今日作るもの
できたやつ: https://github.com/NanimonoDemonai/okaimono/tree/master
プロジェクト準備
パッケージインストール
yarn add react react-dom next@canary mobx mobx-react-lite uuid yarn add -D typescript @types/react @types/react-dom @types/node @types/uuid babel-preset-mobx/*TODO: Next.js 8.1.1がリリースされたら@canaryを消す*/
執筆当時のpackage.json
package.json{ "dependencies": { "mobx": "^5.10.1", "mobx-react-lite": "^1.4.1", "next": "^8.1.1-canary.63", "react": "^16.8.6", "react-dom": "^16.8.6", "uuid": "^3.3.2" }, "devDependencies": { "@types/node": "^12.0.10", "@types/react": "^16.8.22", "@types/react-dom": "^16.8.4", "@types/uuid": "^3.4.4", "babel-preset-mobx": "^2.0.0", "typescript": "^3.5.2" } }各種configの作成
以下のファイルを
package.json
と同階層に作成してください。.babelrc
あとあと、クラスデコレータを使うので".babelrc"を作成して、編集しておく、
preset:"next/babel"
はnext@canary
に付いてくる。.babelrc{ "presets": [ "next/babel", "mobx" ] }indexページの作成
pages/index.tsx
を作成する。pages/index.tsxexport default () => ( <h1>It Works!</h1> );tsconfig.json
"compilerOptions": {}
のメンバに"experimentalDecorators": true,
があれば良い。
tsconfig.json
を作成せずに、yarn next
で開発サーバーを起動することで作成されるデフォルトのtsconfig.json
にこのオプションを挿すか、tsconfig.json
ファイルを作りNext.jsのGitHubに書かれているtsconfig.json
にこのオプションを挿す。tsconfig.json{ "compilerOptions": { "allowJs": true, /* Allow JavaScript files to be type checked. */ "alwaysStrict": true, /* Parse in strict mode. */ "esModuleInterop": true, /* matches compilation setting */ "isolatedModules": true, /* to match webpack loader */ "jsx": "preserve", /* Preserves jsx outside of Next.js. */ "lib": ["dom", "es2017"], /* List of library files to be included in the type checking. */ "module": "esnext", /* Specifies the type of module to type check. */ "moduleResolution": "node", /* Determine how modules get resolved. */ "noEmit": true, /* Do not emit outputs. Makes sure tsc only does type checking. */ "experimentalDecorators": true, /* ここに挿した */ /* Strict Type-Checking Options, optional, but recommended. */ "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ "noUnusedLocals": true, /* Report errors on unused locals. */ "noUnusedParameters": true, /* Report errors on unused parameters. */ "strict": true /* Enable all strict type-checking options. */, "target": "esnext" /* The type checking input. */ } }
執筆当時のデフォルトの
tsconfig.json
tsconfig.json{ "compilerOptions": { "target": "es5", "lib": [ "dom", "dom.iterable", "esnext" ], "allowJs": true, "skipLibCheck": true, "strict": true, "forceConsistentCasingInFileNames": true, "noEmit": true, "esModuleInterop": true, "module": "esnext", "moduleResolution": "node", "resolveJsonModule": true, "isolatedModules": true, "jsx": "preserve" }, "exclude": [ "node_modules" ], "include": [ "next-env.d.ts", "**/*.ts", "**/*.tsx" ] }これに
"experimentalDecorators": true,
を挿すとこうなるtsconfig.json{ "compilerOptions": { "target": "es5", "lib": [ "dom", "dom.iterable", "esnext" ], "allowJs": true, "skipLibCheck": true, "strict": true, "forceConsistentCasingInFileNames": true, "noEmit": true, "esModuleInterop": true, "module": "esnext", "moduleResolution": "node", "resolveJsonModule": true, "isolatedModules": true, "jsx": "preserve", "experimentalDecorators": true }, "exclude": [ "node_modules" ], "include": [ "next-env.d.ts", "**/*.ts", "**/*.tsx" ] }開発用サーバの起動
以下のコマンドを実行すると
yarn next以下にサーバが立つ
http://localhost:3000品物のViewとStoreを作る
storeを作る
まずは、データを取り扱うstoreから手をつけます。
store/Item.ts
を新規作成しましょう。IDEで補完をよしなにしてもらうためにまずは型から書きます。
store/Item.tsexport interface ItemData { readonly name: string; //商品名 readonly price: number; //値段 } export interface ItemModel extends ItemData { readonly uuid: string; //map用のuuid readonly count: number; //個数 readonly fullPrice: number;//合計 readonly decrementable: boolean;//マイナスボタンが押せるか? } export const defaultItemData: ItemData = { name: "ダミー", price: 50 };そして実際に操作するclassを書いていきましょう。
store/Item.tsimport {action, computed, observable, configure} from "mobx"; import uuid from "uuid"; configure({enforceActions: "observed"}); //中略 export class ItemController implements ItemModel { readonly name: string; readonly price: number; readonly uuid: string; @observable private _count: number; constructor(data?: Partial<ItemData>) { const initializer: ItemData = {...defaultItemData, ...data} this.name = initializer.name; this.price = initializer.price; //uuid作成 this.uuid = uuid.v4(); this._count = 0; } @computed get count(): number { return this._count; } @computed get fullPrice(): number { return this._count * this.price; } @computed get decrementable(): boolean { return this._count > 0; } @action.bound increment() { this._count++; } @action.bound decrement() { if (this.decrementable) this._count--; } }上から順に説明します、
configure({enforceActions: "observed"});
はobservable
な値をaction
の外で変更できないようにするための厳し目モードにするための設定です。このクラスは
@observable
な値である_count
を操作するためのクラスです。
@computed
は_count
によって計算される値であり、_count
の変化に応じて、値が更新されます。@action.bound
は@observable
な値を更新する関数です。@action
だとthis
がbind
されないので面白くないです。storeをとりあえず使ってみる
pages/itemTestimport {ItemController} from "../store/Item"; import {Observer} from "mobx-react-lite"; const itemController1 = new ItemController(); const itemController2 = new ItemController({ name: "金塊", price: 50000 }); export default () => ( <> <div> <p>品名:{itemController1.name}</p> <p>値段:{itemController1.price}</p> <p>UUID:{itemController1.uuid}</p> <p>個数:{itemController1.count}</p> <p>総額:{itemController1.fullPrice}</p> <button onClick={()=>{ itemController1.increment(); }} > + </button> <button onClick={()=>{ itemController1.decrement(); }} disabled={!itemController1.decrementable} > - </button> </div> <div> <p>品名:{itemController1.name}</p> <p>値段:{itemController1.price}</p> <p>UUID:{itemController1.uuid}</p> <Observer>{() => <p>個数:{itemController1.count}</p> }</Observer> <Observer>{() => <p>総額:{itemController1.fullPrice}</p> }</Observer> <button onClick={()=>{ itemController1.increment(); }} > + </button> <Observer>{() => <button onClick={() => { itemController1.decrement(); }} disabled={!itemController1.decrementable} > - </button> }</Observer> </div> <div> <p>品名:{itemController2.name}</p> <p>値段:{itemController2.price}</p> <p>UUID:{itemController2.uuid}</p> <Observer>{() => <p>個数:{itemController2.count}</p> }</Observer> <Observer>{() => <p>総額:{itemController2.fullPrice}</p> }</Observer> <button onClick={()=>{ itemController2.increment(); }} > + </button> <Observer>{() => <button onClick={() => { itemController2.decrement(); }} disabled={!itemController2.decrementable} > - </button> }</Observer> </div> </> )http://localhost:3000/itemTest
これがMobXとmobx-react-liteの威力です。
<Observer>{() =>
~}</Observer>
で囲った要素は@observable
および、@computed
な値の変化があれば適切に更新されます。一通り動きがわかったところで、見て呉れを良くしましょう。
viewを作る
component/Item.tsximport {FC} from "react"; import {ItemController, ItemData} from "../store/Item"; import {Observer} from "mobx-react-lite"; export const ItemDescription: FC<ItemData> = props => ( <div> <p>商品名:{props.name}</p> <p>価格:{props.price}</p> </div> ); export interface ItemCountProps { count: number; fullPrice: number; } export const ItemCount: FC<ItemCountProps> = props => ( <div> <span className={"count"}>個数:{props.count}</span> <span>合計金額:<b>{props.fullPrice}</b></span> { /*language=CSS*/} <style jsx>{` .count { padding-right: 2em; } `}</style> </div> ); export const Item: FC<{ controller: ItemController; }> = props => ( <div className={"item"}> <ItemDescription name={props.controller.name} price={props.controller.price}/> <hr/> <Observer>{() => <ItemCount count={props.controller.count} fullPrice={props.controller.fullPrice}/> }</Observer> <button onClick={ () => { props.controller.increment() } }> + </button> <Observer>{() => <button onClick={ () => { props.controller.decrement() } } disabled={!props.controller.decrementable} > - </button> }</Observer> {/* language=CSS*/} <style jsx>{` .item { border: double; padding: 1em; margin: 1em; } `}</style> </div> );pages/itemViewTestimport {ItemController} from "../store/Item"; import {Item} from "../components/Item"; const itemController = new ItemController({ name: "金塊", price: 50000 }); export default () => ( <> <Item controller={itemController}/> </> )http://localhost:3000/itemViewTest
品物のリストを作る
storeを作る
store/ItemList.tsimport {ItemController, ItemData} from "./Item"; import {action, computed, configure, observable, IObservableArray} from "mobx"; configure({enforceActions: "observed"}); export interface ItemListModel { readonly items: ReadonlyArray<ItemController>; readonly fullPrice: number; } export class ItemListController implements ItemListModel { private readonly _items: IObservableArray<ItemController>; constructor(items?: ItemController[] = []) { this._items = observable(items); } @computed get items(): ReadonlyArray<ItemController> { return this._items; } @computed get fullPrice() { return this._items.reduce((acc, cur) => acc + cur.fullPrice, 0) } @action.bound addItem(data: Partial<ItemData>) { this._items.push(new ItemController(data)); } @action.bound removeChildren(child: ItemController) { this._items.remove(child); } }
IObservableArray
はobservable(array)
で作ることができる、observableな配列です。名前の通りですね。IObservableArray
はそれ自体がobservableなので、@observable
をつけなくても動きます。なお、つけても動きます。
参考: MobX公式ドキュメント:Arrayこの
IObservableArray
は通常のArray
に加えていくつかの関数が生えており、特段に便利なのは、remove(value)
です。これはIObservableArray
の子要素を渡せば、その子要素を消す関数です。とりあえずstoreを使ってみる
itemListTest.tsximport {ItemListController} from "../store/ItemList"; import {Observer} from "mobx-react-lite"; import {Item} from "../components/Item"; const list = new ItemListController(); list.addItem({ name: "金塊", price: 50000 }); list.addItem({ name: "蟹", price: 300000 }); let dummy = 0; export default () => ( <> <Observer>{() => <> {list.items.map(e => <Item controller={e} key={e.uuid}/>)} </> }</Observer> <Observer>{() => <p>{list.fullPrice}</p> }</Observer> <button onClick={()=>{ list.addItem({ name: `ダミーくん${dummy}号`, price: 3 }); dummy++; }} > ダミーを増やす </button> </> )ほぼほぼ完成ですが、一応これもコンポーネントにしましょう。
viewを作る
components/ItemList.tsximport {Observer} from "mobx-react-lite"; import {ItemListController} from "../store/ItemList"; import {Item} from "./Item"; import {createContext, FC} from "react"; export const ItemListControllerContext = createContext<ItemListController | null>(null); export const ItemList: FC<{ controller: ItemListController; }> = props => ( <div> <ItemListControllerContext.Provider value={props.controller}> <Observer>{() => <div> {props.controller.items.map(e => <Item controller={e} key={e.name}/>)} </div> }</Observer> <hr/> <p>合計金額総和: <Observer>{() => <span className={"goukei"}>{props.controller.fullPrice}</span> }</Observer> </p> {/* language=CSS*/} <style jsx>{` .goukei { padding-left: 1em; color: red; } `}</style> </ItemListControllerContext.Provider> </div> );工夫した点は、React純正の
Provider
を使って、ItemList
以下のモジュールでItemListController
を使えるようにしたことです。これを使ってみましょう。
参考: useContextのしくみ - QiitaProviderとuseContextを使った、Removerを作ってみる
component/Remover.tsximport {FC, useContext} from "react"; import {ItemController} from "../store/Item"; import {ItemListControllerContext} from "./ItemList"; export const Remover: FC<{ controller: ItemController; }> = props => { const list = useContext(ItemListControllerContext); return ( <> {list != null && <button onClick={() => { list.removeChildren(props.controller) }}> 削除 </button> } </> ) };
Provider
とuseContext
を使って削除ボタンを作ることができました。これを、Item.tsx
に挿しておきましょう。また、これはProvideがないときには表示されません。参考: 条件付きレンダー - React #論理 && 演算子によるインライン If
Item.tsximport {FC} from "react"; import {ItemController, ItemData} from "../store/Item"; import {Observer} from "mobx-react-lite"; import {Remover} from "./Remover"; //追加 //中略 export const Item: FC<{ controller: ItemController; }> = props => ( <div className={"item"}> <ItemDescription name={props.controller.name} price={props.controller.price}/> <hr/> <Observer>{() => <ItemCount count={props.controller.count} fullPrice={props.controller.fullPrice}/> }</Observer> <button onClick={ () => { props.controller.increment() } }> + </button> <Observer>{() => <button onClick={ () => { props.controller.decrement() } } disabled={!props.controller.decrementable} > - </button> }</Observer> <Remover controller={props.controller}/> {/* ここに挿した*/} {/* language=CSS*/} <style jsx>{` .item { border: double; padding: 1em; margin: 1em; } `}</style> </div> );動作確認用に適当に使ってみます。
pages/itemListViewTest.tsximport {ItemListController} from "../store/ItemList"; import {ItemList} from "../components/ItemList"; const list = new ItemListController(); list.addItem({ name: "金塊", price: 50000 }); list.addItem({ name: "蟹", price: 300000 }); export default () => ( <ItemList controller={list}/> );http://localhost:3000/itemListViewTest
品物追加コンポーネントを作る
これでラストです。
storeを作る
store/ItemAdder.tsimport {action, computed, observable,configure} from "mobx"; import {ItemListController} from "./ItemList"; configure({enforceActions: "observed"}); export interface ItemAdderModel { readonly name: string; readonly price: string; } export class ItemAdderController implements ItemAdderModel{ @observable private _name: string; @observable private _price: string; private _list: ItemListController; constructor(list: ItemListController) { this._name = ""; this._price = "0"; this._list = list; } @computed get name(){ return this._name; } @computed get price(){ return this._price; } @computed get isNumberError(): boolean { return this._price == "" || isNaN(Number(this._price)); } @computed get isAddable(): boolean { return !this.isNumberError && this._name.length != 0; } @action.bound onNameChange(text: string) { this._name = text; } @action.bound onNumberChange(text: string) { this._price = text; } @action.bound onAdd() { if (this.isAddable) { this._list.addItem({ name: this._name, price: Number(this._price) }); this._name = ""; this._price = "0"; } } }viewを作る
components/ItemAdder.tsximport {FC} from "react"; import {Observer} from "mobx-react-lite"; import {ItemAdderController} from "../store/ItemAdder"; export const ItemAdder: FC<{ controller: ItemAdderController }> = props => ( <> <Observer>{() => <p>商品名:<input type="text" onChange={ event1 => { props.controller.onNameChange(event1.target.value) } } value={props.controller.name} /></p> }</Observer> <Observer>{() => <p>値段:<input type="number" onChange={ event1 => { props.controller.onNumberChange(event1.target.value) } } value={props.controller.price} /> {props.controller.isNumberError && <span style={{color: "red"}}>入力エラー(数値を入力してください)</span> } </p> }</Observer> <Observer>{() => <button disabled={!props.controller.isAddable} onClick={props.controller.onAdd} > 作成 </button> }</Observer> </> );ポイントは、valueもonChangeも全部MobX任せにすることです。
完成させる
indexに今まで作ったものをまとめましょう。
pages/index.tsximport {ItemListController} from "../store/ItemList"; import {ItemList} from "../components/ItemList"; import {ItemAdderController} from "../store/ItemAdder"; import {ItemAdder} from "../components/ItemAdder"; const list = new ItemListController(); list.addItem({ name: "金塊", price: 50000 }); list.addItem({ name: "蟹", price: 300000 }); const adder = new ItemAdderController(list); export default () => ( <> <ItemAdder controller={adder}/> <ItemList controller={list}/> </> );これで冒頭に作ったものが動いているはずです。
まとめ
- MobXとmobx-react-liteを使えばチャチャっとお買い物リストみたいなアプリを作れる
- MobXのデコレータを使えるようにするには、少し設定がいる
@observable
や@computed
の変化は<Observer>{() =>
~}</Observer>
で囲めばよしなに更新されるconfigure({enforceActions: "observed"});
を入れておくと、@observable
の変更を@action
を通さずにすると怒られが発生して嬉しい