きみはねこみたいなにゃんにゃんなまほう

ねこもスクリプトをかくなり

React で Markdown を扱うときに便利な react-markdown と remove-markdown

Markdown 便利ですよね。使う側にとってはテキストベースでリッチコンテンツを表現できますし、開発者側にとってもアプリを実装する際に Markdown 対応さえしてしまえば、ある程度の表現力を確保できるとともに、XSS対応などもライブラリ側がしてくれている場合が多いです。Markdown はユーザから投稿されるコンテンツを扱う場合の簡便な選択肢の一つですね。

ウェブアプリとして Markdown を扱う際に必要なのは HTML として表示するためのパーサ・レンダラーです。そしてもう一つ、サマリや概要表示をする際には Markdown の見出しや図表といったマークアップを除外するライブラリがあると便利です。

今回は以下のライブラリを利用して、React で Markdown を扱う簡単なアプリを作成してみました。

最終的にこんな感じに表示されます。

f:id:lightbulbcat:20190802031041p:plain

動作するコードは github に置いておきました。

Markdown 表示アプリの実装

といっても説明すべきことはあまりなく、ブログエントリの構造体として

export interface BlogEntry {
  title: string
  body: string
}

を定義し、アプリケーションロジックとしては以下の実装がほぼ全てになります。

import React from 'react'
import Markdown from 'react-markdown'
import { HashRouter, Route, NavLink, Link, Switch } from 'react-router-dom'
import removeMarkdown from 'remove-markdown'
import './App.css'

import { entries } from './data'

const App: React.FC = () => {
  return (
    <HashRouter>
      <div className="App">
        <nav className="blog-list">
          <div className="blog-list-header">My Blog App</div>
          {entries.map((e, i) => (
            <NavLink className="blog-item" activeClassName="blog-item-active" to={`/blog/${i}`}>
              <div className="blog-item-title">{e.title}</div>
              <div className="blog-item-body">{removeMarkdown(e.body)}</div>
            </NavLink>
          ))}
        </nav>
        <main className="main-content">
          <Switch>
            <Route path="/blog/:index" render={({ match: { params: { index }} }) => {
              const e = entries[index]
              return (
                <div className="blog-detail">
                  <nav className="breadcrumbs">
                    <Link to="/">Blog Entries</Link> / {e.title}
                  </nav>
                  <div className="blog-title">{e.title}</div>
                  <Markdown className="blog-body" source={e.body} />
                </div>
              )
            }} />
            <Route path="/" render={() => (
              <div className="home-content">
                <h1>Welcome to My Blog</h1>
                <p>Select Blog Post!</p>
              </div>
            )} />
          </Switch>
        </main>
      </div>
    </HashRouter>
  )
}

スタイルを当てるための className がうるさくて見づらいですが、要点としては Markdown 形式のテキストデータをそれぞれのライブラリに渡しているだけです。

  • サイドバー: remove-markdown に渡してマークアップを外した文字列を得る
    • それを CSStext-overflow: ellipsis で省略表示に
    • もしくは JavaScript 側で substring するのでも可能
  • 本文: react-markdown コンポーネントsource に 流し込むだけ

簡単すぎますね。ありがたいことです。

付録: Markdownマークアップを外す他の方法

remove-markdown を探すまでに remark 方面から探していたら strip-markdown というライブラリも見つけたので、ついでにそのことも書いておきます。

remark というのは react-markdown 内でも使われている Markdown パーサで、Markdown からASTを得る部分の処理を行っています。remark はさらに内部で unified というテキスト処理のための汎用インタフェースを提供するライブラリを使っているようです。unified はパースや出力時の処理を外部ライブラリから指定でき、remark はパース処理を Markdown 用に固定した unified と言えそうです。

実際 remark の実装を見ると以下のようになっています。

var unified = require('unified')
var parse = require('remark-parse')
var stringify = require('remark-stringify')

module.exports = unified()
  .use(parse)
  .use(stringify)
  .freeze()

そして strip-markdown はその remark から

