React でコンポーネント内の全インスタンスで共通の値を使うとき
- カテゴリ:
- JavaScript
- コメント数:
- Comments: 0
◆ 同じコンポーネントで共有する値で更新がある場合
◆ Context はつらいので useEffect でリスナ登録して再レンダリング
◆ 公式フックの useSyncExternalStore を使ったり ライブラリのシグナル使ったりもできる
◆ Context はつらいので useEffect でリスナ登録して再レンダリング
◆ 公式フックの useSyncExternalStore を使ったり ライブラリのシグナル使ったりもできる
つい先日 React で state などに更新があったことを検知したいという内容の記事を書きました
これは props やフックで得られる値の更新についてです
それと近いような遠いようなもので コンポーネントの全インスタンスで共通の値というのもあります
これだと text は固定ですが 変更する場合もあります
単純に text を更新しても React が検知しないので再レンダリングが起きず 即時に画面が更新されません
Provider の中で text を state として保持して 更新します
Component1 も Component2 も Component3 も という感じで アプリ全体で共有する値ならそれでいいと思います
ですが Component1 という 1 つのコンポーネント内での共有だと コンポーネントの種類の数だけ 1 つ 1 つ Context を作ることになります
こういうコンポーネントが複数あったら Context の数 (Provider のネスト) が多くなりますし あまりやりたくないです
更新する内容は毎秒 今の時刻を更新するようにしました
useEffect の中でリスナを登録して 更新があれば再レンダリングするようにしています
毎秒の更新では更新後にイベントを起こして通知します
Component を複数並べて表示するとそのすべてが揃って毎秒更新されます
now と event_target が分かれていたのもひとつにまとめます
EventTarget が値を持って 更新時に自動で通知されるようにします
これらを使って
となりました
今回のケースも一種の外部ストアと呼べそうですし 公式に用意されてるフックがあるなら使ってみようと思います
useSyncExternalStore では subscribe と getSnapshot の 2 つの関数を登録します
subscribe 関数はリスナ関数を受け取ってそれをどこかに登録する処理を行います
外部ストアに更新があったときに受け取ったリスナ関数を呼び出せるようにします
useEffect のようにクリーンアップする関数を返す必要があります
ここで登録を解除します
getSnapshot の方は現在の状態を返す関数です
そんなに変わらなくてどっちでもいいかなというところです
useEffect を直接使ったり 再レンダリングのためだけの useReducer を使わない分マシなんでしょうか
今回は使ってませんが useSyncExternalStore は SSR 対応機能でサーバーサイドで呼び出される関数を登録する機能もあります
そういう用途で必要になるもので SSR しない場合はそこまで積極的に使う必要はないものなのかもです
何かのライブラリが管理しているデータを外部ストアとするとき ライブラリ側で subscribe のような仕組みが提供されていれば自分で作る部分が少なくて済みます
React 公式のフックにこういうのがあることで React 統合を考慮しているライブラリが subscribe 機能を提供してくれるというのはあるかもですね
Solid のシグナルみたいなものを Preact/React で使えるようにしてくれるものです
シグナルの詳しいことは別記事に分ける予定です
これは props やフックで得られる値の更新についてです
それと近いような遠いようなもので コンポーネントの全インスタンスで共通の値というのもあります
let text = "TEXT"
const Component1 = () => {
return <div>{text}</div>
}
これだと text は固定ですが 変更する場合もあります
単純に text を更新しても React が検知しないので再レンダリングが起きず 即時に画面が更新されません
Context
1 つの方法は Context を作ることですProvider の中で text を state として保持して 更新します
Component1 も Component2 も Component3 も という感じで アプリ全体で共有する値ならそれでいいと思います
ですが Component1 という 1 つのコンポーネント内での共有だと コンポーネントの種類の数だけ 1 つ 1 つ Context を作ることになります
こういうコンポーネントが複数あったら Context の数 (Provider のネスト) が多くなりますし あまりやりたくないです
useEffect でリスナ設定
Context を使わない方法を考えてみると 直接リスナの設定が考えられます更新する内容は毎秒 今の時刻を更新するようにしました
let now = new Date()
const event_target = new EventTarget()
setInterval(() => {
now = new Date()
event_target.dispatchEvent(new Event("update"))
}, 1000)
const Component = () => {
const [, rerender] = useReducer(() => ({}), {})
useEffect(() => {
const handler = () => rerender()
event_target.addEventListener("update", handler)
return () => event_target.removeEventListener("update", handler)
}, [])
return <div>{now.toLocaleString()}</div>
}
useEffect の中でリスナを登録して 更新があれば再レンダリングするようにしています
毎秒の更新では更新後にイベントを起こして通知します
Component を複数並べて表示するとそのすべてが揃って毎秒更新されます
const App = () => {
return (
<div>
<Component/>
<Component/>
<Component/>
</div>
)
}
フックにまとめる
コンポーネント内に直接 useReducer や useEffect を使う処理を毎回書かなくていいようにフックにまとめますnow と event_target が分かれていたのもひとつにまとめます
EventTarget が値を持って 更新時に自動で通知されるようにします
class EventTargetValue extends EventTarget {
constructor(value) {
super()
this._value = value
}
get value() {
return this._value
}
set value(v) {
this._value = v
this.dispatchEvent(new Event("update"))
}
}
const useShared = (etv) => {
const [, rerender] = useReducer(() => ({}), {})
useEffect(() => {
const handler = () => rerender()
etv.addEventListener("update", handler)
return () => etv.removeEventListener("update", handler)
}, [])
return etv.value
}
これらを使って
const shared_now = new EventTargetValue(new Date())
setInterval(() => {
shared_now.value = new Date()
}, 1000)
const Component = () => {
const now = useShared(shared_now)
return <div>{now.toLocaleString()}</div>
}
となりました
useSyncExternalStore
React 18 からは useSyncExternalStore というフックがあります今回のケースも一種の外部ストアと呼べそうですし 公式に用意されてるフックがあるなら使ってみようと思います
useSyncExternalStore では subscribe と getSnapshot の 2 つの関数を登録します
subscribe 関数はリスナ関数を受け取ってそれをどこかに登録する処理を行います
外部ストアに更新があったときに受け取ったリスナ関数を呼び出せるようにします
useEffect のようにクリーンアップする関数を返す必要があります
ここで登録を解除します
getSnapshot の方は現在の状態を返す関数です
const listeners = new Set()
const subscribe = (listener) => {
listeners.add(listener)
return () => listeners.delete(listener)
}
let now = new Date()
setInterval(() => {
now = new Date()
for (const listener of listeners) {
listener()
}
}, 1000)
const getSnapshot = () => now
const Component = () => {
const now = useSyncExternalStore(subscribe, getSnapshot)
return <div>{now.toLocaleString()}</div>
}
そんなに変わらなくてどっちでもいいかなというところです
useEffect を直接使ったり 再レンダリングのためだけの useReducer を使わない分マシなんでしょうか
今回は使ってませんが useSyncExternalStore は SSR 対応機能でサーバーサイドで呼び出される関数を登録する機能もあります
そういう用途で必要になるもので SSR しない場合はそこまで積極的に使う必要はないものなのかもです
何かのライブラリが管理しているデータを外部ストアとするとき ライブラリ側で subscribe のような仕組みが提供されていれば自分で作る部分が少なくて済みます
React 公式のフックにこういうのがあることで React 統合を考慮しているライブラリが subscribe 機能を提供してくれるというのはあるかもですね
Signals
外部ライブラリを使うなら Preact のシグナルが簡単ですSolid のシグナルみたいなものを Preact/React で使えるようにしてくれるものです
import { signal } from "@preact/signals-react"
const now = signal(new Date())
setInterval(() => {
now.value = new Date()
}, 1000)
const Component = () => {
return <div>{now.value.toLocaleString()}</div>
}
シグナルの詳しいことは別記事に分ける予定です