Markdown コードブロックの JavaScript を bundle して実行するエディタを作ってみた

ペライチの markdown のコードブロックをビルドして iframe の中で実行できる

https://markdown-code-runner.netlify.com で遊べる。

前々から作れるなと思っていたので作ってみた。3時間ぐらいかかった。

仕組み

  • monaco-editor でコード編集
  • remark で codeblock の AST を収集
  • rollup と rollup-plugin-virtual でインメモリ上に依存を構築して bundle
  • iframe でクロスドメイン制約を与えた状態で実行(ifram.sandbox="allow-scripts")

外部からの入力受け取れないし、そもそも自動実行できないのでコード実行とはいえセキュリティ要件はそこまで厳しくないはず

TODO

  • 実験的に React だけ特別扱いしてるが npm 対応したい
  • monaco の補完用の worker がうまく起動してないので修正する
  • browserfs 対応
  • TypeScrpit のコード診断
  • prettier 対応
  • worker 対応
  • というか https://markdown-buffer.netlify.com 上に実装

2018年良かった漫画・ゲーム

漫画

ドロヘドロ(1) (IKKI COMIX)

ドロヘドロ(1) (IKKI COMIX)

ずっとよみたかったが kindle 版が発売されたのと、また完結したということで

サッカーユースの話

イサック(1) (アフタヌーンコミックス)

イサック(1) (アフタヌーンコミックス)

江戸時代、日本の傭兵がヨーロッパに渡って、30年戦争の中で仇(?)を追う話。

やたら弱っちい宇宙人に侵略される話。浅野いにお苦手だったが、サブカルちらしてはいるもののいつもと違うサブカルちらしかただったので、読みやすかった

ブルーピリオド(1) (アフタヌーンコミックス)

ブルーピリオド(1) (アフタヌーンコミックス)

ヤンキーが美大受験する話。美大受験うんちく

SF。最近売れてる天国大魔境に雰囲気近いかも

ちまちま増えてきた異世界グルメもの。龍を捕まえて食べる話

ニュクスの角灯  (1)

ニュクスの角灯 (1)

文明開化直後の長崎の貿易商の話。全然関係ないが地元が舞台なので土地勘わかって面白かった

ゲーム

  • Slay the Spire
  • Oxygen Not Included
  • Caves of Qud
  • Into the Breach
  • Path of Exile
  • Lucah: Born of a Dream
  • ドキドキ文芸部
  • ゼルダの伝説: BoW

個人的なゲームオブザイヤーは Slay the Spire 。そういえばこの記事書いてから1年が経った。

mizchi.hatenablog.com

当時、全然無名だったが、思えば遠くまで来た。デッキ構築型ローグライクのフォロワーも増えてきて、これからまたジャンルとして一つ花開くんじゃないだろうか。

Into the Breach や Slay the Spire のような、ゼルダのようなAAAタイトルのオープンワールドの真逆というか、ある種のミニマリズムを突き詰めてメカニクスを探求するタイプのゲームが増えてきて、ワクワクしている。ボドゲからの逆輸入というか

プログラマという現代の傭兵

エンジニアの転職とかプログラミング教育周りで考えていたこと。

フランス革命と技術のコモディティ化

最近フランス革命ナポレオン戦争ナショナリズム、そしてクラウゼヴィッツ戦争論などを調べたりしていたんだけど、傭兵や専門技術の扱いについて、示唆的なものが多かった。

当時の傭兵は、扱いが難しかった大砲・銃火器を扱う専門集団で、技能職でもあった。それが 18 世紀になり火器の改良が進み、産業革命で効率的な生産が可能になり、そしてナポレオンによる国民軍の創設、そのヨーロッパにおける戦果によって、傭兵はその役割を終えた。

「傭兵はすぐ逃げる」というのが定説だが、彼らは金で動く専門職なので、負ける側に付く理由がないので、当然とも言える…特に戦争という、敗者の支払いが期待できない場では。そして彼らを雇う王侯貴族の経済力が、そのまま軍団の動員力に直結した。常備軍を持たない分、平時のコストも安くついた。

ナポレオン(と彼を分析したクラウゼヴィッツ)は、前線を支える兵士の士気を、愛国心に求めた。銃火器の扱いが平易になったこと、そして(職業軍人と比較して)高度な訓練を必要としない歩兵の運用のメソッドが確立したことで、それが可能になった。

そして、徴兵によって「自分で自分の国を守る」という意識変革を経た結果、貴族階級の存在理由が否定され、王権と封建制度を中心とした古い体制、アンシャンレジームが破壊されて、「国民国家」の現代に至る。

双方引かない国民国家同士の戦争が、いかに悲惨な消耗戦になるかは、第一次世界大戦第二次世界大戦で明らかとなったわけだが…。

一方、現代では

この話は、本質的には、銃火器の取扱いの平易化という変化が、下部構造の柔軟性を生んだ話だと思っている。ナポレオンのすごいところは、その時代の変化を見逃さず適した組織を作り、軍事的優位を作ったところにある。結果としてナポレオンが敗北した理由も、ナポレオンに対抗するために追い込まれた側がそのメソッドを採用して反撃したところにあるわけで…。

翻ってプログラミング技術について考えると(この話の展開っておっさん臭くて嫌なんだが)…自分は、現代の銃火器は、プログラミング技術なんじゃないかと思っている。しかも、銃火器は戦場でしか役に立たないが、プログラミング技術は日常のあらゆる場面で役に立つ、可能性がある。

プログラミング技術は、個々人がネットワークに接続し、その計算リソースを借りてその能力を拡張するための手段だ。物事を議論の余地がないほどに小さいステップに分解するという、ある種非人間的な訓練を要求される。そして、この訓練メソッドが十分に成熟していない。

コモディティ化しないプログラミング技術

現在のプログラミング技術を取り巻く環境は、銃火器の取扱いが平易になる前のコモディティ化以前の傭兵全盛時代だと思っている。その理由はいくつかあって…

  • 教育による能力の再現性がない
  • 優秀なプログラマが教育によって育つ例がない
  • システム側(社会)による受け入れ体制が整っていない
  • プログラミング技術自体が(歴史が浅いので)進化し続けている

その結果、勝手に育った一部の人間が、プログラマに金を出してくれる会社に集まっている、という状態にある。ITの大手にいる人達は、10年前とそう変わらない。

会社の隆盛も早く、終身雇用を前提することが不可能で(日本社会の終身雇用自体が信用できなくなったのもあるが)、誰もが 10 年後にこの会社にいないと思っている。10 年前と変わったのが、コンピューターサイエンスで優秀な成績を修めたアカデミックエリートが、GAFA に行くようになったぐらいか。

個人としての最適と、社会としての最適が異なるのは前提として、自由度が高いコマとして競争力を持つことが、個人としての最適戦略になっている。少なくとも自分はそう思って動いている。

プログラミング教育について

本質的にプログラミング能力とは、物事の細かいステップへの分解能力と、何らかのルール制約下(プログラミング言語の表現力)での対応を発見することにあると思っている。

というのを前提として、国がプログラミング教育を推進したい気持ちもよく分かる。プログラミング教育を否定する人も、論理的思考力を鍛えることが大事、までは同意が取れると思う。従来、それは数学が担っていた分野だが、プログラミングは、数学教育メソッドの伝統的な制約から踏み出さないと訓練できない領域を多分に含むので、プログラミング教育という分野が創出された、という理解をしている。

それに対し、今のプログラマが育つ環境のリアルは、親方が弟子に伝授するという中世のギルド制度に近いのではないか、というのが界隈を見渡した際の予想としてある。親方は大学の教授かもしれないし、最初に入った会社のメンターかもしれないし、Twitter の強い友人かもしれない。どういう親方に技術を学んだかによって、その人の方向性が決まってしまうところが大きい。

ギルド制とはいえ、あんまり堅苦しくないのは、ハッカー文化は MIT の原始共産制っぽいヒッピー文化に強く影響を受けていて、自由を得るための実力主義社会という側面が強い(GNUOSS)。ただし圧倒的に実力主義なので、実力がないと発言権がない。強者の論理であるとも思う。

プログラミング技術がコモディティ化するときは、ハッカー文化が失われるときであるとも思う。

終わり

こういう記事を書いといて何だが、何が正しいとか、何が正しくないとか言うつもりはない。自分は業界をそういう目で見ている、という話。

現実問題、たぶん、今大規模なサイバー戦争が起きたら(攻殻機動隊をイメージしている)、結果的に頼りになるのはセキュリティ専門家とそれに近いプログラマと手癖が悪いダークウェブ界隈で構成された傭兵部隊だろう。自衛隊も何らかの訓練しているとは思うが、サイバーセキュリティ人材の扱いを見ていると、とても信用できない…

ちょっとプログラミング万能主義的な展開になってしまったが、少なくとも社会のIT技術の受け入れ体制が変わって、かつプログラミング教育が機能したあとの社会を考えると、社会はどう変わっていくか、というのは妄想すると面白いじゃないだろうか、という話。

ナポレオン戦争 - Wikipedia

大陸軍 (フランス) - Wikipedia

mdbuf v1.0.0: 最高の Mardkown Preview を目指して

mdbuf, そこそこ使い物になりそうな品質になったので改めて紹介します。

https://markdown-buffer.netlify.com で遊べます。

コンセプト

  • ブラウザで完結
  • 編集とプレビューのみに注力
  • PWA 機能を最大限に活かす

特長: 高速な Markdown プレビュー

色々頑張ってみた結果、高速な入力が可能です。

試した限り、 100000 文字以上だと流石に重くなっていきます。将来的に領域を分割してレンダリングできないか実験中です。

Desktop PWA 対応

PWA アプリとして、オフラインで起動することが可能です。編集中のデータはブラウザ内に保存されます。

編集位置への自動スクロール

Markdown を編集すると、プレビュー側の対応する行へ自動的にフォーカスします。

自分が知る限りこの機能を実現してるのは mdbuf だけです。

アウトライン機能

指定したアウトラインまでジャンプします。

ブラウザ内 Prettier 対応

Cmd+S / Ctrl+Alt+F で入力中の markdown を prettier で整形します。

それによって、例えばこのようなテーブルも自動的に整形されます。

| a   | b   | c   |
| --- | --- | --- |
| 1   | 2   | 3   |

数式

KaTeX による数式レンダリングをサポートしています。

$ y = x ^2 $

コードハイライト

highlight.js でコードを装飾します。

ワードカウント機能

文字数を数えます。自分が執筆業が多いので付けた機能です。

英語だけ等幅に

日本語を書くときは等幅である必要はないんですが、コードを書く時にアルファベットで等幅が崩れるのが嫌だったのでそこだけ等幅になるようにしています。

インデント

Tab / Shift-Tab での簡易なインデントをサポートしています

今後やりたいこと

  • スマホ UI (縦分割) 対応
  • MonacoEditor/CodeMirror 対応
  • textlint 対応
  • 依存なし単一 html へのパッケージング
  • mdx 対応

@9m の作ってるエディ太郎みたいに、Markdown とインラインのコードのハイライトに MonacoEditor を使いたかったのですが、VSCode + Google IME は日本語入力に補完ボックスの制御に致命的な欠陥があり、この Issue 待ちです。

editaro.com

https://github.com/Microsoft/vscode/issues/45629

なぜ作ってるか

これは next-editor https://nedi.appMarkdown 編集機能を切り出して再設計したものです。

応援してくださる方がいましたら patreon もよろしくおねがいします。

www.patreon.com

他、何か要望などがあれば、 twitter @mizchi までお願いします。

今年お世話になったCLIコマンド集

ヒストリ履歴からよく使ってるものをお焚き上げする。

注意点: npm 周り、グローバルコマンドは npm i -g で入れてて、ローカルで扱うものは yarn で使うという癖がある

追記: シェルじゃなくてCLIだろと言われるのが多かったので訂正した

vscode

$ code . -r

現在ディレクトリを VScode で開く。

-r が肝で、新しいウィンドウを生成せず、既存のウィンドウを開き直す。

yarn

$ yarn install --prefer-offline

yarn install 時にローカルキャッシュを優先する。テザリング環境下でリポジトリを作成するのに便利。

フリーランスになってから出先で作業することが多く、ギガ足りない問題が多々発生した。

git

$ git clone <github-url> --depth 1

HEAD だけ clone する。テザリング環境下で便利

typescript

$ yarn tsc -p . --noEmit
$ yarn tsc -p . --noEmit -w # watch mode

TypeScript でビルドせずに型チェックだけを行う。

これが便利というか、これで型チェック + CI でのテストを行っていて、webpack の ts-loader では transpileOnly: true としている。基本は VSCode 上で型エラーを見ている。

prettier

$ npm i -g prettier
$ prettier 'src/**' --write

指定ディレクトリ以下を prettier で整形する

firebase

$ npm i -g firebase-tools
$ firebase init

firebase プロジェクトの雛形を作成するコマンド。対話型で、質問に答えるとそれに沿ったボイラープレートが作成される。

$ firebase deploy --only firestore:rules

firestore.rules が動いているか検証をするのに、これを叩きまくっていた。 firebase はデプロイに時間がかかるやつが多いので、デバッグ時は --only 必須。

gibo

https://github.com/simonwhitaker/gibo

$ gibo dump Node OSX > .gitignore

.gitginore を自動生成

netlify

https://www.netlify.com

$ npm i -g netlify-cli
$ netlify deploy -d dist --prod
$ netlify open:site

dist 以下を netlify にデプロイする。今年は netlify に本当にお世話になった。代わりに github pages を使わなくなった

hub

https://github.com/github/hub

$ hub create mizchi/myprj

GitHubリポジトリを作成する。前からよく使っていたが、今年は本当によく使った。

しかし、未だに hub で pr を作成するコマンドの引数が覚えられない…

parcel

$ yarn add parcel-bundler -D
$ yarn parcel src/index.html --open

本番環境では相変わらず webpack を使っているが、実験プロジェクトは全部 parcel でやるようになった。

フロントエンドのまどろっこしい諸々をすっ飛ばして SPA を作り始めることができる。

$ parcel build src/index.html && netlify deploy -d dist --prod

netlify と組み合わせて、今年はこのコマンドを叩きまくった。今年の MVP コマンド。

ghq

https://github.com/motemen/ghq

$ ghq get -u <github-url>

GitHub リポジトリはすべて ghq で管理するようにした。

面白半分で作って、途中から GitHub に移したものをどう扱うかはまだ悩んでいる。

pwmetics

$ npm i -g pwmetrics
$ pwmetrics http://localhost:1234

指定 URL をヘッドレス Chrome で開いて、そのパフォーマンスメトリクスを取得する。

lighthouse を起動するのが面倒なときに、パフォーマンス取得するのに使っている。lighthouse はパラメータが多いが、こっちは小さくまとまってる。

lighthouse-cli

$ npm install -g lighthouse
$ lighthouse https://google.com

lighthouse を cli から起動する

webpack-bundle-size-analyzer

$ npm i -g webpack-bundle-size-analyzer
$ yarn webpack --json | webpack-bundle-size-analyzer

webpack-bundle-analyzer が HTML でレポートを作成してブラウザを開かないといけないのに対し、これはターミナルで完結してビルド後の JS のサイズを調査することができる。

実行例

$ webpack-bundle-size-analyzer build/client/stats.json
favalid: 330.87 KB (11.5%)
  ramda: 309.32 KB (93.5%)
  <self>: 21.54 KB (6.51%)
react-virtualized: 326.87 KB (11.3%)
core-js: 243.11 KB (8.42%)
lodash: 237.18 KB (8.21%)
redux-form: 199.7 KB (6.92%)
  hoist-non-react-statics: 2.02 KB (1.01%)
  <self>: 197.68 KB (99.0%)
date-fns: 170.93 KB (5.92%)
react-router: 137.13 KB (4.75%)
  hoist-non-react-statics: 1.35 KB (0.988%)
  <self>: 135.77 KB (99.0%)
react-dom: 98.98 KB (3.43%)
styled-components: 76.72 KB (2.66%)
...

mdbuf

https://markdown-buffer.netlify.com

シェルじゃないが、自作の高速な markdown preview 環境。この記事もここで書いている。

最近執筆が多いので、本当に作ってよかった。

ただ今ちょっとバグっているパターンを色々見つけていて、直したい。

書いた動機

フロントエンドのパフォーマンスチューニングの調査と、検証用の小さなプロトタイプを作ってデプロイすることが多い。それぞれは大したことないが、知ってるかどうかで生産性が大きく変わる実感がある。

他の人の手癖を知りたい。

Elm 2日ほどやった感想

12月はなんとなく新しいことをやりたくなる。ということで、elm をやってみた。

大昔に触った気がするけど、文法が Haskell っぽいこと以外、何も覚えてなかった。というか当時触った signal とかがなくなってたので別物になってた。

作ったもの

勉強がてら作った、球拾いゲームみたいな何か

コードはここ https://github.com/mizchi-sandbox/elm-playground

elm-platform, svg の扱い, キー入力、乱数の副作用の分離などが学べた。乱数は面倒くさくなったので外部(JS)からSeed与える方式にして、気持ち純粋になった。

(自分の)環境構築

Parcel を使った

brew install elm # OSに応じて
mkdir elm-playground
cd elm-playground
yarn init -y
yarn add parcel-bundler node-elm-compiler

これだけ

Elm とはなにか

現時点の Elm の理解。

GUI プラットフォームの為に余分なものを削ぎ落とした関数型プログラミング言語。削ぎ落とした結果、正格評価だし、モナドもない。副作用の表現は後述する elm-platform を経由するしかない。

あくまでも GUI のための状態表現で、UI 表現とは何か?という問いから言語仕様を組み上げていってるので、汎用言語として必要なものも捨てていて、汎用プログラミングには使えない。特に後述する elm-platform で顕著。あくまで GUI 専用。

副作用: elm-platform

elm は IO モナドがないので、副作用を剥がして純粋関数に渡すには、 elm-platform を通る必要がある。redux で例えると elm-platform は redux middleware に相当する。

というわけで elm-platform で乱数を扱うっぽい処理を typescript で書くとこうなるよなー、というやつを書いてみた。

function update(model: Model, msg: Msg): [Model, Cmd | null] {
  switch (msg.msg) {
    case "roll": {
      return [model, random(newFace)];
    }
    case "newFace": {
      return [
        { face: msg.payload, history: [msg.payload, ...model.history] },
        null
      ];
    }
    default: {
      return [model, null];
    }
  }
}

全部はこちら https://gist.github.com/mizchi/bdaa2250f1707d6fb09f4115644da0aa

元ネタはここ https://package.elm-lang.org/packages/elm-lang/core/latest/Random

この関数は [model, cmd] を返す。 cmd によって副作用が発生して、次のアクションが発生する。 redux のreducer に比べると、middleware 的な処理まで一歩踏み込んでいる。

型の安心感

TypeScript の、「努力目標で型ついてたらいいよね」的な世界観と比べると、本物の静的な型の安心感が手に入り、その安心感は圧倒的。今の所ランタイムエラーは一度も発生してない。

乱数の表現の Random(Generator) や Promise 相当の Task がある。

JavaScript 世界のオブジェクトを触るときは Json.Decoder で丁寧にアクセスする必要があったり、port という外部にアクセスする専用の RPC を使い必要があり、そこはだるい。

関数型界隈特有の「サンプル書かなくても型はこうなってるから後はわかるよね?」的な世界観もややあって、 example にないものに手を出そうとすると、慣れが必要。今もまだ慣れきってはいない。

エコシステム

今回は parcel を使ったが、parcel が公式で対応してるので、めちゃくちゃ簡単だった。 import { Elm } from './Main.elm' するだけ。

公式のフォーマッタの elm-format, というかスタイルガイドはかなり癖がある。関数同士は二行空ける。特に強い意見もないので、一応従ったが、なんとなくコードが間延びした印象を受ける。

vscode で elm パッケージを入れてコードを書いている。型チェックは信頼できるが、コード補完はそこそこ程度。型チェックは通っても循環参照でコンパイルエラーが起きたことはあった。

ドキュメントが Elm 0.19 に対応していないのがよく見られる。さっきの Random のドキュメントでも Random.map2 (,) (Random.int 0 10) (Random.int 0 10) が、 Random.map2 Tuple.Pair (Random.int 0 10) (Random.int 0 10) だったり、 公式パッケージや公式ドキュメントだとしても、微妙に追いついていない。最新のドキュメントを追い続けてないと、変更についていくのは大変だろう。 0.19 がどれぐらい安定しているのか不明だが…

Reason との比較

Reason と Elm は表面的な書き心地が似ている。

Reason は一旦 ocamlコンパイルされ、表現力・制約は ocaml と同程度。ocaml が簡単に副作用や例外を発生させられてしまうのに対し、elm はその部分を外に投げることで純粋性を担保している。そのかわりに Reason で JS オブジェクトにアクセスするのが非常に簡単。泥臭いことをするなら Reason のほうがいいが、そもそも泥臭いことをするならJSでいい気がする。

学習コストに関して、 Elm は Elm で閉じた世界観だが、 Reason は ocaml と JS に精通している必要がある。reason は使い手が ocaml に強いならやっと選択肢になりうるという感じ。

使えるか

ある種のUIでは間違いなく強力だが、常に Elm が最適解ではない、という印象を受けた。Elm コミュニティもその辺割り切ってそうな印象がある。

実際に泥臭い処理をやる場合、portを駆使したり、小さいコンポーネントとして分割統治したりする必要がありそう。

おまけ: parcel と webcomponents を使った最小 Elm 入門

Elm を使うのに大仰なセットアップが必要、と思ってる人もいるかもしれないので、parcel を使った最小の例を示す。

Foo をマウントする <elm-foo> 要素を定義する。

-- elements/foo.elm
import Html exposing (div, text)
main = div [] [text "foo"]
// elements/foo.js
import { Elm } from "./Foo.elm";

customElements.define(
  "elm-foo",
  class extends HTMLElement {
    connectedCallback() {
      Elm.Main.init({ node: this });
    }
  }
);

これをマウントする

import "./elements/foo";
document.body.innerHTML = '<elm-foo></elm-foo>'

webpack でも同等の表現できると思うけど、parcel だとデフォルトでそのまま動くので楽、という感じ。

react-redux と useContext を組み合わせて使う実験

react-redux v6 で中身が新しい方の Context API になったと聞いたので、コード読んでみたらContext自体が外部に export されていた。じゃあ hooks の useContext と組み合わせて使えるじゃん、と思って実験してみた。

useContext で connect 相当の処理を置き換えてみる。

https://github.com/reduxjs/react-redux/blob/master/src/index.js

import React, { useContext } from "react";
import ReactDOM from "react-dom";
import { createStore, Store } from "redux";
import { ReactReduxContext, Provider } from "react-redux";

type State = {
  count: number;
};

// いつもの reducer
const initialState: State = { count: 0 };

function reducer(state: State = initialState, action: any) {
  switch (action.type) {
    case "reset": {
      return initialState;
    }
    case "increment": {
      return { count: state.count + 1 };
    }
    case "decrement": {
      return { count: state.count - 1 };
    }
    default: {
      return state;
    }
  }
}

function Counter() {
  // useContext で store を取り出す
  const {
    storeState,
    store: { dispatch }
  } = useContext(ReactReduxContext);
  const counterValue = storeState.count;
  return (
    <div>
      Count: {counterValue}
      <button onClick={() => dispatch({ type: "reset" })}>Reset</button>
      <button onClick={() => dispatch({ type: "increment" })}>+</button>
      <button onClick={() => dispatch({ type: "decrement" })}>-</button>
    </div>
  );
}

function App(props: { store: Store<State> }) {
  return (
    <Provider store={props.store}>
      <Counter />
    </Provider>
  );
}

const store: Store<State> = createStore(reducer);
ReactDOM.render(<App store={store} />, document.querySelector(".root"));

これで動いた。が、いくつか問題がある。

react-redux の connect は map された値の変更を監視していて、HOCが自分に関係ない値の更新処理を抑制してくれていた。だが、この素朴に useContext している版は、 store のすべての値に反応してしまう。

useContext した時点ですべての更新を listen してしまうので、結局これ自身が子への抑制処理を行う必要がある。

色々頑張った結果、こうなった

import React, { useContext, useCallback } from "react";
import ReactDOM from "react-dom";
import { createStore, Store, Dispatch, AnyAction } from "redux";
import { ReactReduxContext, Provider } from "react-redux";

// helper
function useDispatch(): Dispatch<AnyAction> {
  const {
    store: { dispatch }
  } = useContext(ReactReduxContext);
  return dispatch;
}

function useConnect<T, U>(fn: (t: T) => U): U {
  const { storeState } = useContext(ReactReduxContext);
  return fn(storeState);
}

type State = {
  count: number;
  resetCount: number;
};

const initialState: State = { count: 0, resetCount: 0 };

function reducer(state: State = initialState, action: AnyAction) {
  switch (action.type) {
    case "reset": {
      return { ...initialState, resetCount: state.resetCount + 1 };
    }
    case "increment": {
      return { ...state, count: state.count + 1 };
    }
    case "decrement": {
      return { ...state, count: state.count - 1 };
    }
    default: {
      return state;
    }
  }
}

const CounterInner = React.memo(function CounterInner(props: {
  counterValue: number;
  onClickReset: () => void;
  onClickIncrement: () => void;
  onClickDecrement: () => void;
}) {
  console.log("update counter inner");
  return (
    <div>
      Count: {props.counterValue}
      <button onClick={props.onClickReset}>Reset</button>
      <button onClick={props.onClickIncrement}>+</button>
      <button onClick={props.onClickDecrement}>-</button>
    </div>
  );
});

function Counter() {
  console.log("update counter", Date.now());

  const dispatch = useDispatch();
  const counterValue = useConnect<State, number>(state => state.count);
  const onClickReset = useCallback(() => dispatch({ type: "reset" }), []);
  const onClickIncrement = useCallback(
    () => dispatch({ type: "increment" }),
    []
  );
  const onClickDecrement = useCallback(
    () => dispatch({ type: "decrement" }),
    []
  );
  return (
    <CounterInner
      counterValue={counterValue}
      onClickReset={onClickReset}
      onClickIncrement={onClickIncrement}
      onClickDecrement={onClickDecrement}
    />
  );
}

function App(props: { store: Store<State> }) {
  return (
    <Provider store={props.store}>
      <Counter />
    </Provider>
  );
}

const store: Store<State> = createStore(reducer as any);
ReactDOM.render(<App store={store} />, document.querySelector(".root"));

Counter は自分自身で element の詳細を生成せず、CounterInner に props を渡す。その際の関数は、useCallback を使ってメモ化する。

CounterInner 側では React.memo を使って shallow equal の比較によって更新処理を抑制している。CounterInner は count に興味があるが、 resetCount には興味がない。reset を連打した際は CounterInner の更新が行われないようにすることができた。

感想

やってみたけど、メモ化を駆使して自力でuseContextでとった値の更新をコントロールするのは結構だるい。useContext に mapState 相当の処理がほしい。

たぶん react-redux の大きなグローバル変数を持つみたいな考え方じゃなくて、connect される Context を都度生成する、みたいな発想の転換が必要な気がする。もうちょっと研究が必要。