remark().use(stripMarkdown).processSync()

のように利用するライブラリです。細かいオプションには差異がありますが大枠の動作としては remove-markdown と同じように利用できます。ただ処理の内容として、Markdown 形式の文字列を

という違いがあるので単純な置換処理である remove-markdown の方が軽い気がします。バンドルサイズとしても react-markdown と併用するという前提では unified と remark-parse 分は共用になるので無視するとしても remark-stringify 分は必要になります。package.json の dependencies を増やしたくないという観点では、remove-markdown の方に軍配をあげたくなります。

remark や unified について調べられて面白かったのですが、最終的に remove-markdown を見つけてちょっと肩透かしな気分になったのでした。

以上、Markdown 方面の React、JavaScript 対応状況を調べて試してみました。

useEffect の評価タイミングが分からないので泣きながら試す

useEffectuseState を組み合わせた Hook を作成しようとしたのですが、動作のタイミングが今ひとつわからずうまく処理が噛み合わない Hook ができてしまったので、簡単な例で挙動を探っていきます。

色々試してみましたが、結局よくわかりませんでした。以下の内容もいつも以上に理解しないままで書いていますがご容赦ください。

動作するコードは github に置いてあります。

useState と useEffect を組み合わせたシンプルな例

const useMyHook = (label: string) => {
  console.log(label, 'useMyHook')

  const [s1, setS1] = useState(0)

  useEffect(() => {
    setS1(s => {
      console.log(label, `useEffect(s1): increments s1(${s1} → ${s1+1})`)
      return s + 1
    })
  }, [])

  return { s1 }
}

let count = 0

const App: React.FC = () => {
  count++
  const label = '-'.repeat(count-1) + `[${count}]`

  console.log(label, 'App')
  const state = useMyHook(label)
  console.log(label, `App: got ${JSON.stringify(state)}`)

  console.log(label, 'App: render')
  return (
    <div className="App">
      <header className="App-header">
        {JSON.stringify(state)}
      </header>
    </div>
  )
}

関数コンポーネント AppuseMyHook を利用しています。useMyHook の中では useStateuseEffect を利用しており useEffect(fn, [])fn をマウント時の一度のみ実行します。 実行タイミングがわかりやすいように、評価回数をカウントしてコンソール出力のインデントとして表示します。

実行後のコンソール出力は以下のようになりました。実装を読んでいないので適当ですが、こんな流れだろうという解説を添えてみました。

console 解説
[1] App
[1] useMyHook 最初に useMyHook を評価した時点では...
[1] App: got {"s1":0} ...初期状態の state が帰ってくる
[1] App: render Appレンダリング内容を返す
[1] useEffect(s1): increments s1(0 → 1) マウント時に呼ばれるように指定した useEffect が実行される
-[2] App useState の状態が更新されたので App が再評価される
-[2] useMyHook 再び useMyHook を評価し...
-[2] App: got {"s1":1} ...s1 が更新された state を得る
-[2] App: render

注目したいのは useEffect の実行されるタイミングで、useMyHook 内で定義された時点では実行されずに、それを呼び出した Appレンダリング内容を返した後で 実行されています。公式のドキュメントにも書いてありますね。

https://reactjs.org/docs/hooks-effect.html

What does useEffect do? By using this Hook, you tell React that your component needs to do something after render.

これを把握していないと、私のように「なんか副作用的な処理がうまく噛み合わない...」と処理を作り込んだ後で後悔することになります。実際、当初の私の理解では上記のコードを以下のように誤読していました。

  • 初回に useMyHook が返す値は useEffect で更新された後の値 {"s1":1}
  • なので App は一度のみ評価される

実際は App 評価 → useEffect 評価 → App 再評価、という順で評価されます。余談ですが私のやろうとしていたことをクラスコンポーネントのライフサイクルメソッドで実装しようとすると

https://reactjs.org/docs/react-component.html

UNSAFE_componentWillMountUNSAFE_componentWillUpdate に相当しそうなので、React 的にはそもそもあまりよろしくない動作を実装しようとしていたことになりますね。

