Skip to main content

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:

App.tsx
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:

@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

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.

NitroImageViewManager.nitro.ts
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:

class HybridNitroImageViewManager: HybridNitroImageViewManagerSpec {
private let view: NitroImageView
init(withView view: NitroImageView) {
self.view = view
}

var image: Image {
get { return view.image }
set { view.image = newValue }
}
var opacity: Double {
get { return view.opacity }
set { view.opacity = newValue }
}
}

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:

class HybridNitroImageViewManagerRegistry: HybridNitroImageViewManagerRegistrySpec {
func createViewManager(nitroId: Double) -> NitroImageViewManagerSpec {
let view = NitroImage.globalViewsMap.object(forKey: nitroId)
return HybridNitroImageViewManager(withView: view)
}
}

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:

  1. This is pretty verbose. When Nitro gets first-class support for view components, this will be much simpler.
  2. This is updating props synchronously on the JS Thread. You can implement your own batching and thread-dispatching on the native side though.
  3. This does not go through React's prop updater. This means stuff like Reanimated will likely not work, as it was built ontop of setNativeProps.

With that said; just know what you are doing. Then you're good.