以下の内容はhttps://let.blog.jp/tag/Reactより取得しました。


React の ErrorBoundary でキャッチしたエラーがコンソールに残る
React でエラーの表示を個別にせずまとめてやりたいなと思って コンポーネントの中で個別に表示する代わりに throw して親の ErrorBoundary で受け取るようにしてみてました
動作的には期待するものだったのですが ErrorBoundary でエラーをハンドルしてるのにコンソールに Uncaught Error が出るのですよね

実装ミスでもあるのかなと思って探してもなさそうで ErrorBoundary 側に設定があるのかと探してみてもなさそうでした
単純なケースでもこういう動作になるので React 側でハンドルしてもさらに throw してるみたいです
↓のコードで「Uncaught Error: ERROR!」がコンソールに出ます

import React, { useState } from "react"

const App = () => {
return (
<ErrorBoundary>
<Component />
</ErrorBoundary>
)
}

class ErrorBoundary extends React.Component {
constructor(props) {
super(props)
this.state = { error: null }
}

static getDerivedStateFromError(error) {
return { error }
}

render() {
if (this.state.error) {
return (
<div>
<h1>Error!!</h1>
<p>{this.state.error.stack}</p>
</div>
)
}

return this.props.children
}
}

const Component = () => {
const [error, setError] = useState(false)

if (error) {
throw new Error("ERROR!")
}

const onClick = () => {
setError(true)
}

return (
<button onClick={onClick}>click</button>
)
}

export default App

ハンドル済みで期待する動作なのにエラーがコンソールに出るのは気持ち悪いです
単純に catch されなかったエラーをログに出さないならこれでできます

window.addEventListener("error", (event) => {
event.preventDefault()
})

しかしこうすると React の ErrorBoundary でハンドルしてないその他のエラーも非表示になって困ります
getDerivedStateFromError で Error オブジェクトにマークを付けて マークがついていれば preventDefault をしようかと考えたのですが window に自分でつけるハンドラのほうが先に処理が行われるようで うまくいかないです
先に React 側のエラーハンドルの処理をしたいので 中で queueMicrotask で遅延させると先にコンソールにエラーが表示されてしまいます

探してみると issue がありましたが 対応する予定はなさそうです
https://github.com/facebook/react/issues/15069

StrictMode やフック関係でも要望が多く上がっていても変えないスタンスなところはそのままになってたりしますしあまり期待できそうにないですね
ページ内部からヘッダーを更新したい
メインのレイアウトがこんな構造のとき

const Main = () => {
return (
<div>
<Header/>
<Outlet/>
<Footer/>
</div>
)
}

Outlet のコンポーネントがその時点のページを表示します
このページ内の処理で Header や Footer に表示するものを設定したいです
ページタイトルとかそういうの

やろうとすると Main で Context を提供し setHeader みたいな関数にアクセスできるようにして 各ページがマウント時に呼び出します

const Page1 = () => {
const setHeader = useSetHeader()

useEffect(() => {
setHeader({
title: "Page1",
})
}, [])

return (<div>...</div>)
}

あまり気持ちの良い方法ではないです

以前は Outlet のような使い方をする Router を使っていなくて Outlet のところに Router を配置する感じでした
それだと Header などレイアウトを外に配置するメリットもあまりなかったので Page が Main コンポーネントを使うという構造でした

const Page1 = () => {
const header = {
title: "Page1",
}
return (
<Main header={header}>
<div>...</div>
</Main>
)
}

こっちのほうが自然な感じです
とはいえ全部のページで Main を使う必要がありますし Outlet のような機能を持つ Router を使う場合だと Router でマッチしたコンポーネントは外側のコンポーネントの一部として表示する形になります
ページのコンポーネントがレイアウトを選択するのではなく ルーター側でレイアウト内にページを配置するようにしてるので こういう作りにならないです
いい方法はないものなんでしょうか
React で外部から DOM 操作するとき
React 外で DOM を直接操作するとき React の更新を防がないとダメという話を以前聞いたような気がします
クラスコンポーネント時代でいう shouldComponentUpdate で false を返すみたいなことを memo を使ってやらないといけないのかなと思ったのですが 何もしなくても特に問題なかったです

実際の DOM を書き換えても React は実際の DOM は無視して前回のレンダリング時の仮想 DOM と新しい仮想 DOM を比較し差分のみを更新します
なので 再レンダリングで変化する部分以外なら直接更新すればそのままです

const Component = () => {
const ref = useRef()

useEffect(() => {
const instance = init(ref.current)
return () => {
instance.destroy()
}
}, [])

return (
<div ref={ref}></div>
)
}

React では内部で要素の参照を持っているので 要素の間に要素を追加したりしても期待通りに動きます
state で更新する要素をドキュメントから切り離しても 見えないところで要素の中身が更新されています
関数で state を更新するときに更新後の値をその場で取得したい
state が更新されたら別の state も更新したいとき useEffect を使わず最初の state を更新するところで関連する state も更新したいです

const onClick = (event) => {
setState1(event.target.value)
}

useEffect(() => {
setState2(state1 + 1)
}, [state1])

にせず

const onClick = (event) => {
setState1(event.target.value)
setState2(event.target.value + 1)
}

(この例だと +1 するだけなので state にする必要ないし 処理が重たくても state1 から計算できるなら useMemo でいいけど これは説明を簡単にするためのものなのでそこは気にしないでください)

しかし 更新方法が setState の関数を使うタイプの場合に困りました

const onClick = useCallback((event) => {
setState1(prev => prev + 1)
// state1 の更新後の値がここではわからないので state2 を更新できない
}, [])

useEffect(() => {
// 仕方ないので再レンダリング時の useEffect で対処
setState2(state1 + 1)
}, [state1])

setState の関数の中で別の setState を呼び出すこともできますが なんか気持ち悪さがありますし それってどうなのと思います
そんな使い方を見た覚えもないですし 推奨される方法ではない気がします

完全に state1 と state2 の更新タイミングが揃うものならオブジェクトにまとめて 1 つの state にしてもいいのですが そうとも限らないです
それに state2 の更新としてるところが localStorage への書き込みだったり React の state とは関係ない処理の場合もありますし
useEffect が 2 回呼び出されて問題になるケース
最近は新しく環境を作るときに React 18 なので useEffect が 2 回呼び出される問題の影響が増えてきました
多くの場合はちゃんとクリーンアップ処理を書けば大丈夫なのですが そうもいかないケースがあります
例えば マウント時に state にデータを追加するような処理がある場合 2 回呼び出されるので 最初から 2 つの要素が存在することになります

const add = () => {
setState(state => [...state, { at: new Date(), value: Math.random() }])
}

useEffect(() => {
add()
const timer = setInterval(add, 1000 * 60)
return () => clearInterval(timer)
}, [])

あまり問題にはならないですが エラーがないと 1 つだけみたいなケースで 開発中は常に 2 つあることになって 実際のものと見た目が異なるので気持ち悪かったりします
また キーになる情報がタイムスタンプくらいだと useEffect が 2 回呼び出された場合はキーが同じになってしまうことがあります
開発時のみの都合で別のキーを自動生成したり index をキーにするのはあまり良い方法とも思えません

とりあえず最初の実行かどうかを ref を使って判断するのですが 追加で ref が増えますし 開発時の都合で特別なことをするのはあまり気持ちの良いものではないです

const ref = useRef(true)

useEffect(() => {
const is_first = ref.current
ref.current = false

if (is_first) {
add()
}
const timer = setInterval(add, 1000 * 60)
return () => clearInterval(timer)
}, [])

クリーンアップ関数を使ってできないかと思ってやってみたのはこれです

useEffect(() => {
const timer1 = setTimeout(add, 1)
const timer2 = setInterval(add, 1000 * 60)
return () => {
clearTimeout(timer1)
clearInterval(timer2)
}
}, [])

一瞬だけ遅延させます
そうすることで 2 回目の useEffect 呼び出しで 1 回目の処理をキャンセルできます
ref が不要で useEffect の中だけで完結するのが良いです
ただし僅かな遅延があるので 見た目上 ちらつき等になる可能性もあります

また 正常な動作として高速で 2 回呼び出された場合にキャンセルが発生します
それも気にするなら ref に頼ることになりそうです

最近は React で新しいものを作るたびこういう不満点を感じて別のフレームワークを探しては 不足点があってまだ使えないなぁを繰り返してます
React で maxlength 的なことをしたい
input 要素に maxlength 属性をつけると文字数制限ができます
ですが IME で変換している状態では 文字数制限を超えて入力できます
確定時に超えた部分は捨てられる挙動です

これができないと日本語入力だと不便です
例えば 2 文字入力できるところに「入力」と入力したいとします
ひらがなで 「にゅうりょく」 と入力してから変換するわけですが IME の変換中も 2 文字しか入らないと 「にゅ」 までしか入力できません
ローマ字入力だと 「nyu」 ですからこの時点でもオーバーしています

React などで input を制御するときは入力文字や文字数を制御して最初から入力できないようにすることがありますが そのときに maxlength の動きのように IME で変換中は入力不可の文字も一時的に入力できるようにしたいです

IME の状態の変化は compositionstart と compositionend イベントで取得できます
一応 input イベントの isComposing プロパティでもわかりますが これだとその入力が変換中のものかはわかりますが 確定されたことが伝わらないので compositionend イベントのほうがいいです

maxlength 相当なものを作ってみました

const App = () => {
const [text1, setText1] = useState("")
const [text2, setText2] = useState("")
return (
<div>
<Input1 value={text1} onChange={setText1} max={2} />
<p>{text1}</p>
<hr/>
<Input2 value={text2} onChange={setText2} max={2} />
<p>{text2}</p>
</div>
)
}

const Input1 = ({ value, onChange, max }) => {
const [composing, setComposing] = useState(false)
return (
<input
value={value}
onChange={(event) => {
if (composing) {
onChange(event.target.value)
} else {
onChange(event.target.value.slice(0, max))
}
}}
onCompositionStart={() => {
setComposing(true)
}}
onCompositionEnd={() => {
setComposing(false)
onChange(value.slice(0, max))
}}
/>
)
}

const Input2 = ({ value, onChange, max }) => {
const [local, setLocal] = useState("")
const [composing, setComposing] = useState(false)
useEffect(() => {
setLocal(value)
}, [value])
return (
<input
value={local}
onChange={(event) => {
const v = event.target.value
if (composing) {
setLocal(v)
} else {
const fixed = v.slice(0, max)
setLocal(fixed)
onChange(fixed)
}
}}
onCompositionStart={() => {
setComposing(true)
}}
onCompositionEnd={() => {
setComposing(false)
const fixed = local.slice(0, max)
setLocal(fixed)
onChange(fixed)
}}
/>
)
}

Input1 と Input2 があり どちらも IME の変換中は最大文字数を超えられます
Input1 は最大文字数を超えた状態でも onChange を呼び出します
input イベントに近い感じで 入力があれば不正状態でも親に伝えます
Input2 ではローカルステートを用意して IME の変換中は親には変更があったことを伝えず内部のステートのみ更新します

ここでは maxlength 相当の文字数処理しかしてませんが 電話番号入力欄で確定時に数字とハイフン以外を消すようにすれば 「でんわ」 から電話番号に変換できるよう IME に登録してるユーザーを考慮することができたりします

ちなみに React などで IME の変換中に文字数オーバーや使えない文字を消しても IME は内部で入力状態を持っているので 画面には見えないだけで変換候補はちゃんと入力したものとして出てきます
しかし 変換候補を切り替えるときに 見えている文字を置き換えるために元の文字数分を消して現在の変換候補の文字を入力するので その間で無理やり書き換えると関係ない文字が消えてしまい正しく入力できなくなります (Google IME で確認)

そもそも入力自体を不可にする処理っているのですかね?
React などで表示切替をアニメーションさせるとき
アニメーションで少し違和感を感じるところがありました
detail と summary タグ的なもので 高さを切り替えて表示の有無を切り替えているものです
UI ライブラリだとアコーディオンとか呼ばれてたりもします

devtools でゆっくりにしてみると 閉じてる途中で中身が消えていました
開くときには途中までアニメーションしてそこからは一気に本来の高さに切り替わっていました

React などのフレームワークならでは感がありますね
単純な開閉ならともかく 選択したものを表示する系だと 閉じてるときは選択中のものがありません
こういう感じで作ってると自然とそうなりそうです

<Accordion open={!!data}>
{data && <Internal data={data}/>}
</Accordion>

DOM を直接操作するなら選択が解除されても閉じるだけでよく わざわざ中身まで更新するのは面倒なので中身はそのままで放置だったりします
しかし React 的なものだと残すほうが大変です
開閉と状態と選択状態を別に保持して 閉じるのが完了したイベントで選択を解除するとか 選択と画面表示を別にして 選択されてなくても画面表示用のデータは最後の選択を保持するとかしないといけないです

開くときは その時点では開いたときの高さが不明なのでうまくアニメーションできないです
これは useLayoutEffect などでアニメーションさせればできそうな気はしますが ライブラリを使ってる箇所ではうまくいってないようでした
高さ不明なので適当な高さまでアニメーションで開いて 完了後に height を auto に切り替えることで実際の高さになるのですが そこで急にサイズが変わってました

DOM を直接触るとき以外は基本アニメーションはさせないようにして UI ライブラリのデフォルト挙動のみにしてましたが ちゃんとアニメーションさせようとすると大変そうです
関数の代わりにコンポーネントで渡すことでイベントとして扱えるようにする
前記事を書いていて関数のかわりにコンポーネントで渡すメリットもあるなと思ったので

まず前提ですが 関数を受け取り実行して結果を内部で使うコンポーネントがあります

const Component1 = ({ getValue }) => {
const [state, setState] = useState()

useEffect(() => {
getValue().then(value => setState(value))
}, [getValue])

return (
<div>{state}</div>
)
}

const getValue = () => {
return new Promise(resolve => setTimeout(resolve, 100, "VALUE"))
}

これの嫌なところは useEffect が入るところです

関数の代わりにコンポーネントで渡すと コンポーネントに渡した関数が呼び出されてイベントベースな動きにできます

const Component2 = ({ GetValue }) => {
const [state, setState] = useState()

return (
<>
<GetValue onValue={value => setState(value)} />
<div>{state}</div>
</>
)
}

const GetValue = ({ onValue }) => {
useEffect(() => {
getValue().then(value => onValue(value))
}, [])
return null
}

GetValue の中に useEffect はありますが これはそれをするためだけのコンポーネントです
Component2 の中に useEffect が存在しなくなるのが良いところです

元の Component1 がほぼ何もしないコンポーネントなのでこれだとあまり違いは感じられませんが もっと色々し始めると useEffect での呼び出しが減るのは少し助かります
API からのデータ取得はどこでする
WebComonents を使ってると やっぱり標準に合わせたくなります
img や iframe などを見ると URL だけ渡せば後はやってくれます
データを取得するのもコンポーネントに任せたいところです

ただ取得もやると 1 コンポーネントが重たくなるなーと思ったり
またデータはすでに別の理由で手元にあるときがあり そういうときでも URL から再取得するのはムダです
取得と使用で分けようかなと思ったものの どうしようか

書きやすさ的に React で考えます
React ならとりあえずフックでしょうか
フックがデータを取得し それを渡します

const data = useData(url)

return data.state === "loaded"
? <DataViewer data={data.content} />
: <Loader />

コンポーネントとして考えると取得するコンポーネントの props に取得したデータを使用するコンポーネントを渡すとかでしょうか

<FetchData url={url} Viewer={DataViewer} />

DataViewer に追加の props も渡したいことを考えると render 関数として渡す方がよいかもしれないです
内側に表示するものなので見た目的に children として渡します

<FetchData url={url}>
{data => <DataViewer data={data} />}
</FetchData>

これらだとすでにデータを取得している場合は DataViewer だけを使うことができます

反対に DataViewer の方を外側に持ってきて FetchData を渡す方法も考えられます

<DataViewer FetchData={FetchData} />

DataViewer が内部で FetchData へ onData 関数を渡して これが呼び出されたら自身の state に受け取ったデータをセットする感じです
FetchData は中でデータを取得し 取得したら props で受け取った onData 関数を呼び出します
DatViewer の props に data も用意してこっちがあればそのデータを使い なければ FetchData を使ってデータを取得とできます
この場合はわざわざ FetchData をコンポーネントにする必要もないので fetch に関数を渡すだけでもいいと思います

<DataViewer fetch={fetchData} />

ただこうするとイベント時の処理ではなく 自身で関数を呼び出すわけなので API を直接呼び出すのと大差ないです
useEffect とかが入ってきます
API を直接呼び出すのと比べて 受け取ったあとのフォーマットやエラー時の処理などをやらなくて済むくらいです

となると FetchData を外側に持ってくるものか フックを使うものです
子コンポーネントに渡すレンダー関数を渡すというのは React ならではのものなので フックの方でしょうか
フックも React 固有になるので実際にはただの関数として用意するようになりそうです

エラー表示を考えるとコンポーネントである FetchData の方が表示も管理できて便利かもしれません
ただエラー表示が固定であればです
使う側でカスタムするなら 結局エラー表示を制御するのでそれなら 関数にしてもいいかもです
ただデータの管理を親コンポーネントでしないといけなくなるので DataViewer がいっぱいあるところだと 使うほうが面倒にもなります
難しいですね
React で props で渡しても再レンダリングされないとは限らない
React で再レンダリングを防ぐために props として ReactElement を渡すといいとか言うのを見かけました
Parent コンポーネントの内側に Child コンポーネントを配置するとき Parent コンポーネントの中で Child を作るんじゃなくて Parent を作る側で Child も作って Parent に渡すのが良いとか
コードにするとこういうのです

const App = () => {
return (
<Parent />
)
}

const Parent = () => {
console.log("Parent")
const [state, setState] = useState(0)
return (
<div>
<button onClick={() => setState(state + 1)}>{state}</button>
<div><Child /></div>
</div>
)
}

const Child = () => {
console.log("Child")
return (
<div>child</div>
)
}

これの代わりに

const App = () => {
return (
<Parent>
<Child />
</Parent>
)
}

const Parent = ({ children }) => {
console.log("Parent")
const [state, setState] = useState(0)
return (
<div>
<button onClick={() => setState(state + 1)}>{state}</button>
<div>{children}</div>
</div>
)
}

const Child = () => {
console.log("Child")
return (
<div>child</div>
)
}

にします
App コンポーネントで Child をつくって Parent に渡しています
それで再レンダリングが防げるんだっけ?と思ったのですが Parent が返す ReactElement のツリーで {children} の位置が変わらないなら防げてます
例えば上記のコードだと 上側はボタンを押すたび Parent と Child がコンソールに表示されますが 下側だと Parent だけになっていて Child の再レンダリングを防げています

こういう場合には良さそうなのですが children として ReactElement を渡すところって children の表示条件や表示箇所が固定じゃなかったりします

const Parent = ({ children }) => {
console.log("Parent")
const [state, setState] = useState(0)
return (
<div>
<button onClick={() => setState(state + 1)}>{state}</button>
{state % 2 === 0 && (
<div>{children}</div>
)}
</div>
)
}

とか

const Parent = ({ children }) => {
console.log("Parent")
const [state, setState] = useState(0)
return (
<div>
<button onClick={() => setState(state + 1)}>{state}</button>
{state % 2 === 0 ? (
<div>{children}</div>
) : (
<span>{children}</span>
)}
</div>
)
}

上側はボタンを押すたびに children の表示非表示を切り替えます
非表示になると DOM の実体が消えるので 再度表示されるときに Child は再レンダリングされます

下側は常に children を表示していますがラップするタグが変わってます
親のタグが変わるだけでも同じものとして再利用されず再レンダリングされます

なのであんまり再レンダリングを防げてないんですよね
たしかに防げるケースはあるものの これで確実に防げるというものでもないです

以前 React 18 が出る前くらいにアンマウントされても state などを維持して再利用できる機能が紹介されてた気がしますが React 18 の新しいドキュメントではそれらしいコンポーネントやフックを見ないですし どうなったのでしょうね
コンポーネント関数と普通の関数
コンポーネントの関数って props を受け取って JSX の要素を返します
入出力は普通の関数と同じで 違うのはコンポーネント関数は React が呼び出して中でフックが使えるということ
普通の関数でフックを使うこともできますが それはフック関数という扱いになってフックのルールに則って使う必要があります

フックを使わないならコンポーネントにする必要がないようにも思います
例えば

{user && <UserInfo user={user} others={others}/>}

の代わりに

{user && UserInfo({ user, other }) }

と書いても動きます

そう思ったきっかけは逆で コンポーネント内で処理をまとめるために関数化していた部分があって 引数は 1 つのオブジェクトで JSX の要素が返ってくるものでした
これならコンポーネント化しても良さそうと思ったのですが むしろコンポーネントにする必要があるのかと疑問に思いました

コンポーネントだと React.memo が使えて props が同じなら再レンダリングを避けれます
ですが React.memo ってそんなに使うことがなくて 明らかに遅いようなところだけです
フックも使わないような小さいコンポーネントなので基本は memo しないものです

扱いやすさ的には普通の関数のほうが好きです
ただ その利点は React に依存しないという点が大きいですが フックを中で使わないなら命名規則くらいの差しかありません

見やすさ的には 上の例みたいに {} の中で使うならどっちもどっちですが そうでないなら普通の関数は実行するために必ず {} が必要になり 読みづらくなります
コンポーネントのほうが HTML ライクな記法で書けて見やすいです

迷うところですが 将来的にフックを使いたくなるかもと考えればとりあえず最初からコンポーネントにするのが無難かもしれないです

開発時に限れば コンポーネントにすると strict モードで複数回レンダリングされます
マウントされるコンポーネントが増えるほど重たくなります
普通の関数だとこれがないので少しですが速いです
本番ビルドには影響しないものなので これを理由に選ぶのはどうかとは思いますけど
React だとリアルタイム反映のほうがやりやすい
React などのライブラリを使わず DOM 操作で作ってるページだと なにかが変わったときにリアルタイムにあっちもこっちも反映って面倒です
だから確定ボタンを押したみたいなときに全体に反映させて それまでは外部に影響させないとか最小限の部分だけに反映させたりということが多いです

そうなってるものを動きを変えずに React で置き換えそうとすると 逆に面倒なんですよね
React だと state に持ってる情報から今の画面状態を作るので普通にやると全部が変わってしまいます
確定ボタンを押すまでは他に影響しないようにしたいなら 新たに state を用意する必要があります
グローバルの state と ローカルの state を用意して 普段はローカルの方だけを更新して ボタンが押されたらローカルをグローバルに反映するみたいなことになります
(グローバルと言ってもその state を使う範囲でのグローバルなのでページ内だったりタブ内だったりで プログラム的なグローバル変数というわけではないです)

React だと state 特にコンポーネントローカルの state はあまり増やしたくないのですよね
ローカルに state を持つことでそれを更新するための処理も必要になります
特に props や別フックの更新に応じて state を更新する必要があれば 変更を監視して更新するための useEffect もセットで必要になります

