React で Markdown を扱うときに便利な react-markdown と remove-markdown
Markdown 便利ですよね。使う側にとってはテキストベースでリッチコンテンツを表現できますし、開発者側にとってもアプリを実装する際に Markdown 対応さえしてしまえば、ある程度の表現力を確保できるとともに、XSS対応などもライブラリ側がしてくれている場合が多いです。Markdown はユーザから投稿されるコンテンツを扱う場合の簡便な選択肢の一つですね。
ウェブアプリとして Markdown を扱う際に必要なのは HTML として表示するためのパーサ・レンダラーです。そしてもう一つ、サマリや概要表示をする際には Markdown の見出しや図表といったマークアップを除外するライブラリがあると便利です。
今回は以下のライブラリを利用して、React で Markdown を扱う簡単なアプリを作成してみました。
最終的にこんな感じに表示されます。
動作するコードは 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
に渡してマークアップを外した文字列を得る- それを CSS の
text-overflow: ellipsis
で省略表示に - もしくは JavaScript 側で
substring
するのでも可能
- それを CSS の
- 本文:
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 の評価タイミングが分からないので泣きながら試す
useEffect
と useState
を組み合わせた 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> ) }
関数コンポーネント App
が useMyHook
を利用しています。useMyHook
の中では useState
と useEffect
を利用しており 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_componentWillMount
や UNSAFE_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
を更新するための useEffect
を s1
の更新に対して実行されるように追加しました。全体の更新のトリガーはマウント時に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} | s1 も s2 も更新された値を同時に得る(タイミングが謎) |
-[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 } }
s1
と s2
の useEffect
の定義の間に 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 を組んで何とか実装してみます。
経緯としては以下の通りです。
- formik-persist を使ったけどページ遷移しても遷移前のフォームの入力値が残り続ける
- せっかくなので
localStorage
を使って自前で処理を書いてみよう - react-use の
useLocalStorage
を使ってみたけどkey
が変更されても値が切り替わらないので結局同じ問題が発生する - 好みの挙動になるように自前で
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
が変わってフォームの内容が遷移後のページのものに更新されるように
- ページ遷移時に
validate
をonChange
として転用- 本当はこんなことやりたくないのですが...
残念なポイントとしては、上記のダーティハックはもちろんですが、せっかく Formik が State を管理してくれているのに、それをわざわざ親コンポーネントに引っ張り上げて二重管理をしてしまっている所です。
initialValues
が実質 Controlled Component の value
みたいな扱いになってしまっているので、ちょっと気持ち悪いです。
React でコンポーネントを組むときに、どうしようもない場合によく現れるパターンな気がしますが、スッキリしないですね。
とりあえず、一応動いたので今回は良しとします。気が向いたら formik-persist のコードも見ながら他にいい方法がないか調べてみます。
追記
以下のように Formik
と Persist
双方に 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 で注入したい
...といった需要によるものです。 ただ、実際にやってみると、どうやら変換できなさそうな組み合わせがあることに徐々に気づいてきたので、今回はその感覚を整理するために、シンプルな例を題材にして、相互変換の可能性や変換時のポイントなどを試行錯誤してみたいと思います。
実際に動作するコードは 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
の参照が変わるみたいなのです。
のコメントを見なければ気づけませんでした。
- Comma Operator - MDN
- Comma Operator - ECMA Script
- 全然読めなかったので読めるようになりたいです
オブジェクトのメンバとしての関数の呼び出し方
とりあえず知っていることをおさらいしておくと、JavaScript ではオブジェクトが関数フィールドを持つ場合、その呼び出し方によって this
の参照先が変わるという話があります。
単純な例として this
を返す関数を持つオブジェクトがあって
const obj = { method() { return this } }
この obj.method
の参照する関数をふた通りの方法で呼び出すことを考えてみると
// A obj.method() // => global // B const method = obj.method method() // => obj
obj.method
も method
も同じ関数を参照しているのに返ってくる 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 を複数書けないのか
公式ドキュメント を読みながら template
と v-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
への理解が薄いまま変なデータ更新の仕方をしているのかと開発ツールを導入しながら試行錯誤した結果としては、お粗末なハマりどころだったのでした。