This is allowed for ReactNative apps. Source
AppHub.io is one option, open source coming soon.
See facebook/react-native#2648
ReactNative docs are pretty light on details here. Here are my more details steps based on my test project.
- Create a JS bundle that will be packaged with your app
- You must have
react-native-cli
npm module installed (Recommended to to install this globally) react-native bundle
command is used to create a bundle.--help
will list the options you can pass in- For my test project, i have deviated from the basic project template so i need to pass some arguments for it to work
- From the root folder of my project I ran this:
react-native bundle --root App --url ReactNativeTest.js --out iOS/ReactNativeTest/ReactNativeTest.jsbundle --dev --minify
--root <folder>
specifies where all my JS files live--url <pathToRootJSModule>
specifies the path to my root JS component (relative to my root folder)--out <bundleName>
specifies the output path (relative to the current folder) of the bundle--dev
set this if you want to keep the DEV flag set. DEV flag enables some additional safeguards and checks while your app is running.--minify
minifies the bundle. Obviously this should be set when you plan to build a distributable version of your app.
- Update Xcode project to include JS bundle
- Open project in XCode
- Remove
main.jsbundle
if it's there. This is a dummy bundle file. - Expand
ReactNativeTest
project - Right click the
ReactNativeTest
folder and selectAdd files to ...
- Select
ReactNativeTest.jsbundle
from the finder - Select
ReactNativeTest
project, click theReactNativeTest
target and go to theBuild Phases
tab - Under
Copy Bundle Resources
addReactNativeTest.jsbundle
(main.jsbundle can be removed if it's here as well)
- Update AppDelegate to use packaged bundle instead of URL
- Comment or remove the line
jsCodeLocation = [NSURL URLWithString...
- Add the following line
jsCodeLocation = [[NSBundle mainBundle] URLForResource:@"ReactNativeTest" withExtension:@"jsbundle"];
- (Optional step) Don't want the package server to start when you run the app from Xcode anymore? If you always do
npm start
yourself, or if you packaged the JS bundle in the app, this script is kind of a nuisance.
- Expand the
Libararies
folder under your project in Xcode - Select
React
project - Go to Build Phases tab
- Locate the
Run Script
phase that callsopen $SRCROOT/../packager/launchPackager.command
and remove it
It appears that debugging the JS via Chrome or Safari will not work if the JS is bundled with the app. Expect things to blow up badly if you try to enable debugging.
Use jest for unit tests. It automatically mocks everything by default.
npm install —save-dev jest-cli
Add the following to package.json
"scripts": {
"test": "jest"
},
"jest": {
"scriptPreprocessor": "<rootDir>/node_modules/react-native/jestSupport/scriptPreprocess.js",
"setupEnvScriptFile": "<rootDir>/node_modules/react-native/jestSupport/env.js",
"testPathIgnorePatterns": ["/node_modules/"],
"testFileExtensions": ["js"],
"moduleFileExtensions": ["js"],
}
You can debug your tests (2 ways):
-
Via node debugger (console) Add
"test-debug": "node debug --harmony ./node_modules/jest-cli/bin/jest.js —runInBand”
to scripts in package.jsonnpm run-script test-debug
-
Via node-inspector (Chrome)
npm install -g node-inspector
"test-chrome": "node-debug --nodejs --harmony ./node_modules/jest-cli/bin/jest.js —runInBand”
to scripts in package.jsonnpm run-script test-chrome
Jest is slow You can speed it up a little bit by moving your js files into a subdirectory (I called it App) and telling jest to only look there. This avoid searching through iOS and any other folders.
- Create a folder to put all your JS code
- Move
index.ios.js
into that folder (and rename it if to something else if you like) - Update
AppDeledate.m
under iOS folder, set the jsCodeLocation .bundle to point to the path where you moved your index.os.js file
Jest does not play well with ReactNative Source
Current accepted solution is to mock ReactNative using a manual mock, and replace ReactNative with React during testing.
But having both React and ReactNative in your node_modules seems to fuck things up. Solved by isolatating this mess in the App folder where your JS files live.
Here's what I did:
- Create a
package.json
in the App folder that looks like this: package.json - Remove any jest stuff from your root
package.json
- Create a mocks folder and a
react-native.js
file like this: react-native.js
Initially I went with the 'Replace react-native with react' approach, but I felt like that was pretty shitty, and nne of the native components were exposed. So my next idea was to use the real react-native but mock everything native.
- I updated react-native.js
- I also created another mock file for the injected native objects NativeModules.js
You can’t use ES6 import syntax in your tests Source
The imports get hoisted above your jest.dontMock, so everything gets mocked! Need to use require() syntax for importing modules you want to use
We currently use OCMock for unti testing iOS native code
I just discovered an interesting, and shitty, thing today. OCMExpect
did not check if the mock i passed in was nil.
So it's easy to have tests that don't assert anything and the test just passes. Because any messages you send to nil just fail silently.
Making JS testing for view rendering, components, and JS code is one big part of this. But also finding solid, reliable, developer friendly testing solutions for native code is super important.
ReactNative runs JS code in a JS environment via JavascriptCore
Apparently they are compiling JavascriptCore for Android as well Source
requires works not as you would expect
- Facebook handles requires differently from other module frameworks
- They use
@providesModule <ModuleName>
in the module definition and you require<ModuleName>
- They handle resolving where the module lives
- In React they do this via build tasks
- fbjs
- See module-map gulp task
- and rewrite-modules
- fbjs
- In ReactNative the packager handles module resolution
- They are moving away from this system in order to adopt more established standards and do not recommend using it
ReactNative uses React, but not the one you think
EDIT: This is not what happens when running code natively. ReactNative is exported as React here
React
module used in ReactNative is actually coming fromreact-tools
react-tools
has been deprecated Source but they are still using it in ReactNative because they use some of the react objects- This feels a bit messy and is clearly a legacy thing. Hopefully they will clean this up and replace react-tools with a module that more closely matches (something like react-core or just react)
Some objects are pooled to reduce object creation and GC calls
- If you see
getPooled()
orrelease()
being called on class constructors that is for pooled objects - Details of pooling can be found in
PooledClass.js
(react-tools
)
AppDelegate.m
- Creates a
RCTRootView
, passing it the JS bundle and the name of the module for the app root component RCTRootView
is set as the view of aUIViewController
RCTRootView.m
- Creates and owns
RCTBridge.m
- Loads the JS bundle and initializes the JS environment
- Shows a loading view while the JS environment is being initialized
- Once JS env is ready it replaces the loading view with a
RCTRootContentView
- Finally calls into JS env
AppRegistry.runApplication
with the module for the app root component
RCTRootContentView
- The view that contains all the react UI components
RCTBridge.m
- Responsible for loading all of the bridged modules that will be accessible from the JS env
- During class initialization it finds all classes that implement
RCTBridgeModule
protocol - Registers them so they can be added to the JS env later
- Creates and owns the
RCTBatchedBridge.m
RCTBatchedBridge.m
- Responsible for the communication between the native and JS environments
- Owns the JSExecutor
- Instantiates the native modules that were registered by
RCTBridge.m
- Creates the JS env, injects the native hooks and modules, executes the JS bundle script
- Handles the JS run loop and turns batched JS bridged calls into native invocations
- Batches Native calls into JS env and sends them to the JS executor
RCTJavaScriptLoader.m
- Loads and parses a script bundle from a specified location
- Returns the raw string result if successful
- Handles errors related to fetching and parsing the bundle
RCTContextExecutor.m
- Marshalls calls into the JS env
RCTModuleData.m
- Gathers all the bridged config for a module that will be injected into the JS env
RCTModuleMethod.m
- Invokes method calls coming from the JS env
AppRegistry.js
- JS entry point for running ReactNative apps via
runApplication
- runApplication uses
renderApplication.ios.js
to start rendering the root component registerComponent
is called to register the root componentRCTRootView
passes in the root component name when it callsrunApplication
registerConfig
andregisterRunnable
allow you to register other things to be run, but I could not find anywhere they were being used
renderApplication.ios.js
- Contains the definition for the
AppContainer
component. renderApplication
method callsReact.render
, rendering the AppContainer with the root component as its only child.- The JSX is converted into
React.createElement
calls to build up theReactElement
objects that are passed intorender
ReactNative.js
is the module that is exported as React
render
callsReactNativeMount.renderComponent
ReactNativeMount.js
renderComponent
:- creates a
ReactElement
-TopLevelWrapper
- Checks if there is already a component rendered in the container
- If so, check if it should be updated TODO - dig into this
- Otherwise the existing component is unmounted TODO - dig into this
- allocates a root node ID for the container tag
- container tag is set when the
RCTRootView
creates theRCTRootContentView
RCTRootContentView.setUp
is called duringinit
setUp
callsself.reactTag = [_bridge.uiManager allocateRootTag]
to set the root tag- allocateRootTag source
- root tags are always integers where
(x mod 10 = 1)
- a root node ID for a tag = 1 looks like this:
".r[1]{TOP_LEVEL}"
ReactNativeTagHandles
stores the mapping betweentagToRootNodeID
andtagToRootNodeID
- container tag is set when the
- instantiates the react component
instantiateReactComponent.js
passing in the wrapped ReactElement argument fromReact.render
- Instance is added to
_instancesByContainerID
to track which instances are in which containers ReactUpdates.batchedUpdates
is called to mount the components into the node viabatchedMountComponentIntoNode
- creates a
batchedMountComponentIntoNode
gets aReactNativeReconcileTransaction
and calls perform onmountComponentIntoNode
mountComponentIntoNode
is called- calls
ReactReconciler.mountComponent
- calls
RCTUIManager.manageChildren
- calls
ReactUpdates.js
(react-tools
)
batchedUpdates
callsbatchingStrategy.batchedUpdates
batchingStrategy
is injected viaReactNativeDefaultInjection
which is done inReactNative.js
during initial script execution- ReactNative uses
ReactDefaultBatchingStrategy
(react-tools
)
instantiateReactComponent.js
(react-tools
)
- Elements are instantiated as
ReactCompositeComponent
(react-tools
) (For composite components)
ReactReconciler.js
(react-tools
)
- Doesn't seem to add much value. Not too sure why this module needs to exist
- Possibly to prevent duplicating this code in both React and ReactNative?
mountComponent
- Calls
mountComponent
on the component instance
- Calls
ReactCompositeComponent.js
(react-tools
)
mountComponent
- sets default props (from the component class) for props that are not set
- returns a masked context, so that only the context types supported by this component exist on the context
- calls the constructor of the current component (that the composite component wrapped)
- Sets the props, context, refs and update queue for the newly instatiated component
ReactInstanceMap
stores a reference from the instance back to the internal representation- Initializes the pending queue states
- calls
componentWillMount()
if it exists on the component, and then immediately updates any state changes synchronously - begins rendering the component, setting
ReactCurrentOwner.current
while rendering is in process- when ReactElements are instantiated during rendering they set their owner via
ReactCurrentOwner.current
- when ReactElements are instantiated during rendering they set their owner via
- instantiates the component of the returned ReactElement from the render call
- calls
ReactReconciler.mountComponent
on the rendered component created just before
ReactNativeBaseComponent.js
mountComponent
- allocates a native tag for the native component via
ReactNativeTagHandles
- calls
RCTUIManager.createView
to create the native view on the native side - initializes children (which eventually calls
RCTUIManager.manageChildren
- allocates a native tag for the native component via