作った。 yarn add redux-workerize
で入る。
元々は react-hooks で redux へのアダプタを書いていただけのライブラリだったが…
- TypeScript フレンドリーなAPIにする
- ReactRedux.Provider の異なるAPI表現だけじゃ面白くない
- じゃあ Redux.Store を worker に置いて postMessage で更新しよう
- mapStateToProps や更新処理の抑制の処理もCPU使うから、worker に置こう
- JSON飛び交ってるだけだし、 React だけじゃつまらないから Vue Plugin も提供しよう
結果、ビジネスロジックが Worker に切り出された上で、 React と Vue が同じ Store を共有するようになった。
どうなってるかというと
Worker が redux を抱えて、それぞれに Snapshot を切り出し、Snapshot を購読する各 Component がいる、という感じ
コード
import "@babel/polyfill"; import { RootState, increment, Increment, INCREMENT } from "./reducer"; const worker = new Worker("./worker.ts"); // React import React, { useCallback } from "react"; import ReactDOM from "react-dom"; import { createWorkerContext } from "redux-workerize/react"; const { WorkerContext, useSnapshot, useDispatch, ready } = createWorkerContext< RootState, { value: number } >(worker, async (state: RootState) => { return state.counter; }); function CounterApp() { const value = useSnapshot(state => state.value); const dispatch = useDispatch<Increment>(); const onClick = useCallback(() => { dispatch(increment()); }, []); return <button onClick={onClick}>{value}</button>; } ready.then(() => { ReactDOM.render( <WorkerContext> <CounterApp /> </WorkerContext>, document.querySelector(".react-root") ); }); // Vue import Vue from "vue"; import Vuex from "vuex"; import App from "./App.vue"; import { workerPlugin, proxy, SYNC } from "redux-workerize/vue"; Vue.use(Vuex); type CounterSnapshot = { value: number; }; export type State = { remote: CounterSnapshot; }; const store = new Vuex.Store<State>({ state: { remote: { value: 0 } }, mutations: { [SYNC](state, payload: CounterSnapshot) { state.remote = { ...state.remote, ...payload }; }, ...proxy([INCREMENT]) }, plugins: [ workerPlugin( worker, (state: RootState): CounterSnapshot => { return state.counter; } ) ] }); new Vue({ store, el: ".vue-root", render(h) { return h(App); } });
worker が Redux.Store を持つ実体で、フレームワークごとにラップするとその State を購読できる。第二引数で与える selector と snapshot は Worker 側で実行される。どうせWorkerを超えて非同期なので、非同期でも取れるようになってる。
React は hooks を使ってるので 16.7.0-alpha.0 以上じゃないと動かない。
Vue から直接 Redux 使うわけには行かないので Vuex を通してアクセスするようにした。Vuex.Store が ReduxStore の Snapshot を購読している。Redux が主なのは、型付けしやすいのと、Redux にはフレームワーク依存がないため。自分の好みといったらそう。
やってみたが、たぶん Vue Way ではないと思う。
Worker 側
import * as Comlink from "comlinkjs"; import { createStore } from "redux"; import reducer from "./reducer"; import { createWorkerizedStore } from "redux-workerized/worker"; const store = createStore(reducer); const storeAPI = createWorkerizedStore(store); Comlink.expose({ ...storeAPI }, self);
好きなように Redux.createStore して、createWorkerizedStore でラップして、 Comlink でクライアントに向けてAPIを公開する。
この結果、 Vuex から間接的に Redux Middleware を使える状態になっている。
やってみた感想
Worker でやってるのは、パフォーマンス目的もあるが、DOMに触れないという矯正ギプス的な意味のが大きい。
React と Vue でコミュニケーションできるようになったのは、単に結果であって、Worker に 共通の Store があるという設計に意味がある。
SerivecWorker でも動くので、複数タブでの状態の共有なんかにも使えるかもしれない。
本筋と関係ないが、Vue/Vuex の API の TypeScript との相性の悪さが絶望的で辛かった