CSS Transitions hijacks ShadowTree updates for attached nodes
Description
Hey!
A new issue came up and was reported by a Unistyles user. When both a CSS Transition and a Unistyles style are attached to the same component, theme changes don’t apply to those nodes.
I debugged it, my ShadowTree update sends the correct values, but it looks like CSS Transition nodes are handled differently on your side, which blocks me from applying any updates on top.
That’s not the case for regular Animated components using useAnimatedStyle. Could you let me know why this happens and more importantly how I can push an update for those nodes?
At first I thought it was because you spread the styles, but after checking your filterNonCSSStyleProps function, I realized that was a wrong assumption.
Steps to reproduce
- Open the app
- Tap on "Check box" button
- One button will change color immediately and second one will slowly transition
- Tap on "Change theme" button
- One button won't take in count new color applied by Unistyles (from ShadowTree)
Shadow tree styles pushed to ShadowTree by Unistyles are correct:
Each section represent one ShadowNode and styles that should be applied.
Original issue: https://github.com/jpudysz/react-native-unistyles/issues/1007
Snack or a link to a repository
https://github.com/believer/unistyles-repro
Reanimated version
4.1.3
Worklets version
0.6.1
React Native version
0.81.4
Platforms
Android, iOS
JavaScript runtime
Hermes
Workflow
React Native CLI
Architecture
New Architecture (Fabric renderer)
Build type
Debug app & dev bundle
Device
iOS simulator
Host machine
macOS
Device model
iPhone 16e
Acknowledgements
Yes
Hi @jpudysz, thanks for reporting this issue.
Telling from the repro you have provided it looks like you're trying to set backgroundColor style both using Reanimated and Unistyles. Currently, Reanimated uses an approach that wasn't really designed to support style updates from multiple sources at once. This is however something we'd like to address at some point in the future.
Given that no one else has reported this problem so far and the fact that it only occurs if you mix Reanimated and Unistyles styling in the very same component we decided to treat this issue as low priority for now. However, feel free to investigate the codebase on your own and submit a pull request which we'll be more than happy to review if you manage to find a reliable solution.
Can we make Reanimated skip overriding certain commits, for example by checking a commit trait? I’ll soon release Uniwind, which will do something similar, so I expect these issues to start piling up.
Right now, Unistyles uses deprecated React Native Animated escape hatches, which prevents Reanimated from overriding it, but we should probably find a cleaner solution.
I understand why you override commits due to the nature of animations, but sometimes users just want to change the theme. In such cases, the only options are to re-render or duplicate the theming logic in Reanimated hooks.
I can draft a PR with a potential fix if that helps.
We would rather avoid adding any third-party library-specific escape hatches since they tend to pile up and make the logic overly complex. Such workarounds get more and more difficult to maintain and reason about in the long term.
If the bug happens only for CSS Transitions and doesn't happen for useAnimatedStyle animations, perhaps the difference lies in how the updates are propagated to the animated props and CSS transitions registries? This is the place I would investigate first.
Hey @jpudysz!
I'd like to share more details on how CSS Transitions are implemented in Reanimated and why you encounter this issue.
When someone marks a certain property as the transitionProperty, then Reanimated takes the full control over this prop's value. The style passed to the Animated component is used as the source of truth. It is sent from JS to CPP where we calculate a diff and, if properties listed under the transitionProperty change, the CSS transition is triggered for them. Because of this design, we always override the value of transitioned properties in the commit hook, so values from other commits can't override these values.
We obtained this design by purpose, to prevent any changed from RN or other sources from breaking the CSS transition state.
As @tomekzaw already said, I think we won't allow commits from other sources to override these values as it will break the transition state.
Hey @MatiPl01, thanks for the insights. Sorry for the late reply I was OOO.
It seems there’s no other solution, and users need to re-render the app to pass new colors to Reanimated components that use CSS transitions.
I think it would be good to come up with an API or solution to address these issues. The same applies to overriding any other transitions via the ShadowTree produced by Unistyles or Uniwind. We need a next-level integration, as more and more developers will be using these libraries together with REA.
Right now, Reanimated acts like a sentry and assumes that only React Native can push directly to the ShadowTree.
Reanimated acts like a sentry and assumes that only React Native can push directly to the ShadowTree.
Only in the case of CSS Transitions. As @MatiPl01 stated, this was designed on purpose, to prevent any changes from RN or other sources from breaking the CSS transition state.
I understand it's about CSS Transitions, but that's only partially true. Without the nativeProps_DEPRECATED hack, updates are sometimes replaced by other animations as well.
Reanimated was originally designed with the assumption that only React Native pushes updates to the ShadowTree. That’s no longer the case, as more libraries now interact with it, eg: https://github.com/nativewind/react-native-css-nitro
Switching themes during runtime or ongoing animations is a realistic scenario. Publishing an official guide or API for handling this would be highly appreciated, otherwise, related issues will keep piling up.
This also means I can’t implement CSS Transitions directly inside Unistyles StyleSheet, since those changes never reach the ShadowTree. The only workaround is to re-render the view using the CSS Transition to propagate the new color down to C++.
My point is simply to think ahead. This will become more common over time.