preactjs / signals

Manage state with style in every framework
https://preactjs.com/blog/introducing-signals/
MIT License
3.72k stars 91 forks source link

signals in react (three-fiber) doesn't work transiently #115

Open drcmda opened 2 years ago

drcmda commented 2 years ago

codesandbox: https://codesandbox.io/s/interesting-panna-urdtu6?file=/src/App.js

import { signal } from '@preact/signals-react'

const opacity = signal(1)

setTimeout(() => {
  // Should be transparent in 3 seconds ...
  // Works with opacity={opacity.value} but then it re-renders
  // Doesn't work as opacity={opacity}
  opacity.value = 0.5
}, 3000)

function Box() {
  return (
    <mesh>
      <boxGeometry />
      <meshBasicMaterial transparent color="orange" opacity={opacity} />
    </mesh>
  )
}

it's not a div, but it would be fantastic if it could work.

CodyJasonBennett commented 2 years ago

This works fine when using React.createElement in development (there's current a hard-dependency on that to change opacity to opacity.value at runtime):

import * as React from 'react'
import * as ReactDOM from 'react-dom/client'
import { Canvas } from '@react-three/fiber'
import { signal } from '@preact/signals-react'

const opacity = signal(1)
setTimeout(() => (opacity.value = 0.5), 3000)

function Box() {
  console.log('rerender')
  return React.createElement(
    'mesh',
    null,
    React.createElement('boxGeometry'),
    React.createElement('meshBasicMaterial', { transparent: true, color: 'orange', opacity }),
  )
}

ReactDOM.createRoot(window.root).render(React.createElement(Canvas, null, React.createElement(Box)))

I found some other issues that completely break the bindings with a fix in #130, but I don't believe they are directly related.

developit commented 1 year ago

The reason this doesn't work is because we don't currently support fine-grained prop updates in the React integration. The Preact integration supports this because it injects fine-grained prop-to-DOM updates, but we cannot do that in React.

However, it is possible to implement VDOM-based fine-grained updates in a way that works nicely with react-three-fiber as well as React-DOM. Here is a patch that makes this work: (just import anywhere)

import { Signal } from '@preact/signals-react'
import * as rt from 'react/jsx-runtime'
import * as React from 'react'
rt.jsx = wrap(rt.jsx)
rt.jsxs = wrap(rt.jsxs)
let createElement = React.createElement
React.createElement = wrap(createElement)
const wrappers = new Map()
function wrap(method) {
  return function (type) {
    if (typeof type === 'string') {
      let t = wrappers.get(type)
      if (!t) wrappers.set(type, (t = Wrapper.bind(null, type)))
      arguments[0] = t
    }
    return method.apply(this, arguments)
  }
}
function Wrapper(type, props) {
  let p = {}
  for (let i in props) {
    let v = props[i]
    p[i] = i !== 'children' && v instanceof Signal ? v.value : v
  }
  return createElement(type, p)
}

Here it is working with the original demo code unmodified: https://codesandbox.io/s/peaceful-brown-n1qjne?file=/src/App.js:91-121