フォームや state 系のライブラリを見てみても 他の値の更新で state を更新する必要がある以上 どれもこの問題あるように思うのですが いい感じに解決できるものがあるのでしょうか
React コンポーネントと値の初期化
今でもスッキリしない React での初期化処理の扱い方

const Component = (props) => {
// ...

return (
<div>
<Edit
value={value}
onChange={onChange}
/>
</div>
)
}

Edit コンポーネントがあって何かを編集するものです
React なので value を渡して編集があれば onChange で受け取って親側で更新するという 親側で state を管理する作りです
これだと value の値を初期化やチェックして修正するときに親側でやらないといけなくなります
ですが そのロジックは Edit のものなので Edit でやってもらいたいです
そうなると ユーザーの操作なしで Edit のマウント時に onChange を呼び出して更新することになります
仕方ないのですがそれはなんか気持ち悪さがあります

親で state 管理しないものなら 初期値を渡すだけで修正した値は Edit の中で持っておいて 親が値を使いたいときに Edit から取り出します
これだと自然なのですよね

Edit コンポーネントのモジュールから関数もエクスポートすれば ロジックは Edit モジュールの中で持てます
しかし Edit を使うのに 初期値を作る部分とコンポーネント部分で 2 つの処理が必要になるのが面倒です
例えば API で選択肢を取得して初期値が null なら最初の選択肢を初期値にする場合 初期値のために関数で API を呼び出して コンポーネント内でも選択肢を作るために API を呼び出す必要があるので 関数とコンポーネントの両方で API を呼び出すことになります
それを避けるなら関数は初期値の他に選択肢も返して それもコンポーネントに渡すという作りになりますが やることが増えます
やりたいのは単純に Edit コンポーネントを使って props を渡すだけにしたいのですけど

onChange の場合でも困るところはあります
変更イベントを beforeunload などと関連付けていれば ユーザーは何も編集してないのに変更ありとして扱われます
初期化が null の場合に値を入れたり 不正文字を除去など繰り返し行っても問題ないものならいいですが そうでない場合は判断するのが難しいです
入力値を制御するライブラリでは onChange を即呼び出すものを見かけますが 組み合わせるライブラリの相性とかもあるのか ときどきうまく動いてなかったりするのですよね
レンダリング中に同期的に呼び出すと問題ありそうですし useEffect 通しても その他の useEffect との順序が影響しそうです
React 18 で unmount 後に state を更新したときの警告がなくなってた
未だに React 18 より 16 や 17 を使ってることが多いです
そんななので今更気づいたのですが 18 では コンポーネントのアンマウント後に state を更新したときに出ていた警告がなくなっていました

こういうの

Warning: Can't perform a React state update on an unmounted component.
This is a no-op, but it indicates a memory leak in your application.
To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.

表示させる例

export default () => {
const [shown, setShown] = useState(false)
return (
<div>
<button onClick={() => setShown(!shown)}>toggle</button>
{shown && <Component/>}
</div>
)
}

const Component = () => {
const [count, setCount] = useState(0)
const onClick = () => {
setTimeout(() => {
setCount(count => count + 1)
}, 3000)
}
return (
<div>
<button onClick={onClick}>{count}</button>
</div>
)
}

toggle ボタンのクリックで Component の有無を切り替えます
Component はボタンをクリックするとカウントアップするコンポーネントですが 3 秒ディレイしてからカウントアップします
この 3 秒の間に Component を表示しないようにすると アンマウントされて その後に state の更新が起きるので警告が出ます

実際のケースでは fetch など非同期処理を行うコンポーネントで その処理が完了する前に別のページに遷移したりタブを切り替えたりすることで発生することが多いです

これの対処をちゃんとやると面倒です
アンマウント済みかを示すフラグを用意してアンマウント時に更新します
state 更新時にはフラグを見てアンマウント済みなら更新をスキップします
面倒な上にコードも複雑化する嫌なやつです

実際こういう一回限りの setState がアンマウント後に行われてもメモリリークにはならないです
それでも警告として出る以上対処しようとして その結果 面倒が多く複雑になるという問題があったのでなくしたのかもですね

メモリリークになるのは setInterval や外部のサブスクリプションなどで 1 回限りではなくイベントが起きるたびに更新が発生するものです
こういうのです

useEffect(() => {
setInterval(() => {
setState(Date.now())
}, 1000)
}, [])

この警告は出ていても無視していいケースが多かったですが 無くしてしまうとこういうケースに気づけないという問題はありそうです
JSX の ?: と && と key
React で条件演算子を使って

<div>
{value ? <Foo/> : <Bar/>}
</div>

みたいなのがあるときで どちらの場合も同じコンポーネントになる場合は注意が必要です
上の場合の Bar も Foo になっていて value 問わずコンポーネントは Foo になり props の違いだけの場合
こういう記法の時点で書く側は別物のつもりだと思いますが React が認識する ReactElement のツリー構造ではどっちも同じ Foo です
なので props が変わっただけという扱いになりコンポーネントはアンマウントされず使いまわされます
基本は props が変わればそれに応じて表示も変わりますが中で state を持っていた場合には state が引き継がれることで予期しない動きになります
その対処のために key を指定する必要があります
key が変われば同じコンポーネントでも別物扱いになり 既存のコンポーネントがアンマウントされ新規のコンポーネントがマウントされるので state はクリアされます
key は配列を返す場合に使うことが主ですが それ以外で必要になるパターンです

これをみると

<div>
{value && <Foo/>}
<Foo/>
</div>

のように条件付きで表示する場合 これも key がないとズレてしまいそうに思います
value が false から true になって Foo が 2 つ並ぶようになるとき それまでにあった常に表示されている Foo は 2 つめになり新しい Foo が先に来るはずです
でも React 的には Foo が 1 つから 2 つになっただけで順番的に最初の Foo が使い回されるのじゃないかと思いました
しかし試してみるとそういった事は起きません
期待通りに新しい Foo が先に挿入されます
どうなってるの?と思いましたがよく考えてみると 表示しない場合でも {} の中に undefined や null が残ります
これを無視しなければどこに新しく追加されたか判断できそうです
なので 配列以外で key を考えないといけないのは ?: くらいで && の場合は気にしなくても大丈夫そうです

{} に undefined を残さずひとつにまとめてこういうことをする場合は期待する通りには動きませんが これは配列を使ってるわけなので key が必要なケースです

const Component = () => {
const elems = []
if (value) elems.push(<Foo/>)
elems.push(<Foo/>)
return <div>{elems}</div>
}



?: に key を使うとき 同じ階層で同じコンポーネントの場合 key が重複しないよう注意が必要です

const App = () => {
const [state, setState] = React.useState(false)
return (
<div>
<button onClick={() => setState(!state)}>switch</button>
{state ? <Foo key={1} value={1}/> : <Foo key={2} value={2} />}
{state ? <Foo key={2} value={3}/> : <Foo key={1} value={4} />}
</div>
)
}

1 つめの {} と 2 つめの {} では完全に別物のつもりですが同じコンポーネントで階層が同じなので 再レンダリングの前後で同じ key が別の場所にあると使い回されます
state を切り替えると key の 1 と 2 が入れ替わるので 配列の並び替えと同じ扱いになります
配列でラップしたり <></> でラップすると展開後に階層が同じであっても 別階層として扱えるので key の重複がありえそうならラップしてしまうと安全です

<div>
<button onClick={() => setState(!state)}>switch</button>
<>{<Foo key={state ? 1 : 2}/>}</>
<>{<Foo key={state ? 2 : 1}/>}</>
</div>
<div>
<button onClick={() => setState(!state)}>switch</button>
{[
[<Foo key={state ? 1 : 2 }/>],
<Foo key={state ? 2 : 1 }/>,
]}
</div>

同時に同じ階層に同じ key が出現した場合は 作り直しや使い回しが意図したとおりにならないだけでなく表示もおかしくなります
例えばこの場合 state が変わるタイミングで同じ要素が繰り返し作られて要素が増えていきます

<div>
<button onClick={() => setState(!state)}>switch</button>
{[1,2,3].map(x => <div key={state ? 1 : 2}>{x}</div>)}
</div>

こういう同時に同じ key が存在するケースはコンソールに key の重複エラーが出てるので気づけるはずです
同時ではない場合は正規の入れ替え目的として扱われてエラーにならないので見逃しやすいです
react-hook-form の感想
便利そうかもと思ったらやっぱり違うなと思ったりで ちゃんと使うことはなかったライブラリですが 使われてるコードをいじる機会があったので色々使ってみた感想です
簡単に言うと React ぽくなくなったり 単純なフォームじゃなくなると useEffect だらけで辛くなるので そんなに積極的に使いたいものではないです

react-hook-form を使うと 内部で state を管理してくれて 再レンダリングを防げます
各 input が props ではなくコンテキスト内の form から値を取得するので 親の再レンダリングが不要になり 変更があった input のコンポーネント内だけが再レンダリングされます
表示内容が多いページで memo なしだと入力ごとに再レンダリングされる範囲が広くなり 重たくなったりしますが それを防げます

フォームの制御が特になく 項目が多いだけのフォームだと楽に使えて便利です
しかし 入力値の変化に応じてあちこちを変更する場合は面倒になってきます

useEffect(() => {
const subscription = watch((values, { name, type }) => {
if (name === "foo") {
setValue("bar", values.foo)
} else if (name === "bar") {
setState(values.bar)
}
})
return () => subscription .unsubscribe()
}, [watch])

みたいに callback を指定する watch を使い form の値や state を更新していくことになります
useEffect が必要です

更新された場所の名前はわかりますが 更新前の値はわかりません
変化した内容に応じて処理を変えたいなら自分で前の値を保持する仕組みを作っておかないといけません

また イベントハンドラ的な部分での処理ではなく render 関数中の処理で現在の form の値を使いたいなら 名前指定で watch を使います

const [foo, bar] = watch(["foo", "bar"])

この場合はどれが変わったのかわかりません
また 変わるたびに この watch を使ったコンポーネントが再レンダリングされるので useState に入れているのと変わらなくなります
ものによっては form の値のほとんどを watch に入れることになり useState と対して変わらないってこともあります

watch で取得した値を使うのが hook だと仕方ないですが JSX 部分なら input 以外でも form を参照するようにしてしまうのもありです
input のように controller を使って値を取得すれば watch にする必要がなく コンポーネント全体の再レンダリングを減らせます

input に使う以外の form とは関係ない値でも form に入れてしまい state にせず form で管理するのもありかもです
そうすれば form の処理だけに統一できますし 名前指定の watch を減らせます
それに コンポーネント間の共有でコンテキストのようにも使えます
親コンポーネントで form に関数を入れて 深い部分のコンポーネントでそれを参照します
ただしあくまでライブラリとしては form なのでこの使い方はいいのだろうか?という気はします
想定されたことを外れると将来的なアップデートで面倒な目に合うのは十分考えられます
そういう使い方のものを別に用意したほうがいいのかもという気はしてます

深い部分で watch を使う注意点ですが 使ったコンポーネントだけが再レンダリングされそうに見えてしまいますがそうではないです
React の仕組み的にそういう手段はなく useForm を使ったコンポーネントが再レンダリングされます
その結果 その子孫コンポーネントも再レンダリングされてそうみえるだけです

こういうことをしてみるとわかりやすいです

const Input = ({ control, name }) => {
const { field } = useController({ control, name })
return <input {...field} />
}

const Child = ({ control, watch, name }) => {
const value = watch(name)
console.log("render Child", name)
return (
<div>
<div>input value: {value}</div>
<Input control={control} name={name} />
</div>
)
}

const MemoChild = React.memo(Child)

const Parent = () => {
const { watch, control } = useForm({ defaultValues: { a: "1", b: "2", c: "3" } })
console.log("render Parent")
return (
<div>
<Input control={control} name="a" />
<hr />
<Child control={control} name="b" watch={watch} />
<hr />
<MemoChild control={control} name="c" watch={watch} />
</div>
)
}

Parent で useForm して watch を Child に渡します
Child で watch していて その input を書き換えると 「render Parent」 もログに表示されます
Parent コンポーネントも再レンダリングされています
また Child をメモした版の MemoChild を使うと props の control, name, watch には変更がないため MemoChild コンポーネントは再レンダリングされません
結果 Parent は再レンダリングされるのに MemoChild は再レンダリングされず 「render Child c」 はログに表示されません
画面上でも input の現在の状態を表示しているのですが ここが更新されず初期状態のままです

また props や state を見て ちょっと加工するというだけでも useEffect で setValue が必要になります
普通の React なら render 関数内で計算したり JSX 内に式を入れるくらいで済むことのために useEffect が増えていくのは見通しが悪くなるので不満です

さらに問題になるのはあちこちで同じ値を更新したいときです
useEffect やイベントハンドラの中で setValue することになりますが 特に useEffect だと呼び出し順がわかりづらいです
あちこちのコンポーネントから setValue されるとどういう順で呼び出されて値が変わるのかデバッグしづらく 思い通り動かないことがけっこうありました
通常の React なら useEffect は極力なくせて render 関数内での処理です
親から子の順ですし レンダリングごとにイミュータブルのはずなので 困ることは少ないです

完全に独立した入力項目で 初期値以外は外部から変更されることがないような場合はシンプルに書けて良さそうですが 制御する部分が多いと不満点が目立ちます
やっぱり 素の React で state 管理してる方が見やすくていい気がしますね
React で hook はコンポーネントを分けたい
React の hook は便利なんだけど 作るものが大きく複雑になってくるとコンポーネントも複雑になってくる
やっぱり props として受け取った状態から HTML を作るだけ のコンポーネントのほうがスッキリしていて好き

そうするためにコンポーネントを 2 段にして外側では hook を使ってデータを取得したり加工したりするだけで そのデータを内側コンポーネントの props として渡す
内側コンポーネントは hook は使わず 受け取った props から画面を作る JSX を作って返す

スッキリはするけど長くなるのが面倒
それにこの感じ Redux が流行ってた頃に見かけた作り方に近そう
自分でそういうのは作ってないのであまり詳しくはないけど プレゼンテーションコンポーネント?みたいな名前がついてて ストアと通信してデータを受け取る部分と 副作用を持たない props から JSX を作るだけのコンポーネントに分けていたと思う
hook になってもそれでいいのかな と思ったけど こう分けるのは今では推奨しない みたいなのも見かけた
直接 hook を使えばいいから らしいけど使うとコンポーネントがごちゃごちゃしてくるし テストもしづらくなる

useMemo や useCallback はパフォーマンスの都合なもので なくても動作は変わらないからどっちでもいいけど useEffect は入るとかなり複雑化するから コンポーネント外に持っていきたい
useState は関数の引数として状態と更新関数を受け取るのと同じと聞くこともあるけど リセットしたいときとか テスト時のダミーデータを渡すことを考えたらやっぱり違うかなって思う

WebComponents だと標準の HTML 要素みたいなものとして考えればいいので内部で状態を持って外部通信もしてっていうのはありだと思う
img タグとか画像を取得して表示してるし
でも React の考え方だと 親が子のメソッド呼び出したり 自由に子の状態を取り出したりしない
親で最初から持っていて子に渡すだけ
その考え方だと コンポーネント内で外部通信とか状態を保持とか色々しないほうがいいようにも思う

とりあえず 外部通信したりアプリ固有のロジックでデータを変換したり計算したりみたいのをコンポーネントの中じゃなくて外でやってコンポーネントはその結果を受け取るだけにしたい
setState の実行タイミング
ライブラリがちゃんと動いてくれなくて 内部挙動調べるために console.log すると実行順がよくわからないことになってて React だけでの動きを調べたメモ
コールバックタイプの setState を使って 同期的に複数回呼び出されていたので それだけの動きを確認するためにこんな感じのページを用意

const App = () => {
const [state, setState] = useState(0)

const onClick = () => {
console.log("ONCLICK")

console.log("pre setState(1)")
setState(prev => {
console.log("setState(1)")
return 1
})

console.log("pre setState(2)")
setState(prev => {
console.log("setState(2)")
return 2
})

console.log("pre setState(3)")
setState(prev => {
console.log("setState(3)")
return 3
})
}

return <button onClick={onClick}>{state}</button>
}

この画面を開いてボタンをクリックしたときのログは

ONCLICK
pre setState(1)
setState(1)
pre setState(2)
pre setState(3)
setState(2)
setState(3)
setState(2) ///
setState(3) ///

/// を書いてる行は React の devtools を入れてると薄く表示されるログ

もう一度ボタンを押すと少し変わって

ONCLICK
pre setState(1)
pre setState(2)
pre setState(3)
setState(1)
setState(2)
setState(3)
setState(1) ///
setState(2) ///
setState(3) ///

最初の 1 を見ると同期的に実行されてるようにみえるけど 2, 3 を見るとコールバックは即時呼び出されずあとになって呼び出されてるのがわかる
薄く表示されるのは Strict モードで 2 回レンダー関数が呼び出されるときに 2 回目の実行中に呼び出されたものだったと思うけど そんなタイミングで実行されてるの?
コンポーネントの関数の最初に console.log を入れて試してみると本当にそんなタイミングで呼び出されてた

二回目のボタンでは setState の 1 より先に 2 や 3 の pre が呼び出されているのも気になる
どっち先かは運次第のランダムかなと思ったけどリロードして試すと再現性がある

薄いところだけ無視すると一回目は 2 と 3 だけ消えて 1 は残るという変な状態になるから console.log デバッグのときは Strict モードを一時的に無効にしたほうがわかりやすいかも

ちなみに原因になったライブラリは props で受け取った値が変わったときに state を更新するために useMemo + setState するとか変なことしてるのが多くて色々辛かった

useMemo(() => {
setState(value_prop)
}, [value_prop])

みたいなの
メモの中で副作用起こさないでほしいし useEffect か ref に保持して if 文でやってほしい
React で親コンテキストに伝播させる
React のコンテキストの仕組みは useContext を使ったコンポーネントからもっと近い親のプロバイダーコンポーネントの value に渡された値を取得するというもの
ネストしたときに一番近いものを参照できていいんだけど あるイベントが起きた回数を共有するみたいなときに最も近いコンテキストだけじゃなくて更に上にも伝わってほしい
標準の仕組みでそういうことをするものはなさそうなので 自分でそうなるように作る

基本の形

const CounterContext = createContext()
const CounterProvider = CounterContext.Provider
const useCounter = () => useContext(CounterContext)

const useNewCounter = () => {
const [count, setCount] = useState(0)
return {
count,
up: () => setCount(x => x + 1)
}
}

const Parent = () => {
const counter = useNewCounter()
return (
<CounterProvider value={counter}>
<Button />
<hr />
<Child />
<hr />
<Button />
</CounterProvider>
)
}

const Child = () => {
const counter = useNewCounter()
return (
<CounterProvider value={counter}>
<Button />
<Button />
</CounterProvider>
)
}

const Button = () => {
const counter = useCounter()
return (
<button onClick={counter.up}>{counter.count}</button>
)
}

単純にネストしただけなので これだと Child の中のボタンを押しても Child のカウンターしか増えない
Child をこうして Parent のカウンターの up も呼び出す

const Child = () => {
const counter = useNewCounter()
const parent_counter = useCounter()
const chain_counter = {
...counter,
up: () => {
counter.up()
parent_counter.up()
},
}
return (
<CounterProvider value={chain_counter}>
<Button />
<Button />
</CounterProvider>
)
}

毎回書きたくないのでフックにまとめる

const useChainCounter = () => {
const parent_counter = useCounter()
const counter = useNewCounter()
return {
...counter,
up: () => {
counter.up()
parent_counter?.up()
}
}
}

const Child = () => {
const chain_counter = useChainCounter()
return (
<CounterProvider value={chain_counter}>
<Button />
<Button />
</CounterProvider>
)
}
useMemo と Promise
state から計算できる値で毎回計算したくないのを useMemo でメモ化していました
あるとき重めの処理の中に async 関数が入り await が必要になりました
レンダリング処理の途中に await は書けないです
await しないと useMemo の結果は Promise です

const promise = useMemo(() => {
return fn(foo, bar)
}, [fn, foo, bar])

こんなことを考えてみましたが React ではプロパティが途中で変わっても再レンダリングされなければ画面に影響しません

const value = useMemo(() => {
const result = {
value: null,
}
fn(foo, bar).then(result_value => {
result.value = result_value
})
return result
}, [fn, foo, bar])

resolve されたタイミングで再レンダリングさせるには state を更新するしかないです
setState するなら useEffect の中なのでこんな感じになります

useEffect(() => {
const promise = fn(foo, bar)
setState(null)
promise.then(result => {
setState(result)
})
}, [fn, foo, bar])

できれば避けたいやつです
毎回そういう事するのは避けたいのでフック化してこういう感じです

const usePromise = (promise) => {
const [value, setValue] = useState(null)
promise.then(result => setValue(result))
return value
}

const value = usePromise(promise)

ただこれだけだと promise が変わったときにリセットできないのでやはり useEffect は必要です
また何度も then で関数を設定することになるので 1 回だけで済むように useEffect の中で行うようにします

const usePromise = (promise) => {
const [value, setValue] = useState(null)
useEffect(() => {
setValue(null)
let alive = true
promise.then(result => {
alive && setValue(result)
})
return () => {
alive = false
}
}, [promise])
return value
}

const value = usePromise(promise)

引数の Promise は state に入れたり useMemo を使って同じ Promise を渡すようにしないと無限ループになります

const value = usePromise(Promise.resolve(1))

だと then の中で setValue(1) が実行されて再レンダリングされるたびに Promise が新しくなり また useEffect 内で then がセットされてのループです
ちょっとした使い方のミスで無限ループするのは扱いづらいのかもしれないです
useMemo をグローバルで共有したくなる
state から作れる値は state に保存せずレンダリング時に毎回作るわけですが 重くなる処理は毎回作り直したくないので useMemo を使って毎回計算しないようにします
ただ その state を複数のコンポーネントに渡してそれらのコンポーネントでも同じ計算結果を使いたいことがあります
state とそれから作った値をまとめて props で渡すこともできますが 渡す値を増やしたくはないです
state があれば作れるのですから 本来渡すべきデータは state だけのはずです

でもそうすると state とそれから作る値を使う全部のコンポーネントで個別に useMemo で再計算なんですよね
なのでグローバルな useMemo が欲しいというわけです

グローバルならフックに頼れなくなりますが モジュール内に共通の保存する場所を作れば済みます
同じ state を指定したら同じ値になるとすると 同じ state から別の計算によって値を作るケースに対応できないので それを見分けるキーが必要になります
同じ state とキーならどのコンポーネントからでも同じ結果を返します
同じ計算なら関数も同じになるはずなので 関数をキーにします

const data = []
const isSame = (a, b) =>
a.length === b.length &&
a.every((v, i) => v === b[i])
const globalMemo = (fn, deps) => {
const key = [fn, ...deps]
const item = data.find(x => isSame(x.key, key))
if (item) return item.value
const value = fn(...deps)
data.push({ key, value })
return value
}

これでいい感じに動きはするのですが data に古いデータがずっと残ってしまいます
保存を直近◯件に制限したり 保存したのが古くてもよく使うのは残すようにソートするなど工夫が必要そうです
フォームを管理するツールを使うより直接コンポーネントを使うほうが楽な場合
選択肢から選ぶ入力箇所が A, B の 2 つ
B の選択肢は A の選択に依存

const Component = () => {
const [a, setA] = useState("")
const [b, setB] = useState("")

const b_options = {
"": ["", "1"],
"foo": ["", "foo1"],
"bar": ["", "bar1", "bar2"],
}[a]

return (
<div>
<Select
options={["", "foo", "bar"]}
value={a}
onChange={selected => setA(selected)}
/>
<Select
options={b_options}
value={b}
onChange={selected => setB(selected)}
/>
</div>
)
}

A で foo を選ぶと B の選択肢は未入力か foo1 のどちらか
foo1 が選ばれてるときに A を bar に変更したら?

A が bar のときの B の選択肢に foo1 はないので B をリセットする必要がある
b_options が決まったあとに修正済みの B の値を作ってそれを props で渡す

const Component = () => {
const [a, setA] = useState("")
const [b, setB] = useState("")

const b_options = {
"": ["", "1"],
"foo": ["", "foo1"],
"bar": ["", "bar1", "bar2"],
}[a]

const fixed_b = b_options.includes(b) ? b : ""

return (
<div>
<Select
options={["", "foo", "bar"]}
value={a}
onChange={selected => setA(selected)}
/>
<Select
options={b_options}
value={fixed_b}
onChange={selected => setB(selected)}
/>
</div>
)
}

修正済みの B の値は state には入ってないけど save などの関数で参照したい場合に fixed_b を見ればいい

Controlled なコンポーネントで直接 props を渡せる場合はこれでいいけど ライブラリなどでフォームの値を管理してると 直接 props を渡して今の値を指定できないこともある
フォームのオブジェクトの setValue などを呼び出して B の値を変更することになる
更新処理なので副作用になって useEffect の中で呼び出す
これだけのために useEffect はあまりしたくない

A が変わった時点で B の選択肢と B の値が変わることも確定なので A の変更時のイベントで B の選択肢と B の値も更新?

const onChange = selected => {
setA(selected)
const b_options = {...}[selected]
setBOptions(b_options)
setB(b_options.includes(b) ? b : "")
}

B の選択肢は API で取得する可能性もある
onChange で API 呼び出しで選択肢の更新までするのはあまりやりたいものじゃない

変にフォームを管理するよりシンプルに Input 系コンポーネントを扱うほうが楽なことも多い気がしてきた
React のコールバック ref
あるのは知ってましたがたぶん初めて使いました
ref props というと いつも useRef で取得したオブジェクトを渡していました
それだけで困らなかったですし そもそも ref を渡すこと自体ほとんどなかったですし
ですがコールバック ref を使う機会がありました
React 標準以外の方法でイベントリスナをつけたい場合に使えます

例えばこのコードでは state に保存しているカウントが偶数のときのみ h1 を表示します
ref はこの h1 を参照させます
イベントリスナはクリックにつけているので h1 をクリックするごとに console にメッセージが表示されます

const App = () => {
const [count, setCount] = useState(0)
const ref = useRef()

useEffect(() => {
console.log("effect")
const handler = () => console.log("clicked")
ref.current?.addEventListener("click", handler, true)
return () => ref.current?.removeEventListener("click", handler, true)
}, [ref.current])

return (
<div>
<button onClick={() => setCount(count + 1)}>{count}</button>
{(count % 2 === 0) && (
<h1 ref={ref}>click me</h1>
)}
</div>
)
}

useEffect の依存配列で ref に設定された要素の変更を監視して変更があったら再設定してます
これをしないで依存配列を空にしてると h1 が一旦消えて再表示されたときに h1 要素が別物なのでクリックしても反応しなくなります

コールバック ref だと要素が変更されたときに要素を引数に受け取って関数を実行できます

const App2 = () => {
const [count, setCount] = useState(0)
const ref = useRef()

const refCallback = (elem) => {
const prev = ref.current
ref.current = elem

const handler = () => console.log("clicked")
prev?.removeEventListener("click", handler, true)
elem?.addEventListener("click", handler, true)
}

return (
<div>
<button onClick={() => setCount(count + 1)}>{count}</button>
{(count % 2 === 0) && (
<h1 ref={refCallback}>click me</h1>
)}
</div>
)
}

新しい要素にリスナをつけるだけなら useRef は不要ですが 前の要素を保持してリスナを削除するようにしてます
React はレンダリングも副作用も 2 回呼び出されるかもみたいなのが多いですし念のため

今回の例だと意味ないですが リスナを capture フェーズに付けてます
一部の bubbles が false なイベントを親で受け取ったり ライブラリなどが stopPropagation する場合でも受け取りたいとかで結構使うシーンはあるのにこういうことしないとリスナ設定できないのは不便ですね
react-hook-form は変更時の処理をまとめて行えない?
react-hook-form のドキュメント見ていて useForm に onChange がないことに気づいた
個別の onChange でやれってことかもしれないけど 相互に関連する入力が多くて連鎖して変化する場合にそれは使いづらい
A を変更したら B も変わって B と C が空欄になったら D もクリアして みたいなルール
form 全体の onChange イベントでまとめて処理したい

渡せる物を探すと resolver というのがあって values を受け取って values と errors を返す関数を設定できるみたい
これがまとめて変更管理するのを兼ねてるのかと思ったけど values に修正済みを渡しても form の値に変化なし
バリデーション用らしいし 実際の入力値に影響は与えられないみたい

自分で作ってる簡単なフォーム制御するものだと逆に reducer 風にまとめて扱うもののみだから この制限があるのはかなりつらめ
そういう方針のものなんだろうと諦めたところで watch を発見
変更時に再レンダリングするためのものって思ってたけど 再レンダリングなしでコールバック関数を呼び出してもらう使い方もできた

const Component = () => {
const { register, watch, setValue } = useForm()

watch((values, { name, type }) => {
console.log(values, name, type)
const set = (k, v) => {
if (values[k] === v) return
setValue(k, v)
}

if (name === "A") {
set("B", values.A)
}
if (name === "B" || name === "C") {
if (values.B === "" && values.C === "") {
set("D", "")
}
}
})

// ...
}

setValue を使った変更でも watch のコールバックが呼び出されたので 連鎖して変更するときにユーザーの入力で呼び出されたときに一度にまとめて setValue せず ひとつずつ setValue して毎回 name を見ての判定にもできる(無限ループ注意)
A が変われば B も変わり B が変われば C も変わるときに A の変更を検知したら B と C を一度に書き換えず B だけ変える
B が変わればまたコールバックが呼び出されて B の変更なので C を変える という感じ



(注)
この例だと手抜きで省略してるけど コールバック登録型の watch は useEfffect の中でしないとだめ
再レンダリング時にリスナが上書きされたりはせず毎回追加で登録される
レンダリングのたびコールバック関数が呼び出される回数が増えて重くなる
useEffect で初回だけ実行するようにしないといけない
外部の変数を使うために useEffect の依存配列を使うなら 複数回 watch を呼び出すことになるので古い watch の解除が必要
watch の返り値を受け取って useEffect が返す関数の中でサブスクリプションの解除をする
React で props の変更時にリセットしたいとき
コンポーネントが内部で state を持っているときに親から受け取る props に応じてリセットしたいことがある
親が key を変えることでコンポーネントが作り直されるけど 使う側で追加でやることを増やしたくない
忘れて思い通り動かないケースとか出てきそう
なので内部で制御するように useEffect を使って props の変化を検出して setState で更新ってやってることが多い

新しい作りかけドキュメントの方では useEffect をこの目的で使うことを推奨していなくて key を使うほうがいいと言ってる
ドキュメントにまで書かれるならその方向にしようかなと思うけど やっぱり面倒
間にコンポーネントを挟んで 指定の props の変化時に key の制御は自動でやるようにする

const withReset = (Component, names) => {
const diff = (a, b) => {
return names.some(name => a[name] !== b[name])
}
return (props) => {
const ref = useRef({ key: 0, props: {} })
if (diff(ref.current.props, props)) {
ref.current = { key: ref.current.key + 1, props }
}
return <Component key={ref.current.key} {...props} />
}
}

・使用例
Component のボタンを押すとカウントアップ
foo の入力が変わったときのみ key が新しくなってカウントがリセットされる
bar, baz を変更してもカウントは維持される

const Component = withReset(({ foo, bar, baz }) => {
const [num, up] = useReducer(s => s + 1, 1)
return (
<div>
<div><button onClick={up}>{num}</button></div>
<div>foo: {foo}</div>
<div>bar: {bar}</div>
<div>baz: {baz}</div>
</div>
)
}, ["foo"])

const App = () => {
const [foo, setFoo] = useReducer((_, eve) => eve.target.value, "foo")
const [bar, setBar] = useReducer((_, eve) => eve.target.value, "bar")
const [baz, setBaz] = useReducer((_, eve) => eve.target.value, "baz")
return (
<div>
<div>foo: <input value={foo} onChange={setFoo} /></div>
<div>bar: <input value={bar} onChange={setBar} /></div>
<div>baz: <input value={baz} onChange={setBaz} /></div>
<Component foo={foo} bar={bar} baz={baz} />
</div>
)
}

作ってみて 以前似たようなもの作った気がすると思って探すとあった

以前は useMemo を使ってたらしい
新規の key 作成と 変化ありの検出をまとめてできてるし良さそう
今のよりスッキリしてる
以前の方が良かったって成長してないどころか劣ってる……

でも useMemo って React がメモリの都合で勝手にメモを忘れて再計算するかもしれないって言ってるから安全ではないかも
リセット目的じゃない再レンダリングで state がクリアされてしまうと困るし

そう考えるとこれ以外でも変更検出のために useMemo 使ってるところ全部ダメそう
変更無いのに変更ありってみなされるケースも無いとはいえない

変更の有無は標準のフックで用意してほしい
React 18 でクリア処理は要素を消すだけの場合の useEffect
前記事で書いた React 18 からは useEffect が 2 回呼び出される問題
サードパーティライブラリだとアンマウント時に行うクリア処理が提供されず DOM の要素を消せばいいだけって場合に困りそうだったけど useEffect 内で手動で要素を追加してそれを消せば問題なさそう

useEffect(() => {
const child = document.createElement("div")
div_ref.current.append(child)
library.init(child)
return () => {
child.remove()
}
}, [])
React 18 から StrictMode のときに useEffect も 2 回実行される
これまで React の StrictMode では render 処理が 2 回実行されてました
関数コンポーネントでは単純に関数が 2 回実行されます
しかし useEffect の関数の呼び出しは 1 回だけで副作用は 1 回しか起きないようになっていました

それが React 18 では 2 回呼び出されます
一度アンマウントしているのでちゃんとアンマウント時の処理を書いていれば問題にはならないはずです

const Component = () => {
useEffect(() => {
console.log("use effect")
return () => console.log("use effect return")
}, [])
return <div></div>
}

このコンポーネントを StrictMode で使用すると

use effect
use effect return
use effect

という風にログに出ます

個人的に困ってるのは 必要がない場合にはアンマウント時の処理を省略してることが多いからです
依存配列が空の場合はコンポーネントが生存中 最初の 1 度しか呼び出されないはずです
副作用で外部通信や window などのコンポーネント外にリスナをつけるのであればアンマウントの処理も行います
しかし 対象がコンポーネント内で管理する DOM の場合は アンマウントされれば要素は GC で消えるのでリスナの解除は不要です
なのでわざわざ書くことはほとんどなかったです

それが今回の変更で問題になります
例えばこのコード

const Component = () => {
const ref = useRef()
useEffect(() => {
const div = ref.current
div.addEventListener("click", () => {
console.log("clicked")
})
}, [])
return <div ref={ref}>click here</div>
}

div をクリックすると

clicked
clicked

と 2 回表示されます

一度アンマウントされたのなら対応する DOM も破棄して 再作成して欲しいものです

この変更は今後の新機能対応のためらしいです
その動作と StrictMode の一旦アンマウントする処理を無効化できればいいのですが 今のところ個別に無効化はできないみたいです

本来ちゃんと対応すべきなのかもしれませんが 簡単にアンマウント時の処理を書けないケースもあります
ライブラリだとそんな手段は用意されてなくて 要素消せばいいよみたいなもの普通にあったりしますしね
今はわかりませんが 以前は GoogleMaps がそんな感じだったと思います

手抜きの回避策としてはこういうのもありかもですね

const Component = () => {
const ref = useRef()
const initialized_ref = useRef(false)
useEffect(() => {
if (initialized_ref.current) return
initialized_ref.current = true

const div = ref.current
div.addEventListener("click", () => {
console.log("clicked")
})
}, [])
return <div ref={ref}>click here</div>
}

ただ StrictMode の動作としてならともかく 実際に発生する場合に div が新しくなる可能性が 0 なのかはわかりません
初期化時に要素に特定のプロパティを追加する系だとそれがあるかを見るなど 要素を見て初期化済みかの判断をするほうが良いかもしれません
React で ref の更新と useEffect
ref の値を更新するとき useEffect または useLayoutEffect の中で更新してるのを見ることがあります

const Component = ({ value }) => {
const ref = useRef()

useEffect(() => {
ref.current = value
}, [value])

return (
<div></div>
)
}
const Component = () => {
const ref = useRef()

const fn = () => {}

useLayoutEffect(() => {
ref.current = fn
}, [fn])

return (
<div></div>
)
}

useEffect なしで直接でも良さそうなのになんで useEffect を使ってるんでしょう?
単純に副作用は useEffect の中ですべきだから かもしれません

ですが ref ってこの関数の中だけのもので そのプロパティを書き換えるくらい別にいいんじゃないのって気もしてます
render の処理は 2 回実行されても問題ない処理であるべきと言うのを聞いたりしますが 2 回実行されても問題ないと思います

そう思ってましたが それは最新の値を保持するためという視点であって 過去の値を保持するという視点では問題がありました
ただ普通に動かす限りではほぼ問題にはなりません

const Component = ({ value }) => {
const ref = useRef()
const prev = ref.current
ref.current = value

console.log(prev, ref.current)

return (
<div>{prev}/{ref.current}</div>
)
}

const App = () => {
const [state, up] = useReducer(s => s + 1, 1)
return (
<div>
<button onClick={up}>up</button>
<Component value={state} />
</div>
)
}

App ではボタンを押すとカウントアップして Component では前回の値と今回の値を表示しています
「/1」→「1/2」→「2/3」 という風にボタンを押すごとに表示が変わります

StrictMode を有効にして render 処理を 2 回行うようにします

render(
<React.StrictMode>
<App/>
</React.StrictMode>,
document.getElementById("root")
)

すると 「/1」→「2/2」→「3/3」 という風に同じ数値になりました
1 回目の render 処理で ref.current が更新され 2 回目では前回の値がすでに更新後のものになっているためです
console.log をみると↓のようになっています

undefined 1
undefined 1
1 2
2 2
2 3
3 3

ちなみに React 17 では console.log が上書きされ 2 回目の render 処理中のログは出力されません
React のロードより前に console.log を別名で保存して そっちを参照するような一手間が必要です
このせいで変な挙動に見えて嫌だったのですが 18 では毎回表示されるようになってます

実際に 2 回 render 処理が発生することはほぼ無いと思いますし たしか 18 のコンカレントモード用だったと思うのでこれを使わないことにはそもそも発生しなかったと思います
しかし 2 回呼び出しで問題が発生する可能性はありましたし 同じ関数内のみといっても前後の呼び出しで共有する値になるので副作用と考えて とりあえず useEffect にしておくのが無難なのかもです

React で動的なコンポーネント作成はあまりしたくない
動的にコンポーネントを作ってるのはたまに見かけますが 個人的にはあまりしたくないです
というのも中身が同じでもコンポーネントが異なれば DOM は作り直されます
その例です

const Component = () => {
return <div><input/></div>
}

const Foo = () => <Component/>
const Bar = () => <Component/>

const App = () => {
const [foo, toggle] = useReducer(x => !x, false)
const [n, up] = useReducer(x => x + 1, 0)
return (
<div>
<button onClick={toggle}>{foo ? "foo" : "bar"}</button>
<button onClick={up}>{n}</button>
{foo ? <Foo/> : <Bar/>}
</div>
)
}

作り直されたことがわかりやすいように input を配置してます
state で value を管理していないので 作り直されたら入力内容は削除されます

Foo と Bar はどちらも Component を使用して全く同じ動作です
App ではボタンを押すことで Foo と Bar のコンポーネントを切り替えられます
切り替えずに App を再レンダリングするために カウントアップするボタンも置いてます

カウントアップして同じコンポーネントのままだと input は同じ要素のままなので 入力した文字はそのまま残っています
コンポーネントを切り替えると HTML 構造はそのままですが 実際の要素は作り直されて未入力状態に戻っています
key を切り替えたときのような動きです

動的に作るとこういう問題が起きやすいので コンポーネントは静的で props として関数を渡す方法にしたいです

この props に関数を渡すこととコンポーネントを渡すことは似ていて 使う側としては同じように扱えます

<ComponentA
render={
({ name }) => <input name={name} />
}
/>

ComponentA が中で render をどっちとして扱うで動きが違います

const ComponentA = ({ render }) => {
return (
<div>
{render({ name: 1 })}
</div>
)
}

だと 単純に関数を呼び出して結果の React Element が {} に入ります
ComponentA を使うコンポーネントを再レンダリングしても input は残ります

const ComponentA = ({ render: SubComponent }) => {
return (
<div>
<SubComponent name={1} />
</div>
)
}

だと コンポーネントとして扱うことになります
ComponentA を使うコンポーネントが再レンダリングされるとコンポーネントが毎回違うので input の中身が消えます
React で親子関係のコンポーネントを作るときの方法
コンポーネント A がコンポーネント B を使う
B は固定ではなくて使う側で差し替えたい

A の props で B のコンポーネントを受け取るようにする

const ComponentA = ({ ComponentB, componentBProps }) => {
return (
<div>
<ComponentB foo="bar" {...componentBProps} />
</div>
)
}

<ComponentA ComponentB={Foo} componentBProps={{ additional: "a" }} />

A にコンポーネントとそれに渡す props を別に渡すよりは render 関数を渡すほうが便利そう?

const ComponentA = ({ renderB }) => {
return (
<div>
{renderB({ foo: "bar" })}
</div>
)
}

<ComponentA
renderB={
(props) => (<ComponentB {...props} additional="a" />)
}
/>

A が B を直接使うのは A から B へ props を渡したいから
それが入力されたものや選択されたものなら A で持たず A の親で管理することもできる
そうすれば B はコンポーネントではなく Element として A に渡せる

const ComponentA = ({ elementB, onChange, value }) => {
return (
<div>
{elementB}
</div>
)
}

const [state, setState] = useState()
<ComponentA
value={state}
onChange={setState}
elementB={
<ComponentB {...state} additional="a" />
}
/>

でもこうすると B に渡すための state 管理を A を使う側のコンポーネントでしないといけない
A を使う箇所すべてでやるのはつらいのでまとめたい

コンポーネントにすると

const ComponentAB = ({ createElementB }) => {
const [state, setState] = useState()
return (
<ComponentA
value={state}
onChange={setState}
elementB={createElementB(state)}
/>
)
}

<ComponentAB
createElementB={
(props) => (<ComponentB {...props} additional="a" />)
}
/>

まとめたコンポーネント AB に対して ComponentB を使うことを伝えないといけない
renderB を使うケースと同じことになる
A が内部で state を持たなくて AB に移っただけ
再レンダリングの範囲で考えると A だけに収まってたほうが効率は良かった

hook にすると

const useComponentA = (createElementB) => {
const [state, setState] = useState()
return {
value: state,
onChange: setState,
elementB: createElementB(state),
}
}

const componentAProps = useComponentA(
state => (<ComponentB {...state} additional="a" />)
)
<ComponentA {...componentAProps} />

コンポーネントよりマシかと思ったけどあまり変わってない
コンポーネントと hook をペアにしないといけないし
state が A を使うコンポーネントに属することになる
再レンダリング的には影響する範囲が広くなっててコンポーネントの方がまだ良かったかも

ということは renderB の方法がべスト?
単純に input の入力値を state に同期させるときは useState より useReducer の方が向いてた
React で簡単なものを作るとき input の状態を管理するために onChange に直接リスナを書いて setState を呼び出すけど 毎回引数の変換を書くのが面倒

const [state, setState] = useState("")

<input value={state} onChange={(event) => setState(event.target.value)}/>

こうかきたい

<input value={state} onChange={setState}/>

value を取得する方法を最初に指定できて欲しい
デフォルト値は event から value を取り出すものだと良い
自作するとその関数を毎回書かないといけない手間はあるけど state が複数ある場合はそういう hook を作っておけば楽なので作ってみる

const useState2 = (init, selector) => {
const [state, setState] = useState(init)
const toStateValue = selector ?? event => event.target.value
return [state, (...a) => setState(toStateValue(...a))]
}

const [state, setState] = useState2("")

<input value={state} onChange={setState} />

これで単純に入力値を state に同期してくれるだけの input を複数配置するのが楽になった
けどなんか見覚えがある

useReducer と同じだった

const [state, setState] = useReducer((state, action) => {
return action.target.value
}, "")

<input value={state} onChange={setState}/>

action がよく使われる type を持つオブジェクトにならず value そのものだけだけど 単純に state を置き換えるだけだからこれで十分
忘れた頃に使い道が出てくる useReducer
React の Element は使いまわしても良かったみたい
JSX で作る React の Element
DOM みたいに使いまわしてはいけないものって思ってました

あるコードをみたときに props として受け取るのがコンポーネントではなく Element
それを複数箇所で使ってました

例えば

const Component1 = () => {
return (
<Component2 elem={<Foo/>} />
)
}

const Component2 = ({ elem }) => {
return (
<div>
{elem}
<Component3 elem={elem} />
{elem}
</div>
)
}

みたいな感じのもの

他には

export default {
elem: <Bar />
}

みたいにモジュールが export する固定値が Element になっていて 全体で同じ Element が共有されるもの

正しく動かない気がしますが動くんでしょうか
こういうときってコンポーネントを渡して使う側で Element 化するか 関数で渡して使う側で呼び出して Element を受け取るとかしないといけないものだと思ってました

ただどう言う風に問題が出るのかまで把握できてなかったので色々試した……のですが 結局問題は起こせませんでした
state を使っても共有されたりしませんし 使用箇所全てで正常に動作しています
useMemo で Element をメモすることで重たい処理をスキップできていたので Element にいろいろな情報が残っていて共有すると問題が起きそうには思うのですけど

再レンダリングの有無を確認すると 前回のレンダリングのときとツリー構造の同じ場所で同じ Element が使われていればその Element は再レンダリングされてないようです
同じ場所で同じ Element でも一旦別の Element に切り替わると再レンダリングされてました

