yo_waka's blog

418 I'm a teapot

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の useEffectuseReducer を使って置き換えることができる。

こんな感じの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も外していく場合

  1. StoreのgetInitialState、reduceメソッドをstaticメソッドに変更
  2. ContainerコンポーネントuseReducer(Store.reduce, Store.getInitialState) とする
  3. 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.usersconst { users } = globalState に変わるだけ。 この時点だとHTML側の埋め込んでるところはそのままでいいので、他のコンテナで触ってるところがあっても安心。

些細な変更なんだけど、こうしておくと、以降のリファクタがやりやすくなる。

レスポンスタイムが伸びてる場合

  1. コンテナでAPIをコールして、dispatchを呼んでreducerでGlobalStateを更新する
  2. APIコールをPromise.all() で並列呼び出し&まとめて、GlobalStateを更新する

それでも取ってくるデータ(API)が多くてレンダリングまでが遅い場合

  1. APIコールをServiceWorkerで行い、キャッシュしておく
  2. ログイン時に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からWiFiBluetoothの設定ができるのは便利。 昔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発表者の皆様ありがとうございました!

f:id:yo_waka:20141112031225j:plain

f:id:yo_waka:20141112031242j:plain

@ymrl 写真拝借しました。当日はありがとうございました。