flux-utilsをReact Hooksで地道にリファクタしていく
グローバルなJSONリファクタ に続き、地道なリファクタシリーズ第2弾。
Facebook社が公開している flux-utils、使っていますか。
今は大抵の人がReduxでFluxを実装していることでしょう。flux-utilsは3,4年前はシンプル故に普通に選定してもよいと思える選択肢でした。
でも今は選定しないほうがいいです。
選定しないほうがいい理由はいくつかあって、
- 提供されているConainerコンポーネント、提供されているビルド済JS内でコンストラクタをnewを使わずメソッドコールしているため、WebpackでES Moduleを指定したビルドができない(CommonJSを指定しないといけない)
- Facebook社が使っていないためかReactの最新バージョンに追随したバージョンがnpmにリリースされるまでのスピードが恐ろしく遅い
- fbjs, fbemitterというこれまたFacebook社内でおそらくもはや使われていないだろうライブラリに依存していて、こいつらが同様にnpmにリリースされない(fbjsなんかはもう1年以上アップデートされていない)
というわけで、脱flux-utilsです。
といってもいきなり全部React Hooksで置き換えるぞ!というのも規模が大きなアプリケーションでは難しいので、まずは上に挙げたクリティカルな問題を孕んでいるContainerを置き換えていくのがよいと思います。
こんな感じで、2つReduceStoreをsubscribeしたContainerコンポーネントがあるとする。
// @flow import React from 'react'; import { Container as FluxContainer } from 'flux/utils'; import { countUp } from './flux/actions/counter_action'; import { addTask } from './flux/actions/task_action'; import CounterStore from './flux/stores/counter_store'; import TaskStore from './flux/stores/task_store'; type Props = {}; type State = { count: number, tasks: Array<Task> }; class Container extends React.Component<Props, State> { static getStores() { return [CounterStore, TaskStore]; } static calculateState() { return { ...CounterStore.getState(), ...TaskStore.getState() }; } handleAdd(e: SyntheticEvent<>, task: Task) { e.preventDefault(); addTask(task); } handleCountUp(e: SyntheticEvent<>) { e.preventDefault(); countUp(); } render() { return ( <> <div> <Form onSubmit={this.handleAdd} /> <List tasks={this.state.tasks} /> </div> <Counter count={this.state.count} onCountUp={this.handleCountUp} /> </> ); } } export default FluxContainer.create(Container, { withProps: true });
このFluxContainerがやってることは、要はHOCを作ってgetStoresで取れるReduceStoreに対してaddListenerしているだけ。
これはReact Hooksの useEffect
と useReducer
を使って置き換えることができる。
こんな感じのhooksスクリプトを用意する。
// @flow import { useEffect, useReducer } from 'react'; const useFluxStores = (stores, reducer, deps = []) => { const [state, dispatch] = useReducer(reducer, reducer()); useEffect(() => { const tokens = stores.map(store => store.addListener(() => { dispatch(store); })); return () => tokens.forEach(token => token.remove()); }); return state; }; export { useFluxStores };
あとは先程のContainerコンポーネントをSFCにしてhooksを呼び出してあげればいい。
// @flow import React from 'react'; type Props = {}; const getStores = () => [CounterStore, TaskStore]; const calculateState = () => { return { ...CounterStore.getState(), ...TaskStore.getState() }; }; const handleAdd = (e: SyntheticEvent<>, task: Task) => { e.preventDefault(); addTask(task); }; const handleCountUp = (e: SyntheticEvent<>) => { e.preventDefault(); countUp(); }; const Container: React.FC = (_props: Props) => { const state = useFluxStores(getStores(), calculateState); return ( <> <div> <Form onSubmit={handleAdd} /> <List tasks={state.tasks} /> </div> <Counter count={state.count} onCountUp={handleCountUp} /> </> ); }; export default Container;
FluxContainer依存が無くなったのと、SFCになったことでだいぶシンプルになった。
React-Routerなんかを使っていて、Containerコンポーネントにpropsが渡ってくる場合でもちゃんと動く。
こうしておくと、以降のリファクタがしやすくなる。
fbemitterに依存しているReduceStoreも外していく場合
- StoreのgetInitialState、reduceメソッドをstaticメソッドに変更
- Containerコンポーネントで
useReducer(Store.reduce, Store.getInitialState)
とする - ActionCreatorにdispatchメソッドを渡せるようにしてFacebook fluxのDispatcherと置き換える
こんな感じで依存を外せるはず。
Containerコンポーネント単位でflux-utils依存を外せれば、Reduxでいうところの 3 principles も実現しやすくなるはず。
HTMLに埋め込まれたグローバルなJSONをReact Context APIとHooksで地道にリファクタしていく
新年の書き初めにReact Hooksでいろいろ習作を書いたりしてた。
WebアプリケーションだとサーバサイドからHTMLビューにJSONを書き出して、フロントエンドでそれを使うというのはよくやるやつだと思う。
こんなやつ(例はRailsのERB)
<script> window.GLOBAL = {}; window.GLOBAL.users = <% json_escape @users %> </script>
特にマスタデータ系は昔からこんな感じでやってるのが結構多いんじゃないかなと思う。
フロントエンドからはいつでも const users = GLOBAL.users
で引いてこれるので便利っちゃ便利。
そこそこデカいJSONでもあまり気にせず書き出せるのも気楽でよいっちゃよい。
ただ、ビュー内で同じようなのが増えてくると、ビューが書き出されるまでのレスポンスタイムが長くなるのと、いろんな箇所で GLOBAL
を参照してしまいがちでリファクタリング時のネックにもなりがち。HTML内なので型定義もやりづらく、バグの温床にもなる。
これはReact Context APIとHooksで書き直すことができる。
勝手にグローバルステートパターンと呼んでる。
Fluxでアプリケーションを作っているならこんな感じのストアを用意する。
import React, { useReducer } from 'react'; export type State = {| users: Array<User> |}; // 何もしない const reducer = (state = {}, _action): State => state; const Store = React.createContext(); const Provider = ({ children }) => { const [state, dispatch] = useReducer(reducer, window.GLOBAL); return ( <Store.Provider value={{ state, dispatch }}> {children} </Store.Provider> ); }; export { Store, Provider };
useReducer に window.GLOBAL
を指定してあげるのがポイント。
あとはコンテナでProviderを使って、コンポーネントでuseContextすればいい。
// @flow import React from 'react'; import { Store as GlobalStore, Provider as GlobalStateProvider } from './flux/stores/global_store'; const ListItem: React.FC = (props: { user: User }) => { const { user } = props; return ( <ul> <li>name: {user.name}</li> <li>email: {user.email}</li> </ul> ); }; const List: React.FC = () => { const { globalState } = useContext(GlobalStore); const { users } = globalState; return ( <React.Fragment> <h2>List</h2> {users.map(user => <ListItem user={user} />)} </React.Fragment> ); }; const Container: React.FC = () => ( <GlobalStateProvider> <List /> </GlobalStateProvider> ); window.onload = () => { const el = document.getElementById('root'); if (el) { ReactDOM.render(<Container />, el); } };
使う側からは、const users = GLOBAL.users
が const { users } = globalState
に変わるだけ。
この時点だとHTML側の埋め込んでるところはそのままでいいので、他のコンテナで触ってるところがあっても安心。
些細な変更なんだけど、こうしておくと、以降のリファクタがやりやすくなる。
レスポンスタイムが伸びてる場合
- コンテナでAPIをコールして、dispatchを呼んでreducerでGlobalStateを更新する
- APIコールをPromise.all() で並列呼び出し&まとめて、GlobalStateを更新する
それでも取ってくるデータ(API)が多くてレンダリングまでが遅い場合
- APIコールをServiceWorkerで行い、キャッシュしておく
- ログイン時にprefetchを走らせて事前キャッシュしておく
という感じでどんどん展望が立てやすくなる。
MacからLinuxに乗り換えた
そろそろ家のMacbook Air (2013 Mid)が限界にきてたので、Thinkpad X1 Carbon (Gen 5th)を買ってArchLinux入れた。 この時期は毎年Lenovoのオンラインショップで40%以上ディスカウントされてるらしく、SSD以外モリモリでつけても17万円を切って財布にやさしい。
久々のLinux環境だけど、GUIも非常にサクサク動いてて満足度高い。 昔の印象だと輝度やボリュームが調整できなかったり、ファンクションキー効かなかったりした記憶があるけど、特に何もせずに最初から使えて、これが現代か・・という感じだった。 X1 Carbonは当たり前だけどCarbon素材なので、冬にいきなり膝の上においても冷たくないのはMacと比べて地味によい。
インストール
pockeさんの記事が詳しく、ArchWiki と合わせて見ながらやって特に詰まるところはなかった。
自分はデュアルブートにはせず、すべてLinux用にパーティション切った。
arch-chrootした後、reboot後にもwifi-menuが使えるよう pacman -S dialog wpa_supplicant
を入れておかないと無線しかない環境だとつらくて1回やり直した。
デスクトップ環境
特にこだわりないのでGNOME3にしました。GDMからログインしてます。 なんだかんだGUIからWiFiやBluetoothの設定ができるのは便利。 昔Unity使ってて結構重い感じだったけど、GNOME Shellは今のところ快適。
GNOME3.22からはデフォルトでWaylandを使うようになったらしく、.xinitrcや.xsessionにxrdbやxsetの設定を書いても実行されなくてハマった。 Waylandのドキュメントを見たらdesktopファイルに書くという記述があったので、見よう見真似で設定したら動いた。
~/.config/autostart/startup.desktop
[Desktop Entry] Name=Startup GenericName=Startup Comment=Startup settings Exec=/usr/local/bin/startup Terminal=false StartupNotify=true Type=Application Categories=X11;Legacy;Startup
/usr/local/bin/startup
#!/bin/sh xrdb -merge /home/yo_waka/.Xresources xset b off xset r rate 200 30
日本語環境
最初ibus-skkを入れたところ何故かインプットメソッドが動かなくて、ibus-mozcを入れたら動いた。 無変換キーとカタカナ/ひらがなキーを切り替えに使ってる。 たまに変換が暴走することがあるので、よいやつがあれば切り替えたい。
フォントはNoto-FontsをYaourtで入れたら自動で設定された。 正直日本語フォントはMacがやっぱり一番だなーと思うけど、Noto-Fonts(特にsans-serif)も非常に綺麗でいい意味で裏切られた。 特にPDF開いたときにとても読みやすくて、GNOME標準のEvinceがとてもよいものに感じるw
ランチャー
MacのAlfredライクなAlbertというのをYaourtで入れた。 アプリの起動くらいしかしないので特に不満なし。
ターミナル
GNOME Terminalも使いやすいけど、透過させたかったのでrxvt-unicodeを入れた。 urxvtはXResourcesで見た目をカスタマイズできるのがよい感じ。
このような設定になった。
! Common URxvt.geometry: 200x40 URxvt.scrollBar_right: true URxvt.scrollBar_floating: true URxvt.scrollstyle: plain URxvt.cursorBlink: true URxvt.cursorUnderline: false URxvt.pointerBlank: true URxvt.visualBell: false URxvt.saveLines: 3000 URxvt.fading: 40 URxvt.perl-ext-common: default,tabbed !! Tab URxvt.tabbed.tabbar-fg: 2 URxvt.tabbed.tabbar-bg: #001020 URxvt.tabbed.tab-fg: 3 URxvt.tabbed.tab-bg: #001020 !! InputMethod URxvt.perl-ext: xim-onthespot URxvt.preeditType: OnTheSpot !! Font list and Spacing URxvt.font: xft:azuki_font:size=16 URxvt.letterSpace: -1 !! Color Scheme and Opacity - gruvbox-dark https://github.com/morhetz/gruvbox URxvt.depth: 32 URxvt.color0: [90]#282828 URxvt.color1: [90]#cc241d URxvt.color2: [90]#98971a URxvt.color3: [90]#d79921 URxvt.color4: [90]#458588 URxvt.color5: [90]#b16286 URxvt.color6: [90]#689d6a URxvt.color7: [90]#a89984 URxvt.color8: [90]#928374 URxvt.color9: [90]#fb4934 URxvt.color10: [90]#b8bb26 URxvt.color11: [90]#fabd2f URxvt.color12: [90]#83a598 URxvt.color13: [90]#d3869b URxvt.color14: [90]#8ec07c URxvt.color15: [90]#ebdbb2 URxvt.foreground: [90]#ebdbb2 URxvt.background: [80]#282828 URxvt.colorIT: [90]#8ec07c URxvt.colorBD: [90]#d5c4a1 URxvt.colorUL: [90]#83a598 URxvt.scrollColor: [90]#504945
シェル
最近は補完が便利なのでfishに乗り換えた。シェルの設定で重くなりがちだったので設定が少なくてすむのが好き。 fishermanで以下を入れて使っている。
- z
- fzf
- ghq
- git_util
GoogleDrive
memo というCLIツールでメモを書いていて、GoogleDriveでリストを同期させているので無いと日常が死ぬ。 Linux向け公式クライアントはない模様だけど、gdriveというのがシンプルで使いやすそうだったので入れてみた。 fuseでマウントするやつもあったけど、依存パッケージが多かったのでやめた。
# Drive上のmemoディレクトリのIDを取得 $ gdrive list -q 'name contains "memo" and trashed=false' # memoディレクトリに同期ダウンロード $ gdrive sync download <ID> memo # memoディレクトリを同期アップロード $ gdrive sync upload memo <ID>
ディレクトリ名やファイル名を直接指定すると重複して作成されるので、IDを指定する必要がある。 IDは一度作ると変わらないようなので履歴からサクッと同期は可能ではある。 これで今まで通りできるようにはなったものの、公式クライアントが欲しい。
その他開発ツール系
全部Yaourtでインストールできて便利。
- git
- docker
- tmux
- neovim
- tig
dockerは一瞬で立ち上がるのでMacのときよりめっちゃ使っていきそう。
不満なところ
これまで常用していた、Tweetdeck、Kindleのクライアントアプリがないこと。 Wineを入れれば使えるがWine入れたくない。。。 Tweetdeckはブラウザ版がよくできてるのでいいけど、Kindle Cloud Readerはオフライン機能が使いやすくないのでElectronの機運が高まってきた。
ReactiveCocoa Tokyo
先週になりますが、「ReactiveCocoa Tokyoというイベントがあり、そこでfreee社での導入の経緯やMVVMでのReactiveCocoaの使い方について話してきました。
ReactiveCocoaは役割上ロックインされがちで、そこをなるべくゆるやかに導入していくにはというのが ninjinkunさんの発表に対して、ロックインされると各レイヤに統一感出ていいよと、対比ぽい感じができたのでよかったです。
ReactiveCocoaの魅力は、MVVMのコンポーネント間のインターフェースを統一できること、またアプリ固有の非同期処理や入力処理を統一的なインターフェースでラップできるフレームワークとして使えることです。 自分が知る限りでは上記のことをやるならReactiveCocoaが一番よくできていてまた開発も活発だと思います。
@ikesyoさんに懇親会で聞いた限りだと、Swiftブランチは実運用にはもうちょっと時間がかかりそうだなという印象です。でも粛々と開発は進んでいるようなので期待。
終わった後、同日開催のiOS/Swift勉強会@ヤフーをピザとビール片手にみんなで眺めたというのもちょっと新鮮で楽しかった。 会場の迷惑にならないので思う存分ワイワイできるw
freee社で50人規模の勉強会を開催するのは初めてだったのですが、当日欠席する人も思ったより少なくてよかった。これをきっかけにReactiveCocoaの導入事例が増えるといいなあ。
大阪から駆けつけてくれた @ikesyoさん、主催の @ninjinkunさん、社内でいろいろ調整してくれた @yonekawaさん、トップバッターで話してくれた @tinpayさん、LT発表者の皆様ありがとうございました!
@ymrl 写真拝借しました。当日はありがとうございました。