const elem1 = <Foo />
const elem2 = <Bar />

const Component = () => {
const [state, setState] = useState(false)

return (
<div onClick={() => setState(!state)}>
{state ? elem1 : elem2}
</div>
)
}

このコードで state による elem1 と elem2 の分岐をなくし 常に elem1 とすると state を切り替えても Foo 関数は実行されません
elem1 と elem2 で切り替えるようにすると 切り替えるごとに Foo または Bar 関数が実行されました

前回との比較で参照が同じ Element だとスキップみたいな扱いなのでしょうか
Element 自体がイミュータブルで中にレンダリングの情報を持たないなら共有しても影響なさそうです
Element である以上使うときに props は変更できませんし 再レンダリングはされなくても困らないはずです
React18 が出た
https://reactjs.org/blog/2022/03/29/react-v18.html

以前から言われてた concurrent 機能が使えるようになったみたい
Suspense とか

個人的にはあまり必要性を感じてないし 変に複雑になりそうで別になくていいかなと思う機能
リリースの説明のページ見てても 今後こうしていきたいみたいのが多くて そういうのが実装されてからでいいやという感じ
自分で直接使うよりライブラリが使うべきものみたいなことを書いてて そのライブラリの実装を待たないといけないみたいだし

複雑そうな機能な分 気になるのはファイルサイズ
見てみると react-dom のプロダクション用の minify 済みでも 130KB ほどもあるらしい
https://unpkg.com/browse/react-dom@18.0.0/umd/
これにまだ react の方のパッケージもあるし もう十分重たいパッケージになってると思う

サイズ的にも機能的にもうまく動かないときにソースコードを見て自分で対処するのが辛くなってくるレベル
軽量系に移りたいけど React 使う理由が周辺ライブラリの充実度なのでマイナーどころは難しいし ライブラリがこういう新機能を前提にしていくと Preact だと動くのかも心配になってくる
React に新しいドキュメントができてる
まだ Beta 版のようですが新しいドキュメントのサイトがありました
https://beta.reactjs.org/

今ではフックを使った関数コンポーネントが当たり前になったのに 現行のドキュメントだと主な例はクラスコンポーネントのままで フックはオプションとして別のところにまとまってるようなものでした
新しい方ではフックを使った関数コンポーネントに書き直されています
その他 解説用に図が追加されていたり 画面内で例を動かしていてその場で少しコードを変更してみて動作がどう変わるかを確認できたりと色々改善されています

ただ MDN のときも思ったのですが 最近よくあるこういう見た目のサイトってあまり好きじゃないです
見た目的には現行ドキュメントのほうが好きです
フォント的にも現行のもののほうが読みやすいです

リーダーモードを使うと少し見やすくなりますが こういうサイトだとエディタ部分などで表示が変になるんですよね
CRA の 5.0 が来てた
React を使ったプロジェクトを簡単に作れる create-react-app (CRA)
このリポジトリ内に CRA のデフォルトテンプレートや build の処理の react-scripts も入ってる
これらに全然更新がなくてしばらく 4.0.3 のままだった

React の公式サイトに推奨するかのように書かれているし Facebook のリポジトリ
なのに更新がなく 放置されてるリポジトリでよく見る このリポジトリのステータスは? とか このプロジェクトは死んでるの? みたいな issue も出てた
中の人が CRA はメンテナンスモードで本番に適していないとまで書いていて Next とかにすべきなのかなとか思ってた
以前 Next を少し触れた時はわかりづらかったし 求めてるのと違う感があったので そろそろなにか出てないのか探そうかと思ってた
更新が無いままでも CRA の代替となる移行先についての情報くらいないかなと CRA のリポジトリを見に行くと なんと 5.0 がリリースされてた

Webpack は 5 になったらしく ビルド速度がかなり速くなったという書き込みも見かける
ただ しばらく更新されていなかったもののメジャーアップデートなだけあってバグ報告も多い気がする
もう少しして安定した頃に 4 系のものをアップデートしてみようかな
React で state リセットするコンポーネントラッパー
React を使い始めてからずっと扱いづらいと思ってる部分の一つが state のリセット
コンポーネント内なら難しくないけど 外から受け取るデータに応じてのリセットがあまり考慮されてない気がする
そういう使い方を推奨してないんだろうけど やっぱりそういうことしたいときは結構出てくる

特に コンポーネント内の途中の状態を変更のたびに親に伝えるのは無駄だし 親の処理が増えてくる
ページのコンポーネントになってくると 子孫コンポーネントの状態更新の処理があれこれ混ざって複雑になってる
やっぱり理想はコンポーネントの中での操作が完了してから初めて親に伝えるもの
外から受け取ったデータを state に保持して 完了時に onXXX などで受け取った関数を呼び出すだけだからこれは普通にできる
問題なのは 2 回目以降
state は引き継がれるから前回の情報がコンポーネントに残ったまま

外から受け取るデータが変更されたタイミングで state をリセットしたいので こういう方法で対処してた

const Component = ({ value }) => {
const [state, setState] = useState(value)

useEffect(() => {
setState(value)
}, [value])

return (
...
)
}

これの欠点は 変更時に 1 回ムダにレンダリング処理が行われること
それと useState が多いと setState みたいな関数を何度も呼び出さないといけないところ
コンポーネント内でリセット機能があってすでに関数を用意してるならそれを呼び出せばいいけど
あと useEffect のこういう使い方って正しいの?と思うところもある

その他の方法は key を使うこと
親コンポーネントが props を変えるときに key も変えればリセットできる
でもこれはコンポーネントを使う側に 追加でしてもらうことが増えてあまり良い方法じゃないと思う
key の変え忘れでバグになることが多そう

それで思ったのがコンポーネントを 2 重にして key の制御もコンポーネント側でやる方法

const ComponentInternal = ({ value }) => {
const [state, setState] = useState(value)

return (
...
)
}

const Component = (props) => {
const key = useMemo(() => Math.random(), [props.value])
return <ComponentInternal key={key} {...props} />
}

結構いい感じ
useMemo を使って Math.random() を返すことで依存プロパティが変わったときに key を変えることができる
変わったときにコンポーネントをリセットしたいプロパティを useMemo の依存プロパティに設定する

都度書くのも面倒なのでもう少し楽に作れるように

const resetStateComponent = (Component, deps) => (props) => {
const key = useMemo(() => Math.random(), deps.map(d => props[d]))
return <Component key={key} {...props} />
}

を用意しておいて 作るときは元にするコンポーネントと依存プロパティの名前の配列を引数で渡す

const Component = resetStateComponent(ComponentInternal, ["value"])

key を変える以上 コンポーネント全体を作り直すので リスナをつけていて一瞬でも解除したくないとか API の fetch をしていてリセットのタイミングで毎回 fetch したくないとかあるなら 外側コンポーネントを自作してリセットしたくないデータはそっちで準備して props で渡すか key を使わず state リセットする方法にしたほうが良さそう
React の input でユーザ入力完了後に処理したい2
前回のつづき
一旦完結はしたんだけど チューニング関係でやっぱり change イベントがほしいなと思ったので

一文字入力ごとに state に同期させると input にテキスト入力するときにかなりの頻度で再レンダリングされる
タイプが早い人だと 1 秒間に何回も
input の再レンダリングだけなら何の問題もない程度だけど input の value に指定する state は親の親の親とか input より親のコンポーネントから渡されるのがほとんど
ページやアプリのルートに近いコンポーネントで管理してることが多い
そうなると 再レンダリング対象コンポーネントが多くなるし 次の文字入力時にまだ処理中ってこともある
処理が重いコンポーネントは作ったときにメモするようにしておけばいいけど 毎回それも面倒だし チューニングは遅いと感じたらする方針にしてることが多い
それでいざやるときにはあちこちのコンポーネントをメモ化することになってやる気が起きない
問題なのは再レンダリング頻度が異常に高い input の方なんだからこっちをどうにかすべきだと思う

ただ文字入力ごとに状態が変更されてるわけだし できることはやっぱり change イベントにして入力が一旦終わったところで反映するくらい
一文字ごとに状態を更新するメリットって入力途中なものをどこかに反映したり 入力チェックしたりできるくらいで あまり重要じゃないと思う
markdown エディタ兼プレビューツールみたいな input(textarea) からフォーカスを外さないようなのだと change イベントじゃだめだけど input イベントで毎回更新が必要というわけではなくて 変更があった場合に数秒に 1 回くらいの更新でいい
入力チェックは入力中にあれこれ言われたくなくて入力後にしてほしいって言う話を聞くことがあるし 私もどちらかというとその意見に賛成
なので input をラップするコンポーネント内では input イベントごとに state へ反映してもその外側のアプリケーションとしては change イベントなどのタイミングで更新するくらいで十分

こういう Input コンポーネントを用意しておくと良いかも

const Input = ({ value, onChange: notifyChange, ...props }) => {
const [state, setState] = useState("")

useEffect(() => {
if (state !== value) {
setState(value)
}
}, [value])

const onBlur = () => {
if (state !== value && notifyChange) {
notifyChange(state)
}
}

const onChange = (event) => {
setState(event.target.value)
}

return <input {...props} value={state} onChange={onChange} onBlur={onBlur} />
}

const App = () => {
const [state, setState] = useState("aa")
const onChange = (value) => {
console.log("changed")
setState(value)
}

return <Input value={state} onChange={onChange} />
}

二重に保持はしてるけど App コンポーネントにはフォーカス外れたときに反映される
React の input でユーザ入力完了後に処理したい
ユーザ入力を監視して 1 入力ごとに処理するのではなく 一旦入力が完了したタイミングで処理したい
空行や行頭行末の空白を消すなど ユーザの入力のフォーマット関係は入力途中でやると問題が出る

そういうときは change イベントで処理すればいいんだけど React だと onChange が input イベントのリッスンになってる
軽く調べた感じだと本来の change イベントをリッスンする機能は用意されてなさそう
ツールの方針的に常に state と DOM 状態を同期させるべきだから仕方ないのかも
Preact だとそういう余計な変更はしてないから onChange が使えるんだけど……

直接 input の参照を取得すればできるから ref と useEffect 使って addEventListener で直接リスナ設定かな
と思ってたけど blur イベントというのがあった
フォーカスが外れたときだから change イベントとタイミングは一緒
ただ 変更されてなくてもイベントが起きるけど
変更なしなら setState で何も起きないから 気にしないことにして onBlur で state に反映する

ユーザ入力完了後に小文字化する例

const App = () => {
const [value, setValue] = useState("")
const onChange = event => setValue(event.target.value.toLowerCase())

return (
<div>
<input defaultValue={value} onBlur={onChange} />
<div>input value: {value}</div>
</div>
)
}

defaultValue じゃなくて value で良さそうだけど value だと input に入力できなくなる
再レンダリングされてないから大丈夫そうなのに React は value がついてて onChange がついてない場合は入力を毎回リセットするみたい
バグ対策なのかもしれないけど迷惑

defaultValue にすると value による state の反映ができないから blur イベントが起きて state が更新されたあとは div のところは小文字化されてるけど input はユーザ入力そのままで大文字が入ってる
key をつけて state 更新時には input を再作成して defaultValue を反映させる

const App = () => {
const [value, setValue] = useState("")
const onChange = event => setValue(event.target.value.toLowerCase())

return (
<div>
<input key={value} defaultValue={value} onBlur={onChange} />
<div>input value: {value}</div>
</div>
)
}

key までついて複雑になってきたし value を onChange で state に保持して blur イベントで state を変換する方が単純かもしれない

