20200119のReactに関する記事は12件です。

Rails+WebpackerにVue.jsとReactの両方を入れる

環境:Rails 6.0、Webpacker 4.2、Vue.js 2.6、React 16.12

Webpackerを使ってVue.jsとReactの両方を動かすことに成功したので、メモしておきます。

コピー元の作成

まず、設定ファイルのコピー元とするだけのアプリケーションを作成します。すでにVueを入れたアプリケーションがある場合は、Reactで作ります。

% rails new reactapp --webpack=react

Reactを入れたアプリケーションがある場合は、Vueを指定します。

% rails new vueapp --webpack=vue

yarn add

Vueで作ってあるアプリケーションには、Reactのモジュールをインストールします。prop-typesはWebpackerがデフォルトで入れるものですが、必須ではありません。

% yarn add @babel/preset-react babel-plugin-transform-react-remove-prop-types prop-types react react-dom

Reactで作ってある場合は、Vueのモジュールをインストールします。

% yarn add vue vue-loader vue-template-compiler vue-turbolinks

babel.config.js

ルートにあるbabel.config.jsは、Reactで生成したものを使います。つまり、すでにReactならそのままにし、VueならReact用のbabel.config.jsを上書きします。

config/webpacker.yml

config/webpacker.ymlは、元からあるものを使い、Reactを加える場合は.jsxを、Vueを加える場合は.vueを追加します。

config/webpacker.yml
  extensions:
    - .vue
    - .jsx

config/webpacker/

ReactにVueを加える場合は、Vueで生成したものからconfig/webpacker/loaders/vue.jsをコピーします。

また、config/webpacker/environment.jsはVueのもので上書きします。

両方動かしてみる

次のような感じで両方を動かすapplication.jsを書いて動けば成功です。

app/javascript/packs/application.js
require("@rails/ujs").start();
require("turbolinks").start();

import React from 'react';
import ReactDOM from 'react-dom';

import Vue from 'vue';
import TurbolinksAdapter from 'vue-turbolinks';

import VueApp from '../vueapp';
import ReactApp from '../reactapp';

Vue.use(TurbolinksAdapter);

document.addEventListener('turbolinks:load', () => {
  if($('#vue-app').length) {
    new Vue(VueApp).$mount('#vue-app');
  }

  if($('#react-app').length) {
    ReactDOM.render(React.createElement(ReactApp), $('#react-app')[0]);
  }
});

実際にこんなアプリケーションを作ることはないと思いますが、現実的な使い方として考えられるのは、Railsアプリケーションの中でVueを使う部分とReactを使う部分を分けるケースです。その場合は、app/javascript/packsの下にVue用とReact用のxxx.jsを作り、レイアウトテンプレートを複数作ってjavascript_pack_tagを切り替える、ということになるでしょう。

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

【SharePoint Framework】Failed to load component エラー対処

はじめに

開発したWebパーツをSharePoint ページに展開した所、以下の画像のようなエラーが発生しました。
error.png

解決策

以下のコマンドを実行する事で解決しました。

gulp clean
gulp build
gulp bundle --ship
gulp package-solution --ship

参考

SPFx | Failed to load component.
https://www.koskila.net/failed-to-load-component-original-error-failed-to-load-path-dependency-contosospfxwebpartlocalization-from-component-guid-contosospfxwebpart/

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

カンマ区切りで複数ワードをsubmitできるテキストフィールド[React, Material-UI]

やりたいこと

Material-UIベースで、テキストを入力し、Tabを押したらChipでテキストを登録して、複数ワードを配列でsubmitできるテキストフィールドが欲しい。

↓こんなの

Jan-19-2020 18-33-28.gif

サンプル

codesandboxにサンプルを上げたので触ってみてください

Edit creatable-chip-input-comma-separatable

ロジック

materual-ui-chip-inputという、ベストなライブラリがあったので使っています。
やっていることは、これにpropsを色々詰めているだけですが、カンマ区切りのところだけイベントハンドラで制御しています。

条件は、

  • カンマで区切られた場合は複数を一度に入力
  • カンマはじまり、カンマおわりで空の配列を登録してしまわないようにする
  • 重複は登録しない

といった感じです。
カンマがなければ普通に入力されます。

こちらがイベントハンドラです。

  // Enable comma separation, Do not allow duplicates.
  handleAddKeywords = (...chips) => {
    const separetedChips = chips.shift().split(",");
    const combinedChips = [...this.state.keywords, ...separetedChips];
    const newKeywords = combinedChips.filter(
      (v, i, self) => [...self, ...this.state.keywords].indexOf(v) === i && v
    );
    this.setState({ keywords: [...newKeywords] });
  };
  1. 入力されたワードをカンマで区切って
  2. 既に入力されている配列と結合して
  3. 重複チェック & 空チェック
  4. stateにset

という流れです。
spread operatorが多くて少しわかりにくいですが、割と簡単に実装できました。

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

ReactでのJest + Enzyme導入

はじめに

本記事は、Udemyの2019 Update! React Testing with Jest and Enzymeという講座を聴講した内容をまとめたものです。自身のメモと、同じ初心者の方のJest導入のつかみになればと思いまとめました。

著者もプログラミング自体始めて2ヶ月程度の初心者のため、間違いや不適切な表現などがありましたらぜひぜひコメント欄にてお知らせください。

Enzyme導入

Enzyme概要について、以下、Enzyme公式docより引用。

Enzyme is a JavaScript Testing utility for React that makes it easier to test your React Components' output. You can also manipulate, traverse, and in some ways simulate runtime given the output.
Enzyme's API is meant to be intuitive and flexible by mimicking jQuery's API for DOM manipulation and traversal.

Enzymeをセットアップ

CRA (create-react-app)にはEnzymeがないので別途インストールが必要。

必要なパッケージをインストール。

npm install —save-dev enzyme jest-enzyme enzyme-adapter-react-16

※enzyme-adapter-react-[version]とする

パッケージをインポート

インストールしたパッケージをインポートし、Enzymeインスタンスのconfigureメソッドを用いて設定を行う。

テストを実行するファイルのファイル名は、[テスト対象コンポーネント名].test.jsとする。
import ReactDOM from 'react-dom'は不要なので削除する。

App.test.js
import Enzyme, { shallow } from 'enzyme';
import EnzymeAdapter from 'enzyme-adapter-react-[version]';

Enzyme.configure( { adapter: new EnzymeAdapter() });

実際にテストする

テストするコンポーネント

