signals
signals copied to clipboard
signals in react (three-fiber) doesn't work transiently
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.
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.
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