DEV Community

CycleUse
CycleUse

Posted on

Using a Marquee (Scrolling Text) as an Example: How React Native Calls and Controls a Native HarmonyOS Component via Fabric

  1. Overview: End-to-End Flow
  2. Code Walkthrough
    • RN Side: Declaring the Fabric Component
    • Using the Component in RN
    • Native HarmonyOS Side: ETS Implementation
    • Native C++ Glue: Property and Event Binding for Fabric
    • Native Event Emission Back to RN
    • RN -> Native: Controlling Native State via Commands
  3. Deep Dive: The Fabric Architecture
    • What is Fabric?
    • How Does Fabric Work?
    • Benefits vs the Old Bridge
    • How to Write Your Own Fabric Component
  4. Summary

1. Overview: End-to-End Flow

Fabric is React Native's new rendering system, designed for performance and reliability. When integrating a custom native component like a "marquee" (text that scrolls horizontally) into a HarmonyOS app, Fabric allows React Native JS code to seamlessly declare, configure, and control the native view, while also receiving native events.

The flow:

  • JS/TS side: Declare the component's props and events using codegenNativeComponent (Fabric).
  • Use in RN: Import and use the component in a React Native screen, passing props and handling events.
  • C++/Native bridge: Properties and events are mapped automatically to native code via Fabric's codegen/binder logic.
  • HarmonyOS (ETS): The component is implemented as a HarmonyOS custom UI, which responds to JS props, emits events, and can be controlled via commands.
  • Event emission: Native can fire events, which are received in JS as callbacks.
  • Command/control: JS can send commands to control native component state at runtime (e.g., pause/resume marquee).

2. Code Walkthrough

2.1 RN Side: Declaring the Fabric Component

```typescript name=MarqueeView.tsx
import {
ViewProps,
HostComponent,
} from 'react-native';
import type { DirectEventHandler } from "react-native/Libraries/Types/CodegenTypes";
import codegenNativeComponent from "react-native/Libraries/Utilities/codegenNativeComponent";

export type OnStopEventData = Readonly<{
isStop: boolean,
type: string,
}>;

export interface MarqueeViewProps extends ViewProps {
src: string, // The text to scroll
onStop?: DirectEventHandler; // Event from native
}

// Fabric codegen: registers 'MarqueeView' with property/event types
export default codegenNativeComponent(
'MarqueeView',
) as HostComponent;




**Key Points:**

- `codegenNativeComponent` is critical: it tells React Native's codegen to generate all the glue for a Fabric component.
- `src` is a prop passed from JS to native.
- `onStop` is an event: native can call this to notify JS (e.g., when the marquee stops).

---

### 2.2 RN Usage: Using the Native Component in JS



```typescript name=GoodsMainPage.tsx (excerpt)
import MarqueeView from '../basic/MarqueeView';

function AppGoods() {
  const [marqueeStopped, setMarqueeStopped] = useState(false);
  const nativeRef = useRef<any>(null);

  // Listen for native events (DeviceEventEmitter also used for custom events)
  DeviceEventEmitter.addListener('clickMarqueeEvent', e => {
    // ... handle custom event from native
  });

  return (
    <ScrollView>
      <View style={styles.container}>
        <MarqueeView
          src="Super Sale! Consumption is a key part of the social reproduction process..."
          ref={nativeRef}
          style={{ height: 180, width: '100%', backgroundColor: '#1980E6' }}
          onStop={e => {
            setMarqueeStopped(e.nativeEvent.isStop);
          }}
        />
        <GoodsButton
          buttonText={"Pause/Resume Marquee: " + (marqueeStopped ? "Stopped" : "Running")}
          onPress={() => {
            // Send a command to native, using Fabric bridge
            UIManager.dispatchViewManagerCommand(
              findNodeHandle(nativeRef.current),
              'toggleMarqueeState',
              []
            );
          }}
        />
      </View>
    </ScrollView>
  );
}
Enter fullscreen mode Exit fullscreen mode

Key Points:

  • Component used just like any RN component.
  • Props (src) go down to native.
  • Event (onStop) is handled as a callback.
  • Native methods can be triggered via UIManager.dispatchViewManagerCommand.

2.3 Native HarmonyOS Side: ETS Implementation

```typescript name=MarqueeView.ets
import { Descriptor, ViewBaseProps } from '@rnoh/react-native-openharmony';
import { RNOHContext, RNViewBase } from '@rnoh/react-native-openharmony';

@Component
export struct MarqueeView {
static NAME: string = "MarqueeView"; // Matches JS codegen
ctx!: RNOHContext;
tag: number = 0;
@State private descriptor: Descriptor<"MarqueeView", MarqueeViewProps> = {} as any;
@State start: boolean = false;
@State src: string = "Default text";

aboutToAppear() {
this.start = true;
// Listen for prop changes from RN
this.descriptor = this.ctx.descriptorRegistry.getDescriptor(this.tag);
this.ctx.descriptorRegistry.subscribeToDescriptorChanges(this.tag, (newDescriptor) => {
this.descriptor = newDescriptor as MarqueeViewDescriptor;
this.src = (this.descriptor.rawProps as MarqueeViewProps).src;
});

// Listen for RN commands (e.g., pause/resume)
this.ctx.componentCommandReceiver.registerCommandCallback(this.tag, (commandName) => {
  if (commandName === "toggleMarqueeState") {
    this.start = !this.start; // Toggle running state
    // Fire event back to RN
    this.ctx.rnInstance.emitComponentEvent(
      this.descriptor.tag,
      "onStop",
      { isStop: !this.start, type: "custom" }
    )
  }
});
Enter fullscreen mode Exit fullscreen mode

}

aboutToDisappear() {
this.start = false;
// Cleanup listeners...
}

build() {
RNViewBase({ ctx: this.ctx, tag: this.tag }) {
Column() {
Marquee({
start: this.start,
src: this.src
})
.onTouch(() => {
// Emit a custom event to RN JS
this.ctx.rnInstance.emitDeviceEvent("clickMarqueeEvent", { params: { age: 18 } });
})
}
.height(180)
.justifyContent(FlexAlign.Center)
}
}
}




**Key Points:**

- The `MarqueeView` component receives props (`src`) from RN and reacts to command invocations (e.g., pause/resume).
- It can emit events (`onStop`) back to RN.
- Custom device events (via `emitDeviceEvent`) can be sent as well.

---

### 2.4 Native C++ Glue: Property and Event Binding for Fabric

#### Property Mapping



```cpp name=MarqueeViewJSIBinder.h
facebook::jsi::Object createNativeProps(facebook::jsi::Runtime &rt) override {
    auto object = ViewComponentJSIBinder::createNativeProps(rt);
    object.setProperty(rt, "src", "string"); // Expose 'src' to JS
    return object;
}
Enter fullscreen mode Exit fullscreen mode

Event Mapping

```cpp name=MarqueeViewJSIBinder.h
facebook::jsi::Object createDirectEventTypes(facebook::jsi::Runtime &rt) override {
facebook::jsi::Object events(rt);
events.setProperty(rt, "topStop", createDirectEvent(rt, "onStop")); // Map native event to JS callback
return events;
}




**Explanation:**

- `createNativeProps` tells Fabric which JS props are supported and their types.
- `createDirectEventTypes` registers which events the native component can emit, and how they're mapped back to JS.

---

### 2.5 Native Event Emission Back to RN

#### Native Event Emitter



```cpp name=MarqueeViewEventEmitter.h
class MarqueeViewEventEmitter : public ViewEventEmitter {
  public:
    using ViewEventEmitter::ViewEventEmitter;
    struct OnStop {
      bool isStop;
      std::string type;
    };

    void onStop(OnStop value) const;
};
Enter fullscreen mode Exit fullscreen mode

```cpp name=MarqueeViewEventEmitter.cpp
void MarqueeViewEventEmitter::onStop(OnStop event) const {
dispatchEvent("stop", event = std::move(event) {
auto payload = jsi::Object(runtime);
payload.setProperty(runtime, "isStop", event.isStop);
payload.setProperty(runtime, "type", event.type);
return payload;
});
}




**Explanation:**

- When the HarmonyOS component wants to notify RN (e.g., that the marquee has stopped), it calls `onStop`.
- This dispatches a JS event named `"stop"` with a payload containing `isStop` and `type`.
- The JS side receives this as the `onStop` callback.

#### Event Dispatching Handler



```cpp name=MarqueeViewEventEmitRequestHandler.h
void handleEvent(EventEmitRequestHandler::Context const &ctx) override {
    ArkJS arkJs(ctx.env);
    auto eventEmitter = ctx.shadowViewRegistry->getEventEmitter<react::MarqueeViewEventEmitter>(ctx.tag);
    if (!eventEmitter) return;

    if (ctx.eventName == "onStop") {
        bool isStop = ...;
        std::string type = ...;
        react::MarqueeViewEventEmitter::OnStop event = {isStop, type};
        eventEmitter->onStop(event);
    }
}
Enter fullscreen mode Exit fullscreen mode

2.6 RN -> Native: Controlling Native State via Commands

```typescript name=GoodsMainPage.tsx (excerpt)
buttonText={"Pause/Resume Marquee"}
onPress={() => {
UIManager.dispatchViewManagerCommand(
findNodeHandle(nativeRef.current),
'toggleMarqueeState', // Command name matches native implementation
[],
);
}}
/>




- The JS side can trigger imperative actions on the native component by sending commands.
- The native component listens for these commands (see `registerCommandCallback`) and updates its state accordingly.

---

## 3. Deep Dive: The Fabric Architecture

### 3.1 What is Fabric?

Fabric is React Native’s new rendering system that:

- Makes UI updates synchronous and more predictable.
- Uses a structure that mirrors the JS and native view trees (Shadow Tree).
- Leverages codegen for type-safe, fast prop/event bridging between JS and native.

### 3.2 How Does Fabric Work?

- **Component Registration**: In JS, you use `codegenNativeComponent` to declare your native view and its props/events.
- **Codegen**: React Native’s codegen tool scans these declarations and generates the C++/Java/ObjC glue code.
- **Property/Event Synchronization**: Updates to props/events are fast, type-safe, and avoid the old "bridge" serialization step.
- **Direct Command/Events**: Fabric supports direct commands and event emission between JS and native, reducing latency.

### 3.3 Benefits vs the Old Bridge

| Old Bridge          | Fabric                    |
| ------------------- | ------------------------- |
| Asynchronous        | Synchronous               |
| JSON Serialization  | Direct struct mapping     |
| Manual glue code    | Codegen-driven            |
| Potential for drift | Type-safe, always in sync |
| Slower, less robust | Faster, more reliable     |

### 3.4 How to Write Your Own Fabric Component

1. **JS/TS: Declare with `codegenNativeComponent`**
   - Define all props and events with proper types.
2. **Implement Native Component**
   - For HarmonyOS, write the ETS component.
   - For iOS/Android, implement their native UI logic.
3. **Binder/Glue (C++)**
   - Implement property and event mapping, usually with help from codegen.
4. **Use in JS**
   - Import and use your component like any other React Native component.

---

## 4. Summary

- **Fabric** enables seamless, high-performance integration of native components in React Native apps.
- The marquee example illustrates: property passing, event handling, and command-based control between RN and HarmonyOS via Fabric.
- **Best practices**: Always use codegen for new native components, keep prop/event naming consistent, and leverage Fabric's glue for robust, maintainable cross-platform UI code.

---

> **References**
>
> - [React Native New Architecture Guide](https://reactnative.dev/docs/architecture-overview)
> - [HarmonyOS JS/ETS Documentation](https://developer.harmonyos.com/en/docs/documentation/js-ets-overview)
Enter fullscreen mode Exit fullscreen mode

Top comments (0)