Both Animated.Value
(from React Native's Animated API) and SharedValue
(from Reanimated) are containers for values that can change without triggering re-renders, and are typically used to drive animations. But since they are from different libraries, there is no interoperability between them.
If you install a third-party component that uses Animated API, and you use Reanimated in your app, you can’t use your Reanimated value to drive animations in the component using Animated and vice versa.
Both Animated and Reanimated have a way to listen to changes in Animated.Value
and SharedValue
, respectively, so you could subscribe to changes in one and update the other by using animatedValue.setValue
or sharedValue.set
respectively. However, this isn’t ideal, as the listeners run in JavaScript thread. So if the JavaScript thread is busy, the values won’t update in time, and animations won’t be smooth.
To solve this challenge, I decided to create a new library - react-native-animated-observer
- to address this issue by allowing you to convert between these formats natively.
How it works
Both Animated API and Reanimated have ways to drive animations from the UI thread. This makes it possible to perform smooth animations based on scroll, gestures, etc. It works by attaching the Animated or Reanimated values to such events. Animated provides the Animated.event
API, and Reanimated provides the useEvent
API to handle this.
This library uses the same concept:
- It renders a native component that receives an
Animated.Node
orSharedValue
. When the value changes, the library dispatches an event with this value. - Then an Animated or Reanimated value is attached to this event to update it without going through the JavaScript thread.
Here is some pseudo-code to demonstrate conversion between a Reanimated SharedValue
and an Animated.Value
:
<SomeComponent
// The component receives a reanimated shared value in a prop
value={reanimatedSharedValue}
// The value change is dispatched in `onValueChange` event
onValueChange={Animated.event(
// The value from e.nativeEvent.value is used to update animatedValue
[{ nativeEvent: { value: animatedValue } }]
)}
/>
Challenges
While the concept is simple, dispatching an event from a Fabric UI component that works with Animated and Reanimated APIs is quite tricky. So let’s go through how to make it work.
Codegen
First, if we’re using Codegen, we need to define the type of the event. It can be DirectEventHandler
or BubblingEventHandler
.
In our case, we want to use DirectEventHandler
:
type ValueChangeEvent = {
value: CodegenTypes.Double;
};
interface NativeProps extends ViewProps {
// other props
onValueChange?: CodegenTypes.DirectEventHandler<ValueChangeEvent>;
}
Android
To dispatch an event from a UI component on Android, first, we need to define a getExportedCustomDirectEventTypeConstants
method in our view manager:
class MyViewManager : SimpleViewManager<MyView>(),
MyViewManagerInterface<MyView> {
// ...
override fun getExportedCustomDirectEventTypeConstants() = mapOf(
"onValueChange" to mapOf("registrationName" to "onValueChange")
}
}
Next, define a class for our event:
inner class ValueChangeEvent(
surfaceId: Int, viewId: Int, private val value: Double
) : Event<ValueChangeEvent>(surfaceId, viewId) {
override fun getEventName() = "onValueChange"
override fun getEventData(): WritableMap = Arguments.createMap().apply {
putDouble("value", value)
}
}
Then, we can dispatch the event using the id of the view that should receive the event:
val reactContext = context as ReactContext
val surfaceId = UIManagerHelper.getSurfaceId(reactContext)
val eventDispatcher = UIManagerHelper.getEventDispatcherForReactTag(reactContext, surfaceId)
?: throw IllegalStateException("$NAME: EventDispatcher is not available for surfaceId: $surfaceId")
eventDispatcher.dispatchEvent(ValueChangeEvent(surfaceId, this.id, value))
Now we should receive events when we add an event listener on our component in JS.
Making it work with useNativeDriver
While this setup usually works for standard event handling, it fails when using Animated.Event
with useNativeDriver: true
. This is because Fabric expects all events to be fired with a top
prefix. It normalizes event names by adding top
if the user hasn’t already when using the interop layer.
However, in Fabric views that don’t use the interop layer, the native driver listens directly to the raw native event name and does not go through this normalization step, which means it only responds to events that are explicitly prefixed with top
.
So we need to go back and replace onValueChange
with topValueChange
.
iOS
Events can be dispatched on iOS in the following way:
auto eventEmitter = std::static_pointer_cast<const MyViewEventEmitter>(self->_eventEmitter);
if (eventEmitter) {
eventEmitter->onValueChange(MyViewEventEmitter::OnValueChange{
.value = value
});
}
That’s it! As you can see, the necessary code is already generated by codegen, meaning we don’t have to worry about the actual event names and normalization.
Making it work with useNativeDriver
Unfortunately, this still doesn’t work when using useNativeDriver: true
. The reason is that the native driver listens for events directly on the native side. The only way to reach it is through a deprecated event-dispatcher API that, in current React Native, is only used internally by ScrollView
.
To notify the Animated module yourself, you must explicitly fire this deprecated event.
There’s an open item in React Native to replace this legacy mechanism with a cleaner integration for native-driven animations, at which point this workaround will no longer be needed.
First, we need to define a class similar to Android. Let’s make a header file RCTOnValueChangeEvent.h
:
@interface RCTOnValueChangeEvent : NSObject <RCTEvent>
- (instancetype) initWithReactTag:(NSNumber *)reactTag
value:(NSNumber *)value;
@end
Then the class RCTOnValueChangeEvent.mm
:
@implementation RCTOnValueChangeEvent
{
NSNumber* _value;
}
@synthesize viewTag = _viewTag;
- (NSString *)eventName {
return @"onValueChange";
}
- (instancetype) initWithReactTag:(NSNumber *)reactTag
value:(NSNumber *)value
{
RCTAssertParam(reactTag);
if ((self = [super init])) {
_viewTag = reactTag;
_value = value;
}
return self;
}
RCT_NOT_IMPLEMENTED(- (instancetype)init)
- (uint16_t)coalescingKey
{
return 0;
}
- (BOOL)canCoalesce
{
return YES;
}
+ (NSString *)moduleDotMethod
{
return @"RCTEventEmitter.receiveEvent";
}
- (NSArray *)arguments
{
return @[self.viewTag, RCTNormalizeInputEventName(self.eventName), @{
@"value": _value
}];
}
- (id<RCTEvent>)coalesceWithEvent:(id<RCTEvent>)newEvent;
{
return newEvent;
}
@end
Finally, we can use this class to dispatch the event:
ValueChangeEvent *event = [[ValueChangeEvent alloc] initWithReactTag:@(self.tag)
value:value];
NSDictionary *userInfo = @{@"event": event};
[[NSNotificationCenter defaultCenter] postNotificationName:@"RCTNotifyEventDispatcherObserversOfEvent_DEPRECATED"
object:nil
userInfo:userInfo];
Now our event will work with Animated.event
with native driver.
Wrapping up
As you can see, dispatching events from Fabric UI components is simple, unless you are dealing with legacy architecture or older APIs (for example, useNativeDriver
which behaves quite differently in how and when it subscribes to native updates).
Now that you are aware of some of these gotchas, it should be easier if you need to dispatch events in your own component. Feel free to take a look at the source code of https://github.com/satya164/react-native-animated-observer if you are still having issues. It’s a small library, so hopefully it’ll be easier to understand.
And thanks to @Piotr Trocki, @Oskar Kwaśniewski and @Mike Grabowski for sharing gotchas around Fabric and events with me, otherwise I’d be lost as well.
Learn more about
React Native
Here's everything we published recently on this topic.
We can help you move
it forward!
At Callstack, we work with companies big and small, pushing React Native everyday.
React Native Performance Optimization
Improve React Native apps speed and efficiency through targeted performance enhancements.
Code Sharing
Implement effective code-sharing strategies across all platforms to accelerate shipping and reduce code duplication.
Mobile App Development
Launch on both Android and iOS with single codebase, keeping high-performance and platform-specific UX.
React Native Development
Hire expert React Native engineers to build, scale, or improve your app, from day one to production.
