20190701のReactに関する記事は10件です。

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.js
import 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.js
import 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()

実行画面

エラー表示

image.png

今回は以上です。

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

【備忘録】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.js

package.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.

起動後、localhost:5000 にアクセスする。
スクリーンショット 2019-07-01 16.26.03.png

次は、ビルドが実行できるかの確認をする。以下のコマンドを実行する。

$ 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というファイルが作成される。
スクリーンショット 2019-07-01 16.32.16.png

本番環境用コマンドの動作も確認。
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

以上で確認完了。

作成したボイラープレートは こちら

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

はじめてのreact

作業ログ

$ npx create-react-app my-app
$ cd my-app
$ npm start

Chromeブラウザが立ち上がって地球のまわりに衛生が回る軌道みたいなページが表示されたら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...

道のりは長い

参考

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

[勉強用] 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タブを確認してください。

image.png

このように、ローディング中にURLを変更すると、canceledとなっていることを確認できると思います。

おわりに

本コードは勉強用ですので、そのままでは使わないでください。(ちゃんとした実装はこちら)
さらなる課題と解決は次のステップへ。

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

【ReactNative+Redux】ActionとReducerの記述を改善?した話

はじめに

Typescript+ReactNative+Reduxを勉強中なのですが、
下記のような記述が個人的にはすごく気持ち悪い。

HogeAction.ts
export 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.ts
import {
  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.ts
export 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.ts
import 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.ts
import {
  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;
  }
}

これで少しはモヤっとする書き方をせずに済むかも。
もっと良い書き方があれば教えていただければ幸いです。

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

なる早でTypescript + Next + MobX + mobx-react-lite で非同期処理をサクッと扱う

はじめに

この記事ではTypescript(.tsx)でNext.js@canaryのサーバーを立て、MobXの動くものをちょいと作ります。

また、axiosでモックアダプターを作る方法を少し説明します。

そして、GitHubのNoopsChalangeのAPIに接続します。
スクリーンショット 2019-07-01 10.48.47.png

  • これはとりあえず動くものを素早く作るためのチュートリアルです
  • Typescriptを手っ取り早く設定するために、執筆時点ではNext.jsのcanaryカナリヤバージョンを使います

想定読者

以下のいずれか

今日作るもの

画面収録 2019-07-01 12.06.54.gif

できたやつ: 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.tsx
export 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:3000

Hexbotと接続用のAPIを作る

今回は、GitHubが公開しているのNoopsChalangeの中のHexbotを使ってみようと思います。
スクリーンショット 2019-07-01 10.49.00.png

これは至極単純なAPIで、

GET https://api.noopschallenge.com/hexbot

を叩くと

{
  "colors": [
    {"value": "#52a351"}
  ]
}

このような形式でランダムなRGB値が帰ってきます。

では早速、axiosでAPIをを使ってみましょう。

まずは型から入ります。

store/api.ts
export interface ColorType {
    "value": string | null;
}

export interface ColorResponse {
    "colors": ColorType[]
}

export const noopURI = "https://api.noopschallenge.com";
export const hexBotEndPoint = "/hexbot";

接続するためのaxiosインスタンスを作ります。

store/api.ts
import 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.ts
import 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型です。

AxiosAdapterconfig: 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.ts
import {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.ts
export 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.ts
export 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.ts
export 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);

に書き換えawaityieldに書き換えるだけのことです。ただしJavascript由来のやたら挙動が不気味なthisの片鱗が見え隠れして、見た目が少し気持ち悪いです。
参考: async actions & flows | MobX

とにかく、この気持ち悪さはジェネレーター関数にアロー関数表記がない為であり、あきらめましょう。やっていることはシンプルです。

動かす

以上をReactで使ってみるとこうなります。

pages/index.tsx
import * 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;

http://localhost:3000

画面収録 2019-07-01 12.06.54.gif

まとめ

  • axiosのアダプターをTypescriptで型をつけながらか書くのは若干めんどい
  • MobXは非同期処理をサクッと描ける
  • ただし、configure({enforceActions: "observed"});をつけて非同期処理を行おうとすると少し気持ち悪い
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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を使用したい際は、 ConnectHOC を自作すれば簡単に実現可能になります。

