Open drcmda opened 4 years ago
And this is how we do it today: https://github.com/react-spring/react-spring/tree/master/src/targets
The original idea for this is by @vjeux (from the animated library). Each target defines a function that is able to transport props into the target system. This of course isn't dynamic, targets have to added and maintained, and it's also superfluous since reconcilers have that knowledge.
When you mean "reconcilers", you mean "renderers", right?
Yes. As an example, here's react-three-fibers commitUpdate: https://github.com/react-spring/react-three-fiber/blob/master/src/reconciler.tsx#L382 which calls an apply function here: https://github.com/react-spring/react-three-fiber/blob/master/src/reconciler.tsx#L108
commitUpdate has full knowledge of platform details. This is of course also how React handles props on re-rendering.
Let me try to rephrase your request to make sure I understand it.
I think you're asking for a way for renderer-agnostic libraries to tell React to imperatively synchronously update a host node with given props. But without actually specifying how that update gets applied because presumably the renderer's host config already knows that. So the host node itself is opaque.
Yes, that is exactly it.
Do you always know the desired prop values at the moment of the call? Or can they depend on previous prop values?
How would you express a request to delete a prop with your proposed API? Should that even be possible?
The values are known, it wouldn't have to depend on previous values. Although they are available anyway through ref.current pointing to the actual object. But,
const [ref, set] = useMutation()
set(obj => ({ ... }))
would be more than welcome, why not.
Deletion isn't required, just like you can do:
<xyz something={123} />
// later
<xyz />
set({ x: 0 })
set({ x: undefined }) // i guess? it's just like setState, which technically doesn't delete
The reconciler handles it through commitUpdate(..., oldProps, newProps).
Deletion isn't required, just like you can do:
There is a difference between deleting a prop and having a prop with a null
or undefined
value. Whether or not the renderer interprets them the same or differently is up to the renderer, but technically these are two different things. So I think we need to be clearer about the behavior here. If I pass set({ a: 1 })
and then set({ a: 2, b: 1 })
and then set({ b: 2 })
, what happens exactly?
It's interesting that commitUpdate
doesn't exist in the persistent mode. Which the new React Native renderer uses. If this is built into React, we'd need a way for it to work across both modes somehow.
const [ref, set] = useMutation()
set({ a: 1 }) ---> obj.a === 1
set({ a: 2, b: 1 }) ---> obj.a === 2, b === 1
set({ b: 2 }) ---> obj.a === 2, b === 2
no deletion required imo. it's a escape hatch, similar to dangerouslysetinnerhtml. i guess the name "useMutation" is also too harmless, it should have a threatening, evil name with underscores.
commitUpdate doesn't exist in the persistent mode. Which the new React Native renderer uses. If this is built into React, we'd need a way for it to work across both modes somehow.
Im curious how it can do that. I have never understood these modes in depth.
Im curious how it can do that. I have never understood these modes in depth.
Mutation mode is for host APIs where you're dealing with mutable nodes that have methods like insert
/ append
/ remove
/ update
. Like DOM.
Persistent mode is for host APIs where the only way to update something is to create a copy of the tree with changes and then replaceRoot(copy)
. Essentially an immutable host API. The new RN engine is written like this to better take advantage of multi-threading. I'm not sure how this proposal would work with it.
I guess this is similar to the concept of the NativeDriver animations in RN, except the "native" part is actually still JavaScript. The way they solve the problem there is that only non-layout props are animatable — therefore, it is safe to do imperative updates "out of band" without worrying about what happens to the persistent data structures used for layout.
I think what you're essentially asking for is a generalized setNativeProps
which is (was) a thing in RN. However, I think that was deprecated in Fabric. @sebmarkbage and @shergin might be able to tell us more about how the thinking has been changing there. I know there's been a few iterations.
In animatedjs, react-spring and framer-motion the user makes a specific commitment. Something like:
const props = useAnimation({ x: props.x }, [props.x])
return <a.div style={{ left: x }} />
these libs use "<a.xyz>" so that they can receive props directly, so that it is all still declarative on the outside and there is no conflict between props and mutations. They do mutate internally and "a" packs the platform knowledge.
But i imagine that hook as a very simple, no-rules tool. Whoever's using this knows what they are doing, and it's their responsibility to serve this in a way that's safe to use from the outside.
I think what you're essentially asking for is a generalized setNativeProps
Yes. Something like this as a hook, basically going through the same channel as a props update, but sync and without re-render.
setNativeProps was removed in Fabric because it had undefined behavior that couldn't be modeled the same way between paper and fabric. Also, in paper all of the communication is async so this function could t happen sync. That could work in Fabric though.
The land mines with setNativeProps is that it was essentially setting state on the native views and there wasn't a guarantee for when that would get reset since it can be out of sync with a React render.
For example: React renders a view with background blue SetNativeProps to set the background to green React update to change border color. Should background reset to blue?
React doesn't know that the background is green, only the native view (or div) does. This makes it more complicated when that native view is actually removed from the hierarchy because of view flattening. That should be totally transparent to the user, but it can change the views that are rendered.
If something like this is added, it should have defined behavior for these kinds of gotchas.
Dan asked me to open up an issue: https://twitter.com/dan_abramov/status/1246883821477339139
My proposal is to extend React with a small hook that allows us to mutate nodes without causing render. React has no official means to deal with fast occurring updates and libraries like react-spring and framer-motion already do something similar but in a way that forces them to carry a lot of burden.
It uses the fact that reconcilers know how to handle props, something we don't know in userland unless we cause render to set fresh props, which is not at all optimal for animation or anything frame based. react-dom for instance knows what
margin: 3px
is, react-three-fiber knows whatposition: [1,2,3]
is, and so on. These details are defined in the reconciler:If libraries could use this knowledge from outside they could deal with any platform. Animation libraries like react-spring or framer-motion would turn x-platform in one strike, they could animate everything: dom nodes, react native views, meshes, hardware diodes. We could finally write libraries that are not reliant on platforms.