- 投稿日:2020-01-19T21:40:05+09:00
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=reactReactを入れたアプリケーションがある場合は、Vueを指定します。
% rails new vueapp --webpack=vueyarn add
Vueで作ってあるアプリケーションには、Reactのモジュールをインストールします。prop-typesはWebpackerがデフォルトで入れるものですが、必須ではありません。
% yarn add @babel/preset-react babel-plugin-transform-react-remove-prop-types prop-types react react-domReactで作ってある場合は、Vueのモジュールをインストールします。
% yarn add vue vue-loader vue-template-compiler vue-turbolinksbabel.config.js
ルートにあるbabel.config.jsは、Reactで生成したものを使います。つまり、すでにReactならそのままにし、VueならReact用のbabel.config.jsを上書きします。
config/webpacker.yml
config/webpacker.ymlは、元からあるものを使い、Reactを加える場合は
.jsx
を、Vueを加える場合は.vue
を追加します。config/webpacker.ymlextensions: - .vue - .jsxconfig/webpacker/
ReactにVueを加える場合は、Vueで生成したものからconfig/webpacker/loaders/vue.jsをコピーします。
また、config/webpacker/environment.jsはVueのもので上書きします。
両方動かしてみる
次のような感じで両方を動かすapplication.jsを書いて動けば成功です。
app/javascript/packs/application.jsrequire("@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を切り替える、ということになるでしょう。
- 投稿日:2020-01-19T20:07:25+09:00
【SharePoint Framework】Failed to load component エラー対処
はじめに
開発したWebパーツをSharePoint ページに展開した所、以下の画像のようなエラーが発生しました。
解決策
以下のコマンドを実行する事で解決しました。
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/
- 投稿日:2020-01-19T19:08:32+09:00
カンマ区切りで複数ワードをsubmitできるテキストフィールド[React, Material-UI]
やりたいこと
Material-UIベースで、テキストを入力し、Tabを押したらChipでテキストを登録して、複数ワードを配列でsubmitできるテキストフィールドが欲しい。
↓こんなの
サンプル
codesandboxにサンプルを上げたので触ってみてください
ロジック
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] }); };
- 入力されたワードをカンマで区切って
- 既に入力されている配列と結合して
- 重複チェック & 空チェック
- stateにset
という流れです。
spread operatorが多くて少しわかりにくいですが、割と簡単に実装できました。
- 投稿日:2020-01-19T18:36:20+09:00
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.jsimport Enzyme, { shallow } from 'enzyme'; import EnzymeAdapter from 'enzyme-adapter-react-[version]'; Enzyme.configure( { adapter: new EnzymeAdapter() });実際にテストする
テストするコンポーネント
App.jsimport 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.jstest('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の動画を見終わっていないので、全て見たら改めて追加情報をまとめようと思います。
- 投稿日:2020-01-19T18:25:28+09:00
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.2nginxの起動
$ sudo service nginx startnginxへの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アプリが起動していないためです。
reactアプリの作成
nodejsのインストール(インストール済なら不要)
$ curl --silent --location https://rpm.nodesource.com/setup_10.x | sudo bash - $ sudo yum -y install nodejsreactアプリの作成
$ 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へのバージョンダウン)を行ってください。
?ブラウザで確認
- 投稿日:2020-01-19T18:04:29+09:00
【React】useReducer をもっと自由に活用しよう
useReducer をもっと自由に
皆さん、
useReducer
は活用していますか?
useState
で十分と思っている場合でもuseReducer
に置き換えることで、コードがシンプルかつ、わかりやすくなります。そのためには、Reduxの呪縛を解き払ってください。
useReducer
に Action type は必要ないですし、Flux Standard Action も必要ありません。また、今回はdispatch
もdispatch
らしい使い方はしていません。
つまり、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
) を、useState
とuseReducer
でそれぞれ作成します。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
に置き換えることで、コードがシンプルかつ、わかりやすくなり、テスタビリティがあがってパフォーマンスも上がります!
- 投稿日:2020-01-19T17:05:28+09:00
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
はフックの一種であり、関数コンポーネントのステートを宣言する能力を持ちます。ステートの宣言はuseState
とuseReducer
の2種類の方法がありますが、useReducer
は複雑なロジックが絡んだステートを宣言するのに適しています。
useReducer
は以下のように使います。こちらが用意するのはreducer
とinitialState
の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
の活用です(クラスコンポーネント時代のshouldComponentUpdate
やPureComponent
に相当)。これを活用してコンポーネントの余計な再レンダリングを避けることが、Reactアプリの基本的なパフォーマンス・チューニングです。この例では、
useReducer
がReact.memo
の利用の助けになる例を示し、丁寧に解説します。初期状態のサンプル
まず、改善前の初期状態を見てみましょう。以下のCodeSandboxで実際に動作を確かめることができます。初期状態のコードは
App1.tsx
に入っています。今回の題材はこの画像のようなものです。
4つの入力欄があり、それぞれに数値を入力することができます。下には4つの数値を合計した値が表示されます。また、入力欄の横にある「check」ボタンを押すと、そのときの数値が合計の何%かを一番下に表示します。画像は「123」の横のボタンを押したあとの状態です。
一見意味不明な例に見えますが、これは実は筆者が実際に業務で経験した例をかなり単純化したものになっています。
この記事にも初期状態のコードを一気に貼り付けます。記事を読みつつコードを見たいという方は適宜CodeSandboxをご活用ください。記事中でも部分ごとに解説していきますから、ここで全部読む必要はありません。
src/App1.tsximport 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つの状態を持ちます。以下に示すvalues
とmessage
です。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
に再レンダリングが発生してしまいます。今回のゴールは、
NumberInput
にReact.memo
を適用して無駄な再レンダリングを減らすことです。特に、ひとつの数値が変更されたらそのNumberInput
だけが再レンダリングされて、他のNumberInput
は再レンダリングされないという状態が理想です。お察しの通り、最終的には
useReducer
を用いてこれを達成することになります。
React.memo
導入への努力とりあえず、まずは
useState
のまま努力してみましょう。NumberInput
にReact.memo
を適用して効果を得るためには、他の入力値が変わってもpropsの内容が変化しないようにしなければいけません。現状ではvalue
は問題ありませんが、onChange
とonCheck
が問題です。あの位置に関数をベタ書きということは、これらのpropsには毎回異なる関数オブジェクトが作られて渡されています。これではReact.memo
は効きません。こういうときの定石は
useCallback
です。とはいえ、今回はループでNumberInput
を表示しているのでひと工夫必要です。筋のいい方法としては、NumberInput
に「自分が何番目か」を表すpropsを渡すという方法があります1。これをコールバックに渡してもらうことで、onChange
とonCheck
は全ての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
を受け取るようになりました。これが、自身が何番目かを表す数値です。onChange
とonCheck
の型も変更され、これらの関数にはindex
がオウム返しで渡されるようになっています。先ほども説明した通り、これによりonChange
とonCheck
を各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
が追加されているのに加え、onChange
とonCheck
が事前に用意されるようになりました。次に、これらを用意する部分のコードです。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 /* 省略 */ }
onChange
とonCheck
はuseCallback
に囲まれています。それぞれの関数の中身は、index
を引数で受け取るようになった以外は変わりません。できることは全部やったように見えますが、残念ながらこのコードはまだ目的を達成できていません。
onChange
はuseCallback
により常に同じ関数オブジェクトになっているのでOKですが、onCheck
が問題です。
onCheck
はuseCallback
の第二引数が[values]
となっています。これは、values
が変わるたびに、すなわち何か入力が変わるたびに、onCheck
が作りなおされるということを意味します。これによりNumberInput
に渡されるonCheck
関数が毎回別物になるため、React.memo
が無意味になっています。では、なぜ
useCallback
の第二引数がvalues
を含んでいなければいけないのでしょうか。それはもちろん、onCheck
がvalues
に依存しているからです。つまり、onCheck
が中で「入力値の合計」を求めるためにvalues
を使用しているのです。onCheck
のインターフェースが(index: number) => void
である、すなわちindex
のみを受け取るという関数である以上、values
というデータについてはonCheck
に内包されていなければいけません。これにより、必然的にvalues
が変わるたびにonCheck
という関数は別物になります。一方で、
onChange
はvalues
に依存していません。これは、useState
が提供するステート更新関数が、関数によるステートの更新をサポートしているからです。上のコードではsetValues
関数の引数として「現在の状態を受け取って次の状態を返す関数」を渡しています。この機能により、onChange
からvalues
への依存を消しているのです。となると、
onCheck
がmessage
というステートを更新するにあたって、それとは別のvalues
というステートに依存していることが問題だと分かります。これを解消するためには、2つのステートを合体させて1つのステートにする必要があります。このような状況に適しているのが
useReducer
です。ということで、App
をuseReducer
を用いて書き換えることで問題を解決しましょう。(一応、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)}%です` }; } } };型定義を読むと、
State
はvalues
とmessage
をひとつにまとめたオブジェクトであることが分かります。アクションは"input"
と"check"
の2種類があり、それぞれ前回のコードのonChange
とonCheck
に相当するロジックが書かれています。次に
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
になりました。従来のonChange
とonCheck
がひとつにまとまっていますね。それ以外は特に変わっていません。最後に
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
により行われています。従来onChange
とonCheck
が担っていたロジックはreducer
の中に押し込められましたので、ここでは何もせずにただNumberInput
にdispatch
を渡すだけになっています。前のコードと比べると、ここに本質的なポイントがあります。それは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つのステート(
values
とmessage
)を1つに合体させる」ということです。これにより、onCheck
でも関数によるステート更新ができるようになりました。実を言えばuseState
でも頑張ればこれは達成できますが、このような複雑なステートを扱うにはuseReducer
が適しているのでここではuseReducer
を選択することになります。useReducer
を使う場合はステート更新関数(dispatch
)は自動的にステートに非依存になります(reducer
はそもそも「現在のステートを受け取って次のステートを計算する」というものであるため)。
useReducer
のすすめこのように、
useReducer
を用いることで、ステート更新関数をステート非依存にすることを強制できます。実際のアプリ開発においては、アプリが複雑化するにつれて、あるステートと別のステートが関わりを持ち始めるかもしれません。もっと具体的に言えば、あるステートを更新するときに別のステートを見る必要が発生するかもしれません。そのときがuseReducer
導入のサインです。ぜひリファクタリングしてuseReducer
を導入しましょう。なぜ
useReducer
が必要なのか、この記事を読んだ皆さんはしっかりと説明できることでしょう。ステート更新関数がステートに非依存であることは、React.memo
の活用には必須だからです。また、
useReducer
とReact.memo
の恩恵を最大限受けるためには、できるだけreducer
にロジックを詰め込むことが鍵となります。そのためには、アプリの状態は何でもステートで表現することが重要です。言い換えれば、これは手続き的なロジックを書かず、状態は明示的・宣言的に扱うということです。また、
useReducer
を活かすためにはそのためのコンポーネント設計も重要です。今回の例では多少天下り的でしたが、NumberInput
がindex
を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
で解決できると思った方も多いでしょう。実際、以下のようにすればonCheck
をvalues
に非依存にすることができます。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
に反映され、onCheck
はvalues
を参照するかわりにvaluesRef.current
を参照するようにしています。useRef
が返すvaluesRef
は常に同じオブジェクトであることが保証されていますから、onCheck
はvaluesRef
に依存することはありません。この方法でも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
を導入しましょう。
ややアクロバットな別解として、
useMemo
を用いて各NumberInput
用のコールバックを用意するというものもあります。 ↩
- 投稿日:2020-01-19T15:43:30+09:00
Reactでの条件分岐 4つの方法のメモ
はじめに
Reactでの条件付きレンダリングの方法のメモです。
目次
- returnするJSXを分岐
- 変数にJSXを格納
- もう少し短く!!
- これで最後!!
- まとめ
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つがシンプルかなと思います。
ありがとうございました。
- 投稿日:2020-01-19T15:00:56+09:00
[React-router-dom]使い方
npmでパッケージ管理している前提です
#パッケージのインストール $ npm install --save react-router-domBrowserRouter,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'))
- 投稿日:2020-01-19T14:13:24+09:00
型で強化された、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 })) });
- 投稿日:2020-01-19T12:55:53+09:00
TruffleBox (React&Truffle)を用いたDockerでのdapps(ブロックチェーンアプリ)の開発環境の構築
やったこと
- Docker上でTrufflebox(React&Truffle)を用いたdappsの開発環境を構築した。
- ホスト側のIPアドレスを設定することで、Dockerのコンテナからホストで立ち上がっているganacheに接続できるようにした。
今回の成果
環境構築手順
各種ファイルの用意
まずは、docker-compose.ymlとDockerfileを用意します。
docker-compose.yml
docker-compose.ymlversion: '3' services: truffle: build: context: ./truffle/ dockerfile: Dockerfile volumes: - ./truffle:/usr/src/app command: sh -c "cd client && yarn start" ports: - "8003:3000"DockerFile
DockerfileFROM 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 boxtruffle-config.jsの編集
truffle-config.jsconst 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/262d2344b9e1ef4d2d88netmaskとganacheの設定
参考記事に従って、設定してください。
参考記事: https://qiita.com/kane-hiro/items/b1381cc1c8dd5559a9d2ganacheの設定で、一つだけ追加の設定が必要です。
上で設定したIPアドレス:10.200.10.1でサーバーを立ててほしいので、その設定を行います。
設定 -> server -> lo0:10.200.10.1に変更
truffle-config.jsonの設定
truffleが参照するganacheサーバーの情報(IPとポート)について、記述していきます。
truffle-config.jsconst 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
でエラーを吐きます。
- 投稿日:2020-01-19T12:38:24+09:00
【React】おなじみのカウンターをカスタムフック化してみた
概要
おなじみのカウンターアプリをカスタムフックとしてコンポーネントから処理を切り出してみたので共有しておきます。
ソース
CodeSandBox
https://codesandbox.io/s/zen-black-828c9
Counterコンポーネント
Counter.tsximport 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.tsximport 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を返す代わりにインターフェースを提供する、それだけの事です。コードが抽象化されるのはもちろん、カプセル化のような効果もあり、保守性があがります。
カウンターは何が起きてる?
useCounter
はcount
(カウントの値)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>〆
まだまだ初心者なので、ツッコミどころがあればご指摘お願いします。