Native mobile apps with React.js
Brent Vatne
github.com/brentvatne
@notbrent
1. Fetch data to be used in view
2. Put that data into a template, result: HTML
Option 1: Fetch HTML partials from server
- Send request to server to render some partial.
- Replace an entire portion of the page with that partial.
Option 2: Client-side templating
- Same as Option 1, but do rendering on client side. Replace entire portion of page with the result.
Option 3: Direct DOM manipulation
- Imperatively add / remove / modify DOM elements.
$('.todo').addClass('completed'); $('.list').append("<li class='todo'>"+todoName+</li>");
Thanks @steveluscher for letting me use this example!
What if we could have the ease of rendering the entire page each time but the benefits of direct manipulation?
Object.keys(document.createElement('div')) // Array[122] Object.keys(React.DOM.div({})) // ["type", "key", "ref", "_owner", "_context", "props"]
Modified from source: http://www.ibm.com/developerworks/library/wa-react-intro/index.html
var CounterApp = React.createClass({getInitialState() { return {count: 0}; }, incrementCount() { this.setState({count: this.state.count + 1}); }, render() { return ( <div> <h2>My first React app</h2> <span onClick={this.incrementCount}> {this.state.count} clicks </span> </div> ) } });React.render(<CounterApp />, document.querySelector('body'));
var CommentList = React.createClass({ render: function() { return ( <div className="commentList"> Hello, world! I am a CommentList. </div> ); } }); var CommentForm = React.createClass({ render: function() { return ( <div className="commentForm"> Hello, world! I am a CommentForm. </div> ); } }); var CommentBox = React.createClass({ render: function() { return ( <div className="commentBox"> <h1>Comments</h1> <CommentList /> <CommentForm /> </div> ); } });
var CounterApp = React.createClass({
getInitialState() {
return {count: 0};
},
incrementCount() {
this.setState({count: this.state.count + 1});
},
render() {
return (
<View style={styles.container}>
<Text style={styles.welcome}>
My first React Native app
</Text>
<Text onPress={this.incrementCount} style={styles.counter}>
{this.state.count} clicks
</Text>
</View>
);
}
});
RCTUIManager.createView([3,"RCTView",{"flex":1,"justifyContent":"center","alignItems":"center"}])
RCTUIManager.createView([4,"RCTText",{"accessible":true,"isHighlighted":false,"fontSize":26,
"textAlign":"center","margin":10}])
RCTUIManager.createView([5,"RCTRawText",{"text":"My first React Native app"}])
RCTUIManager.createView([6,"RCTText",{"accessible":true,"isHighlighted":false,"fontSize":24}])
RCTUIManager.createView([7,"RCTRawText",{"text":"0"}])
RCTUIManager.createView([8,"RCTRawText",{"text":" clicks"}])
RCTUIManager.updateView([7,"RCTRawText",{"text":"1"}])
RCTUIManager.updateView([7,"RCTRawText",{"text":"2"}])
View, Text, Image, ScrollView, DatePickerIOS, PickerIOS, ListView,
Navigator, SwitchIOS, MapView, AlertIOS, WebView, Touchable, TabBarIOS, SegmentedControlIOS, ActivityIndicatorIOS, CameraRoll, NetInfo, PanResponder, StatusBarIOS, TextInput, SliderIOS, PushNotificationIOS, LinkingIOS, ActionSheetIOS, AsyncStorage, Geolocation, VibrationIOS, Video, Camera, AudioRecorder, etc..
@implementation FacebookLoginManager RCT_EXPORT_MODULE(); RCT_EXPORT_METHOD(newSession:(RCTResponseSenderBlock)callback) { FBSDKLoginManager *login = [[FBSDKLoginManager alloc] init]; [login logInWithReadPermissions:@[@"public_profile", @"email"] handler:^(FBSDKLoginManagerLoginResult *result, NSError *error) { if (error) { callback(@[@"Error", [NSNull null]]); } else if (result.isCancelled) { callback(@[@"Canceled", [NSNull null]]); } else { FBSDKAccessToken *token = result.token; NSString *tokenString = token.tokenString; NSString *userId = token.userID; NSDictionary *credentials = @{ @"token" : tokenString, @"userId" : userId }; callback(@[[NSNull null], credentials]); } }]; }; @end
var FacebookLoginManager = require('NativeModules').FacebookLoginManager; var FacebookLogin = React.createClass({ login() { FacebookLoginManager.newSession((error, info) => { if (!error) { console.log(info) } }); }, render() { return ( <TouchableHighlight onPress={this.login}> <Text style={styles.loginButton}>Facebook Login</Text> </TouchableHighlight> ); } });
@implementation RCTSliderManager RCT_EXPORT_MODULE() - (UIView *)view { UISlider *slider = [[UISlider alloc] init]; [slider addTarget:self action:@selector(sliderValueChanged:) forControlEvents:UIControlEventValueChanged]; return slider; } - (void)sliderValueChanged:(UISlider *)sender { NSDictionary *event = @{ @"target": sender.reactTag, @"value": @(sender.value), @"continuous": @YES, }; [self.bridge.eventDispatcher sendInputEventWithName:@"topChange" body:event]; } RCT_EXPORT_VIEW_PROPERTY(minimumValue, float); RCT_EXPORT_VIEW_PROPERTY(maximumValue, float); @end
var SliderIOS = React.createClass({ mixins: [NativeMethodsMixin], propTypes: { style: View.propTypes.style, minimumValue: PropTypes.number, maximumValue: PropTypes.number, onChange: PropTypes.func, }, render: function() { return ( <RCTSlider style={[styles.slider, this.props.style]} value={this.props.value} maximumValue={this.props.maximumValue} minimumValue={this.props.minimumValue} onChange={this.props.onChange} /> ); } }); var styles = StyleSheet.create({ slider: { height: 40, }, }); var RCTSlider = requireNativeComponent('RCTSlider', SliderIOS); module.exports = SliderIOS;
var MyApp = React.createClass({ onChange(e) { console.log(e); }, render() { return ( <View> <SliderIOS minimumValue={1} maximumValue={42} onChange={this.doSomething} /> </View> ) } });
- Uses iOS run loop rather than poke, 60 fps
- MessageQueue on JS/ObjC sides
- Serialized as JSON using custom RPC protocol - stores callbacks on JS side for later invocation with result.
- Handles conversion of JS data types automatically - support for other conversions (eg: UIColor) too
- Completely asynchronous.
Exercise 1: Add a breakpoint in RCTUIManager.m updateView, explore stack.
Exercise 2: Open up MessageQueue.js and set DEBUG_SPY_MODE to true, run UIExplorer
var SomeComponent = React.createClass({ render() { return <Text>5</Text> } }) var TestCompositeComponents = React.createClass({ render() { return ( <View style={styles.container}> <SomeComponent /> <SomeComponent /> </View> ); } });
function renderApplication(rootComponent, initialProps, rootTag) { var warningBox = shouldRenderWarningBox ? <WarningBox /> : null; React.render( <View style={styles.appContainer}> <RootComponent {...initialProps} /> {warningBox} </View>, rootTag ); }
#import "RCTBridgeModule.h" @interface AppReloader : NSObject <RCTBridgeModule> @end @implementation AppReloader RCT_EXPORT_MODULE() -(dispatch_queue_t)methodQueue { return dispatch_get_main_queue(); } /** * var AppReloader = require('NativeModules').AppReloader; * AppReloader.reloadAppWithURLString('https://example.com/index.ios.bundle', 'App') */ RCT_EXPORT_METHOD(reloadAppWithURLString:(NSString *)URLString moduleNamed:(NSString *)moduleName) { AppDelegate *delegate = (AppDelegate *)[[UIApplication sharedApplication] delegate]; NSURL *JSBundleURL = [NSURL URLWithString:URLString]; [delegate.viewController reloadWithJSBundleURL:JSBundleURL moduleNamed:moduleName]; } @end
Go to http://exp.host on your phone to install