7
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

React@18 での Signals もどき

Last updated at Posted at 2022-09-10

Signals とは

Preact に Signals という新しい機能が追加されました。

Introducing Signals - PREACT
Signals - PREACT

次のような感じで、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.valuecount2.value を直接使うのは pure 原則 に反する気もするので、useSignal() の戻り値を使うように変更しました。Signals っぽくなくなるのが残念ですが、、、
7
0
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?