Skip to content

Instantly share code, notes, and snippets.

@nsimmons
Last active October 9, 2023 07:48
Show Gist options
  • Save nsimmons/d2ae624d2336f4ac436b to your computer and use it in GitHub Desktop.
Save nsimmons/d2ae624d2336f4ac436b to your computer and use it in GitHub Desktop.
ReactNative Deep Dive

Updates without submitting to App Store

This is allowed for ReactNative apps. Source

AppHub.io is one option, open source coming soon.

Building a ReactNative app with Swift

See facebook/react-native#2648

Bundle JS with application (Offline mode)

ReactNative docs are pretty light on details here. Here are my more details steps based on my test project.

  1. 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.
  1. 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 select Add files to ...
  • Select ReactNativeTest.jsbundle from the finder
  • Select ReactNativeTest project, click the ReactNativeTest target and go to the Build Phases tab
  • Under Copy Bundle Resources add ReactNativeTest.jsbundle (main.jsbundle can be removed if it's here as well)
  1. 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"];
  1. (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 calls open $SRCROOT/../packager/launchPackager.command and remove it

Gotchas:

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.

Testing

Use jest for unit tests. It automatically mocks everything by default.

Setup:

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):

  1. Via node debugger (console) Add "test-debug": "node debug --harmony ./node_modules/jest-cli/bin/jest.js —runInBand” to scripts in package.json npm run-script test-debug

  2. 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.json npm run-script test-chrome

Gotchas:

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.

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 Internals

ReactNative runs JS code in a JS environment via JavascriptCore

Apparently they are compiling JavascriptCore for Android as well Source

Gotchas

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
  • 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 from react-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() or release() being called on class constructors that is for pooled objects
  • Details of pooling can be found in PooledClass.js (react-tools)

Starting up

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 a UIViewController

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 component
  • RCTRootView passes in the root component name when it calls runApplication
  • registerConfig and registerRunnable 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 calls React.render, rendering the AppContainer with the root component as its only child.
  • The JSX is converted into React.createElement calls to build up the ReactElement objects that are passed into render

Rendering process

ReactNative.js is the module that is exported as React

  • render calls ReactNativeMount.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 the RCTRootContentView
      • RCTRootContentView.setUp is called during init
      • setUp calls self.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 between tagToRootNodeID and tagToRootNodeID
    • instantiates the react component instantiateReactComponent.js passing in the wrapped ReactElement argument from React.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 via batchedMountComponentIntoNode
  • batchedMountComponentIntoNode gets a ReactNativeReconcileTransaction and calls perform on mountComponentIntoNode
  • mountComponentIntoNode is called
    • calls ReactReconciler.mountComponent
    • calls RCTUIManager.manageChildren

ReactUpdates.js (react-tools)

  • batchedUpdates calls batchingStrategy.batchedUpdates
  • batchingStrategy is injected via ReactNativeDefaultInjection which is done in ReactNative.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

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
    • 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

Update process

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment