Skip to main content
Native UI components allow you to integrate native views into your React Native application, providing access to platform-specific UI elements and optimized rendering.

What are Native UI Components?

Native UI components are platform-specific views (Android Views or iOS UIViews) that are wrapped and exposed to React Native. They allow you to:
  • Use platform-specific UI widgets
  • Optimize performance for complex views
  • Integrate existing native UI libraries
  • Access native UI features not available in React Native

Architecture

Native UI components consist of:
  1. View Manager: Creates and manages native view instances
  2. View Implementation: The actual native view code
  3. Props: Properties that can be set from JavaScript
  4. Events: Callbacks that send data back to JavaScript

Android Native UI Components

Creating a View Manager

package com.yourapp.views

import android.graphics.Color
import android.view.View
import com.facebook.react.uimanager.SimpleViewManager
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.annotations.ReactProp
import android.widget.ProgressBar

class CustomProgressBarManager : SimpleViewManager<ProgressBar>() {
    
    override fun getName(): String {
        return "CustomProgressBar"
    }

    override fun createViewInstance(reactContext: ThemedReactContext): ProgressBar {
        return ProgressBar(reactContext, null, android.R.attr.progressBarStyleHorizontal)
    }

    @ReactProp(name = "progress")
    fun setProgress(view: ProgressBar, progress: Float) {
        view.progress = (progress * 100).toInt()
    }

    @ReactProp(name = "color", customType = "Color")
    fun setColor(view: ProgressBar, color: Int?) {
        color?.let {
            view.progressDrawable.setTint(it)
        }
    }
}

Registering the View Manager

Add to your ReactPackage:
override fun createViewManagers(
    reactContext: ReactApplicationContext
): List<ViewManager<*, *>> {
    return listOf(CustomProgressBarManager())
}

Using in JavaScript

import { requireNativeComponent, StyleSheet, View } from 'react-native';

const CustomProgressBar = requireNativeComponent('CustomProgressBar');

export default function App() {
  return (
    <View style={styles.container}>
      <CustomProgressBar
        style={styles.progressBar}
        progress={0.7}
        color="#4CAF50"
      />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  progressBar: {
    width: 300,
    height: 20,
  },
});

iOS Native UI Components

Creating a View Manager

// CustomProgressBarManager.h
#import <React/RCTViewManager.h>

@interface CustomProgressBarManager : RCTViewManager
@end

// CustomProgressBarManager.m
#import "CustomProgressBarManager.h"
#import "CustomProgressBar.h"

@implementation CustomProgressBarManager

RCT_EXPORT_MODULE(CustomProgressBar)

- (UIView *)view
{
  return [[CustomProgressBar alloc] init];
}

RCT_EXPORT_VIEW_PROPERTY(progress, CGFloat)
RCT_EXPORT_VIEW_PROPERTY(color, UIColor)

@end
// CustomProgressBar.h
#import <UIKit/UIKit.h>

@interface CustomProgressBar : UIProgressView
@end

// CustomProgressBar.m
#import "CustomProgressBar.h"

@implementation CustomProgressBar

- (instancetype)init
{
  if (self = [super initWithProgressViewStyle:UIProgressViewStyleDefault]) {
    self.progress = 0.0;
  }
  return self;
}

@end

View Properties

Android Property Types

Use @ReactProp annotation to expose properties:
// Basic types
@ReactProp(name = "text")
fun setText(view: TextView, text: String?) {
    view.text = text
}

// Numbers
@ReactProp(name = "fontSize")
fun setFontSize(view: TextView, fontSize: Float) {
    view.textSize = fontSize
}

// Booleans
@ReactProp(name = "enabled")
fun setEnabled(view: View, enabled: Boolean) {
    view.isEnabled = enabled
}

// Colors
@ReactProp(name = "color", customType = "Color")
fun setColor(view: TextView, color: Int?) {
    color?.let { view.setTextColor(it) }
}

// Arrays (using ReadableArray)
@ReactProp(name = "items")
fun setItems(view: CustomView, items: ReadableArray?) {
    items?.let {
        val list = mutableListOf<String>()
        for (i in 0 until it.size()) {
            list.add(it.getString(i))
        }
        view.setItems(list)
    }
}

// Maps (using ReadableMap)
@ReactProp(name = "config")
fun setConfig(view: CustomView, config: ReadableMap?) {
    config?.let {
        if (it.hasKey("title")) {
            view.setTitle(it.getString("title"))
        }
    }
}

iOS Property Macros

// Basic property export
RCT_EXPORT_VIEW_PROPERTY(text, NSString)
RCT_EXPORT_VIEW_PROPERTY(fontSize, CGFloat)
RCT_EXPORT_VIEW_PROPERTY(enabled, BOOL)

// Custom property with transformation
RCT_CUSTOM_VIEW_PROPERTY(color, UIColor, CustomView)
{
  if (json) {
    view.textColor = [RCTConvert UIColor:json];
  } else {
    view.textColor = defaultView.textColor;
  }
}

// Remapping property names
RCT_REMAP_VIEW_PROPERTY(textAlign, textAlignment, NSTextAlignment)

Handling Events

Android Events

class CustomSliderManager : SimpleViewManager<CustomSlider>() {
    
    override fun getName() = "CustomSlider"

    override fun createViewInstance(reactContext: ThemedReactContext): CustomSlider {
        return CustomSlider(reactContext)
    }

    override fun addEventEmitters(
        reactContext: ThemedReactContext,
        view: CustomSlider
    ) {
        view.setOnValueChangeListener { value ->
            val event = Arguments.createMap().apply {
                putDouble("value", value.toDouble())
            }
            reactContext
                .getJSModule(RCTEventEmitter::class.java)
                .receiveEvent(view.id, "onValueChange", event)
        }
    }

    override fun getExportedCustomBubblingEventTypeConstants(): Map<String, Any> {
        return mapOf(
            "onValueChange" to mapOf(
                "phasedRegistrationNames" to mapOf(
                    "bubbled" to "onValueChange",
                    "captured" to "onValueChangeCapture"
                )
            )
        )
    }
}
JavaScript usage:
<CustomSlider
  onValueChange={(event) => {
    console.log('Value:', event.nativeEvent.value);
  }}
/>

iOS Events

@interface CustomSlider : UISlider
@property (nonatomic, copy) RCTBubblingEventBlock onValueChange;
@end

@implementation CustomSlider

- (instancetype)init
{
  if (self = [super init]) {
    [self addTarget:self
             action:@selector(valueChanged:)
   forControlEvents:UIControlEventValueChanged];
  }
  return self;
}

- (void)valueChanged:(CustomSlider *)sender
{
  if (self.onValueChange) {
    self.onValueChange(@{
      @"value": @(sender.value)
    });
  }
}

@end

@implementation CustomSliderManager

RCT_EXPORT_MODULE()

- (UIView *)view
{
  return [[CustomSlider alloc] init];
}

RCT_EXPORT_VIEW_PROPERTY(onValueChange, RCTBubblingEventBlock)
RCT_EXPORT_VIEW_PROPERTY(value, CGFloat)

@end

Example: Activity Indicator View Manager

Here’s a real-world example from React Native’s source code (packages/react-native/React/Views/RCTActivityIndicatorViewManager.m:28-63):
@implementation RCTActivityIndicatorViewManager

RCT_EXPORT_MODULE()

- (UIView *)view
{
  return [RCTActivityIndicatorView new];
}

RCT_EXPORT_VIEW_PROPERTY(color, UIColor)
RCT_EXPORT_VIEW_PROPERTY(hidesWhenStopped, BOOL)

RCT_CUSTOM_VIEW_PROPERTY(size, UIActivityIndicatorViewStyle, UIActivityIndicatorView)
{
  /*
    Setting activityIndicatorViewStyle overrides the color,
    so restore the original color after setting the indicator style.
  */
  UIColor *oldColor = view.color;
  view.activityIndicatorViewStyle = json 
      ? [RCTConvert UIActivityIndicatorViewStyle:json] 
      : defaultView.activityIndicatorViewStyle;
  view.color = oldColor;
}

RCT_CUSTOM_VIEW_PROPERTY(animating, BOOL, UIActivityIndicatorView)
{
  BOOL animating = json ? [RCTConvert BOOL:json] : [defaultView isAnimating];
  if (animating != [view isAnimating]) {
    if (animating) {
      [view startAnimating];
    } else {
      [view stopAnimating];
    }
  }
}

@end

Advanced Topics

View Lifecycle

Android

override fun onDropViewInstance(view: CustomView) {
    super.onDropViewInstance(view)
    // Clean up resources
    view.cleanup()
}

override fun onAfterUpdateTransaction(view: CustomView) {
    super.onAfterUpdateTransaction(view)
    // Called after props are updated
    view.applyChanges()
}

iOS

- (void)dropView:(UIView *)view
{
  // Clean up resources
  [view removeFromSuperview];
}

Commands

Expose imperative methods that can be called from JavaScript:

Android

override fun getCommandsMap(): Map<String, Int> {
    return mapOf(
        "focus" to COMMAND_FOCUS,
        "blur" to COMMAND_BLUR
    )
}

override fun receiveCommand(
    view: CustomView,
    commandId: String,
    args: ReadableArray?
) {
    when (commandId) {
        "focus" -> view.requestFocus()
        "blur" -> view.clearFocus()
    }
}

companion object {
    private const val COMMAND_FOCUS = "focus"
    private const val COMMAND_BLUR = "blur"
}

iOS

RCT_EXPORT_METHOD(focus:(nonnull NSNumber *)reactTag)
{
  [self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, 
                                       NSDictionary<NSNumber *, UIView *> *viewRegistry) {
    UIView *view = viewRegistry[reactTag];
    [view becomeFirstResponder];
  }];
}
JavaScript usage:
import { UIManager, findNodeHandle } from 'react-native';

const ref = useRef();

const handleFocus = () => {
  UIManager.dispatchViewManagerCommand(
    findNodeHandle(ref.current),
    'focus',
    []
  );
};

<CustomView ref={ref} />

TypeScript Support

Create type-safe interfaces for your native components:
import { ViewProps } from 'react-native';
import { HostComponent, requireNativeComponent } from 'react-native';

interface CustomProgressBarProps extends ViewProps {
  progress: number;
  color?: string;
  onValueChange?: (event: { nativeEvent: { value: number } }) => void;
}

const CustomProgressBarNative = 
  requireNativeComponent<CustomProgressBarProps>('CustomProgressBar');

export default CustomProgressBarNative;

Best Practices

  • Keep view managers focused on view creation and property mapping
  • Use events for data flowing from native to JavaScript
  • Use props for data flowing from JavaScript to native
  • Clean up resources in lifecycle methods
  • Implement proper error handling
  • Document your component’s API and props
  • Consider performance implications of frequent prop updates
  • Test on both platforms when building cross-platform components
  • Use TypeScript for better type safety
  • Follow platform-specific design guidelines

Build docs developers (and LLMs) love