-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
When setting a transform related prop to undefined r3f noops the update instead of resetting to default values #2755
Comments
This is handled by react-three-fiber/packages/fiber/src/core/utils.ts Lines 279 to 296 in 963a83d
Not sure if #2726 is related, but I'd also try on |
I've tried on both latest |
this did work at some point. hmr seems completely broken atm, i see memoizedProps crashing daily. generally we need to be careful because fixing undefined can create risk, for instance undefined component props. function Foo({ color }) {
return <meshBasicColor color={color} /> the problem is that three has no idea what identity is for most classes. not to mention that identity is contextual in some cases, like "up", or "scale". scale is a vec3, it's identify is not 111 but 000, it's all just super confusing. |
one way could be to completely remove each and every trace of memoizedProps and start from scratch in v9. we either could ignore undefined with values "sticking", like
because after all this is how threejs fundamentally works. the whole problem seems to be that we take for granted how the dom works and assume three must behave similar. or, we find a really good model that makes it happen, but until now nothing worked a 100%. |
an idea would be to accumulate a global value cache of prototype classes and their identity values on a need to know basis, for instance createInstance could quickly populate this.
it still would never cover all cases, things like matrixAutoUpdate etc, but perhaps be "good enough". also getting cold feet ... what if custom object foo has a field that holds 2 gigabyte of data, should we copy this so that we have a default fallback? class Foo {
bar = Float32Array(100000000000)
}
extend({ Foo })
<foo bar={undefined} /> // 💀 and then there's what mrdoob suggested here mrdoob/three.js#21209 (comment) a whitelist const DEFAULTS = {
'Color': new Color(),
'Object3D': new Object3D(),
'Vector3': new Vector3()
} ofc this wouldn't work with anything 3rd party or custom. but probably could be good enough for all three classes. although it could be burden to run around with a big value list. this prototype hack seems good, too mrdoob/three.js#21209 (comment) const thingsNeededForReact = [Object3D, Color, Vector2, Vector3, Matrix3, ...moar_stuff]
thingsNeededForReact.forEach(thing=>{
thing.prototype.reset = (function(){
const _default = new thing()
return function(){ this.copy(_default) }
})()
}) |
made a test branch, removed every trace of memoProps and implement defaults like so export const DEFAULT = '__default'
export const DEFAULTS = new Map()
if (value === DEFAULT + 'remove') {
if (currentInstance.constructor) {
// create a blank slate of the instance and copy the particular parameter.
let ctor = DEFAULTS.get(currentInstance.constructor)
if (!ctor) {
// @ts-ignore
ctor = new currentInstance.constructor()
DEFAULTS.set(currentInstance.constructor, ctor)
}
value = ctor[key]
} else {
// instance does not have constructor, just set it to 0
value = 0
}
} the value will then be object.copy(value)'d by the code following, ensuring that the value isn't double-used. ...
// Test again target.copy(class) next ...
else if (
targetProp.copy &&
value &&
(value as ClassConstructor).constructor &&
targetProp.constructor.name === (value as ClassConstructor).constructor.name
) {
targetProp.copy(value)
} Screen.Recording.2023-02-14.at.15.35.50.movlooks good imo |
So, we added a test to v9 for nooping on undefined. This was because at least one Drei component relied on this behavior to avoid compiling junk shaders if a material was passed undefined. The current behavior of export function applyProps<T = any>(object: Instance<T>['object'], props: Instance<T>['props']): Instance<T>['object'] {
const instance = object.__r3f
const rootState = instance?.root.getState()
const prevHandlers = instance?.eventCount
for (const prop in props) {
let value = props[prop]
// Don't mutate reserved keys
if (RESERVED_PROPS.includes(prop)) continue
// Deal with pointer events, including removing them if undefined
if (instance && /^on(Pointer|Click|DoubleClick|ContextMenu|Wheel)/.test(prop)) {
if (typeof value === 'function') instance.handlers[prop as keyof EventHandlers] = value as any
else delete instance.handlers[prop as keyof EventHandlers]
instance.eventCount = Object.keys(instance.handlers).length
}
// Ignore setting undefined props
if (value === undefined) continue
/// ... Is instead the expected behavior that a value of undefined should reset to some default? How would we know what the default value should be? Also a side note, it looks like the more Cody and I work on v9 the more it is drifting significantly away from v8. Maybe we should consider a freeze on v8 some time soon. It's already becoming an issue where I got to work on v9 and there are changes I didn't know pushed to v8 that need to get ported to v9 slowing things down. |
I'm not sure I follow. Is this creating a new instance on HMR and copying only the values that are explicitly defined so that the new instance inherits whatever defaults would be set by the class? If so, I think this is the best option so we don't need to keep any kind of internal record of what default values should be and non-three class defaults would also be supported.
AFAIK |
HMR works by looking for a variable which was completely removed between renders, which would not include Indeed, |
it creates an instance (only one for each class prototype), takes the string key of the removed prop, grabs the default off the instance with it.
this code hasn't been touched, it's what is currently in fiber. but makes sense, i will change it to i'll report back with a PR. |
here's the pr #2757 |
Okay so this HMR fix won't affect a prop being set to
Okay I think I understand, but I am foggy brained today and I want to be sure. We create a reference instance of each constructor that gets mounted with R3F using empty args and then look up values from this reference instance for what the default should be. What happens if the constructor requires args? |
we have access to args, but im scared of what this could unleash. this is my nightmare scenario, and very realistic, too <bufferGeometry usage={THREE.DynamicDrawUsage} attach="attributes-position" args={[A500mbFloatArray, 3]} /> not something we would want to copy and hold on to in cache indefinitively. that would require the map key to be the class constructor and a deep-clone of the args. in this case removing tbh im much more at peace with not messing with args, like withholding them, classes should mostly be side effect free anyway, and all use cases i can think of would work without args, |
I'm currently leaning on memoizedProps in TRIPLEX - when you click on a scene object I traverse down the three.js tree from the event object looking for usage of the __r3f memoized props - I need to find the scene object that has its transform props set via react and memoizedProps allowed me to do that (...mostly, it isn't a perfect solution). I can look into alternatives if we think getting rid of it is the path forward. There's definitely a distinction with it being Also is this really just HMR? In regular runtime you could have the transform props set behind state and have the same beiavour happen? When HMR is mentioned are we really just talking about React updates? function HelloWorld() {
const [pos, setPos] = useState([2,2,2]);
const unset = () => setPos(undefined);
return (
<mesh><boxGeometry onClick={unset} position={position} args={[1,1,1]} /></mesh>
);
} |
undefined as a noop is at least expected behaviour, it was deliberate. could be made into identity, not sure if worth it because blowing an undefined into a class from user-land imo is not something anyone should do, nor expect it to have a certain outcome. react-dom disallows it as well since 16 i believe. removal w/o hot reload would be more like this: const [yes, set] = useState(true)
const props = yes ? { position: [1,1,1] } : {}
return <mesh onClick={() => set(false)} {...props} /> |
Oh so for that use case it's to be expected (where a position prop for example changes from [1,1,1] to undefined? |
so far it's treated like a noop. undefined doesn't qualify as a prop removal so to my knowledge it never ran into identity. i discussed this use case with mrdoob back then. |
Ah okay, well this is the issue I was having, not not complete removal just changing to undefined. I'm not sure how common it will be in practice TBH maybe im just running into it because of testing a bunch of different things. It would be nice if it did reset though. What was mrdoobs thinking around it? |
it comes from this issue: #274 the problem is that you are not doing obj.position = undefined, which is what an actual prop removal would do, you are doing obj.position.set(undefined). as per fiber rules everything with a set or copy is being set or copied. according to mrdoob set(undefined) should always be a noop. not saying it must be so, an argument could be made for that to change, though i would like that to be v9 if that were the case. |
Hmm ok that makes sense from the rules at least. But it is a little confusing when in react. Do you have any thoughts for when in the context of an editor I can get the behaviour of undefined setting to arbitrary values (default) instead of nooping? I could imagine either making undefined impossible with ast transformation or remounting with react keys but not ideal. 🤔 |
If the idea is to copy the behavior of React DOM elements then I just tested it and setting a visual value (such as The problem case we have is something like |
Closing as fixed with #2757. |
Video of the bug
Screen.Recording.2023-02-14.at.10.34.56.pm.mov
Steps to reproduce
This affects all transform related props (position/rotation/scale). I would expect them all to instead of noop reset back to the default values:
If I can be pointed to where in the code base I should look at I can contribute a fix 😄.
The text was updated successfully, but these errors were encountered: