Extending React Native’s Dev Menu

Guy Blank
Capriza Engineering
5 min readNov 22, 2017

One of the cool things in the react native framework is that it comes with integrated dev tools. Though I’m not a big fan of the shake gesture, I find myself using the development menu a lot, both on the simulator/emulator and on real devices.

But what about adding your own development tools in the react native development menu, tools that are relevant to your specific domain?

Although I couldn’t find how to do this in react native’s documentation, sieving through the source code it appears that the development menu has an API for custom menu entries — both for Android and for iOS!

I’ve put together a quick, 2-step how-to guide and uploaded a sample project for reference.

  • First we’ll just add custom dev menu entries to both iOS & Android which don’t do much except write to the log
  • Next, since we are in react native, we would like to call the JavaScript side and toggle a react native UI in response to a menu item click
React Native dev menus with custom entries in Android & iOS

The iOS Menu Item

Let’s create a new native module with an RCTDevMenuItem property which will be our custom entry.

@interface CustomDevMenuModule : NSObject <RCTBridgeModule>#if __has_include("RCTDevMenu.h")@property (nonatomic, strong, readonly) RCTDevMenuItem *devMenuItem;#endif@end

We’ll synthesize the bridge property and override its setter in-order to use the [bridge.devMenu addItem:] api to add our custom dev menu.
Next, we override the devMenuItem getter to instantiate a “My Test” menu item that will write to the log on click.

Note: we are implementing a bridge module here simply to get a reference to the bridge instance. If you already have a reference to a bridge instance you can add the dev menu item there.

@implementation CustomDevMenuModule@synthesize bridge = _bridge;#if __has_include(<React/RCTDevMenu.h>)RCTDevMenuItem *_devMenuItem;#endifRCT_EXPORT_MODULE();...- (void)setBridge:(RCTBridge *)bridge
{
_bridge = bridge;
#if __has_include(<React/RCTDevMenu.h>) [_bridge.devMenu addItem:self.devMenuItem];#endif}#if __has_include(<React/RCTDevMenu.h>)- (RCTDevMenuItem *)devMenuItem
{
if (!_devMenuItem) {
_devMenuItem =
[RCTDevMenuItem buttonItemWithTitleBlock:^NSString *{
return @"My Test";} handler:^{
NSLog(@"My Test Clicked!");}];
}
return _devMenuItem;
}
#endif@end

The Android Menu Item

Let’s create a CustomDevOptionsHandler class which implements the DevOptionHandler interface.

public class CustomDevOptionsHandler implements DevOptionHandler {

@Override
public void onOptionSelected() {
Log.d("CustomDevOptionsHandler", "My Test Clicked!");
}
}

In the MainActivity, use the DevSupportManager.addCustomDevOption API (which we get from the ReactInstanceManager) to add our CustomDevOptionsHandler.

public class MainActivity extends ReactActivity {

...

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getReactInstanceManager().getDevSupportManager()
.addCustomDevOption("My Test",
new CustomDevOptionsHandler());
}
}

We have just created a custom dev menu entry in both iOS and Android!

If you just want to call a native API or show some native UI, you can go ahead and extend the sample above (you can find the full code here).

Click Handling — Native Side

Next lets see how to call react native and display a react native view on click.

The way native code calls JavaScript in react native is with events — so that’s exactly what we’ll do. The dev menu item click handler will trigger an event to the JavaScript side that in turn will toggle a view.

First we implement an EventEmitter bridge module:

@interface CustomDevMenuEventEmitter : RCTEventEmitter <RCTBridgeModule>@end@implementation CustomDevMenuEventEmitterRCT_EXPORT_MODULE();+ (BOOL)requiresMainQueueSetup
{
return NO;
}
- (dispatch_queue_t)methodQueue
{
return dispatch_get_main_queue();
}
- (NSArray<NSString *> *)supportedEvents
{
return @[@"toggleTestView"];
}
- (void)receiveTestNotification
{
[self sendEventWithName:@"toggleTestView" body:nil];
}
- (void)startObserving
{
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(receiveTestNotification)
name:@"toggleTestView" object:nil];
}
- (void)stopObserving
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
@end

Our event emitter, which fires a “toggleTestView” event, is triggered by an iOS notification with the same name that we’ll fire from our (previously created) CustomDevMenuModule:

@implementation CustomDevMenuModule...#if __has_include(<React/RCTDevMenu.h>)- (RCTDevMenuItem *)devMenuItem
{
if (!_devMenuItem) {
_devMenuItem =
[RCTDevMenuItem buttonItemWithTitleBlock:^NSString *{
return @"My Test";} handler:^{
NSLog(@"My Test Clicked!");
[[NSNotificationCenter defaultCenter]
postNotificationName:@"toggleTestView"
object:nil];}];
}

return _devMenuItem;
}
#endif@end

In Android things are a bit simpler — we’ll just fire the event directly from our CustomDevOptionsHandler. The only thing missing is that we need a reference to the ReactInstanceManager, so we’ll modify our code to pass in a reference from the MainActivity:

public class MainActivity extends ReactActivity {...
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getReactInstanceManager().getDevSupportManager()
.addCustomDevOption("My Test",
new CustomDevOptionsHandler(
getReactInstanceManager()));
}
}

and in the CustomDevOptionsHandler:

public class CustomDevOptionsHandler implements DevOptionHandler {

private ReactInstanceManager _instanceManager;

public CustomDevOptionsHandler(ReactInstanceManager instanceManager) {
this._instanceManager = instanceManager;
}

@Override
public void onOptionSelected() {
Log.d("CustomDevOptionsHandler", "My Test Clicked!");
this._instanceManager
.getCurrentReactContext()
.getJSModule(DeviceEventManagerModule
.RCTDeviceEventEmitter.class)
.emit("toggleTestView", null);
}
}

Click Handling — JavaScript Side

Let’s create a component that listens to the “toggleTestView” event and toggles a modal view:

export default class CustomDevMenuView extends Component {
constructor(props) {
super(props);

this.state = {
modalVisible: false
};
}

componentWillMount() {
this.subscription = new NativeEventEmitter(
NativeModules.CustomDevMenuEventEmitter
).addListener("toggleTestView", () => {
this.setState({ modalVisible: !this.state.modalVisible });
});
}

componentWillUnmount() {
this.subscription.remove();
}

render() {
return (
<Modal
animationType="slide"
transparent={false}
visible={this.state.modalVisible}
onRequestClose={() => this.setState({ modalVisible: false })}
>
<View style={styles.devMenuContainer}>
<Text style={styles.welcome}>Hello custom dev menu!</Text>
</View>
</Modal>
);
}
}

Add the component to our app (note that it only renders in dev mode):

export default class App extends Component<{}> {
render() {
return (
<View style={styles.container}>
{__DEV__ ? <CustomDevMenuView /> : null}
<Text style={styles.welcome}>Welcome to React Native!</Text>
<Text style={styles.instructions}>To get started, edit App.js</Text>
<Text style={styles.instructions}>{instructions}</Text>
</View>
);
}
}

And that’s it! We now have custom dev menu entries that toggle a react native view.

I hope you can use this example to create tools that help you and your team in your react native development.

Full code is available here.

--

--