もう少し複雑なパターン

const useMyHook = (label: string) => {
  console.log(label, 'useMyHook')

  const [s1, setS1] = useState(0)
  const [s2, setS2] = useState(0) // will get increment by s1 update

  useEffect(() => {
    setS1(s => {
      console.log(label, `useEffect(s1): increments s1(${s1} → ${s1+1})`)
      return s + 1
    })
  }, [])

  useEffect(() => {
    setS2(s => {
      console.log(label, `useEffect(s1→s2): increments s2(${s2} → ${s2+1}) caused by s1(${s1}) update`)
      return s + 1
    })
  }, [s1])

  return { s1, s2 }
}

s2 を更新するための useEffects1 の更新に対して実行されるように追加しました。全体の更新のトリガーはマウント時に1回だけ更新される s1 のみです。

実行後のコンソール出力は以下のようになりました。もうよく分からないですね。

console 解説
[1] App
[1] useMyHook
[1] App: got {"s1":0,"s2":0}
[1] App: render
[1] useEffect(s1): increments s1(0 → 1) #1
-[2] App #1 による再評価
-[2] useMyHook
[1] useEffect(s1→s2): increments s2(0 → 1) caused by s1(0) update #2
-[2] App: got {"s1":1,"s2":1} s1s2 も更新された値を同時に得る(タイミングが謎)
-[2] App: render
--[3] App #2, #3 両方による再評価...?
--[3] useMyHook
-[2] useEffect(s1→s2): increments s2(1 → 2) caused by s1(1) update #3
--[3] App: got {"s1":1,"s2":2}
--[3] App: render

二つの useEffect の評価されるタイミングの間に App の評価タイミングが割り込んでいるのが気になりますね。 あと App の再評価と State の更新のタイミングが前後しているように見えるのが気になります。 短期間に複数の useEffect 内で State が更新されたせいでこんな挙動になっているのでしょうか。

useEffect に対して 0 に指定した setTimeout を適用してみると App の評価回数が1回多く増えました。

| [1] App |  |
| [1] useMyHook |  |
| [1] App: got {"s1":0,"s2":0} |  |
| [1] App: render |  |
| [1] useEffect(s1): increments s1(0 → 1) | #1 |
| -[2] App | #1 による再評価 |
| -[2] useMyHook |  |
| -[2] App: got {"s1":1,"s2":0} | #1 が反映 |
| -[2] App: render |  |
| --[3] App | ...おそらく #2 による再評価 |
| --[3] useMyHook |  |
| [1] useEffect(s1→s2): increments s2(0 → 1) caused by s1(0) update | #2 |
| --[3] App: got {"s1":1,"s2":1} | #2 が反映 |
| --[3] App: render |  |
| ---[4] App | ...おそらく #3 による再評価 |
| ---[4] useMyHook |  |
| -[2] useEffect(s1→s2): increments s2(0 → 1) caused by s1(1) update | #3 |
| ---[4] App: got {"s1":1,"s2":2} | #3 が反映 |
| ---[4] App: render |  |

JavaScript のタイマー処理は目的の時間ちょうどには行われない、ということによるものなのでしょうか。ただ、動作的にはこちらの方がしっくりくる気がします。が、依然として useEffect とコンポーネントの再描画のタイミングがよく分からないことになっています

もっと複雑なパターン

const useMyHook = (label: string) => {
  console.log(label, 'useMyHook')

  const [s1, setS1] = useState(0)
  const [s2, setS2] = useState(0) // will get increment by s1 update
  const [s3, setS3] = useState(0) // will get increment by s2 update

  useEffect(() => {
    setS1(s => {
      console.log(label, `useEffect(s1): increments s1(${s1} → ${s1+1})`)
      return s + 1
    })
  }, [])

  useEffect(() => {
    setS3(s => {
      console.log(label, `useEffect(s2→s3): increments s3(${s3} → ${s3+1}) caused by s2(${s2}) update`)
      return s + 1
    })
  }, [s2])

  useEffect(() => {
    setS2(s => {
      console.log(label, `useEffect(s1→s2): increments s2(${s2} → ${s2+1}) caused by s1(${s1}) update`)
      return s + 1
    })
  }, [s1])

  return { s1, s2, s3 }
}

s1s2useEffect の定義の間に s2 の更新タイミングによる s3 更新処理を挟みました。

console 解説
[1] App
[1] useMyHook
[1] App: got {"s1":0,"s2":0,"s3":0}
[1] App: render
[1] useEffect(s1): increments s1(0 → 1) #1
-[2] App #1 による再評価
-[2] useMyHook
[1] useEffect(s1→s2): increments s2(0 → 1) caused by s1(0) update #2
[1] useEffect(s2→s3): increments s3(0 → 1) caused by s2(0) update #3
-[2] App: got {"s1":1,"s2":1,"s3":1} #1, #2, #3 が反映
-[2] App: render
--[3] App
--[3] useMyHook
-[2] useEffect(s1→s2): increments s2(1 → 2) caused by s1(1) update #4
-[2] useEffect(s2→s3): increments s3(1 → 2) caused by s2(1) update #5
--[3] App: got {"s1":1,"s2":2,"s3":2} #4, #5 が反映
--[3] App: render
---[4] App #4, #5
---[4] useMyHook
--[3] useEffect(s2→s3): increments s3(2 → 3) caused by s2(2) update #6
---[4] App: got {"s1":1,"s2":2,"s3":3} #6 が反映
---[4] App: render

やっぱり App の評価後に実行された useEffect の結果が useMyHook の返り値に反映されていますね...よくわからないです。useMyHook の評価が始まった後で、それ以前の useEffect が処理されているように見えるのでもしかしたら useEffect の定義時に同じ箇所の useEffect が呼ばれたかを確認して、呼ばれていなければそのタイミングで呼ぶ、みたいな処理をしている...とか...? でもそれだと useState の値がよくわからなく...

ちょっとお手上げです。振る舞いから推測するより React Hook の実装を理解した方が近道かもしれません。

気が向いたら眺めますが、今回は以上です。

要点としては useEffect の評価タイミングはレンダリング後、ということで、それ以上のことはよくわかりませんでした。コンポーネントの再描画のタイミングと useState のタイミングが前後してコンソールに出力される理由がよく分からないままです。 もしかしたらコンソール出力の部分がよくないのかもしれませんね。

Prettier のデフォルト設定を ~/.prettierrc に置く

最近 React のちょっとした挙動を確認するために crate-react-app でプロジェクトを作成することが多くなりました。; いらない派の私としては生成されたファイルをそのまま使いたくないので、毎回 Prettier を適用するために .prettierrc をプロジェクトルートに置いていました。しかし親階層、例えばホームディレクトリに置いておいても良いみたいです。

https://prettier.io/docs/en/configuration.html

