Signals とは
Preact に Signals という新しい機能が追加されました。
次のような感じで、State のバケツリレー問題を Context などよりも軽やかに解決してくれそうなものです。
import { signal } from "@preact/signals"
const count = signal(0)
const Counter = () => {
const increment = () => {
count.value++
}
return (
<div>
<p>Count: {count.value}</p>
<button onClick={increment}>click me</button>
</div>
)
}
Signals は SolidJS にも存在し、最近のトレンドといってもよいでしょう。
もしかしたら、Signals 目当てで React から Preact や SolidJS に鞍替えしたくなっている方もいらっしゃるかもしれません。
React@18 でも Signals もどきは簡単に実装できる
しかしながら、useSyncExternalStore
という Hook を使えば、Signals もどきを React@18 に簡単に実装できますので、これを用いて React の中の人がどう動くかを長い目で見守ってみるのもいいかもしれません。
以下サンプルコードです。
sample.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width">
</head>
<body>
<div id="app"></div>
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<script>
////////////////////////////////////////////////////
const gV = {}
const signal = iniValue => {
const key = Symbol()
gV[key] = {value: iniValue, fList: new Set()}
return {
get value() {
return gV[key].value
},
set value(newValue) {
if (gV[key].value === newValue) return;
gV[key].value = newValue
gV[key].fList.forEach(f => f())
},
_subscribe: notify => gV[key].fList.add(notify),
_unsubscribe: notify => gV[key].fList.delete(notify),
_remove: () => delete gV[key],
}
}
const useSignal = signal => React.useSyncExternalStore(
// subscribe
notify => {
signal._subscribe(notify)
return () => {
// unsubscribe
signal._unsubscribe(notify)
}
},
// getSnapshot
() => signal.value,
)
const count1 = signal(0)
const count2 = signal(0)
const Button1 = props => {
const c = useSignal(count1)
return React.createElement('button', {
onClick: e => count1.value++,
}, c /* count1.value */)
}
const Button2 = props => {
const c = useSignal(count2)
return React.createElement('button', {
onClick: e => count2.value++,
}, c /* count2.value */)
}
const App = props => {
return React.createElement(React.Fragment, {}, [
React.createElement(Button1),
React.createElement(Button1),
React.createElement(Button1),
React.createElement(Button2),
React.createElement(Button2),
React.createElement(Button2),
])
}
const root = ReactDOM.createRoot(document.getElementById("app"))
root.render(React.createElement(App))
////////////////////////////////////////////////////
</script>
</body>
</html>
現場からは以上です
最後までこのような拙文に付き合っていただき、まことにありがとうございました。
更新履歴 2024/10/12
-
useSignal()
の引数と戻り値をシンプルにしました。 - コンポーネント内で
count1.value
やcount2.value
を直接使うのは pure 原則 に反する気もするので、useSignal()
の戻り値を使うように変更しました。Signals っぽくなくなるのが残念ですが、、、