View Components
As of today, Nitro does not yet provide first-class support for view components. There are ongoing efforts to bring first-class support for view components, which would also go through the Nitro typing system. This requires coordination with Meta as this requires APIs to be made public in react-native core.
Workaround via native
As a temporary workaround, you could create a Hybrid Object that acts as your view manager, which you can then natively access via some sort of global map.
1. Create your View (props)
First, create your view using Turbo Modules (Fabric) or Expo Modules. Let's build a custom Image component:
function App() {
return (
<NitroImage
image={someImage}
opacity={0.5}
/>
)
}
opacity
is a number which can be represented using Turbo-/Expo-Modules, but image
is a custom Hybrid Object from Nitro.
This can not be handled by Turbo-/Expo-Modules, so we cannot simply pass the props as-is to the native view:
interface NativeProps extends ViewProps {
opacity: number
image: Image
// ^ Error: `Image` is not supported by Turbo-/Expo-Modules!
}
Instead of declaring props for the view component like usual, we want to route everything through Nitro - which is faster and supports more types.
For this, we'll only create one prop for our view which will be used to connect the Turbo-/Expo-View with our Nitro View Manager - let's call it nitroId
:
interface NativeProps extends ViewProps {
opacity: number
image: Image
nitroId: number
}
2. Implement your view
Now implement your view using Turbo-/Expo-Modules like usual. It only has one React prop: nitroId
.
Follow the respective guides to implement this view.
Make sure the view can be mounted on screen, and nitroId
properly updates the native property:
function App() {
return <NitroImage nitroId={13} />
}
3. Generate nitroId
in JS
In the JS implementation of <NitroImage>
, we now need to generate unique nitroId
values per instance:
const NativeNitroImageView = /* From Turbo-/Expo- APIs */
let nitroIdCounter = 0
export function NitroImage() {
const nitroId = useMemo(() => nitroIdCounter++, [])
return <NativeNitroImageView nitroId={nitroId} />
}
4. Register the native view in a global map
To allow your Nitro Hybrid Object to find the Turbo-/Expo-View, we need to throw it into some kind of global map, index by nitroId
.
It is up to the developer on how to handle this most efficiently, but here's an example:
- iOS (Swift)
- iOS (Objective-C)
- Android (Kotlin)
class NitroImageView : UIView {
static var globalViewsMap: NSMapTable<NSNumber, NitroImageView> = NSMapTable(keyOptions: .strongMemory, valueOptions: .weakMemory)
@objc var nitroId: NSNumber = -1 {
didSet {
NitroImageView.globalViewsMap.setObject(self, forKey: nitroId)
}
}
}
@implementation NitroImageView
// Global map of nitroId to view instances
+ (NSMapTable<NSNumber*, NitroImageView*>*) globalViewsMap {
static NSMapTable<NSNumber*, NitroImageView*>* _map;
if (_map == nil) {
_map = [NSMapTable strongToWeakObjectsMapTable];
}
return _map;
}
// Override `nitroId` setter to throw `self` into global map
- (void)setNitroId:(NSNumber*)nitroId {
[self.globalViewsMap setObject:self forKey:nitroId];
}
@end
class NitroImageView {
companion object {
// Global map of nitroId to view instances
val globalViewsMap = HashMap<Double, WeakReference<NitroImageView>>()
}
// Override `nitroId` setter to throw `this` into global map
fun setNitroId(nitroId: Double) {
globalViewsMap[nitroId] = WeakReference(view)
}
}
5. Create a custom view manager with Nitro
Fasten your seatbelts and get ready for Nitro: We now want a Nitro Hybrid Object that acts as a binding between our JS view, and the actual native Swift/Kotlin view.
export interface Image extends HybridObject {
// ...
}
export interface NitroImageViewManager extends HybridObject {
image: Image
opacity: number
}
Now implement NitroImageViewManager
in Swift and Kotlin, and assume it has to be created with a valid NitroImageView
instance:
- iOS (Swift)
- Android (Kotlin)
class HybridNitroImageViewManager: HybridNitroImageViewManagerSpec {
public var hybridContext = margelo.nitro.HybridContext()
public var memorySize: Int {
return getSizeOf(self)
}
private var nitroId: Double? = nil
private var view: NitroImageView? {
get {
guard let viewId = self.nitroId else {
return nil
}
return NitroImageView.globalViewsMap.object(forKey: NSNumber(value: viewId))
}
}
var image: Image {
get { return view.image }
set { view.image = newValue }
}
var opacity: Double {
get { return view.opacity }
set { view.opacity = newValue }
}
func setNitroId(nitroId: Double) {
self.nitroId = nitroId
}
}
class HybridNitroImageViewManager: HybridNitroImageViewManagerSpec() {
private var nitroId: Double? = null
private var view: WeakReference<NitroImageView>? = null
get() {
return nitroId.let {
return NitroImageView.globalViewsMap[it]?.get()
}
}
override var image: Image
get() = view.image
set(newValue) = view.image = newValue
override var opacity: Double {
get() = view.opacity
set(newValue) = view.opacity = newValue
fun setNitroId(nitroId: Double?) {
this.nitroId = nitroId
}
}
6. Connect the Nitro view manager to the native View
To actually create instances of HybridNitroImageViewManager
, we need to first find the view for the given nitroId
. For that, we created a helper NitroImageViewManagerRegistry
:
export interface NitroImageViewManagerRegistry extends HybridObject {
createViewManager(nitroId: number): NitroImageViewManager
}
..which we need to implement in native:
- iOS (Swift)
- Android (Kotlin)
class HybridNitroImageViewManagerRegistry: HybridNitroImageViewManagerRegistrySpec {
func createViewManager(nitroId: Double) -> NitroImageViewManagerSpec {
let viewManager = HybridNitroImageViewManager()
viewManager.setNitroId(nitroId: nitroId)
return viewManager
}
}
class HybridNitroImageViewManagerRegistry: HybridNitroImageViewManagerRegistrySpec() {
fun createViewManager(nitroId: Double): NitroImageViewManagerSpec {
val viewManager = HybridNitroImageViewManager()
viewManager.setNitroId(nitroId)
return viewManager
}
}
7. Use the view manager from JS:
After setting up those bindings, we can now route all our props through Nitro - which makes prop updating faster, and allows for more types!
const NativeNitroImageView = /* From Turbo-/Expo- APIs */
const NitroViewManagerFactory = NitroModules.createHybridObject("NitroViewManagerFactory")
let nitroIdCounter = 0
export function NitroImage(props: NitroImageProps) {
const nitroId = useMemo(() => nitroIdCounter++, [])
const nitroViewManager = useRef<NitroViewManager>(null)
useEffect(() => {
// Create a View Manager for the respective View (looked up via `nitroId`)
nitroViewManager.current = NitroViewManagerFactory.createViewManager(nitroId)
}, [nitroId])
useEffect(() => {
// Update props through Nitro - this natively sets them on the view as well.
nitroViewManager.current.image = props.image
nitroViewManager.current.opacity = props.opacity
}, [props.image, props.opacity])
return <NativeNitroImageView nitroId={nitroId} />
}
Considerations
While this works and is pretty extensible, there are a few trade-offs with this workaround:
- This is pretty verbose. When Nitro gets first-class support for view components, this will be much simpler.
- This is updating props synchronously on the JS Thread. You can implement your own batching and thread-dispatching on the native side though.
- This does not go through React's prop updater. This means stuff like Reanimated will likely not work, as it was built on top of
setNativeProps
.
With that said; just know what you are doing. Then you're good.