App.js
import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';

class App extends Component {
    render() {
        return (
            <div className="App">
                <h1>Hello World</h1>
            </div>
        );
    }
}

export default App;

テストを実行するファイル

App.test.js
test('renders without crashing', () => {
    const wrapper = shallow(<App />);
    expect(wrapper).toBeTruthy();
});

shallow()

引数に渡したコンポーネントのみテストを行う。コンポーネント内の子コンポーネントはプレースホルダーとして扱われて実際にはレンダーされないため、子コンポーネントを干渉させずに純粋に単一コンポーネントをテストできる。shallowは「浅い」の意。

wrapper

Enzymeに標準搭載されているAPIで、レンダーされたコンポーネントを格納することで、多くのメソッドを使うことができる。
例えば、ShallowWrapperインスタンスのメソッドの一つであるdebug()は、レンダーしたコンポーネントをHTMLライクなStringとして返す。上記の例のようにAppコンポーネントを格納した状態でconsole.log(wrapper.debug());とすると、以下のような結果がString(文字列)として返ってくる。

<div className="App">
    <h1>
        Hello World
    </h1>
</div>

各メソッドについて、詳しくは公式docのShallow Rendering APIを参照。

expect()

Jestの標準搭載メソッドで、テストしたいコンポーネントの様々な値をテストすることができる。例えば、上記例のtoBeTruthy()ではコンポーネントの値には興味がなく、trueを返すかどうかをテストする。JavaScriptでは、false, 0, '', null, undefined,NaNがfalse値として扱われるので、それ以外であればテストが通ることになる。

こういったメソッドを使い分けながら様々な値をテストしていく流れ。
各メソッドの詳細は公式docのExpectを参照。

さいごに

まだUdemyの動画を見終わっていないので、全て見たら改めて追加情報をまとめようと思います。

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

reactアプリをnginxを通してhttpsで公開する

確認環境

OS: Amazon Linux release 2 (Karoo)

nginxのインストール

Amazon Linux 2 では標準で Nginx の YUM 向けパッケージが提供されていないため、extraリポジトリからインストールします。

$ sudo amazon-linux-extras install nginx1.12

バージョンの確認

$ nginx -v
nginx version: nginx/1.12.2

nginxの起動

$ sudo service nginx start

nginxへのSSL設定

/etc/nginx/conf.dに、以下のファイルを配置します。

  • confファイル(nginx設定ファイル)
  • crtファイル(サーバー証明書ファイル)
  • csrファイル(公開鍵ファイル)
  • keyファイル(秘密鍵ファイル)

オレオレ証明書の作成

$ cd ~ 
$ openssl genrsa 2048 > server.key
$ openssl req -new -key server.key > server.csr
$ openssl x509 -days 3650 -req -signkey server.key < server.csr > server.crt
$ sudo mv server.* /etc/nginx/conf.d/
$ cd /etc/nginx/conf.d
$ sudo chown root:root server.*

server.scr作成時の答えは全てenterで回答しました。

設定ファイルの追加

設定ファイルを追加します。ここでは、my_ssl_appという名前にしました。
ちなみに、/etc/nginx/conf.d/*.confは、/etc/nginx/nginx.confから呼び出されます。ワイルドカード(*.conf)で呼び出されるため、名前は何でも構いません。

$ cd /etc/nginx/conf.d
$ touch my_ssl_app.conf

my_ssl_app.confを以下のように編集します。

server {
    listen       443 ssl;
    server_name  localhost;
    ssl_certificate      /etc/nginx/conf.d/server.crt;
    ssl_certificate_key  /etc/nginx/conf.d/server.key;
    location / {
        proxy_pass http://127.0.0.1:3000;
    }
}

nginxの再起動

sudo nginx -s reload

?ブラウザで確認

ブラウザからhttps接続して確認すると、502 Bad Gateway のエラーページが表示されます。これは、reactアプリが起動していないためです。
bad_gateway.png

reactアプリの作成

nodejsのインストール(インストール済なら不要)

$ curl --silent --location https://rpm.nodesource.com/setup_10.x | sudo bash -
$ sudo yum -y install nodejs

reactアプリの作成

$ npx create-react-app my_app

アプリの起動

$ cd my_app
$ npm start

※最新(2020/1/7時点)のreact-scriptでssl接続時に問題が発生しているようです。react-scriptが3.3.0だった場合、リンク先の対応(react-scriptの3.2.0へのバージョンダウン)を行ってください。

?ブラウザで確認

ブラウザからhttps接続して確認すると、以下のページが表示されます。
hello_react.png
以上。

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

【React】useReducer をもっと自由に活用しよう

useReducer をもっと自由に

皆さん、useReducer は活用していますか?
useState で十分と思っている場合でも useReducer に置き換えることで、コードがシンプルかつ、わかりやすくなります。

そのためには、Reduxの呪縛を解き払ってください。useReducerに Action type は必要ないですし、Flux Standard Action も必要ありません。また、今回はdispatchdispatchらしい使い方はしていません。
つまり、Redux と同じ使い方をする必要はありません。

?‍♀️別にActionType必須ではない
const [state, dispatch] = useReducer((state, action) => {
  switch(action.type) {
    case 'FOO_ACTION':
      return { ...state, foo: action.foo }
    case 'BAR_ACTION':
...

useReducer 活用例

では、useReducer を使用すると、どのように変わるのでしょうか。簡単なサンプルコードをもとに紹介します。

例)テキストフィールド
const { value, onChange } = useInput()

return (
  <input type="text" value={value} onChange={onChange} />
)

上記はなんの変哲もないテキストフィールドです。
これに合うカスタム Hooks (useInput) を、useStateuseReducer でそれぞれ作成します。

useState の場合

useStateを使用した場合
export const useInput = () => {
  const [value, setValue] = useState('')

  const onChange = useCallback((event) => {
    setValue(event.currentTarget.value)
  }, [])

  return { value, onChange }
}

useState を使用した場合、Hooks は2つ使用します。今回の用途では useCallback はオーバーキル感がありますが、他コンポーネントや他 Hooks で使用する可能性がありますので、メモ化しておくのが良いでしょう。
いずれにしても、onChange 関数を作成するには setValue をラップします。

useReducer の場合

useReducerを使用した場合
const inputAction = (state, event) =>
  event.currentTarget.value

export const useInput = () => {
  const [value, onChange] = useReducer(inputAction, '')

  return { value, onChange }
}

useState を使用した場合よりも、シンプルになりました。
useReducer で記述するメリットは下記の2つです。

  • 状態とそれを更新する関数が、ワンセットになる
  • reducer 部分は純粋関数であり、Hooks やコンポーネントの外に出せる

1つ目について、setValue のような中間の Setter が生まれませんし、用途に合わせる関数(onChange)を別途作る必要もありません。
また、2つ目の関数外部化は、テストがしやすくなるだけでなく、無駄なオブジェクトを生成しないというパフォーマンス面のメリットもあります。

まとめ

useReducer を使用する場合、Redux 等の使い方に縛られる必要はありません。
単純な state でも useReducer に置き換えることで、コードがシンプルかつ、わかりやすくなり、テスタビリティがあがってパフォーマンスも上がります!

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

useReducerの本質:良いパフォーマンスのためのロジックとコンポーネント設計

React Hooksの正式リリース(2019年2月)からそろそろ一年が経とうとしています。Hooksの登場によってReactのコンポーネントは関数コンポーネントが一気に主流になり、クラスコンポーネントが新規に作られる機会は激減しました。

また、React 17.x系ではConcurrent Modeの導入とともにさらに2種類の新フックが追加される見込みであり、いよいよ関数コンポーネントの能力がクラスコンポーネントを真に上回る時代が来ることになります。

この記事では、フックの一種であるuseReducerに焦点を当てて、どのようなときにuseReducerが適しているのかを説明します。究極的には、useReducerによって達成できるパフォーマンス改善があり、ときにはそれがコンポーネント設計にまで影響を与えることを指摘します。

useStateの影に隠れたり、なぜかReduxと比較されたりといまいちぱっとしないuseReducerですが、この記事でその真の魅力を知っていただければ幸いです。

まとめ

  • useReducerは、ステートに依存するロジックをステートに非依存な関数オブジェクト(dispatch)で表現することができる点が本質である。
  • このことはReact.memoによるパフォーマンス改善につながる。
  • useReducerを活かすには、ステートを一つにまとめることで、ロジックをなるべくreducerに詰め込む。

背景: useReducerとは

まずは、初心者の方向けにuseReducerの動作を説明します。すでに知っているという方は次の節まで飛ばしても構いません。

useReducerはフックの一種であり、関数コンポーネントのステートを宣言する能力を持ちます。ステートの宣言はuseStateuseReducerの2種類の方法がありますが、useReducerは複雑なロジックが絡んだステートを宣言するのに適しています。

useReducerは以下のように使います。こちらが用意するのはreducerinitialStateの2つです。reducerは「現在のステート」と「アクション」を受け取って「新しいステート」を返す関数であり、initialStateはステートの初期値です。

const [currentState, dispatch] = useReducer(reducer, initialState);

useReducerの返り値は2つで、currentStateはステートの現在の値、dispatchはアクションを発火する関数です。dispatchにアクションを渡すと、内部でreducerが呼び出されて新しいステートが計算され、コンポーネントが再レンダリングされて新しいステートが反映されます。

一応簡単な例を示しておきます。まずはreducerの例です。分かりやすさのためにTypeScriptを用いています。

type State = {
  count: number
};

type Action = {
  type: "increment" | "decrement";
};

const reducer = (state: State, action: Action): State => {
  if (action.type === "increment") {
    return {
      count: state.count + 1
    };
  } else {
    return {
      count: state.count - 1
    }
  }
};

ここではアクションは{ type: "increment" }または{ type: "decrement" }です。見て分かる通り、これはそれぞれ「カウンタを1増やす」操作と「カウンタを1減らす」操作に相当します。このreducerによって管理されるState{ count: number }です。つまり、カウンタの数値をひとつ持っているだけのオブジェクトです。この場合type State = numberでも別に構いませんが、今後の拡張性を考えてこの定義にしています。

これはconst [state, dispatch] = useReducer(reducer, { count: 0 })のように使用します。このdispatchを用いて、dispatch({ type: "increment" })とすればステートが変化してカウンタの値が1増えるでしょう。

これがuseReducerの使い方です。useReducerは、ステートの種類が増えたりロジックが増えたりしてもその操作の窓口がdispatchという一点に集約されている点がポイントです。子コンポーネントが何かしらのロジックを発火したいときはdispatchをpropsで渡すだけでいいし、コンポーネントツリーが大きい場合はコンテキストを用いて子に伝えるのも有効でしょう。

useReducerがパフォーマンス改善につながる例

Reactアプリのパフォーマンス改善において大きな効果が出やすいのはReact.memoの活用です(クラスコンポーネント時代のshouldComponentUpdatePureComponentに相当)。これを活用してコンポーネントの余計な再レンダリングを避けることが、Reactアプリの基本的なパフォーマンス・チューニングです。

この例では、useReducerReact.memoの利用の助けになる例を示し、丁寧に解説します。

初期状態のサンプル

まず、改善前の初期状態を見てみましょう。以下のCodeSandboxで実際に動作を確かめることができます。初期状態のコードはApp1.tsxに入っています。

今回の題材はこの画像のようなものです。

2fdbb139875c810db99b763f8a9f7c33.png

4つの入力欄があり、それぞれに数値を入力することができます。下には4つの数値を合計した値が表示されます。また、入力欄の横にある「check」ボタンを押すと、そのときの数値が合計の何%かを一番下に表示します。画像は「123」の横のボタンを押したあとの状態です。

一見意味不明な例に見えますが、これは実は筆者が実際に業務で経験した例をかなり単純化したものになっています。

この記事にも初期状態のコードを一気に貼り付けます。記事を読みつつコードを見たいという方は適宜CodeSandboxをご活用ください。記事中でも部分ごとに解説していきますから、ここで全部読む必要はありません。

src/App1.tsx
import React, { useState } from "react";
import { sum } from "./util";
import "./styles.css";

const NumberInput: React.FC<{
  value: string;
  onChange: (value: string) => void;
  onCheck: () => void;
}> = ({ value, onChange, onCheck }) => {
  return (
    <p>
      <input
        type="number"
        value={value}
        onChange={e => onChange(e.currentTarget.value)}
      />
      <button onClick={onCheck}>check</button>
    </p>
  );
};

export default function App1() {
  const [values, setValues] = useState(["0", "0", "0", "0"]);
  const [message, setMessage] = useState("");
  return (
    <div className="App">
      {values.map((value, i) => {
        return (
          <NumberInput
            key={i}
            value={value}
            onChange={v =>
              setValues(current => {
                const result = [...current];
                result[i] = v;
                return result;
              })
            }
            onCheck={() => {
              const total = sum(values);
              const ratio = Number(value) / total;
              setMessage(
                `${value}は${total}の${(ratio * 100).toFixed(1)}%です`
              );
            }}
          />
        );
      })}
      <p>合計は{sum(values)}</p>
      <p>{message}</p>
    </div>
  );
}

コードの解説

上記のサンプルのコードを少しずつ解説します。

まず、ひとつの入力欄とボタンのセットが、以下に抜粋するNumberInputコンポーネントで表現されています。入力状態は親のApp1コンポーネントが持つvaluesステートに保存されており、NumberInput自体はステートを持っていません。現在の値はvalueとしてpropsを通じて渡されています。これは、「合計を表示する」といったロジックが親コンポーネントにあることから来る必然的な選択です。

src/App1.tsx(抜粋)
const NumberInput: React.FC<{
  value: string;
  onChange: (value: string) => void;
  onCheck: () => void;
}> = ({ value, onChange, onCheck }) => {
  return (
    <p>
      <input
        type="number"
        value={value}
        onChange={e => onChange(e.currentTarget.value)}
      />
      <button onClick={onCheck}>check</button>
    </p>
  );
};

親コンポーネントであるAppは2つの状態を持ちます。以下に示すvaluesmessageです。

src/App1.tsx(抜粋)
  const [values, setValues] = useState(["0", "0", "0", "0"]);
  const [message, setMessage] = useState("");

valuesは4つの入力欄の内容が配列で入っています。messageは「check」ボタンを押したときに表示されるメッセージを管理するステートです。数値の入力が想定されていますが、ステートを数値にしてしまうとちょっと扱いづらいフォームになるので生の入力状態は文字列で持っています。あるあるですね。

ステートの更新部分はNumberInputのpropsに渡す関数にベタ書きです。onChangeが呼び出されたら、setValuesを呼び出してi番目の値がvに書き換えた新しい配列を用意してステートを更新します。onCheckも同様に、メッセージを組み立ててsetMessageを呼び出します。

src/App1.tsx(抜粋)
onChange={v =>
  setValues(current => {
    const result = [...current];
    result[i] = v;
    return result;
  })
}
onCheck={() => {
  const total = sum(values);
  const ratio = Number(value) / total;
  setMessage(
    `${value}${total}${(ratio * 100).toFixed(1)}%です`
  );
}}

以上のコードの問題点は、レンダリングのパフォーマンス最適化が何も考えられていないことです。ひとつの数値が変更されるたびに全てのNumberInputに再レンダリングが発生してしまいます。

今回のゴールは、NumberInputReact.memoを適用して無駄な再レンダリングを減らすことです。特に、ひとつの数値が変更されたらそのNumberInputだけが再レンダリングされて、他のNumberInputは再レンダリングされないという状態が理想です。

お察しの通り、最終的にはuseReducerを用いてこれを達成することになります。

React.memo導入への努力

とりあえず、まずはuseStateのまま努力してみましょう。NumberInputReact.memoを適用して効果を得るためには、他の入力値が変わってもpropsの内容が変化しないようにしなければいけません。現状ではvalueは問題ありませんが、onChangeonCheckが問題です。あの位置に関数をベタ書きということは、これらのpropsには毎回異なる関数オブジェクトが作られて渡されています。これではReact.memoは効きません。

こういうときの定石はuseCallbackです。とはいえ、今回はループでNumberInputを表示しているのでひと工夫必要です。筋のいい方法としては、NumberInputに「自分が何番目か」を表すpropsを渡すという方法があります1。これをコールバックに渡してもらうことで、onChangeonCheckは全てのNumberInputからのコールバックをひとつの関数で対応できます。

以上の工夫を導入して得られたのが、上記のCodeSandboxでいうApp2.tsxです。全体像を見たいからはCodeSandboxをご覧ください。

ここでは部分ごとに変更点を見ていきます。まずNumberInputです。

src/App2.tsx(抜粋)
const NumberInput: React.FC<{
  value: string;
  index: number;
  onChange: (index: number, value: string) => void;
  onCheck: (index: number) => void;
}> = memo(({ value, index, onChange, onCheck }) => {
  return (
    <p>
      <input
        type="number"
        value={value}
        onChange={e => onChange(index, e.currentTarget.value)}
      />
      <button onClick={() => onCheck(index)}>check</button>
    </p>
  );
});

NumberInputはpropsとしてindexを受け取るようになりました。これが、自身が何番目かを表す数値です。onChangeonCheckの型も変更され、これらの関数にはindexがオウム返しで渡されるようになっています。先ほども説明した通り、これによりonChangeonCheckを各NumberInputごとに異なる関数を用意する必要が無くなります。

次に、Appの変更点を見ます。まずレンダリング部分だけ抜粋すると、こうなりました。

src/App2.tsx(抜粋)
  return (
    <div className="App">
      {values.map((value, i) => {
        return (
          <NumberInput
            key={i}
            index={i}
            value={value}
            onChange={onChange}
            onCheck={onCheck}
          />
        );
      })}
      <p>合計は{sum(values)}</p>
      <p>{message}</p>
    </div>
  );

NumberInputに渡すpropsにindexが追加されているのに加え、onChangeonCheckが事前に用意されるようになりました。次に、これらを用意する部分のコードです。

src/App2.tsx(抜粋)
export default function App() {
  const [values, setValues] = useState(["0", "0", "0", "0"]);
  const [message, setMessage] = useState("");

  const onChange = useCallback((index: number, value: string) => {
    setValues(values => {
      const newValues = [...values];
      newValues[index] = value;
      return newValues;
    });
  }, []);
  const onCheck = useCallback(
    (index: number) => {
      const total = sum(values);
      const ratio = Number(values[index]) / total;
      setMessage(
        `${values[index]}${total}${(ratio * 100).toFixed(1)}%です`
      );
    },
    [values]
  );

  return /* 省略 */
}

onChangeonCheckuseCallbackに囲まれています。それぞれの関数の中身は、indexを引数で受け取るようになった以外は変わりません。

できることは全部やったように見えますが、残念ながらこのコードはまだ目的を達成できていませんonChangeuseCallbackにより常に同じ関数オブジェクトになっているのでOKですが、onCheckが問題です。

onCheckuseCallbackの第二引数が[values]となっています。これは、valuesが変わるたびに、すなわち何か入力が変わるたびに、onCheckが作りなおされるということを意味します。これによりNumberInputに渡されるonCheck関数が毎回別物になるため、React.memoが無意味になっています。

では、なぜuseCallbackの第二引数がvaluesを含んでいなければいけないのでしょうか。それはもちろん、onCheckvaluesに依存しているからです。つまり、onCheckが中で「入力値の合計」を求めるためにvaluesを使用しているのです。onCheckのインターフェースが(index: number) => voidである、すなわちindexのみを受け取るという関数である以上、valuesというデータについてはonCheckに内包されていなければいけません。これにより、必然的にvaluesが変わるたびにonCheckという関数は別物になります。

一方で、onChangevaluesに依存していません。これは、useStateが提供するステート更新関数が、関数によるステートの更新をサポートしているからです。上のコードではsetValues関数の引数として「現在の状態を受け取って次の状態を返す関数」を渡しています。この機能により、onChangeからvaluesへの依存を消しているのです。

