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.
Nitro Views require React Native 0.88 or newer with the New Architecture.
Create a Nitro View
1. Declaration
Declare a class extending HybridView in a *.nitro.ts file. Its writable properties become React props, and its methods are available through the native ref:
import { iOS, type HybridView } from 'react-native-nitro-modules'
@iOS({
language: 'swift',
implementation: { className: 'HybridCameraView' }
})
export declare class CameraView extends HybridView {
enableFlash: boolean
}
export * from '#nitro/CameraView'
Fabric creates the native view. Omit a public constructor from the spec.
2. Code Generation
Then, run nitrogen:
- npm
- yarn
- pnpm
- bun
npx nitrogen
yarn nitrogen
pnpm nitrogen
bun nitrogen
Nitrogen generates the native view bindings and the CameraViewComponent export, including its React props and ref types.
3. Implementation
Now it's time to implement the View - simply create a new Swift/Kotlin class/file, implement HybridCameraViewSpec and implement your .enableFlash property, as well as the common .view accessor:
- Swift
- Kotlin
class HybridCameraView : HybridCameraViewSpec {
// Props
var enableFlash: Bool = false
// View
var view: UIView = UIView()
}
class HybridCameraView(val context: ThemedReactContext) : HybridCameraViewSpec() {
// Props
override var enableFlash: Boolean = false
// View
override val view: View = View(context)
}
4. Link the generated sources
The @iOS decorator's implementation selects HybridCameraView to automatically link the native class.
4.1. Register View Manager (Android)
On Android, you additionally need to register the generated view manager in your React Native package:
// ...
public class CameraPackage: BaseReactPackage() {
// ...
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
val viewManagers = ArrayList<ViewManager<*, *>>()
viewManagers.add(HybridCameraViewManager())
return viewManagers
}
}
5. Render the component
Finally, import the generated component from the spec and render it:
import { CameraViewComponent } from './CameraView.nitro'
function App() {
return <CameraViewComponent 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:
@iOS(...)
export declare class Image extends HybridObject {
readonly width: number
readonly height: number
save(): Promise<string>
}
import { type Image } from './Image.nitro.ts'
@iOS(...)
export declare class ImageView extends HybridView {
image: Image
}
export * from '#nitro/ImageView'
Render the generated component with an existing image:
import { ImageViewComponent } from './ImageView.nitro'
function App({ image }: { image: Image }) {
return <ImageViewComponent 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 theHybridViewis passed to aHybridObjectin 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:
- Swift
- Kotlin
class HybridCameraView: HybridCameraViewSpec {
// View
var view: UIView = UIView()
func beforeUpdate() { }
func afterUpdate() { }
}
class HybridCameraView(context: ThemedReactContext): HybridCameraViewSpec() {
// View
override val view: View = View(context)
override fun beforeUpdate() { }
override fun afterUpdate() { }
}
Callbacks
Pass functions directly to Nitro View props:
@iOS(...)
export declare class CameraView extends HybridView {
onCaptured: (image: Image) => void
}
export * from '#nitro/CameraView'
// In a component file, import CameraViewComponent from './CameraView.nitro'.
function App() {
return <CameraViewComponent onCaptured={(image) => console.log(image)} />
}
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:
- Swift
- Kotlin
import NitroModules
class HybridMyView: HybridMyViewSpec, RecyclableView {
// ...
func prepareForRecycle() {}
}
import com.margelo.nitro.views.RecyclableView
class HybridMyView: HybridMyViewSpec(), RecyclableView {
// ...
override fun 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 <CameraViewComponent> component has a takePhoto() function like so:
@iOS(...)
export declare class CameraView extends HybridView {
enableFlash: boolean
takePhoto(): Promise<Image>
}
export * from '#nitro/CameraView'
To call the function, you would need to get a reference to the HybridObject first using hybridRef:
function App() {
return (
<CameraViewComponent
hybridRef={(ref) => {
void ref.takePhoto().then((image) => console.log(image))
}}
/>
)
}
The ref from within hybridRef's callback is pointing to the HybridObject directly - you can also pass this around freely.