skip to content
Andrei Calazans

Leveraging You.i Engine's Counterparts to Extend React Native You.i

/ 14 min read

In this tutorial, I go over how to create a native module in C++ to extend the functionality of React Native You.i components by tapping into its counterparts

Extending Functionality Of Native Elements

The lists in react-native-youi: ListRef, ScollView, FlatList, all use CYIListView underneath as their counterpart.

For your knowledge. All scene views in You.i Engine extends from CYISceneView and CYISceneNode.

CYIListView extends CYISceneView extends CYISceneNode

Knowing so helps when you need to check if a view element supports a given feature. You can view the public methods supported by these classes in the Engine’s documentation.

Since not all features available in CYIListView are bridged to React Native, you can extend the lists with a native module to enable these features.

For example, the ScrollView and FlatList don’t have a prop for when its descendants receives or loses focus. But, the underlying CYISceneNode has event signals for when its descendants received or lost focus. We can implement a native module to bridge these events.

The Goal

  • Add onListLostFocus Event
  • Add onListGainedFocus Event
  • Add onFocusChanged Event

We want to encapsulate our module in a component that can be used by both the ScrollView and FlatList.

The type interface will look like this:

type Base = {
    onListLostFocus?: () => void;
    onListGainedFocus?: (index: number) => void;
    onFocusChanged?: (index: number) => void;
};

type WithScrollViewProps = {
    scrollComponent: typeof ScrollView;
    children?: ReactNode;
} & Base &
    ScrollViewProps;

type WithFlatListProps<T> = {
    scrollComponent: typeof FlatList;
} & Base &
    FlatListProps<T>;


const ScrollViewExtension<T> = (props: WithScrollViewProps | WithFlatListProps<T>) => ReactNode;

The Steps

To achieve our goal let’s break down the work in multiple steps:

  1. Implement Native Module

  2. Implement JS Component Interface

Write the native module class header in C++:

For now we are going to:

  • Define the header file for the CounterpartExtension class
  • Add a method called extendCounterpart that takes a tag as an argument
  • Export the module with the name CounterpartExtension

Create a file called CounterpartExtension.h inside <root>/youi/src.

@@ -0,0 +1,11 @@
+#pragma once
+
+#include <youireact/NativeModule.h>
+
+class YI_RN_MODULE(CounterpartExtension) {
+public:
+    CounterpartExtension();
+    YI_RN_EXPORT_NAME(CounterpartExtension);
+    YI_RN_EXPORT_METHOD(extendCounterpart)(uint64_t tag);
+};
+

Few things to notice here is the usage of macros like YI_RN_MODULE and others. These are to make our life easier. If you are curious to see what they are translating to take a look at this file: ~/youiengine/engine_version/include/react/youireact/NativeModule_inl.h

Implement the native module class in C++:

Next we want to:

  • Write the implementation file (.cpp)
  • Write the method extendCounterpart
  • Get the counterpart element from the shadow tree using our React tag number

Create a file called CounterpartExtension.cpp inside <root>/youi/src with the following content.


@@ -0,0 +1,28 @@
+#include "CounterpartExtension.h"
+
+#include <youireact/NativeModuleRegistry.h>
+#include <scenetree/YiSceneManager.h>
+
+#include <youireact/IBridge.h>
+#include <youireact/ShadowTree.h>
+
+using namespace folly;
+using namespace std;
+
+#define TAG "CounterpartExtension"
+
+YI_RN_INSTANTIATE_MODULE(CounterpartExtension);
+YI_RN_REGISTER_MODULE(CounterpartExtension);
+
+
+YI_RN_DEFINE_EXPORT_METHOD(CounterpartExtension, extendCounterpart)(uint64_t tag)
+{
+    // ShadowRegistry contains all of the items available in the ShadowTree (similar to the virtual DOM).
+    auto &shadowRegistry = GetBridge().GetShadowTree().GetShadowRegistry();
+    auto pComponent = shadowRegistry.Get(tag);
+    YI_ASSERT(pComponent, TAG, "Shadow view with tag %" PRIu64 " not found in ShadowRegistry.", tag);
+
+    // For every React Native component we have a corresponding Widget (counterpart) in the Engine.
+    auto pCounterpart = pComponent->GetCounterpart(); //CYISceneNode
+    YI_ASSERT(pCounterpart, TAG, "Shadow view with tag %" PRIu64 " doesn't have a counterpart.", tag);
+}