const App = () => {
const [value, setValue] = useState("")
const onChange = event => setValue(value.toLowerCase())
const onInput = event => setValue(event.target.value)

return (
<div>
<input value={value} onChange={onInput} onBlur={onChange} />
<div>input value: {value}</div>
</div>
)
}

React だと input は必ず常に state と同期させるという前提で考えたほうがいいのかも
JSX 中で useMemo をしたい
React で input イベントごとにレンダリングしてると 入力が遅く感じる時がある
大きめの配列でそれぞれの処理も複雑なときにありがち

単純に要素ごとに 100ms 待機させた例

const Component = ({ items }) => {
const [text, setText] = useState("")
const onChange = (event) => setText(event.target.value)

return (
<div>
<input value={text} onChange={onChange} />
<p>{text}</p>
<ul>
{items.map((item, index) => {
const d = Date.now()
while (Date.now() < d + 100) { }
return (
<li key={index}>{item.name}</li>
)
})}
</ul>
</div>
)
}

これで文字を打つと遅く感じる
items が変わらなければ ul の中はずっと同じなんだから useMemo でメモしておけばいい
理想的な書き方は ul の中に直接書いてこういう感じ

const Component = ({ items }) => {
const [text, setText] = useState("")
const onChange = (event) => setText(event.target.value)

return (
<div>
<input value={text} onChange={onChange} />
<p>{text}</p>
<ul>
{useMemo(() => {
return items.map((item, index) => {
const d = Date.now()
while (Date.now() < d + 100) { }
return (
<li key={index}>{item.name}</li>
)
})
}, [items])}
</ul>
</div>
)
}

この場合はこれでも動くけどフックは関数の最初にまとめておきたい
フックは呼び出し順の違いや呼び出されない場合があったらダメなど特殊なルールがあるので 普通の関数みたいに自由に使うと思わぬ問題が出てきそうで それのために使うたびにここは大丈夫かみたいなことを慎重に確認とかしたくないし
でもそうなると ul の中だけが先に来てコンポーネント定義が読みづらくなる
先にフックの use だけ書いておいて JSX 中にメモしたい内容をかけるようなフックを useRef を使って自作

const useCache = () => {
const ref = useRef()
return (...a) => {
const fn = a.pop()
if (ref.current &&
ref.current.deps.length === a.length &&
ref.current.deps.every((e, i) => e === a[i])
) {
return ref.current.cache
}
const next = fn(...a)
ref.current = { deps: a, cache: next }
return next
}
}

const Component = ({ items }) => {
const [text, setText] = useState("")
const onChange = (event) => setText(event.target.value)
const cache = useCache()

return (
<div>
<input value={text} onChange={onChange} />
<p>{text}</p>
<ul>
{cache(items, () => {
return items.map((item, index) => {
const d = Date.now()
while (Date.now() < d + 100) { }
return (
<li key={index}>{item.name}</li>
)
})
})}
</ul>
</div>
)
}

見やすくかけるようになった
React の state をミュータブルにする
React の state ってイミュータブルが前提になってます
深い部分のプロパティを変えるのが面倒です
state.foo.bar.baz[1] をインクリメントするだけでこうです

setState({
...state,
foo: {
...state.foo,
bar: {
...state.foo.bar,
baz: state.foo.bar.baz.map((x, i) => i === 1 ? x + 1 : x)
}
}
})

オブジェクトのプロパティを書き換えたらその情報をもとにイミュータブルなまま新しいオブジェクトを作ってくれるライブラリもありますが これだけのためにライブラリ入れるのもなという気持ちがあります
単純にこういうフックを使えば良い気がしました

const useMutableState = (init) => {
const [state, setState] = useState({ value: init })
const updateState = useCallback((update) => {
update(state.value)
setState({ value: state.value })
})
return [state.value, updateState]
}

useState の setState 関数は引数として受け取る値が同じならレンダリングをスキップします
オブジェクトのプロパティ変更だと同じオブジェクト扱いでレンダリングされません
それを回避するために setState に渡すオブジェクトの参照だけを毎回変えています

setState の不要なレンダリングを避けてくれるメリットを捨てていますが 普段の setState でも毎回新しいオブジェクトを渡していて毎回レンダリングされます

setState({ ...state, [prop]: value })

それならこれでもいいと思うんです
このフックの使い方はこういう感じで プロパティを直接変更します

const Component = () => {
const [state, updateState] = useMutableState(
{foo: {bar: {baz: [0, 0]}}}
)

const onClick = () => {
updateState(s => {
s.foo.bar.baz[1]++
})
}

return (
<button onClick={onClick}>{state.foo.bar.baz[1]}</button>
)
}

少し使っただけでは問題なさそうだったのですが 実際の使用になると問題がありました
state.foo をコンポーネントの props やフックの依存配列に渡すときです
foo の中に変更があったのに foo が同じオブジェクトなので変更を検知できません

複雑なオブジェクトを state に持つ場合って部分的なプロパティを props や依存配列に渡すことが多いので 実用はできなさそうです
やっぱり面倒なコードを書くかライブラリだよりになりそうです
Jest と React の組み合わせがあんまり便利じゃなかった
どっちも Facebook 製だし Jest は React のテストにも向いてるというから使ってみた
けどあんまり使いやすくなかった
Jest 自体に React のレンダリング機能はなし
別ライブラリが必要

○ React Renderer
○ Testing Library
○ Enzyme

など

React Renderer が公式
とりあえずこれを使ったけどそこまで便利というほどでもなかった
Testing Libraryとかのほうがいいかも

○ useState の更新は面倒
useState を mock 化して 返り値の setXXX を保持して それを呼び出し順から推測して取得して呼び出したり
もっと便利できる機能があるのを期待してたのに

○ shallow render が必要になる
普通に render すると子コンポーネントも render される
色々と処理がされすぎる
テスト用環境だと動かない部分の mock 化が面倒すぎる

shallow render だと子コンポーネントは render されない
関数ならコンポーネントの返り値 クラスコンポーネントなら redner メソッドの返り値を受け取れる
それなら直接関数呼び出しや インスタンス作って render 呼び出しでもいいような?
とも思ったけど それだと useMemo や useRef みたいなフックで問題出るかも

○ useEffect の実行
useEffect が実行されると 処理が複雑なコンポーネントでつらい
useEffect は無効にして useEffect の処理内で state が変わった結果の 仮想 DOM 構造のチェックだけしたい
かと言って useEffect で何もしないようにすると問題が起きる

useEffect の置き換えは全コンポーネントに影響するから 子コンポーネントでも useEffect が実行されない
ライブラリでは useEffect の処理で props.children を仮想 DOM に追加することがある
useEffect が実行されないと仮想 DOM 内は空で必要な要素が結果に含まれてない
特定モジュールからの require/import だけ mock できたらいいけどそれらしいのはなさそう

○ shallow renderer だと仮想 DOM 内の検索とかができない
取得できるものは JSX で作る部分の生のオブジェクト
querySelector 的な便利機能はついてない
子要素や N 番目という場所を固定で自力で取り出して確認することになる
固定の場所の props の onClick などを取り出して 関数呼び出しでボタン押された扱いにして……
ってちょっと不便すぎ
要素の追加や順番の置き換えだけで動かなくなる

○ そこまでしてのやる必要性
React のコンポーネントをテストしようとすると その他モジュールのテストに比べて mock しないといけないのが多い
ほとんどのコンポーネントってロジック的に複雑というわけでもなくテストしておきたい感はあんまりない
基本的に 機能は別モジュールに分けてるし コンポーネントのテストは React などのライブラリ使う前と同じくそこまでしなくてもいい感じがする

ロジック部分がモジュールに分かれていて そっちがテスト済みなら DOM 周りはもっとゆるいのでいい
特定文字が含むかどうかの確認だけとか
React でのテストをしないで 通常の DOM として jsdom か実際のブラウザで一通りの画面遷移ができてればそれでいいと思う
React の Context の更新時のレンダリング
- ComponentA
- ComponentB
- ComponentC
- ComponentD
- ComponentE

こういうツリー構造で
A に Provider
C と E で useContext
E で Context 更新

Context を更新すると C と E が変わるけど 親の C が先にレンダリングされるのかや E って 2 回レンダリングされる可能性あるのかとか考えたけど Provider が A だから更新時は絶対 A がレンダリングされる
A がレンダリングされればその子コンポーネントもレンダリングされるので B,C,D,E 全部がレンダリングされるというだけ
特に特別なことはなかった
コンポーネントごとにユニークな ID を作る hook
ランダムな文字列よりも連番のほうが見やすくて重複が絶対ないので ID 値は連番

◯ ref に保存する

let count = 0

const useUID = () => {
const ref = useRef()
if (!ref.current) ref.current = ++count
return ref.current
}

◯ useMemo のコールバック関数を使う

let count = 0

const useUID = () => {
return useMemo(() => ++count, [])
}
React で fetch で取得したデータのリロード
React で useEffect の中で API からデータを fetch してるコンポーネント
受け取ったデータのリロードをしたいとき

親から子コンポーネントの state をリセットするときの key のような感じで useEffect を再実行させるために refresh_id を用意して useEffect の比較条件に設定
リロードしたいときに この参照を更新

const App () => {
const [refresh_id, setRefreshId] = useState({})
const [data, setData] = useState(null)
const refresh = () => setRefreshId({})

useEffect(() => {
fetch("/foo/bar").then(e => e.json()).then(data => setData(data))
}, [refresh_id])

return html`
<div>${data}</div>
<button onclick=${refresh}>Refresh</button>
`
}

data 自体を useEffect の条件にしてリロードしたいときに null に変更する
fetch 後のセット時に再帰実行しないように data をセットしたときには useEffect 内の処理をしないように条件分岐

const App () => {
const [data, setData] = useState(null)

useEffect(() => {
if (data !== null) return
fetch("/foo/bar").then(e => e.json()).then(data => setData(data))
}, [data])

return html`
<div>${data}</div>
<button onclick=${() => setData(null)}>Refresh</button>
`
}

そもそも useEffect 通す必要がなかった
リロードしたいときに直接 useEffect と同じように fetch して結果をセットする

const App () => {
const [data, setData] = useState(null)

const load = () => {
fetch("/foo/bar").then(e => e.json()).then(data => setData(data))
}

useEffect(load, [])

return html`
<div>${data}</div>
<button onclick=${load}>Refresh</button>
`
}
React って親でクラス指定できないの?
WebComponents だと CustomElement がコンポーネントの実体としてあるので そこに属性を設定できます
class をつけることもできて ShadowDOM とのスタイルは親側での指定が優先されます

<component-foo class="abc"></component-foo>

内側で :host に指定したものより優先度が高いので コンポーネントを使う親側で上書き可能です
margin なんかはコンポーネント側で決めるのではなくて 使う側での配置によって決めたいので良い仕組みだと思います

React でもそういう感じでコンポーネントのスタイルを使う親側で指定しようとしたのですが そういう仕組みはなさそうです
コンポーネントの実体は DOM にないので className を指定してもそれはプロパティに渡されるだけです

const App = () => {
return (
<Foo className="abc" />
)
}

自分でコンポーネント内の DOM に props.className を指定しないとダメです

const Foo = props => {
return (
<div className={props.className}>Foo component</div>
)
}

基本全部のコンポーネントでやることになるのでけっこう面倒です
ルートコンポーネント以外にもつけることができる自由度はありますが 普通にそんなことしないのでただ面倒なだけに感じます
自動でやってくれる仕組みはないものですかねー



以上の内容はhttps://let.blog.jp/tag/Reactより取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14