となると、onCheckmessageというステートを更新するにあたって、それとは別のvaluesというステートに依存していることが問題だと分かります。これを解消するためには、2つのステートを合体させて1つのステートにする必要があります。

このような状況に適しているのがuseReducerです。ということで、AppuseReducerを用いて書き換えることで問題を解決しましょう。(一応、useStateを使っていても2つのステートをまとめて問題を解決することはできますが、その状況でわざわざuseReducerではなくuseStateを使う意味は薄いのでここでは考えません。)

useReducerによる解決

ということで、最終版です。全体像は以下のCodeSandboxのApp3.tsxでご覧ください。

まず、useReducerを使うのでreducerを用意しましょう。今回何気なくTypeScriptを使っているので型定義もちゃんとあります。

src/App3.tsx(抜粋)
type State = {
  values: string[];
  message: string;
};

type Action =
  | {
      type: "input";
      index: number;
      value: string;
    }
  | {
      type: "check";
      index: number;
    };

const reducer = (state: State, action: Action) => {
  switch (action.type) {
    case "input": {
      const newValues = [...state.values];
      newValues[action.index] = action.value;
      return {
        ...state,
        values: newValues
      };
    }
    case "check": {
      const total = sum(state.values);
      const ratio = Number(state.values[action.index]) / total;
      return {
        ...state,
        message: `${state.values[action.index]}${total}${(
          ratio * 100
        ).toFixed(1)}%です`
      };
    }
  }
};

型定義を読むと、Statevaluesmessageをひとつにまとめたオブジェクトであることが分かります。アクションは"input""check"の2種類があり、それぞれ前回のコードのonChangeonCheckに相当するロジックが書かれています。

次にNumberInputのコードです。

src/App3.tsx(抜粋)
const NumberInput: React.FC<{
  value: string;
  index: number;
  dispatch: Dispatch<Action>;
}> = memo(({ value, index, dispatch }) => {
  return (
    <p>
      <input
        type="number"
        value={value}
        onChange={e =>
          dispatch({
            type: "input",
            index,
            value: e.currentTarget.value
          })
        }
      />
      <button
        onClick={() =>
          dispatch({
            type: "check",
            index
          })
        }
      >
        check
      </button>
    </p>
  );
});

propsとして受け取るのはvalue, index, dispatchになりました。従来のonChangeonCheckがひとつにまとまっていますね。それ以外は特に変わっていません。

最後にAppコンポーネントのコードです。ロジックがreducerの中に移ったのでこちらはシンプルになりました。

src/App3.tsx(抜粋)
export default function App() {
  const [{ values, message }, dispatch] = useReducer(reducer, {
    values: ["0", "0", "0", "0"],
    message: ""
  });

  return (
    <div className="App">
      {values.map((value, i) => {
        return (
          <NumberInput key={i} index={i} value={value} dispatch={dispatch} />
        );
      })}
      <p>合計は{sum(values)}</p>
      <p>{message}</p>
    </div>
  );
}

ステートの宣言はuseReducerにより行われています。従来onChangeonCheckが担っていたロジックはreducerの中に押し込められましたので、ここでは何もせずにただNumberInputdispatchを渡すだけになっています。

前のコードと比べると、ここに本質的なポイントがあります。それは2つのステートがひとつのuseReducerに押し込められたことにより、「valuesを見てmessageを決める」という計算が「今のステートから次のステートを計算する」という枠組み(reducer)の中に入ったことです。よって、それを呼び出す側であるdispatchステートに非依存の関数となりました。NumberInputのpropsはindex, value, dispatchだけとなり、自分以外の値が変わっても再レンダリングされることは無くなりました。これで目標達成です。

ポイントの整理

改めてポイントを整理すると、今回の最も重要だったことは「ステートの更新関数をステートに非依存にする」ということでした。useReducerの場合は、更新関数(dispatch)が非依存であることが保証されています。従来のコード(2番目の例)ではonCheckという関数がステート(values)に依存している関数だったのでうまくいきませんでした。

ステートの更新関数をステートに非依存にするには、「現在のステートを受け取って次のステートを計算する」ということを徹底する必要がありました。useStateの場合はステート更新関数に関数を渡すのを徹底することになります。つまりsetValues(newValues)ではなくsetValues(currentValues => {...; return newValues })とするということです。従来のコードではonChangeではこれができていましたが、onCheckではできていませんでした。

これを改善するために今回行なったことは「2つのステート(valuesmessage)を1つに合体させる」ということです。これにより、onCheckでも関数によるステート更新ができるようになりました。実を言えばuseStateでも頑張ればこれは達成できますが、このような複雑なステートを扱うにはuseReducerが適しているのでここではuseReducerを選択することになります。useReducerを使う場合はステート更新関数(dispatch)は自動的にステートに非依存になります(reducerはそもそも「現在のステートを受け取って次のステートを計算する」というものであるため)。

useReducerのすすめ

このように、useReducerを用いることで、ステート更新関数をステート非依存にすることを強制できます。実際のアプリ開発においては、アプリが複雑化するにつれて、あるステートと別のステートが関わりを持ち始めるかもしれません。もっと具体的に言えば、あるステートを更新するときに別のステートを見る必要が発生するかもしれません。そのときがuseReducer導入のサインです。ぜひリファクタリングしてuseReducerを導入しましょう。

なぜuseReducerが必要なのか、この記事を読んだ皆さんはしっかりと説明できることでしょう。ステート更新関数がステートに非依存であることは、React.memoの活用には必須だからです。

また、useReducerReact.memoの恩恵を最大限受けるためには、できるだけreducerにロジックを詰め込むことが鍵となります。そのためには、アプリの状態は何でもステートで表現することが重要です。言い換えれば、これは手続き的なロジックを書かず、状態は明示的・宣言的に扱うということです。

また、useReducerを活かすためにはそのためのコンポーネント設計も重要です。今回の例では多少天下り的でしたが、NumberInputindexをpropsで受け取るようにしたという点にこれが表れています。dispatchを呼び出して自分のvalueを更新するためには自分が何番目かをdispatchに教える必要があるからです("input"アクションがindexを含んでいたことを思い出しましょう)。

副作用はどうするのか? あとReduxの話

ところで、今回の例では「check」ボタンを押すと起こることが「別のステートが更新される」でした。なので、useReducerによってステートをひとつにまとめることで、onCheckコールバックをステートに非依存にすることができたのでした。

では、もし「check」ボタンを押すと起こることが何らかの副作用だったらどうするのでしょうか。例えば、押すとHTTPリクエストが発生するとかです。現時点では、副作用はreducerの中に書くべきではないという原則がありますから、この記事で使った手を使うことはできません。

残念なことに、現時点では対処法はありません。副作用をどこかのコールバック関数に書いた時点で、その関数がステートに依存することとなり、React.memoによるパフォーマンス改善の妨げになります。

実は、これに対する一つの解がReduxの使用です。Reduxを用いたステート管理の場合、Reduxミドルウェアの活用によって、ステートに依存する副作用ですらdispatchの中に押し込めてステート非依存性を達成できてしまうのです。Reduxの本質はReactのツリーの外でステートを管理してくれることであり、それによりReact本体のみでは困難なステート非依存性が実現しているのです。Reduxはただステート管理に関する統一的な方法論を与えるだけでなく、このようなパフォーマンス上のメリットもあるということは覚えておいて損はないでしょう。

React 17.x 系の展望

しかし、React 17.x系(いわゆるConcurrent Modeが導入されると期待されています)ではまた情勢が変わると筆者は期待しています。端的に言えば、Concurrent Modeにおいては(主に非同期的な)副作用ですらステート内で管理されるようになるでしょう。そのための道具がSuspenseです。詳細はそのうち別の記事でお届けしようと思いますが、Concurrent Modeでは副作用とステート管理の概念が大きく様変わりし、Reduxなどに頼らずともパフォーマンス的に最適な副作用の扱いが達成できる場面が増えると予期されます。

useRefに関する注意

ところで、「コールバック関数がステートに依存するのが問題」ということであれば、useRefで解決できると思った方も多いでしょう。実際、以下のようにすればonCheckvaluesに非依存にすることができます。

src/App4_useRef.tsx(抜粋)
  const [values, setValues] = useState(["0", "0", "0", "0"]);
  const [message, setMessage] = useState("");

  const valuesRef = useRef<string[]>([]);
  valuesRef.current = values;

  const onCheck = useCallback((index: number) => {
    const values = valuesRef.current;
    const total = sum(values);
    const ratio = Number(values[index]) / total;
    setMessage(`${values[index]}${total}${(ratio * 100).toFixed(1)}%です`);
  }, []);

この例ではvaluesの値はつねにvaluesRef.currentに反映され、onCheckvaluesを参照するかわりにvaluesRef.currentを参照するようにしています。useRefが返すvaluesRefは常に同じオブジェクトであることが保証されていますから、onCheckvaluesRefに依存することはありません。この方法でもReact.memoを活用するという目的は達成できています。

しかし、筆者はこの方法はお勧めしません。なぜなら、このようにuseRefを使うのはReact 17.x系でうまく動作しなくなる可能性があるからです。Concurrent Modeにおいては、refへの書き込みはもはや副作用と見なされます。関数コンポーネントの処理中にこのようにrefへの書き込みを行うのは思わぬ動作を引き起こす可能性があるのです(特にレンダリングが中断される場合)。

このことは実はReactの公式ドキュメントにも明記されています。「将来的にはより使いやすい代替手段を提供することを計画しています」とありますので、React 17.x系ではよりよい別の手段が提供されるかもしれません。

まとめ

この記事では、コールバック関数がステートに依存する場合に、React.memoの恩恵を受けられないという問題に対してuseReducerを用いて対処する方法を示しました。ポイントはステート更新関数をステート非依存にすることであり、(useStateでもそれは可能なものの)useReducerはそのような書き方に適しています。

記事冒頭のまとめを再掲しておきます。

  • useReducerは、ステートに依存するロジックをステートに非依存な関数オブジェクト(dispatch)で表現することができる点が本質である。
  • このことはReact.memoによるパフォーマンス改善につながる。
  • useReducerを活かすには、ステートを一つにまとめることで、ロジックをなるべくreducerに詰め込む。

useReducerはreducerを用いてステート更新を記述できるものでしたが、reducerの存在価値は単にReduxと同じ書き方ができるというだけではありません。useReducerはこの記事で説明したような本質的な問題を解決するための優れた道具なのです。

useStateに比べると使い方がややこしいので尻込みしてしまうかもしれませんが、useStateを多く並べれば並べるほど、いざ必要になったときのリファクタリングが難しくなります。時期を見極めてuseReducerを導入しましょう。


  1. ややアクロバットな別解として、useMemoを用いて各NumberInput用のコールバックを用意するというものもあります。 

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

Reactでの条件分岐 4つの方法のメモ

はじめに

Reactでの条件付きレンダリングの方法のメモです。

目次

  1. returnするJSXを分岐
  2. 変数にJSXを格納
  3. もう少し短く!!
  4. これで最後!!
  5. まとめ

1. returnするJSXを分岐

stateを条件に、判定してJSXをそのままreturnしてあげる方式

import React, { Component } from 'react'

export default class Greeting extends Component {

    constructor(props) {
        super(props);
        this.state = {
            isMorning: true
        };
    }
    render() {
        if (this.state.isMorning) {

            return <h2> Good Morning Tom</h2>

        } else {

            return <h2> Hye ! Tom </h2>

        }

    }
}

2. 変数にJSXを格納

変数にJSXを格納することで、returnの箇所が1つになりました。

import React, { Component } from 'react'

export default class Greeting extends Component {