**SIDE NOTE - ** This is where you can start modifying the settings of your counterpart. When you retrieve a CYISceneNode counterpart you can cast it to the node it extends. For example, a FlatList extends the CYIListView so you can cast the CYISceneNode retrieved for a FlatList to a CYIListView by doing: CYIListView * pListView = dynamic_cast<CYIListView *>(pCounterpart) and so on.


Connect to the CYISceneNode focus signals:

Since we want to access methods available to the CYISceneNode. We do not need to cast it to any other CYINode.

Now we can connect to the focus signals.

M youi/src/CounterpartExtension.cpp
@@ -25,4 +25,16 @@ YI_RN_DEFINE_EXPORT_METHOD(CounterpartExtension, extendCounterpart)(uint64_t tag
     // For every React Native component we have a corresponding Widget (counterpart) in the Engine.
     auto pCounterpart = pComponent->GetCounterpart(); //CYISceneNode
     YI_ASSERT(pCounterpart, TAG, "Shadow view with tag %" PRIu64 " doesn't have a counterpart.", tag);
+
+    pCounterpart->DescendantLostFocus.Connect([](){
+        // Do what you need here.
+    });
+
+    pCounterpart->DescendantGainedFocus.Connect([](){
+        // Do what you need here.
+    });
+
+    pCounterpart->DescendantsChangedFocus.Connect([](){
+        // Do what you need here.
+    });
 }

Within these lambda callbacks we can implement any logic we like. Now, every time a child of our view receives or loses focus one of these callbacks will be called.

Since we rely only on the CYISceneNode, this module can be used with any React Native component (View, FlatList, ScrollView, TouchableOpacity, etc.).

How to send events back to React Native’s JavaScript thread?

We can send events by:

  • Extend yi::react::EventEmitterModule
  • Emit events with ReactComponent::EmitEvent (See ~/youiengine/engine_version/include/react/youireact/nodes/ReactComponent.h)

Extend Event Emitter

M youi/src/CounterpartExtension.h
@@ -1,8 +1,9 @@
 #pragma once

 #include <youireact/NativeModule.h>
+#include <youireact/modules/EventEmitter.h>

-class YI_RN_MODULE(CounterpartExtension) {
+class YI_RN_MODULE(CounterpartExtension, yi::react::EventEmitterModule) {
 public:
     CounterpartExtension();
     YI_RN_EXPORT_NAME(CounterpartExtension);
M youi/src/CounterpartExtension.cpp
@@ -11,7 +11,7 @@ using namespace std;

 #define TAG "CounterpartExtension"

-YI_RN_INSTANTIATE_MODULE(CounterpartExtension);
+YI_RN_INSTANTIATE_MODULE(CounterpartExtension, yi::react::EventEmitterModule);
 YI_RN_REGISTER_MODULE(CounterpartExtension);

Add Event Emitter To Implementation

Now we can emit events back to JavaScript EventEmitter made available by extending yi::react::EventEmitterModule.

Notice what is the type definition of EmitEvent

void EmitEvent(const std::string &event, folly::dynamic arguments);

You can check it out here ~/youiengine/engine_version/include/react/youireact/modules/EventEmitter.h

The folly::dynamic argument is a dynamic object. (@TODO - link basic intro into using folly::dynamic.)

M youi/src/CounterpartExtension.cpp
@@ -11,6 +11,10 @@ using namespace std;

 #define TAG "CounterpartExtension"

+#define LIST_GAINED_FOCUS "ListGainedFocus"
+#define LIST_LOST_FOCUS "ListLostFocus"
+#define LIST_CHANGED_FOCUS "ListChangedFocus"
+
 YI_RN_INSTANTIATE_MODULE(CounterpartExtension, yi::react::EventEmitterModule);
 YI_RN_REGISTER_MODULE(CounterpartExtension);

@@ -28,13 +32,16 @@ YI_RN_DEFINE_EXPORT_METHOD(CounterpartExtension, extendCounterpart)(uint64_t tag

-    pCounterpart->DescendantLostFocus.Connect([](){
+    pCounterpart->DescendantLostFocus.Connect([this](){
         // Do what you need here.
+        EmitEvent(LIST_LOST_FOCUS, {});
     });

-    pCounterpart->DescendantGainedFocus.Connect([](){
+    pCounterpart->DescendantGainedFocus.Connect([this](){
+        EmitEvent(LIST_GAINED_FOCUS, {});
         // Do what you need here.
     });

-    pCounterpart->DescendantsChangedFocus.Connect([](){
+    pCounterpart->DescendantsChangedFocus.Connect([this](){
+        EmitEvent(LIST_CHANGED_FOCUS, {});
         // Do what you need here.
     });
 }

But, if you try the above you will notice this won’t work. This is because EventEmitterModule requires you to define the supported events by this class. You can do that by calling SetSupportedEvents in the constructor:

M youi/src/CounterpartExtension.cpp
@@ -18,6 +18,13 @@ using namespace std;
 YI_RN_INSTANTIATE_MODULE(CounterpartExtension, yi::react::EventEmitterModule);
 YI_RN_REGISTER_MODULE(CounterpartExtension);

+CounterpartExtension::CounterpartExtension() {
+    SetSupportedEvents({
+        LIST_GAINED_FOCUS,
+        LIST_LOST_FOCUS,
+        LIST_CHANGED_FOCUS
+    });
+}

 YI_RN_DEFINE_EXPORT_METHOD(CounterpartExtension, extendCounterpart)(uint64_t tag)
 {

Write the React Component to extend the counterpart functionality

In this part we want to create a component that will add the functionality of listening to focus events from children to any React Native component.

To do this we will create a component that takes any other component as prop and adds the props we want.

type Props = {
    onListLostFocus?: () => void;
    onListGainedFocus?: (index: number) => void;
    onFocusChanged?: (index: number) => void;
};

To go step by step. Let’s first just add the file and a component that renders the component passed via props.

A CounterpartExtender.js
@@ -0,0 +1,6 @@
+import React from 'react';
+
+export const CounterpartExtender = ({ component, ...remainingProps }) => {
+  const Comp = component;
+  return <Comp  {...remainingProps} />
+}

The component above can be used as follow <CounterpartExtender component={View} />, component can be any React Native component for now.

Get rendered component reference

M CounterpartExtender.js
@@ -1,6 +1,7 @@
-import React from 'react';
+import React, { useRef } from 'react';

 export const CounterpartExtender = ({ component, ...remainingProps }) => {
+  const compRef = useRef(null);
   const Comp = component;
-  return <Comp  {...remainingProps} />
+  return <Comp ref={compRef}  {...remainingProps} />
 }

Since this is a functional component. We must do two things. Use forwardRef case anyone needs to reference the component passed down, and also use useImperativeHandle to pass the ref back up.

Note, if the usage of forwardRef & useImperativeHandle new to you read React docs or check this blog post

M CounterpartExtender.js
@@ -1,7 +1,9 @@
-import React, { useRef } from 'react';
+import React, { useRef, forwardRef, useImperativeHandle } from 'react';

-export const CounterpartExtender = ({ component, ...remainingProps }) => {
+export const CounterpartExtender = forwardRef(({ component, ...remainingProps }, ref) => {
   const compRef = useRef(null);
+  // Send the ref back up to parent
+  useImperativeHandle(ref, () => compRef.current);
   const Comp = component;
   return <Comp ref={compRef}  {...remainingProps} />
-}
+})

Call extendCounterpart

First, the CounterpartExtension is a native module you created, so it is available in the NativeModules singleton exported by react-native.

We can get the CounterpartExtension and call the extendCounterpart method by doing:

M CounterpartExtender.js
@@ -1,9 +1,16 @@
-import React, { useRef, forwardRef, useImperativeHandle } from 'react';
+import React, { useRef, forwardRef, useImperativeHandle, useEffect } from 'react';
+import { NativeModules, findNodeHandle } from 'react-native';

 export const CounterpartExtender = forwardRef(({ component, ...remainingProps }, ref) => {
   const compRef = useRef(null);
   // Send the ref back up to parent
   useImperativeHandle(ref, () => compRef.current);
   const Comp = component;
+
+    useEffect(() => {
+        const listTag = findNodeHandle(compRef.current);
+        NativeModules.CounterpartExtension.extendCounterpart(listTag);
+    }, []);
+
   return <Comp ref={compRef}  {...remainingProps} />
 })

Remember that our extendCounterpart method accepts a uint64_t tag which you can get it by using the findNodeHandle method provided by react-native.

Listen to events

Import the NativeEventEmitter to listen to events coming from the CounterpartExtension module.

M CounterpartExtender.js
@@ -1,15 +1,51 @@
 import React, { useRef, forwardRef, useImperativeHandle, useEffect } from 'react';
-import { NativeModules, findNodeHandle } from 'react-native';
+import { NativeModules, findNodeHandle, NativeEventEmitter } from 'react-native';

-export const CounterpartExtender = forwardRef(({ component, ...remainingProps }, ref) => {
+const CounterpartExtensionEmitter = new NativeEventEmitter(
+    NativeModules.CounterpartExtension
+);
+
+export const CounterpartExtender = forwardRef(({
+  component,
+  onListLostFocus,
+  onListGainedFocus,
+  onFocusChanged,
+  ...remainingProps
+}, ref) => {
   const compRef = useRef(null);
   // Send the ref back up to parent
   useImperativeHandle(ref, () => compRef.current);
   const Comp = component;

     useEffect(() => {
-        const listTag = findNodeHandle(compRef.current);
-        NativeModules.CounterpartExtension.extendCounterpart(listTag);
+      const listTag = findNodeHandle(compRef.current);
+      NativeModules.CounterpartExtension.extendCounterpart(listTag);
+
+      const listeners = [];
+
+      if (onListLostFocus) {
+        listeners.push(CounterpartExtensionEmitter.addListener(
+          "ListLostFocus",
+          onListLostFocus
+        ));
+      }
+
+      if (onListGainedFocus) {
+        listeners.push(CounterpartExtensionEmitter.addListener(
+          "ListGainedFocus",
+          onListGainedFocus
+        ));
+      }
+
+      if (onFocusChanged) {
+        listeners.push(CounterpartExtensionEmitter.addListener(
+          "ListChangedFocus",
+          onFocusChanged
+        ));
+      }
+
+      return () => listeners.forEach(listener => listener.remove());
+
     }, []);

   return <Comp ref={compRef}  {...remainingProps} />

Two things are happening here: we attach listeners in useEffect and remove them during the clean up callback.

Using CounterpartExtender

Add the native files to your SourceList.cmake:

M youi/SourceList.cmake
@@ -4,8 +4,10 @@
 set (YI_PROJECT_SOURCE
     src/App.cpp
     src/AppFactory.cpp
+    src/CounterpartExtension.cpp
 )

 set (YI_PROJECT_HEADERS
     src/App.h
+    src/CounterpartExtension.h
 )

Use with your components:

We will remove the init templated JSX.


M index.youi.js
@@ -2,47 +2,29 @@
  * Basic You.i RN app
  */
 import React, { Component } from "react";
-import { AppRegistry, Image, StyleSheet, Text, View } from "react-native";
+import { AppRegistry, Button, StyleSheet, View } from "react-native";
 import { FormFactor } from "@youi/react-native-youi";
+import { CounterpartExtender } from './CounterpartExtender';

 export default class YiReactApp extends Component {
   render() {
     return (
-      <View style={styles.mainContainer}>
-        <View style={styles.headerContainer}>
-          <View
-            style={styles.imageContainer}
-            focusable={true}
-            accessible={true}
-            accessibilityLabel="You i TV logo"
-            accessibilityHint="Image in your first app"
-            accessibilityRole="image"
-          >
-            <Image
-              style={styles.image}
-              source={{ uri: "res://drawable/default/youi_logo_red.png" }}
-            />
-          </View>
-        </View>
-        <View style={styles.bodyContainer} focusable={true} accessible={true}>
-          <Text
-            style={styles.headlineText}
-            accessibilityLabel="Welcome to your first You I React Native app"
-          >
-            Welcome to your first You.i React Native app!
-          </Text>
-          <Text
-            style={styles.bodyText}
-          >
-            For more information on where to go next visit
-          </Text>
-          <Text
-            style={styles.bodyText}
-            accessibilityLabel="https://developer dot you i dot tv"
-          >
-            https://developer.youi.tv
-          </Text>
-        </View>
+      <View
+        style={styles.mainContainer}
+      >
+        <CounterpartExtender
+          component={View}
+          onListLostFocus={(e) => console.log('Lost Focus', e)}
+          onListGainedFocus={(e) => console.log('Gained Focus', e)}
+          onFocusChanged={(e) => console.log('Changed Focus', e)}
+          style={{ backgroundColor: 'lightblue' }}
+        >
+          <Button title="Button One" />
+          <Button title="Button Two" />
+          <Button title="Button Three" />
+          <Button title="Button Four" />
+        </CounterpartExtender>
+          <Button title="Outside box" />
       </View>
     );
   }

Then build your app:

youi-tv build -p osx

Start the metro bundler

yarn start

Run the executable app

youi-tv run -p osx

Move around and you will see how the callbacks get called.

Supporting multiple CounterpartExtenders

You will notice that if you use more than one CounterpartExtender, each one will get notified when there is an event. This happens because the native modules are in essence a singleton class. Thus, it requires some extra logic to know which component to notify.

Identifying via React Tag

We can use the React tag number to only notify the correct component. In your component you can filter the events coming from the module by the React tag.

Thus, let’s send the React Tag as a param of the event so our CounterpartExtender can differentiate, we will use the folly::dynamic library to help us create a dynamic object:

M youi/src/CounterpartExtension.cpp
@@ -37,18 +37,23 @@ YI_RN_DEFINE_EXPORT_METHOD(CounterpartExtension, extendCounterpart)(uint64_t tag
     auto pCounterpart = pComponent->GetCounterpart(); //CYISceneNode
     YI_ASSERT(pCounterpart, TAG, "Shadow view with tag %" PRIu64 " doesn't have a counterpart.", tag);

-    pCounterpart->DescendantLostFocus.Connect([this](){
-        // Do what you need here.
-        EmitEvent(LIST_LOST_FOCUS, {});
+    pCounterpart->DescendantLostFocus.Connect([this, tag](){
+        dynamic data = dynamic::object;
+        data["tag"] = tag;
+        EmitEvent(LIST_LOST_FOCUS, data);
     });

-    pCounterpart->DescendantGainedFocus.Connect([this](){
-        EmitEvent(LIST_GAINED_FOCUS, {});
+    pCounterpart->DescendantGainedFocus.Connect([this, tag](){
+        dynamic data = dynamic::object;
+        data["tag"] = tag;
+        EmitEvent(LIST_GAINED_FOCUS, data);
         // Do what you need here.
     });

-    pCounterpart->DescendantsChangedFocus.Connect([this](){
-        EmitEvent(LIST_CHANGED_FOCUS, {});
+    pCounterpart->DescendantsChangedFocus.Connect([this, tag](){
+        dynamic data = dynamic::object;
+        data["tag"] = tag;
+        EmitEvent(LIST_CHANGED_FOCUS, data);
         // Do what you need here.
     });
 }

All events now return the React tag to which parent they belong to.

Then in the CounterpartExtender we can now filter the events — we create whenTag function to only pipe the event callback when the tags match:


M CounterpartExtender.js
@@ -5,6 +5,10 @@ const CounterpartExtensionEmitter = new NativeEventEmitter(
     NativeModules.CounterpartExtension
 );

+function whenTag(parentTag, callback) {
+  return ({ tag }) => parentTag == tag && callback();
+}
+
 export const CounterpartExtender = forwardRef(({
   component,
   onListLostFocus,
@@ -26,21 +30,21 @@ export const CounterpartExtender = forwardRef(({
       if (onListLostFocus) {
         listeners.push(CounterpartExtensionEmitter.addListener(
           "ListLostFocus",
-          onListLostFocus
+          whenTag(listTag, onListLostFocus)
         ));
       }

       if (onListGainedFocus) {
         listeners.push(CounterpartExtensionEmitter.addListener(
           "ListGainedFocus",
-          onListGainedFocus
+          whenTag(listTag, onListGainedFocus)
         ));
       }

       if (onFocusChanged) {
         listeners.push(CounterpartExtensionEmitter.addListener(
           "ListChangedFocus",
-          onFocusChanged
+          whenTag(listTag, onFocusChanged)
         ));
       }

Further Customization for CYIListViews

We can take this further. Our CounterpartExtender already supports using it with FlatList and ScrollView. But, we want to add more functionality to it so we can get the index of the element that received focused.

To achieve so we will have to learn two things, how to cast our CYISceneNode to a CYIListView and how to use its methods inside the lambda callback.

Cast CYISceneNode to a CYIListView

If we know the React tag we have is a React Native component that underneath extends the CYIListView we can cast its CYISceneNode to it.

How do you find out?

You can ask the Engine team or dig around the headers for the React Counterparts in ~/youiengine/<engine_version>/include/react/youireact/nodes.

For example, the FlatList and ScrollView both use the ShadowScrollView which uses the CYIListView counterpart.

Casting

M youi/src/CounterpartExtension.cpp
@@ -5,6 +5,7 @@

 #include <youireact/IBridge.h>
 #include <youireact/ShadowTree.h>
+#include <view/YiListView.h>

 using namespace folly;
 using namespace std;
@@ -37,6 +38,8 @@ YI_RN_DEFINE_EXPORT_METHOD(CounterpartExtension, extendCounterpart)(uint64_t tag
     auto pCounterpart = pComponent->GetCounterpart(); //CYISceneNode
     YI_ASSERT(pCounterpart, TAG, "Shadow view with tag %" PRIu64 " doesn't have a counterpart.", tag);

+    CYIListView * pListView = dynamic_cast<CYIListView *>(pCounterpart);
+
     pCounterpart->DescendantLostFocus.Connect([this, tag](){
         dynamic data = dynamic::object;
         data["tag"] = tag;

By casting you transformed you CYISceneNode into a CYIListView, and now you can call all of its public methods.

Return the item index in an event

We only want to return an index when the parent is a CYIListView. And know this since the dynamic cast will return a nullptr if the cast fails.

M youi/src/CounterpartExtension.cpp
@@ -46,16 +46,28 @@ YI_RN_DEFINE_EXPORT_METHOD(CounterpartExtension, extendCounterpart)(uint64_t tag
         EmitEvent(LIST_LOST_FOCUS, data);
     });

-    pCounterpart->DescendantGainedFocus.Connect([this, tag](){
+    pCounterpart->DescendantGainedFocus.Connect([this, tag, pListView](){
         dynamic data = dynamic::object;
         data["tag"] = tag;
+
+        if (pListView)
+        {
+            data["index"] = pListView->GetFocusedItemIndex();
+        }
+
         EmitEvent(LIST_GAINED_FOCUS, data);
         // Do what you need here.
     });

-    pCounterpart->DescendantsChangedFocus.Connect([this, tag](){
+    pCounterpart->DescendantsChangedFocus.Connect([this, tag, pListView](){
         dynamic data = dynamic::object;
         data["tag"] = tag;
+
+        if (pListView)
+        {
+            data["index"] = pListView->GetFocusedItemIndex();
+        }
+
         EmitEvent(LIST_CHANGED_FOCUS, data);
         // Do what you need here.
     });

This way we will only receive an index when the CYIListView cast succeeds.

And don’t forget to pass down the index in the callback of your events in CounterpartExtender.

M CounterpartExtender.js
@@ -6,7 +6,7 @@ const CounterpartExtensionEmitter = new NativeEventEmitter(
 );

 function whenTag(parentTag, callback) {
-  return ({ tag }) => parentTag == tag && callback();
+  return ({ tag, index }) => parentTag == tag && callback(index);
 }

 export const CounterpartExtender = forwardRef(({

But, how do I know which methods to use in the CYIListView?

Remember to check the documentation for it. In general, the You.i Engine has many features that are no exposed to React Native since the react-native-youi is relative new compared to the engine.

Conclusion

That’s it. We hope with this guide you are able to get a better grasp of how the You.i Engine works plus how you can take the React Native You.i binding even further by leveraging C++ and the Engine One’s existing features.

Link to sample

The sample is stored here.