Skip to main content

View Components

Nitro provides first-class support for creating React Native Views.

Such views can be rendered within React Native apps using Fabric, and are backed by a C++ ShadowNode. The key difference to a Fabric view is that it uses Nitro for prop parsing, which is more lightweight, performant and flexible.

note

Nitro Views require react-native 0.78.0 or higher, and require the new architecture. On react-native 0.81.0 or higher, callbacks can be passed to Nitro Views directly (see "Callbacks").

Create a Nitro View

1. Declaration

To create a new Nitro View, declare its props and methods in a *.nitro.ts file, and create a type that specializes HybridView<P, M> - here CameraView:

Camera.nitro.ts
import type { HybridView, HybridViewProps, HybridViewMethods } from 'react-native-nitro-modules'

export interface CameraProps extends HybridViewProps {
enableFlash: boolean
}
export interface CameraMethods extends HybridViewMethods { }

export type CameraView = HybridView<CameraProps, CameraMethods>

2. Code Generation

Then, run nitrogen:

npx nitrogen

This will create a C++ ShadowNode, with an iOS (Swift) and Android (Kotlin) interface, just like any other Hybrid Object. Additionally, a view config (CameraViewConfig.json) will be generated - this is required by Fabric.

3. Implementation

Now it's time to implement the View - simply create a new Swift/Kotlin class/file, extend from HybridCameraViewSpec and implement your .enableFlash property, as well as the common .view accessor:

HybridCameraView.swift
class HybridCameraView : HybridCameraViewSpec {
// Props
var enableFlash: Bool = false

// View
var view: UIView = UIView()
}

Just like any other Hybrid Object, add the Hybrid View to your nitro.json's autolinking configuration:

nitro.json
{
// ...
"autolinking": {
"CameraView": {
"ios": {
"language": "swift",
"implementationClassName": "HybridCameraView"
},
"android": {
"language": "kotlin",
"implementationClassName": "HybridCameraView"
}
}
}
}

Now run nitrogen again.

4.1. Android: Register the View Manager

On Android, you need to register the generated view manager in your React Native package:

CameraPackage.kt
// ...
public class CameraPackage: BaseReactPackage() {
// ...

override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
val viewManagers = ArrayList<ViewManager<*, *>>()
viewManagers.add(HybridCameraViewManager())
return viewManagers
}
}

5. Initialization

Then, to use the view in JavaScript, use getHostComponent(..):

import { getHostComponent } from 'react-native-nitro-modules'
import CameraViewConfig from '../nitrogen/generated/shared/json/CameraViewConfig.json'

export const Camera = getHostComponent<CameraProps, CameraMethods>(
'Camera',
() => CameraViewConfig
)

6. Rendering

And finally, render it:

function App() {
return <Camera enableFlash={true} />
}

Props

Since every HybridView is also a HybridObject, you can use any type that Nitro supports as a property - including custom types (interface), ArrayBuffer, and even other HybridObjects!

For example, a custom <ImageView> component can be used to render custom Image types:

Image.nitro.ts
export interface Image
extends HybridObject<{ ios: 'swift' }> {
readonly width: number
readonly height: number
save(): Promise<string>
}
ImageView.nitro.ts
import { type Image } from './Image.nitro.ts'
export interface ImageProps
extends HybridViewProps {
image: Image
}
export type ImageView = HybridView<ImageProps>

Then;

function App() {
const image = await loadImage('https://...')
return <ImageView image={image} />
}

Threading

Since Nitro bridges props directly to JS, you are responsible for ensuring thread-safety.

  • If props are set normally via React, they will be set on the UI Thread.
  • If the user sets props on the view hybridRef (e.g. also if the HybridView is passed to a HybridObject in native), props could be set on a different Thread, like the JS Thread.

Before/After update

To batch prop changes, you can override beforeUpdate() and afterUpdate() in your views:

HybridCameraView.swift
class HybridCameraView: HybridCameraViewSpec {
// View
var view: UIView = UIView()

func beforeUpdate() { }
func afterUpdate() { }
}

Callbacks

Nitro passes JS functions to native code directly, so callbacks are just regular props:

export interface CameraProps extends HybridViewProps {
onCaptured: (image: Image) => void
}
export type CameraView = HybridView<CameraProps>

function App() {
return <Camera onCaptured={(i) => console.log(i)} />
}

react-native 0.78 - 0.80: callback(...)

Historically, React Native core did not allow this. Instead, functions were wrapped in an event listener registry, and a simple boolean was passed to the native side. Since react-native 0.81 a View Config can opt out of that conversion, which Nitro does - so functions arrive on the native side unchanged.

On react-native 0.78 - 0.80 you still need to wrap every function in an object to bypass React Native's conversion. Nitro exposes the callback(...) method for this:

function App() {
return <Camera onCaptured={callback((i) => console.log(i))} />
}
warning

callback(...) is deprecated. On react-native 0.81 and above it is a no-op, so upgrade to react-native 0.81 or newer and remove all callback(...) calls.

Recycling

For improved performance and lower memory footprint, Nitro Views can be recycled. To allow your view to be recycled, implement the RecyclableView interface/protocol from Nitro:

HybridMyView.swift
import NitroModules

class HybridMyView: HybridMyViewSpec, RecyclableView {
// ...
func prepareForRecycle() {}
}

When Fabric decides to re-use a previously created view, the prepareForRecycle() method will be called. Inside that method you should reset any internal state to its default values.

For example, an asynchronous Image component should reset its displayed image when it is being recycled, otherwise it would display an old image while the new one is still loading:

class HybridImageView: HybridImageViewSpec, RecyclableView {
var view: UIView { imageView }
private var imageView = UIImageView()

func prepareForRecycle() {
imageView.image = nil
}
}

Methods

Since every HybridView is also a HybridObject, methods can be directly called on the object. Assuming our <Camera> component has a takePhoto() function like so:

export interface CameraProps extends HybridViewProps { ... }
export interface CameraMethods extends HybridViewMethods {
takePhoto(): Promise<Image>
}

export type CameraView = HybridView<CameraProps, CameraMethods>

To call the function, you would need to get a reference to the HybridObject first using hybridRef:

function App() {
return (
<Camera
hybridRef={(ref) => {
const image = ref.takePhoto()
}}
/>
)
}

Note: On react-native 0.78 - 0.80, this has to be wrapped in callback(...), see "Callbacks".

The ref from within hybridRef's callback is pointing to the HybridObject directly - you can also pass this around freely.