Skip to content
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

BREAKING - RCTEvent improvements, remove deprecated [sendInputEventWithName:body:] #15894

Closed
33 changes: 31 additions & 2 deletions RNTester/RNTesterUnitTests/RCTEventDispatcherTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@ - (void)dispatchBlock:(dispatch_block_t)block
@end

@implementation RCTDummyBridge
- (void)dispatchBlock:(dispatch_block_t)block
queue:(dispatch_queue_t)queue
- (void)dispatchBlock:(__unused dispatch_block_t)block
queue:(__unused dispatch_queue_t)queue
{}
@end

Expand Down Expand Up @@ -222,6 +222,35 @@ - (void)testDifferentEventTypesDontCoalesce
[_bridge verify];
}

- (void)testDifferentViewTagsDontCoalesce
{
RCTTestEvent *firstEvent = [[RCTTestEvent alloc] initWithViewTag:@(1)
eventName:_eventName
body:_body
coalescingKey:0];
RCTTestEvent *secondEvent = [[RCTTestEvent alloc] initWithViewTag:@(2)
eventName:_eventName
body:_body
coalescingKey:0];

__block dispatch_block_t eventsEmittingBlock;
[[_bridge expect] dispatchBlock:[OCMArg checkWithBlock:^(dispatch_block_t block) {
eventsEmittingBlock = block;
return YES;
}] queue:RCTJSThread];
[[_bridge expect] enqueueJSCall:[[firstEvent class] moduleDotMethod]
args:[firstEvent arguments]];
[[_bridge expect] enqueueJSCall:[[secondEvent class] moduleDotMethod]
args:[secondEvent arguments]];


[_eventDispatcher sendEvent:firstEvent];
[_eventDispatcher sendEvent:secondEvent];
eventsEmittingBlock();

[_bridge verify];
}

- (void)testSameEventTypesWithDifferentCoalesceKeysDontCoalesce
{
NSString *eventName = RCTNormalizeInputEventName(@"firstEvent");
Expand Down
19 changes: 19 additions & 0 deletions React/Base/RCTComponentEvent.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/

#import <React/RCTEventDispatcher.h>

/**
* Event that is dispatched to React components.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we discribe here that it is general purpose event? Should/can it be subclasses?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It could be subclassed but just reimplementing RCTEvent is probably better especially now that a bunch of methods are optional.

*/
@interface RCTComponentEvent : NSObject<RCTEvent>
janicduplessis marked this conversation as resolved.
Show resolved Hide resolved

- (instancetype)initWithName:(NSString *)name body:(NSDictionary *)body;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, why we initialize it with untyped NSDictionary? I thought whole point of having RCTEvent objects is typesafty and explicitly.

How do you see the perpose of this class? Something simple which should replace all current untyped events? Should we have it?

Copy link
Contributor Author

@janicduplessis janicduplessis Sep 11, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the main use of RCTEvent is to support event coalescing. RCTComponentEvent is a generic event that is sent to a component so it could be something like a touch or scroll event and those can all have different bodies. It is also used by event blocks created by view managers (RCTDirectEventBlock, RCTBubblingEventBlock).

If someone want to make a type safe event I think the best way is to create a new class that implements RCTEvent or subclass RCTComponentEvent. An example of this is https://github.com/facebook/react-native/blob/master/React/Views/RCTScrollView.m#L26

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah!
(How can it be used by RCT*EventBlock if you just added this?)
So, in my mind we have to advice to product developers use RCT*EventBlock or Custom implementation of RCTEvent. We have to discourage them to use this untyped and generic RCTComponentEvent. But, it is okay to use it as a temporary solution for migration from sendEventWithName.

  • RCT*EventBlock Is easy to use.
  • Custom RCTEvent implementation is powerful and semantically explicit.

This should be the most important point of this change. Seems that Nick Lockwood's original idea when he started this migration awhile ago.

Do you agree with this vision? Is this reasonable?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Oh, I understood where we use it as a bearer for RCTEvent*Block, that's great! Fantastic!)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea this class is used internally and could also be used as an easy migration from sendEventWithName. Other events should just implement RCTEvent


@end
57 changes: 57 additions & 0 deletions React/Base/RCTComponentEvent.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/

#import "RCTComponentEvent.h"

#import "RCTAssert.h"

@implementation RCTComponentEvent
{
NSArray *_arguments;
}

@synthesize eventName = _eventName;
@synthesize viewTag = _viewTag;

- (instancetype)initWithName:(NSString *)name body:(NSDictionary *)body
{
if (self = [super init]) {
NSNumber *target = body[@"target"];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why should we extract target from body? Should we pass it directly?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is because target needs to be in the event body for JS so instead of putting it both in the body and passing it as an arg we just extract it from the body.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mmm... So, it means that our API reflects details of internal implementation? Does not sound cool. No?
Do we modify body somewhere down the road before it does to serialization process? If yes, that coupling target and body cannot be counted as optimization and we have to fix our API.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could probably pass target as a parameter and add it to the body in the init.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I think so. We have to know for sure should we do this or not. If it is natural performancewise, I would do it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought the extra copy to make the NSDictionary mutable would be bad but we actually did it already here https://github.com/facebook/react-native/pull/15894/files#diff-b9fc78351b7b976a36356ef6f08c1f79L117. So the main difference performance wise is the extra allocation of RCTEvent

name = RCTNormalizeInputEventName(name);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#ifdef ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea that would be better (just copied that code :))

if (RCT_DEBUG) {
RCTAssert([target isKindOfClass:[NSNumber class]],
@"Event body dictionary must include a 'target' property containing a React tag");
}

_eventName = name;
_viewTag = target;
_arguments = @[target, name, body];
}
return self;
}

RCT_NOT_IMPLEMENTED(- (instancetype)init)

- (NSArray *)arguments
{
return _arguments;
}

- (BOOL)canCoalesce
{
return NO;
}

+ (NSString *)moduleDotMethod
{
return @"RCTEventEmitter.receiveEvent";
}

@end
19 changes: 10 additions & 9 deletions React/Base/RCTEventDispatcher.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,18 +37,25 @@ RCT_EXTERN NSString *RCTNormalizeInputEventName(NSString *eventName);
@protocol RCTEvent <NSObject>
@required

@property (nonatomic, strong, readonly) NSNumber *viewTag;
@property (nonatomic, copy, readonly) NSString *eventName;
@property (nonatomic, assign, readonly) uint16_t coalescingKey;

- (BOOL)canCoalesce;
- (id<RCTEvent>)coalesceWithEvent:(id<RCTEvent>)newEvent;

// used directly for doing a JS call
+ (NSString *)moduleDotMethod;
// must contain only JSON compatible values
- (NSArray *)arguments;

@optional

// Can be implemented for view based events that need to be coalesced
// by it's viewTag.
@property (nonatomic, strong, readonly) NSNumber *viewTag;
// Coalescing related methods must only be implemented if canCoalesce
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New line.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And /* **/.

// returns YES.
@property (nonatomic, assign, readonly) uint16_t coalescingKey;
- (id<RCTEvent>)coalesceWithEvent:(id<RCTEvent>)newEvent;

@end

/**
Expand Down Expand Up @@ -83,12 +90,6 @@ __deprecated_msg("Subclass RCTEventEmitter instead");
- (void)sendDeviceEventWithName:(NSString *)name body:(id)body
__deprecated_msg("Subclass RCTEventEmitter instead");

/**
* Deprecated, do not use.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have two usage of this in internal code base, which is cool and manageable!

*/
- (void)sendInputEventWithName:(NSString *)name body:(NSDictionary *)body
__deprecated_msg("Use RCTDirectEventBlock or RCTBubblingEventBlock instead");

/**
* Send a text input/focus event. For internal use only.
*/
Expand Down
82 changes: 36 additions & 46 deletions React/Base/RCTEventDispatcher.m
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@
#import "RCTAssert.h"
#import "RCTBridge.h"
#import "RCTBridge+Private.h"
#import "RCTUtils.h"
#import "RCTComponentEvent.h"
#import "RCTProfile.h"
#import "RCTUtils.h"

const NSInteger RCTTextUpdateLagWarningThreshold = 3;

Expand All @@ -31,7 +32,7 @@
static NSNumber *RCTGetEventID(id<RCTEvent> event)
{
return @(
event.viewTag.intValue |
([event respondsToSelector:@selector(viewTag)] ? event.viewTag.intValue : 0) |
(((uint64_t)event.eventName.hash & 0xFFFF) << 32) |
(((uint64_t)event.coalescingKey) << 48)
);
Expand Down Expand Up @@ -81,20 +82,6 @@ - (void)sendDeviceEventWithName:(NSString *)name body:(id)body
completion:NULL];
}

- (void)sendInputEventWithName:(NSString *)name body:(NSDictionary *)body
{
if (RCT_DEBUG) {
RCTAssert([body[@"target"] isKindOfClass:[NSNumber class]],
@"Event body dictionary must include a 'target' property containing a React tag");
}

name = RCTNormalizeInputEventName(name);
[_bridge enqueueJSCall:@"RCTEventEmitter"
method:@"receiveEvent"
args:body ? @[body[@"target"], name, body] : @[body[@"target"], name]
completion:NULL];
}

- (void)sendTextEventWithType:(RCTTextEventType)type
reactTag:(NSNumber *)reactTag
text:(NSString *)text
Expand Down Expand Up @@ -136,10 +123,8 @@ - (void)sendTextEventWithType:(RCTTextEventType)type
body[@"key"] = key;
}

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
[self sendInputEventWithName:events[type] body:body];
#pragma clang diagnostic pop
RCTComponentEvent *event = [[RCTComponentEvent alloc] initWithName:events[type] body:body];
[self sendEvent:event];
}

- (void)sendEvent:(id<RCTEvent>)event
Expand All @@ -151,34 +136,39 @@ - (void)sendEvent:(id<RCTEvent>)event
}

[_observersLock unlock];

[_eventQueueLock lock];

NSNumber *eventID = RCTGetEventID(event);

id<RCTEvent> previousEvent = _events[eventID];
if (previousEvent) {
RCTAssert([event canCoalesce], @"Got event %@ which cannot be coalesced, but has the same eventID %@ as the previous event %@", event, eventID, previousEvent);
event = [previousEvent coalesceWithEvent:event];

if (event.canCoalesce) {
[_eventQueueLock lock];

NSNumber *eventID = RCTGetEventID(event);

id<RCTEvent> previousEvent = _events[eventID];
if (previousEvent) {
event = [previousEvent coalesceWithEvent:event];
janicduplessis marked this conversation as resolved.
Show resolved Hide resolved
} else {
[_eventQueue addObject:eventID];
}
_events[eventID] = event;

BOOL scheduleEventsDispatch = NO;
if (!_eventsDispatchScheduled) {
_eventsDispatchScheduled = YES;
scheduleEventsDispatch = YES;
}

// We have to release the lock before dispatching block with events,
// since dispatchBlock: can be executed synchronously on the same queue.
// (This is happening when chrome debugging is turned on.)
[_eventQueueLock unlock];

if (scheduleEventsDispatch) {
[_bridge dispatchBlock:^{
[self flushEventsQueue];
} queue:RCTJSThread];
}
} else {
[_eventQueue addObject:eventID];
}
_events[eventID] = event;

BOOL scheduleEventsDispatch = NO;
if (!_eventsDispatchScheduled) {
_eventsDispatchScheduled = YES;
scheduleEventsDispatch = YES;
}

// We have to release the lock before dispatching block with events,
// since dispatchBlock: can be executed synchronously on the same queue.
// (This is happening when chrome debugging is turned on.)
[_eventQueueLock unlock];

if (scheduleEventsDispatch) {
[_bridge dispatchBlock:^{
[self flushEventsQueue];
[self dispatchEvent:event];
} queue:RCTJSThread];
}
}
Expand Down
12 changes: 12 additions & 0 deletions React/React.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,10 @@
14F7A0F01BDA714B003C6C10 /* RCTFPSGraph.m in Sources */ = {isa = PBXBuildFile; fileRef = 14F7A0EF1BDA714B003C6C10 /* RCTFPSGraph.m */; };
191E3EBE1C29D9AF00C180A6 /* RCTRefreshControlManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 191E3EBD1C29D9AF00C180A6 /* RCTRefreshControlManager.m */; };
191E3EC11C29DC3800C180A6 /* RCTRefreshControl.m in Sources */ = {isa = PBXBuildFile; fileRef = 191E3EC01C29DC3800C180A6 /* RCTRefreshControl.m */; };
1968A25F1F67275300EB3D1D /* RCTComponentEvent.h in Headers */ = {isa = PBXBuildFile; fileRef = 1968A25D1F67275300EB3D1D /* RCTComponentEvent.h */; };
1968A2601F67275300EB3D1D /* RCTComponentEvent.h in Headers */ = {isa = PBXBuildFile; fileRef = 1968A25D1F67275300EB3D1D /* RCTComponentEvent.h */; };
1968A2611F67275300EB3D1D /* RCTComponentEvent.m in Sources */ = {isa = PBXBuildFile; fileRef = 1968A25E1F67275300EB3D1D /* RCTComponentEvent.m */; };
1968A2621F67275300EB3D1D /* RCTComponentEvent.m in Sources */ = {isa = PBXBuildFile; fileRef = 1968A25E1F67275300EB3D1D /* RCTComponentEvent.m */; };
19F61BFA1E8495CD00571D81 /* bignum-dtoa.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 139D7E3A1E25C5A300323FB7 /* bignum-dtoa.h */; };
19F61BFB1E8495CD00571D81 /* bignum.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 139D7E3C1E25C5A300323FB7 /* bignum.h */; };
19F61BFC1E8495CD00571D81 /* cached-powers.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 139D7E3E1E25C5A300323FB7 /* cached-powers.h */; };
Expand Down Expand Up @@ -1842,6 +1846,8 @@
191E3EBD1C29D9AF00C180A6 /* RCTRefreshControlManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTRefreshControlManager.m; sourceTree = "<group>"; };
191E3EBF1C29DC3800C180A6 /* RCTRefreshControl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTRefreshControl.h; sourceTree = "<group>"; };
191E3EC01C29DC3800C180A6 /* RCTRefreshControl.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTRefreshControl.m; sourceTree = "<group>"; };
1968A25D1F67275300EB3D1D /* RCTComponentEvent.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTComponentEvent.h; sourceTree = "<group>"; };
1968A25E1F67275300EB3D1D /* RCTComponentEvent.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTComponentEvent.m; sourceTree = "<group>"; };
19DED2281E77E29200F089BB /* systemJSCWrapper.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = systemJSCWrapper.cpp; sourceTree = "<group>"; };
27B958731E57587D0096647A /* JSBigString.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = JSBigString.cpp; sourceTree = "<group>"; };
2D2A28131D9B038B00D4039D /* libReact.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libReact.a; sourceTree = BUILT_PRODUCTS_DIR; };
Expand Down Expand Up @@ -2588,6 +2594,8 @@
830213F31A654E0800B993E6 /* RCTBridgeModule.h */,
68EFE4EC1CF6EB3000A1DE13 /* RCTBundleURLProvider.h */,
68EFE4ED1CF6EB3900A1DE13 /* RCTBundleURLProvider.m */,
1968A25D1F67275300EB3D1D /* RCTComponentEvent.h */,
1968A25E1F67275300EB3D1D /* RCTComponentEvent.m */,
83CBBACA1A6023D300E9B192 /* RCTConvert.h */,
83CBBACB1A6023D300E9B192 /* RCTConvert.m */,
C60128A91F3D1258009DF9FF /* RCTCxxConvert.h */,
Expand Down Expand Up @@ -2852,6 +2860,7 @@
134D63C41F1FEC65008872B5 /* RCTCxxBridgeDelegate.h in Headers */,
3D302F801DF828F800D6DDAE /* RCTNavItem.h in Headers */,
C6194AAD1EF156280034D062 /* RCTPackagerConnectionBridgeConfig.h in Headers */,
1968A2601F67275300EB3D1D /* RCTComponentEvent.h in Headers */,
3D302F811DF828F800D6DDAE /* RCTNavItemManager.h in Headers */,
135A9C061E7B0F7800587AEB /* RCTJSCHelpers.h in Headers */,
3D302F841DF828F800D6DDAE /* RCTPointerEvents.h in Headers */,
Expand Down Expand Up @@ -3045,6 +3054,7 @@
3D7BFD211EA8E351008DFB7A /* RCTReloadPackagerMethod.h in Headers */,
3D80DA231DF820620028D040 /* RCTBridgeDelegate.h in Headers */,
3D80DA241DF820620028D040 /* RCTBridgeMethod.h in Headers */,
1968A25F1F67275300EB3D1D /* RCTComponentEvent.h in Headers */,
3D7BFD151EA8E351008DFB7A /* RCTPackagerClient.h in Headers */,
3D80DA251DF820620028D040 /* RCTBridgeModule.h in Headers */,
3D80DA261DF820620028D040 /* RCTBundleURLProvider.h in Headers */,
Expand Down Expand Up @@ -3630,6 +3640,7 @@
2D3B5E9B1D9B08A000451313 /* RCTFrameUpdate.m in Sources */,
2D3B5EE41D9B09BB00451313 /* RCTSegmentedControlManager.m in Sources */,
13134C9F1E296B2A00B9F3CB /* RCTCxxModule.mm in Sources */,
1968A2621F67275300EB3D1D /* RCTComponentEvent.m in Sources */,
2D3B5EE31D9B09B700451313 /* RCTSegmentedControl.m in Sources */,
130443A41E3FEAC600D93A67 /* RCTFollyConvert.mm in Sources */,
3D7BFD201EA8E351008DFB7A /* RCTPackagerConnection.m in Sources */,
Expand Down Expand Up @@ -3892,6 +3903,7 @@
130E3D891E6A082100ACE484 /* RCTDevSettings.mm in Sources */,
13513F3C1B1F43F400FCE529 /* RCTProgressViewManager.m in Sources */,
14F7A0F01BDA714B003C6C10 /* RCTFPSGraph.m in Sources */,
1968A2611F67275300EB3D1D /* RCTComponentEvent.m in Sources */,
3D7BFD171EA8E351008DFB7A /* RCTPackagerClient.m in Sources */,
14F3620D1AABD06A001CE568 /* RCTSwitch.m in Sources */,
13134C8E1E296B2A00B9F3CB /* RCTMessageThread.mm in Sources */,
Expand Down
7 changes: 3 additions & 4 deletions React/Views/RCTComponentData.m
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

#import "RCTBridge.h"
#import "RCTBridgeModule.h"
#import "RCTComponentEvent.h"
#import "RCTConvert.h"
#import "RCTShadowView.h"
#import "RCTUtils.h"
Expand Down Expand Up @@ -116,10 +117,8 @@ static RCTPropBlock createEventSetter(NSString *propName, SEL setter, RCTBridge

NSMutableDictionary *mutableEvent = [NSMutableDictionary dictionaryWithDictionary:event];
mutableEvent[@"target"] = strongTarget.reactTag;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
[weakBridge.eventDispatcher sendInputEventWithName:RCTNormalizeInputEventName(propName) body:mutableEvent];
#pragma clang diagnostic pop
RCTComponentEvent *componentEvent = [[RCTComponentEvent alloc] initWithName:propName body:mutableEvent];
[weakBridge.eventDispatcher sendEvent:componentEvent];
};
}
((void (*)(id, SEL, id))objc_msgSend)(target, setter, eventHandler);
Expand Down