    constructor(props) {
        super(props);
        this.state = {
            isMorning: false
        };
    }
    render() {
        let message

        if (this.state.isMorning) {

            message = <h2> Good Morning Tom</h2>

        } else {

            message = <h2> Hye ! Tom </h2>

        }

        return <div>{message}</div>

    }
}

3. もう少し短く!!

もう少し短く書けます
条件式 ? (trueの時) : (falseの時)

import React, { Component } from 'react'

export default class Greeting extends Component {

    constructor(props) {
        super(props);
        this.state = {
            isMorning: true
        };
    }
    render() {
        return (this.state.isMorning ?<h2> Good Morning Tom</h2> :<h2> Hye ! Tom </h2>)
    }
}

4.これで最後!!

falseの時に、何も表示させない時とかは
条件式 && (trueの時)
みたいに書ける

条件式が評価されて、trueなら次に行くので
条件式がfalseなら次に行かずに何も返さない

import React, { Component } from 'react'

export default class Greeting extends Component {

    constructor(props) {
        super(props);
        this.state = {
            isMorning: true
        };
    }
    render() {
        return (this.state.isMorning && <h2> Good Morning Tom</h2>)
    }
}

5. まとめ

4つのパターンを見ましたが、最後の2つがシンプルかなと思います。
ありがとうございました。

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

[React-router-dom]使い方

npmでパッケージ管理している前提です

#パッケージのインストール

$ npm install --save react-router-dom

BrowserRouter,Route,Switchをインポートします。
path指定し、そのpathに当てはまるコンポーネントを指定することで、Reactでのroutingを実現しています。

import React from 'react';
import ReactDOM from 'react-dom';
import {
    BrowserRouter,
    Route,
    Switch,
  } from 'react-router-dom';
import Home from './pages/Home'
import Example from './pages/Example'
import PostEdit from './pages/PostEdit';

function App() {

    return (
        <div>
            <Switch>
                <Route path='/' exact component={Home} />
                <Route path='/posts' exact component={Home} />
                <Route path='/example' exact component={Example} />
                <Route path='/post/edit/:id' exact component={PostEdit} />
            </Switch>
        </div>
    );
}


ReactDOM.render((
    <BrowserRouter>
      <App />
    </BrowserRouter>
  ), document.getElementById('root'))


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

型で強化された、ReactのContext

// Aaa.ts

import produce, { Draft } from "immer";
import { createAction, createReducer, ActionType } from "typesafe-actions";

export const initialState: DeepReadonly<RootState> = {
  cnt: 0,
  gomi: "stateがあっさりしすぎるので追加",
  foo: {
    bar: 123
  }
};

type State = typeof initialState;

type P = {
  foo: number;
};

export const actions = {
  increment: createAction("increment")(),
  decrement: createAction("decrement")(),
  setX: createAction("setX")<P>()
};

export type Actions = ActionType<typeof actions>;

// immer 使うとこんな感じ
const reducers = {
  increment: produce((state: Draft<State>) => {
    state.cnt++;
    // ネスト深くても良い
    state.foo.bar = 999;
  }),
  setX: produce(
    (state: Draft<State>, action: ReturnType<typeof actions.setX>) => {
      state.cnt = action.payload.foo;
    }
  )
};

export const reducer = createReducer<State, Actions>(initialState)
  .handleAction(actions.increment, reducers.increment)
  .handleAction(actions.decrement, s => ({ ...s, cnt: s.cnt - 1 }))
  .handleAction(actions.setX, reducers.setX);

// ------------------------------------------------------------------------
import { createReducer as createReducer2 } from "react-use";
import logger from "redux-logger";

// https://github.com/streamich/react-use/issues/856
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// @ts-ignore
export const useReducer2 = createReducer2<State, Actions>(logger);
// Bbb.tsx

import React from "react";
import {
  initialState,
  reducer,
  useReducer2,
  Actions
} from "./Aaa";
import { Dispatch, createContext } from "react";

export type CreateContext = {
  state: typeof initialState;
  dispatch: Dispatch<Actions>;
};

export const Context = createContext<CreateContext>({
  state: initialState,
  dispatch: () => undefined
});

type ContextProviderType = (Component: React.ComponentType) => React.FC;

export const ContextProvider: ContextProviderType = Component => props => {
  // const [state, dispatch] = React.useReducer(reducer, initialState);
  const [state, dispatch] = useReducer2(reducer, initialState);
  return (
    <Context.Provider value={{ state, dispatch }}>
      <Component {...props} />
    </Context.Provider>
  );
};
import { useContext } from "react";
import { Context } from "./Bbb";
import { actions } from "./Aaa";

const foo = 5;
export const useOperations = (dispatch = useContext(Context).dispatch) => ({
  onIncrement: () => dispatch(actions.increment()),
  onDecrement: () => dispatch(actions.decrement()),
  set5: () => dispatch(actions.setX({ foo }))
});
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

TruffleBox (React&Truffle)を用いたDockerでのdapps(ブロックチェーンアプリ)の開発環境の構築

やったこと

  • Docker上でTrufflebox(React&Truffle)を用いたdappsの開発環境を構築した。
  • ホスト側のIPアドレスを設定することで、Dockerのコンテナからホストで立ち上がっているganacheに接続できるようにした。

今回の成果

231us-n7dpv.gif

環境構築手順

各種ファイルの用意

まずは、docker-compose.ymlとDockerfileを用意します。
スクリーンショット 2020-01-19 11.03.12.png

docker-compose.yml

docker-compose.yml
version: '3'

services:
  truffle:
    build: 
      context: ./truffle/
      dockerfile: Dockerfile
    volumes:
      - ./truffle:/usr/src/app
    command: sh -c "cd client && yarn start"
    ports:
      - "8003:3000"

DockerFile

Dockerfile
FROM node:8-alpine  

RUN apk add --update alpine-sdk
RUN apk add --no-cache git python g++ make \
    && npm i -g --unsafe-perm=true --allow-root truffle 

WORKDIR /usr/src/app

コマンドの実行

$ docker-compose build
$ docker-compose run truffle truffle unbox react
✔ cleaning up temporary files
✔ Setting up box

truffle-config.jsの編集

truffle-config.js
const path = require("path");

module.exports = {
  // See <http://truffleframework.com/docs/advanced/configuration>
  // to customize your Truffle configuration!
  networks: {
    development: {
      host: "10.200.10.1",
      port: 7545,
      network_id: "*" // Match any network id
    }
  },
  contracts_build_directory: path.join(__dirname, "client/src/contracts")
};

ホストOS側でのIPアドレスの設定

ganacheを使って、ローカル開発環境上でEthereumのブロックチェーンを構築するのですが、
Mac(ホストOS)上で立ち上がってるganacheにDockerのコンテナ上のtruffleからアクセスできるようにするために、Mac側のIPアドレスを独自に設定します。

MacのTerminalで
$ sudo ifconfig lo0 alias 10.200.10.1/24
$ ifconfig
lo0:
    inet 127.0.0.1 netmask 0xff000000 
        ...
    inet 10.200.10.1 netmask 0xffffff00 

これにより、10.200.10.1でDockerからホストOSへアクセスできるようになった。
参考記事: https://qiita.com/ynii/items/262d2344b9e1ef4d2d88

netmaskとganacheの設定

参考記事に従って、設定してください。
参考記事: https://qiita.com/kane-hiro/items/b1381cc1c8dd5559a9d2

ganacheの設定で、一つだけ追加の設定が必要です。
上で設定したIPアドレス:10.200.10.1でサーバーを立ててほしいので、その設定を行います。
設定 -> server -> lo0:10.200.10.1に変更
スクリーンショット 2020-01-19 12.36.37.pngスクリーンショット 2020-01-19 12.36.51.png

truffle-config.jsonの設定

truffleが参照するganacheサーバーの情報(IPとポート)について、記述していきます。

truffle-config.js
const path = require("path");

module.exports = {
  // See <http://truffleframework.com/docs/advanced/configuration>
  // to customize your Truffle configuration!
  networks: {
    development: {
      host: "10.200.10.1",
      port: 7545,
      network_id: "*" // Match any network id
    }
  },
  contracts_build_directory: path.join(__dirname, "client/src/contracts")
};

migrateとフロントの立ち上げ

$ docker-compose run truffle truffle migrate
$ docker-compose up

ハマったポイント

DockerFileの記述はかなりハマりました。

npm install truffleのパーミッションエラーを回避する

$ npm i -g truffle
...
EACCES: permission denied, open '/root/.config/truffle/config.json

何も考えずにnpm i -g truffleすると、エラーを吐くと思います。
これは、

  • Dockerでnpm installするときは、rootでインストールを始めようとする
  • --unsafe-permオプションをtrueにしないとrootでnpm installができないようになっている?(rootでのインストールが推奨されていないから???)

参考記事: https://qiita.com/village_21/items/8ed91270271261752c8a

gitとかpythonを入れましょう。

何もいれないと、truffle unbox reactでエラーを吐きます。

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

【React】おなじみのカウンターをカスタムフック化してみた

概要

おなじみのカウンターアプリをカスタムフックとしてコンポーネントから処理を切り出してみたので共有しておきます。

ソース

CodeSandBox

https://codesandbox.io/s/zen-black-828c9

Counterコンポーネント

Counter.tsx
import React from "react";
import useCounter from "../customHooks/useCounter";

type CounterProps = {
  minCount: number;
  maxCount: number;
};

function Counter(props: CounterProps) {
  const { minCount, maxCount } = props;

  const { count, addCount, message: countMessage, resetCount } = useCounter({
    minCount: minCount,
    maxCount: maxCount,
    initMessage: "初期値"
  });

  return (
    <div>
      <div style={{ fontSize: 12 }}>
        ({minCount} ~ {maxCount})
      </div>
      <div>COUNT: {count}</div>
      <div>MESSAGE: {countMessage}</div>
      <div>
        <button
          onClick={() => {
            addCount(1);
          }}
        >
          {" +1 "}
        </button>
        <button
          onClick={() => {
            addCount(-1);
          }}
        >
          {" -1 "}
        </button>
        <button
          onClick={() => {
            resetCount();
          }}
        >
          RESET
        </button>
      </div>
    </div>
  );
}

export default Counter;

useCounter

useCounter.tsx
import React, { useState } from "react";

type UseCounterProps = {
  initCount?: number;
  initMessage?: string;
  minCount?: number;
  maxCount?: number;
};

type UseCounterReturn = {
  count: number;
  addCount: (value: number) => void;
  message: string;
  resetCount: () => void;
};

function useCounter({
  initCount = 5,
  initMessage = "",
  minCount = 0,
  maxCount = 10
}: UseCounterProps): UseCounterReturn {
  const [count, setCount] = useState<number>(initCount);
  const [message, setMessage] = useState<string>(initMessage);

  const addCount = (value: number) => {
    const nextCount = count + value;
    if (nextCount < minCount) {
      setMessage("最小値を下回ります");
    } else if (nextCount > maxCount) {
      setMessage("最大値を上回ります");
    } else {
      setMessage("成功");
      setCount(prev => prev + value);
    }
  };

  const resetCount = () => {
    setMessage("カウントリセット");
    setCount(initCount);
  };

  return { count, addCount, message, resetCount };
}

export default useCounter;

カスタムフックって?

簡単な話、カスタムフックはただの関数コンポーネントで、hooksを利用し状態(state)や副作用(effect)を持つこともでき、使いまわすことも出来る。ただ、DOMを返す代わりにインターフェースを提供する、それだけの事です。コードが抽象化されるのはもちろん、カプセル化のような効果もあり、保守性があがります。

カウンターは何が起きてる?

useCountercount(カウントの値) addCount(カウント操作) message(結果メッセージ) resetCount(カウントリセット)を提供し、Counterコンポーネントでは提供された値とインターフェースを利用して表示させています。

ここでインスタンスが生成されます。

Counter.tsx_抜粋
const { count, addCount, message: countMessage, resetCount } = useCounter({
  minCount: minCount,
  maxCount: maxCount,
  initMessage: "初期値"
});

ボタンがおされると、useCounterの提供しているaddCount()が走ります。

Counter.tsx_抜粋
<button
  onClick={() => {
    addCount(1);
  }}
>
  {" +1 "}
</button>

useCounterのaddCount()が実行され、状態が更新されます。

useCounter.tsx_抜粋
const addCount = (value: number) => {
  const nextCount = count + value;
  if (nextCount < minCount) {
    setMessage("最小値を下回ります");
  } else if (nextCount > maxCount) {
    setMessage("最大値を上回ります");
  } else {
    setMessage("成功");
    setCount(prev => prev + value);
  }
};

伴いCounterのビューも更新されます。

Counter.tsx_抜粋
<div>COUNT: {count}</div>
<div>MESSAGE: {countMessage}</div>

まだまだ初心者なので、ツッコミどころがあればご指摘お願いします。

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