The configuration file will be resolved starting from the location of the file being formatted, and searching up the file tree until a config file is (or isn't) found.

なので設定ファイルが見つかるまで親ディレクトリへと辿って行ってくれるみたいですね。Prettier は設定ファイルに cosmiconfig というライブラリを利用しているらしく、この動作もそれによるものみたいです。今度何かツールを作る機会があれば cosmiconfig を試してみたいです。

プロジェクト自体に .prettierrc が含まれる場合はそちらが優先される(と思う)ので、とりあえず自分用のデフォルト設定として ~/.prettierrc に置いて運用してみようかと思います。

ReactRouter のページ遷移で値が切り替わるように Formik の入力値を localStorage に保存する

Formik のフォームの値を localStorage に保存しておくライブラリとして formik-persist というものがありますが、ReactRouter でページ遷移した際に値がうまく切り替わらなかったので自前で Hook を組んで何とか実装してみます。

経緯としては以下の通りです。

  1. formik-persist を使ったけどページ遷移しても遷移前のフォームの入力値が残り続ける
  2. せっかくなので localStorage を使って自前で処理を書いてみよう
  3. react-useuseLocalStorage を使ってみたけど key が変更されても値が切り替わらないので結局同じ問題が発生する
  4. 好みの挙動になるように自前で useLocalStorage Hook を実装しよう (˘꒳˘) 意識高い

もしかしたら key を適切に与えれば解消するような、react-router でよくある感じのものなのかもしれません。 動作するコードは以下のリポジトリに置いてあります。

useLocalStorage を定義する

useLocalStorage のコードは https://usehooks.com/useLocalStorage/ を始め色々見つかりますので参考にしつつ書いていきます。

import { useState, useEffect, useCallback } from 'react'

function getPersistedValue<V>(key: string, defaultValue: V) {
  const v = window.localStorage.getItem(key)
  if (!v) return defaultValue
  try {
    return JSON.parse(v) as V
  } catch (err) {
    console.warn(err)
    return defaultValue
  }
}

function persistValue<V>(key: string, value: V) {
  try {
    const v = JSON.stringify(value)
    window.localStorage.setItem(key, v)
  } catch (err) {
    console.warn(err)
  }
}

// Hook
export default function useLocalStorage<V>(key: string, initialValue: V) {
  const [value, setValue] = useState(() => getPersistedValue(key, initialValue))

  // `key` が変わったら値を再取得する (*˘꒳˘*) 
  useEffect(() => {
    setValue(getPersistedValue(key, initialValue))
  }, [key, setValue, initialValue])

  // 値を更新しつつ `localStorage` に保存する (*˘꒳˘*)
  const setValuePersistently = useCallback((v: V) => {
    persistValue(key, v)
    setValue(v)
  }, [key, setValue])

  // ちょっと色気を出してクリア用のメソッドも提供してみる (*˘꒳˘*)
  const clearValue = useCallback(() => {
    setValuePersistently(initialValue)
  }, [setValuePersistently, initialValue])

  return [value, setValuePersistently, clearValue] as const
}

今回の肝は key の変更に合わせて対応する localStorage の値で value を更新することと、関連するハンドラも忘れずに更新されるようにしておくことです。

Formik と合わせる

Formik と組み合わせて以下の動作を実現します。

  • initialValues が変更されたらフォームの内容を新しい initialValues でリセットする
  • onChange でフォームの値を localStorage に保存する
  • onSubmit でフォームの値を初期化する
    • 送信後コメントフォームを空にするというケースを想定
interface FormValue {
  title: string
  body: string
}

const createInitialValue = () => ({ title: '', body: '' })

const FormView: React.FC<RouteComponentProps<{ id: string }>> = props => {
  const { id } = props.match.params
  const formKey = `FormView:${id}`
  const initialValues = useMemo(createInitialValue, [])
  const [values, persistValues, clearValues] = useLocalStorage(
    formKey,
    initialValues
  )

  // `Formik` には `onChange` がないので `validate` で代用する (*˘꒳˘*) きたない
  const fakelyValidateToHandleChange = useCallback(
    (v: FormValue) => {
      persistValues(v)
      return Promise.resolve()
    },
    [persistValues]
  )

  return (
    <Formik
      initialValues={values}
      onSubmit={clearValues}
      validate={fakelyValidateToHandleChange}
      validateOnChange
      enableReinitialize
    >
      {() => (
        <Form>
          <Field name="title" component="input" />
          <Field name="body" component="textarea" />
          <button type="submit">Submit</button>
        </Form>
      )}
    </Formik>
  )
}

ポイントとしては以下の通りです。

  • enableReinitialize を指定して initialValues の変更時にフォームの値を新しい initialValues の値でリセットする
    • ページ遷移時に initialValues が変わってフォームの内容が遷移後のページのものに更新されるように
  • validateonChange として転用
    • 本当はこんなことやりたくないのですが...

残念なポイントとしては、上記のダーティハックはもちろんですが、せっかく Formik が State を管理してくれているのに、それをわざわざ親コンポーネントに引っ張り上げて二重管理をしてしまっている所です。 initialValues が実質 Controlled Component の value みたいな扱いになってしまっているので、ちょっと気持ち悪いです。 React でコンポーネントを組むときに、どうしようもない場合によく現れるパターンな気がしますが、スッキリしないですね。

とりあえず、一応動いたので今回は良しとします。気が向いたら formik-persist のコードも見ながら他にいい方法がないか調べてみます。

追記

以下のように FormikPersist 双方に key を設定すればページ遷移しても、適切に入力値が保存されたものに切り替わるようになりました。

const FormView: React.FC<RouteComponentProps<{ id: string }>> = props => {
  const { id } = props.match.params
  const formKey = `FormView:${id}`
  const initialValues = useMemo(createInitialValue, [])
  return (
    <Formik
      initialValues={initialValues}
      onSubmit={console.log}
      enableReinitialize
      key={formKey}
    >
      {() => (
        <Form>
          <Field name="title" component="input" />
          <Field name="body" component="textarea" />
          <button type="submit">Submit</button>
          <Persist name={formKey} key={formKey} />
        </Form>
      )}
    </Formik>
  )
}

これで一件落着したような気がします。react-persist は connect を使ってこんな風に Formik にアクセスできるんだなっていう良い例ですね。インスピレーションの幅が広がりそうです。

HOC と RenderProp と Hook の相互変換はどこまで可能か

React で Cross-Cutting-Concern の文脈で語られ比較されるものと言えば HOC と RenderProp と Hook ですね。

実際に開発を行っているとライブラリから提供される機能がそのどれかのみで、プロジェクトとしては別の形で用いたいのでそれらを相互変換する、ということがたまにあります。

  • Hook は便利だけどクラスコンポーネントでは使いづらいので HOC や RenderProp 版が欲しい
  • Hook は便利だけどコンディショナルな箇所や map する箇所ではそのまま使えないので RenderProp が欲しい時もある
  • HOC だと props の型定義が面倒なので RenderProp でサクッと実装したい
  • HOC だと型パラメータが潰されるので RenderProp で回避したい
  • RenderProp だとネストが増えるので HOC で注入したい

...といった需要によるものです。 ただ、実際にやってみると、どうやら変換できなさそうな組み合わせがあることに徐々に気づいてきたので、今回はその感覚を整理するために、シンプルな例を題材にして、相互変換の可能性や変換時のポイントなどを試行錯誤してみたいと思います。

f:id:lightbulbcat:20190727032318p:plain

実際に動作するコードは github においておきました。

共通する型定義

まずは型定義です。foo という何かを提供する MyFeature という機能をコンポーネントから利用可能にするライブラリを考えることにします。

export interface MyFeature {
  foo: string
}

export const myFeature: MyFeature = {
  foo: 'Yes!'
}

// For RenderProp ver.
export interface MyFeatureProps {
  children: (feature: MyFeature) => JSX.Element
}

// For HOC ver.
export type WithMyFeature = { feature: MyFeature }

各パターンを組んでみる

HOC から変換する

ライブラリから HOC として機能が提供される場合、それを RenderProp や Hook として変換できないか試してみます。

import React, { createElement } from 'react'
import { myFeature, WithMyFeature, MyFeatureProps } from './my-feature'

// ライブラリから HOC として機能が提供
export function withFeatureFromHoc<P extends {}>(C: React.ComponentType<P & WithMyFeature>) {
  return (props: P) => <C feature={myFeature} {...props} />
}

// HOC → RenderProp (*˘꒳˘*) ちょっときもちわるい...
export function FeatureFromHoc(props: MyFeatureProps) {
  return createElement(withFeatureFromHoc(({ feature }) => {
    return props.children(feature)
  }), props)
}

// HOC → Hook (*˘꒳˘*) かけない...
export function useFeatureFromHoc() {
  return null
}

残念ながら Hook はうまく組めませんでした。

HOC → RenderProp も動きはするのですが、コードがちょっと不安になりますね。

RenderProp から変換する

ライブラリから RenderProp として機能が提供される場合、それをHOC や Hook として変換できないか試してみます。

import React from 'react'
import { myFeature, MyFeatureProps, WithMyFeature } from './my-feature'

// ライブラリから RenderProp として機能が提供
export function FeatureFromRenderProp(props: MyFeatureProps) {
  return props.children(myFeature)
}

// RenderProp → HOC (*˘꒳˘*) すんなり
export function withFeatureFromRenderProp<P extends {}>(C: React.ComponentType<P & WithMyFeature>) {
  return (props: P) => (
    <FeatureFromRenderProp>
      {feature => <C feature={feature} {...props} />}
    </FeatureFromRenderProp>
  )
}

// RenderProp → Hook (*˘꒳˘*) かけない...
export function useFeatureFromRenderProp() {
  return null
}

RenderProp → HOC への変換はかなり自然に書けている気がします。

これも残念ながら Hook はうまく組めませんでした。

Hook から変換する

最後に、ライブラリから Hook として機能が提供される場合、それを HOC や RenderProp として変換できないか試してみます。

import React, { useMemo } from 'react'
import { myFeature, MyFeatureProps, WithMyFeature } from './my-feature'

// ライブラリから Hook として機能が提供
export function useFeatureFromHook() {
  return useMemo(() => myFeature, [])
}

// Hook → HOC (*˘꒳˘*) すんなり
export function withFeatureFromHook<P extends {}>(C: React.ComponentType<P & WithMyFeature>) {
  return (props: P) => {
    const feature = useFeatureFromHook()
    return <C feature={feature} {...props} />
  }
}

// Hook →RenderProp (*˘꒳˘*) すんなり
export function FeatureFromHook(props: MyFeatureProps) {
  const feature = useFeatureFromHook()
  return props.children(feature)
}

Hook → HOC or RenderProp の変換は双方ともすんなり書けている気がします。

Hook 化で試したこと

無理やり組めばいけるんじゃないか、と思いクロージャを利用したアクセスで createElement 時に値の参照を得るコードを書いてみました。

export function useFeatureFromHoc() {
  let extractedFeature: MyFeature | undefined
  createElement(withFeatureFromHoc(({ feature }) => {
    extractedFeature = feature
    return null
  }))
  if (!extractedFeature) throw new Error('Feature unextracted')
  return extractedFeature
}

しかしこの Hook をコンポーネント内で評価してもエラーが投げられるばかりで値は得られませんでした。 こんな感じで何とかこねくり回せばいける気がしなくはないのですが...

まとめ

  • HOC → RenderProp: OK?, Hook: 無理
  • RenderProp → HOC: OK, Hook: 無理
  • Hook → HOC: OK, RenderProp: OK

Hook は RenderProp に変換可能で、それらは HOC に変換可能です。 変換の柔軟さでいうと Hook > RenderProp > HOC という感覚です。

  • 一応 HOC を RenderProp 化することはできる
    • でも書いていてちょっと不安になるコードになる
  • RenderProp と HOC を Hook に変換する方法は見つからなかった
    • なので Hook が提供されていないライブラリをの機能を自前で Hook に変換する、といったことはできなさそう、少なくともすんなりとは...
    • 例えば react-router の withRouter とかをちょちょっとやって useRouter できた、みたいなことはできなさそう
  • プロジェクト内で機能の切り出しを考える際は、まず Hook として切り出しておけば、RenderProp や HOC などの使い方をしたい場合にも数行のコードを追加すれば対応可能

仕事でコードを組んでいると複雑で「これ変換できるのかな...」としばらく試行錯誤することが多いのですが、一度シンプルな例で試しておくと、変換のパターンとその要所が把握できて多少理解が整理されますね。

でも正直このあたりの変換可能性はまだ全然自分の中で固まったイメージができていません。 なぜ変換可能性のグラフがこのようになるのか、の部分がです。

ものすごくふんわりした感覚でいうと Hook という枠組みは React Component の外にあり、そして React では一旦 Component の内部に入ってしまった値は外に出しにくい、という感覚があります。 React って動作原理は単純なのですが、その上で現れてくる使用感には単純にその動作原理に帰結させづらいものが多いです。 シンプルな枠組みの上に構築される多相な世界、という感じは数学とも似たところがありますね。

こういうところが React の面白さですね。

(0, obj.method) は this の参照を obj から外せるらしい

Hyperapp を始めようとしたら Parcel という Webpack がシンプルになったようなツールがあるのを知り、バンドルされたコードを見てみたら

var _hyperapp = require("hyperapp");

(0, _hyperapp.app)({
  view: function view(state) {
    return (0, _hyperapp.h)('div');
  }
});

という表現があり、初見では (0, _hyperapp.app) どう評価されるのかが分からなかったので少しだけ試したり調べたりしました。

挙動は多少わかった気がしますが、この表現が必要な理由までは読み解けませんでした。

, オペレータ

node の REPL で確認したところ , はどうやら最後の値を返すようです。

(0, 1) // => 1
(0, 1, 2, 3) // => 3
('this', 'is', 'a', 'pen') // => 'pen'

つまり

(0, _hyperapp.app) // => _hyperapp.app

ということですね。これだけだと何の意味もないワンクッションに見えるのですが、これにより this の参照が変わるみたいなのです。

https://stackoverflow.com/questions/40967162/what-is-the-meaning-of-this-code-0-function-in-javascript

のコメントを見なければ気づけませんでした。

オブジェクトのメンバとしての関数の呼び出し方

とりあえず知っていることをおさらいしておくと、JavaScript ではオブジェクトが関数フィールドを持つ場合、その呼び出し方によって this の参照先が変わるという話があります。

単純な例として this を返す関数を持つオブジェクトがあって

const obj = { method() { return this } }

この obj.method の参照する関数をふた通りの方法で呼び出すことを考えてみると

// A
obj.method() // => global

// B
const method = obj.method
method() // => obj

obj.methodmethod も同じ関数を参照しているのに返ってくる this の値が異なりますね。

ハンドラとして obj.method 形式で関数を渡したのに this 云々なエラーが返ってきて、理由もわからないまま .bind(obj) でとりあえず想定通りの動きになった、というのは初心者によくあるパターンです。

StackOverflow のコメントを見るに、this の参照を obj から外したい場合に

const method = obj.method
method()

と書く代わりに

(0, obj.method)() // => global

と書くことができて楽、ということなのでしょうか。

Parcel が何のためにこういうことをしているのか読み解くのが先ですね。今日はここまでにします。

追記

AST Explorer で確認してみたところ SequenceExpression という評価式らしいです。しかし ECMA Script Specification にはこの表現は見つかりませんでした。

.vue ファイルには template を複数書けないのか

公式ドキュメント を読みながら templatev-if を合わせられることを知って以下のように書きました。 Vue の単一ファイルコンポーネントの書き方ですね。

<template v-if="loaded">
  ここが表示されるのを待てども...(*˘꒳˘*) 
</template>

<template v-else>
  いつまでたっても Now Loading...(*˘꒳˘*)
</template>

<script>
// ...
</script>

<style scoped>
</style>

そしたらいつまでも Now Loading... から描画が遷移しないので、Vue の描画更新のハマりどころを一生懸命調べながら四苦八苦して、ふと後者の template しか有効でないのではと思い以下のように書き直したら動作するようになりました。

<template>
  <div>
    <template v-if="loaded">
      ...
    </template>

    <template v-else>
      Now Loading...
    </template>
  </div>
</template>

若干もっさりした書き方になったのと、div が不要な場合もあるので、React のような Fragment が欲しいですね。

this の扱いでミスしているのか、Observable への理解が薄いまま変なデータ更新の仕方をしているのかと開発ツールを導入しながら試行錯誤した結果としては、お粗末なハマりどころだったのでした。