-
-
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
feat(core): add onMount instance callback #2308
Conversation
This pull request is automatically built and testable in CodeSandbox. To see build info of the built libraries, click here or the icon next to each commit SHA. Latest deployment of this branch, based on commit 29865cf:
|
I'm being very nitpicky, please forgive me:
Is using |
<camera onMount={(self) => void self.lookAt(1, 2, 3)} />
<geometry onMount={(self) => void self.translate(1, 2, 3)} /> The const elementRef = React.useRef()
const previous = React.useRef()
React.useLayoutEffect(() => {
// Bail if already ran for this instance
const element = elementRef.current
if (previous.current === element.uuid) return
// Run sync effect
console.log(element.uuid)
// Mark instance as updated
previous.current = element.uuid
})
<element ref={elementRef} /> I know Krispy asked for something like |
im not sure if i like it, even onUpdate was too much and i wish i didn't add it, especially as it started to conflict with a real world method called "onUpdate" that some three objects have. adding lifecycles to threejs elements other than interaction events seems a little out of order. imo this would be a better place and less confusing. useLayoutEffect(() => void ref.current.callMethod(), []) there's a temptation to bridge or lessen the impact of the imperative nature of some three elements and i think r3f shouldn't encourage it as declarative and imperative can co-exist harmoniously through hooks. i would also fear that once we start adding lifecycles it'll be a can of worms that will never stop: onUnMount, onBeforeMount, onAfterMount, onBeforeUnmount, ... all in all, i would prefer to stick to how react categorises after-render effects. |
I worry that this is a wider issue of R3F with how it architects instances. This wouldn't happen if the instance wasn't the actual three object. I wouldn't consider rewriting R3F for the sake of either prop, but this is definitely something for a future major nonetheless.
This confuses me about the existence of Also, speaking of gotchas and to clarify, that effect can go out of sync with the underlying instance if it's ever exchanged. It can also be disastrous if called more than once on an instance with destructive methods. One might think to just create an instance in a useMemo, but that is very prone to memory leaks if disconnected from the scene graph.
This is an issue unsolved by react and something that we have to solve ourselves. For instance, a react hook version of this would look like: function useSyncEffect(elementRef, effect) {
const [previous] = React.useState(() => new Set())
React.useLayoutEffect(() => {
const element = elementRef.current
if (previous.has(element)) return
effect(element)
previous.add(element)
})
}
//
const cameraRef = React.useRef()
useSyncEffect(cameraRef, camera => camera.lookAt(1, 2, 3))
<camera ref={cameraRef} /> I'm not completely satisfied by this since three's design would often require set up outside of the constructor and would need to be re-run whenever the underlying instance changes (which IMO should be an implementation detail). This is not something that React considers in its lifecycle (and we fight against it in reconstruction). Consequently, a hook like this would be needed in user-land for this case. I'd prefer to have something native to R3F rather than Drei (or the already too-large user-land hooks/utils folder) as this is quite a sore area between react and three and works around R3F specifics. |
i think i don't follow, i don't understand why the hook has to be that complicated. why not useLayoutEffect(() => {
// do everything imperative here
}, []) no set, no checking, just so this is about switching args which changes the ref? which should re-run the effect? but doesn't that make too many assumptions? shouldn't it be a dependency/condition that tells something to, for instance, re-run a lookat? and then there's also this mrdoob/three.js#20575 (which three-stdlib has since fixed), that was the only ever problem i am aware of in all these years.
i regard it as a mistake. it just invites people to sidestep the one place that should manage effects: use[Layout]Effect. i added it in the very beginning because i didn't think much about the consequences. in retrospect a parallel, optional lifecycle-api just causes confusion: hooks, props, and three's own (obj.onBeforeRender et al). |
ps here's the original react issue facebook/react#20090 but imo this is something that react and three need to solve. still being used in drei: https://github.com/pmndrs/drei/blob/master/src/helpers/useEffectfulState.tsx for a single component, "Stats" https://github.com/pmndrs/drei/blob/master/src/core/Stats.tsx#L13 because it is dirty. sorry for all the text and links, just trying to figure out if this is a user land thing that nobody faces or deep down internals more akin to technical debt. |
Yes, this is particularly problematic with destructive methods of geometry classes. I generally avoid doing this in three or in anything beyond a useMemo and binding attributes declaratively as vertex transform/compute methods aren't safe to repeat. I'm not sure if the hook can be simplified to the same effect, it has to brace against interruption/double-execution and then have args as dependencies or store a reference as a render-effect to keep things in sync. You can remove checks depending on which edge cases matter, or hijack the ref callback, but it would be some combination of those checks.
The premise behind this PR was whether effects like these should be allowed in the element body since they're aware of their own lifecycles outside of react. I'm not comfortable adding on to onUpdate if it's proved to be problematic, perhaps these should be hooks instead. |
is there a real world use case, a codesandbox, something that exemplifies the issue that react useEffect would create? |
For me, the usecase is usually to do with preparing geometry. For my grass I have a more complicated one in an old CS that I const geometry = useMemo(() => {
const _geometry = new THREE.PlaneBufferGeometry(bW, bH, 2, joints);
// Make the grass geom start from the floor.
const _trans = new THREE.Matrix4().makeTranslation(0, -bH / 2, 0);
_geometry.applyMatrix4(_trans);
// Make the grass geom oriented up.
const _rot = new THREE.Matrix4().makeRotationX(-Math.PI / 2);
_geometry.applyMatrix4(_rot);
// Fold the grass to give it more dimension.
const vertices = _geometry.attributes.position.array;
for (let i = 0; i < vertices.length; i += 3) {
if (vertices[i + 0] === 0) {
vertices[i + 1] = bW / 2;
}
}
return _geometry;
}, [bW, bH, joints]); And then: <mesh geometry={geometry}> ... </mesh> I tried doing this in a I could extend the PlaneBufferGeometry class and then The thought was having an event to run effects based on the R3F lifecycle would alleviate trying to navigate the pitfalls of React and also make for a friendly API. As I understand it, React themselves do not suggest use[Layout]Effect for initialization cases, but synchronization cases. Which is why they suggest the run-once-flag if that is required, but in our case we need an instance-has-reconstructed flag. |
@krispya but this is exactly what use memo is for, why doing this as an effect when you want to memorize a computationally expense op that yields a result? As for use layout effect, it's being used to access the view after render but before paint, on the dom it's typically for measuring and then applying the result. It's totally fine too run effects here imo. Though in your case, I'd also use memo. The HMR woes you observe can also have other causes, three doesn't know defaults, once you set an object and remove the prop we can only guess. This has been a long standing issue and we do have a solution but it's spotty. |
That makes sense to me. I get why you don't want to mix R3F events with the three JSX. What I take from this conversation is:
|
i would prefer a conservative approach when it comes to extending the api surface since this stuff can add to complexity or perceived complexity quick. on mount in react is useEffect(() => ..., []) and im thinking, if there are issues with this approach react-dom probably faces them too. if the issues are prevalent enough react should fix it, or there's a pre-existing thing in use-hooks or sth like that. but re-inventing a mount just in r3f, i would like to see some more real world use cases. my experience with such helpers has been troubled. r3f had hooks like useUpdate and useResource react-three-fiber/src/hooks.ts Lines 36 to 59 in 8eea1b5
|
I think the behavior of I wanted to hide this complexity from users, but I see the problems that this can create. Informing them of how to deal with these cases in a react-like way would be an important first step. I'm inclined to close this PR and follow up with an example in the docs of a case where a sync effect might go out of sync if |
Adds an
onMount
callback for one-of effects for synchronization (i.e. Camera#lookAt, Geometry#translate, etc.). This is called once safely mounted or whenever reconstructed (i.e. args change).TODO: