- 投稿日: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>〆
まだまだ初心者なので、ツッコミどころがあればご指摘お願いします。