余談

面白そうな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版
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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を使用したい際は、 ConnectHOC を自作すれば簡単に実現可能になります。

余談

面白そうな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版
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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.js
plugins: [
...
  {
      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.js
const 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.js
const 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.jscreatepage()で受け取った変数(ページID)を使って問い合わせを行っている点です。
これでページごとの情報を取得して表示しています。

templates/post.js
import 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を書くときに便利なプラグイン

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

なる早でTypescript + Next + MobX + mobx-react-lite でグリグリ動く「買い物リスト」みたいなのを作る

はじめに

この記事ではTypescript(.tsx)でNext.js@canaryのサーバーを立て、MobXの動くものをちょいと作ります。

  • これはとりあえず動くものを素早く作るためのチュートリアルです
  • Typescriptを手っ取り早く設定するために、執筆時点ではNext.jsのcanaryカナリヤバージョンを使います

想定読者

以下のいずれか

筆者の環境
  • OS: macOS Mojave 10.14.5
  • ブラウザ: Safari バージョン12.1.1
  • Node.js: v10.16.0
  • Yarn: 1.15.2

今日作るもの

画面収録 2019-06-30 22.26.11.gif

できたやつ: 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.tsx
export 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を作る

まずはこれを作りましょう
ww.gif

storeを作る

まずは、データを取り扱うstoreから手をつけます。store/Item.tsを新規作成しましょう。

IDEで補完をよしなにしてもらうためにまずは型から書きます。

store/Item.ts
export 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.ts
import {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だとthisbindされないので面白くないです。

storeをとりあえず使ってみる

pages/itemTest
import {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
画面収録 2019-07-01 0.30.33.gif

これがMobXとmobx-react-liteの威力です。<Observer>{() =>~}</Observer>で囲った要素は@observableおよび、@computedな値の変化があれば適切に更新されます。

一通り動きがわかったところで、見て呉れを良くしましょう。

viewを作る

component/Item.tsx
import {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/itemViewTest
import {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
スクリーンショット 2019-07-01 0.48.00.png

品物のリストを作る

storeを作る

store/ItemList.ts
import {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);
    }
}

IObservableArrayobservable(array)で作ることができる、observableな配列です。名前の通りですね。IObservableArrayはそれ自体がobservableなので、@observableをつけなくても動きます。なお、つけても動きます。
参考: MobX公式ドキュメント:Array

このIObservableArrayは通常のArrayに加えていくつかの関数が生えており、特段に便利なのは、remove(value)です。これはIObservableArrayの子要素を渡せば、その子要素を消す関数です。

とりあえずstoreを使ってみる

itemListTest.tsx
import {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>
    </>
)

2qvh4-81l0o.gif

ほぼほぼ完成ですが、一応これもコンポーネントにしましょう。

viewを作る

components/ItemList.tsx
import {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のしくみ - Qiita

ProviderとuseContextを使った、Removerを作ってみる

component/Remover.tsx
import {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>
            }
        </>
    )
};

ProvideruseContextを使って削除ボタンを作ることができました。これを、Item.tsxに挿しておきましょう。また、これはProvideがないときには表示されません。

参考: 条件付きレンダー - React #論理 && 演算子によるインライン If

Item.tsx
import {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.tsx
import {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
c80e2-ypyoj.gif

品物追加コンポーネントを作る

これでラストです。

storeを作る

store/ItemAdder.ts
import {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.tsx
import {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.tsx
import {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}/>
    </>
);

http://localhost:3000

これで冒頭に作ったものが動いているはずです。

まとめ

  • MobXとmobx-react-liteを使えばチャチャっとお買い物リストみたいなアプリを作れる
  • MobXのデコレータを使えるようにするには、少し設定がいる
  • @observable@computedの変化は<Observer>{() =>~}</Observer>で囲めばよしなに更新される
  • configure({enforceActions: "observed"});を入れておくと、@observableの変更を@actionを通さずにすると怒られが発生して嬉しい

続き見たいみたいなのを書きました

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