@heartlandone/vega-react-native v0.2.0
Vega React Native
A React Native Library for Vega Design System. Different with other wrappers for Vega (e.g.
@heartlandone/vega-angular,@heartlandone/vega-react,@heartlandone/vega-vue), this package is not a simple wrapper of@heartlandone/vega(built on top ofvega-stencil), but a complete standalone library to provide vega components that can be used in React Native apps.
Table of Contents
- About
React Native - Why not re-use
@heartlandone/vegawithin a React Native wrapper library? - High level picture about how to tackle this situation
- Development Guide
- Best practices
- Troubleshooting
About React Native
React Native brings React's declarative UI framework to iOS and Android. With React Native, you use native UI controls and have full access to the native platform with the following benefits:
- Declarative. React makes it painless to create interactive UIs. Declarative views make your code more predictable and easier to debug.
- Component-Based. Build encapsulated components that manage their state, then compose them to make complex UIs.
- Developer Velocity. See local changes in seconds. Changes to JavaScript code can be live reloaded without rebuilding the native app.
- Portability. Reuse code across iOS, Android, and other platforms.
Why not re-use @heartlandone/vega within a React Native wrapper library?
React Native renders to general platform views (host views) instead of DOM nodes (which can be considered web’s host views). Rendering to host views is made possible by the Fabric Renderer. Fabric lets React talk to each platform and manage its host view instances. The Fabric Renderer exists in JavaScript and targets interfaces made available by C++ code,
Which is making the RN component is coming with the following restrictions compared with DOM nodes (We only list major ones):
1. DOM structure is gone:
1. No querySelector() supported
2. No appendChild() and parent supported
3. No interface of component properties directly exposed
2. CSS is replaced by StyleSheet:
1. No complex CSS selectors (class, element tag, etc.). There is only support for simple CSS class selector that maps 1-to-1 with an element.
2. There is no cascade, CSS properties are not inherited from parent elements
3. Many CSS properties are element specific, e.g. you can not give Text properties (font-family, etc.) to a View and vice versa.
4. React Native only implements a subset of CSS. The more complex CSS features are left out, and what you get is a set of CSS features that work well to do styling in both browsers and native apps.
5. There are some new styling properties in React Native that do not exist in regular CSS.
Hence, if we simply create a wrapper library from heartlandone/vega most of the UI and functionality won't be working anymore in RN apps.
High level picture about how to tackle this situation
To address this gap between native platform and web browser, we came up with the following architecture to build the vega-react-native

The main idea behind this graph is:
1. Use vega-stencil generated component interface as specifications of vega-react-native
Instead of directly importing the component implementation from vega-stencil, we are using the built interface declaration as the backbone in vega-react-native to help us determine what UI behavior we should support for the specific component, which also saves us from duplicating the work to declare component props in vega-react-native.
2. Create adapters/polyfill in order to consume the code from vega-stencil
To re-use some helper/utility methods in vega-stencil, we need to create polyfill/adapters to make sure the method logic is applied to the native platform as well. For example in vega-stencil/src/utils/breakpoint.ts we have:
export const getCurrentBreakpoint = (): BreakpointsTokenType => {
//...
const windowWidth: number = window.innerWidth;
//...
}; In the native platform, there is no global variable window with innerWidth supported, hence we need to provide a polyfill as below:
// in vega-react-native/src/adapters/global.ts
import {Dimensions} from 'react-native';
/**
* A polyfill for {@link window.innerWidth} by using {@link Dimensions.get('window').width}
*/
function defineWindowWidth() {
Object.defineProperty((global as any).window, 'innerWidth', {
value: Dimensions.get('window').width,
});
}
defineWindowWidth();3. Create VegaNodeContainer to support the DOM API for RN components
VegaNodeContainer is a HOC component which is a DOM API polyfill in RN env.
3.1 Glossary
HostElementor RN Component: The original component created on top of RN which including all the essential properties for the vega components, which will be wrapped byVegaNodeContainerVegaNode: the instance created from RN component constructor wrapped byVegaNodeContainer.VegaNodeContainer: the HOC component used for wrapping the Host Element and return aVegaNodeContainedas the creator of theVegaNodeVegaNodeContained: Creator (constructor) created byVegaNodeContainerVegaNodeInterface: The interface used to describe the shape ofVegaNodein terms of the public API exposed.VegaNodeRefObjectorVegaNodeRef: The reference of theVegaNodewhich is used to get the current linkedVegaNodereference byVegaNodeRef#current.
The functionality supported by VegaNode can be found in VegaNodeInterface:
export interface VegaNodeInterface {
getInternalType(): string;
getClassList(): string[];
addClass(className: string): Promise<void>;
removeClass(className: string): Promise<void>;
setProperty(key: string, value: unknown): Promise<void>;
getProperty<T>(key: string): T;
setParent(parent: VegaNodeInterface): void;
getParent(): VegaNodeInterface | null;
appendChild(child: VegaNodeInterface | ReactNode): Promise<void>;
removeChild(child: VegaNodeInterface): Promise<void>;
getChildren(): VegaNodeInterface[];
hasChild(child: VegaNodeInterface): boolean;
getRendered(): VegaNodeRendered | null;
setRendered(rendered: VegaNodeRendered): void;
querySelector(selector: string): VegaNodeInterface | null;
querySelectorAll(selector: string): VegaNodeInterface[];
render(): VegaNodeRendered;
}Most of the api methods should be straightforward as they are used to resolve the gaps we mentioned previously for DOM API in RN env.
And here is a graph explaining how the VegaNode is internally working to consume the external pass-in attributes into the host elements and connect the children of the host elements to build the virtual DOM tree:

Within the above diagram:
1. We have a state constructed during constructor to include:
newProps: to override the originalPropspassed to the host elements.addedClassNameSet/removeClassNameSet: to override the originalclassListpassed to the host elements which will be transferred toStylesheetbyVegaClassRendererchildren: to maintain the references ofVegaNoderendered output and passed to the host element as the actual children.
- The
statewill be the bridge between component consumer and the host element that is consumed, to make sure all the UI manipulation (e.g. overriding props, adding class, appending child) on the vega component(VegaNodeContained) is state-controlled and can be reflected on the host elements. - Out of the
state, we will have the private propertychildrenwithin theVegaNodeto contain the reference ofVegaNodeContainedfor all the children of the given host elements, this part is important as it is building up the tree structured hierarchy between parent and child vega nodes (by invokinglinkChildNodeRefinternally), which will be taken as the virtual DOM tree.
❗Note before using DOM manipulation API to control children rendering❗
you have to explicitly set the property stateControlChild={true} before using DOM manipulation API appendChild() or removeChild() to control children rendering.
This is mainly for avoiding the conflicts between props#children control flow and state#children control flow, to be more specific, if you have a component that is using this.props to control the children UI/UX like below:
render() {
return (
<VegaTouchableOpacity>
<VegaText classList={['v-font-btn-label']}>{props.label}</VegaText>
</VegaTouchableOpacity>
)
}You cannot use DOM manipulation API (by stateControlChild={true}) anymore on both VegaText and VegaTouchableOpacity, since the embed children for both of them is related with the propagated property props.
After getting an overview about how the VegaNode is working, we will move forward about how to integrate VegaNode with the vega component development in React Native.
3.2 Constructing VegaNode connected to RN components
In order to make your created RN component integrated with VegaNode along with all the API listed in the VegaNodeInterface you need:
1. Call the VegaNode builder method VegaNodeContainer and pass in your RN component to get a VegaNodeContained component for your RN component, here we take View as an example:
import {View, ViewProps} from 'react-native';
import {VegaNodeContainer, VegaNodeContained} from '../core/vega-node';
export const VegaView: VegaNodeContained<ViewProps> =
VegaNodeContainer<ViewProps>(View);After you wrapped the component creator with
VegaNodeContainer, all the component instance created byVegaViewwill support all the methods inVegaNodeInterface(Meanwhile it will behave exactly same as RN nativeView), so you can do something like<VegaView classList={['v-border-secondary-btn', 'v-bg-action-focus']} // ==> the class will be transferred to RN Stylesheet which we will cover shortly ref={viewRef} // ==> get the reference of the RN node style={styles.viewStyleA} /> <VegaView classList={['v-border-secondary-btn']} ref={viewRefB} style={styles.viewStyleB} /> <VegaButton block={true} label="Switch" ref={buttonRef} variant="primary" icon={'arrow-expand'} danger={true} onVegaClick={buttonClickHandler} />// append a RN component directly which will render it before appending viewRefA.current?.appendChild( <VegaText ref={textRef} classList={['v-font-btn-label']}> Hello Vega! </VegaText>, ); // append a child by using the reference await viewRefB.current?.appendChild(textRef.current!); // set/get the property from a vega node contained RN component await buttonRef.current.setProperty( 'danger', !buttonRef.current.getProperty('danger'), );
3.3 Vega Node Root
In some use cases we are not able to get the reference we would like to operate on before hands, and in web env, we can simply use the root element document to query the targeting element from top to bottom.
In RN, there is no such way to achieve it by default. Hence, we came up a dummy root node as the default parent for all the VegaNode, which means as long as the VegaNode is not a child of other VegaNode, its parent will be VegaNodeRoot.
After we established this connection, we can make use of VegaNodeRoot to do the query from top to bottom:
const nestedVegaViewA: VegaNodeInterface = VegaNodeRoot.querySelector(
'.nested-view VegaView.v-bg-accent1-primary.v-border-secondary-btn',
)!;
const nestedVegaViewB: VegaNodeInterface = VegaNodeRoot.querySelector(
'.nested-view VegaView.v-bg-accent2-primary',
)!;
const nestedVegaText: VegaNodeInterface = VegaNodeRoot.querySelector(
'.nested-view VegaView.v-border-secondary-btn VegaText',
)!;3.4 API usages:
getInternalType()This API is for getting the concrete component name wrapped withVegaNodeContainerwhich can be used for example inquerySelector(). You can take this API as the same one withElement.tagNamevegaViewRef.getInternalType() // 'VegaView' vegaButtonRef.getInternalType() // 'VegaButton'getClassList(): string[]; addClass(className: string): Promise<void>; removeClass(className: string): Promise<void>;Those APIs are the CRUD operation for theclassListstored inVegaNode, which will not only re-rendering the style for theVegaNodeif there is a class registered but can also be reflected in the query behavior on theVegaNode.<VegaView ref={viewRef} classList={['classA']}></VegaView> viewRef.current.getClassList() // classList: ['classA'] viewRef.current.addClass('classB') // classList: ['classA', 'classB'] viewRef.current.removeClass('classA') // classList: ['classB']setProperty(key: string, value: unknown): Promise<void>; getProperty<T>(key: string): T;Update/retrieve the properties of the component wrapped inVegaNodeContainer<VegaButton ref={buttonRef} danger={true} //... /> // flip the VegaButton#danger property await buttonRef.current.setProperty( 'danger', !buttonRef.current.getProperty('danger'), );VegaNode#parentandVegaNode#childrenVegaNode#parentandVegaNode#childrenis used to build the hierarchy structure of the vega node graph. for example, we have<VegaView> <VegaButton/> <VegaText/> </VegaView>which comes to the vega node graph like
VegaNode<VegaView> |___ VegaNode<VegaButton> |___ VegaNode<VegaText>and here are the APIs related to this structure:
setParent(parent: VegaNodeInterface): void;: set the parent of the currentVegaNode, this methods doesn't need to be called explicitly sinceVegaNode#appendChild/removeChildalready helps you to update the parent of the child.getParent(): VegaNodeInterface | null;: get the parent of the currentVegaNodeappendChild(child: VegaNodeInterface | ReactNode): Promise<void>;append the child node to the currentVegaNode- Note: this method can either be passed in with a node reference or a JSX syntax component which will be rendered first and then append to the
VegaNode:viewRefA.current?.appendChild( <VegaText ref={textRef} classList={['v-font-btn-label']}> Hello Vega! </VegaText>, ); // textRef.current will be the last child of viewRefA.current
- Note: this method can either be passed in with a node reference or a JSX syntax component which will be rendered first and then append to the
removeChild(child: VegaNodeInterface): Promise<void>;: remove child of from the currentVegaNodeNote: this method will be automatically called when
appendChild()is called with an existingVegaNode:viewRefA.current?.appendChild( <VegaText ref={textRef} classList={['v-font-btn-label']}> Hello Vega! </VegaText>, ); await viewRefB.current?.appendChild(textRef.current!); // which will internally call: // viewRefA.current.removeChild(textRef.current!), // then: // viewRefB.current?.appendChild(textRef.current)
getChildren(): VegaNodeInterface[];: get all the children of the currentVegaNodehasChild(child: VegaNodeInterface): boolean;: check if theVegaNodeis one of the children of the currentVegaNode
4. Create VegaClassRenderer to support native CSS styling
VegaClassRenderer is used for fill the gap that RN is not supporting rendering the style to the component based on the class name.
Internally, it will use a class name mapping to register the class and styleSheet pair. And within VegaNode#render method, it will retrieve the registered style per class and set into HostElement#style.
Just like what we did in vega-stencil, we pre-defined lots of classes based on the design token and registered them within the VegaClassRenderer before any VegaNode created during runtime (code link)
4.1. How to
If you want to register new classes, you can try:
VegaClassRenderer.registerClasses([
{
classSelector: 'vega-button',
style: VegaClassRenderer.render(['v-self-start', 'v-rounded-full']),
},
{
classSelector: 'vega-button-size-small',
style: VegaClassRenderer.render(['v-px-size-16', 'v-py-size-4']),
},
{
classSelector: 'vega-button-size-default',
style: VegaClassRenderer.render(['v-px-size-24', 'v-py-size-8']),
},
{
// we also support compound class group (non-nested) registration to make the stylesheet rendered differently for different class combinations
classSelector: ['vega-button-variant-secondary', 'vega-button-size-small'],
style: VegaClassRenderer.render(['v-pt-size-2', 'v-pb-size-2']),
},
]);After class registration, we can simply use it by classList={['your defined class']}. For example:
<VegaTouchableOpacity
classList={[
'vega-button',
`vega-button-size-${props.size}`,
`vega-button-variant-${props.variant}`,
props.disabled ? 'vega-button-disabled' : '',
props.danger ? 'vega-button-danger' : '',
props.block ? 'v-w-full' : '',
].filter(Boolean)}
>
{/** children... */}
</VegaTouchableOpacity>4.2 Style inheritance
In React Native, style inheritance is not supported, which means you shouldn't expect the child component using the same fontColor if you are setting it on its parent.
This gap is fixed by VegaNode by creating an internal used property inheritedClasses. And for a complete list of style property that can be inherited, please refer to this link.
Development Guide
1. Setting up the development environment
Please follow the setup guidance under section Installing dependencies in React Native official document.
Note:
1. Make sure you are under the tab React Native CLI Quickstart as vega-react-native is created by RN CLI instead of Expo CLI.
2. Please follow both target OS Android and IOS since we need to build the components onto both platforms.
2. Running your React Native application
First, you will need to start Metro, the JavaScript bundler that ships with React Native. Metro "takes in an entry file and various options, and returns a single JavaScript file that includes all your code and its dependencies."—Metro Docs
yarn run startOnce Metro is running, you can leave it open to re-build the application when code is changed.
3. Start the application on the virtual device
- To run the app on Android device, please run
yarn run android - To run the app on IOS device, please run
yarn run ios
4. During debugging
- Press
Rto reload your app manually - Press
Dto open the RN developer menu - If you want to use the chrome like inspector for inspecting your component node, please use
react-devtools
For other information, please refer to this link
5. Testing
Currently, we have set up 2 levels testing specifications:
5.1 Component unit test
Component unit test is built on top of @testing-library/react-native.
In order to run the component level testing, you can run:
yarn run test:unitTo rectify the snapshot testing, please run:
yarn run test:unit -- --updateSnapshotNote: test coverage is collected in component unit test by jest
5.2 E2E Test
We are using detox to run the e2e tests on simulator device. In order to run the e2e test on your local workspace, please follow the Environment setup guidance first to make sure detox is ready to run locally, then run the following commands: 1. Run E2E test on IOS simulator (iPhone 11):
yarn run test:e2e:ios- Run E2E test on Android emulator (Pixel_3a_API_32_arm64-v8a):
Note:yarn run test:e2e:androidCurrently, we are not able to include the E2E tests into our pipeline due to the following reasons:
- For IOS:
xcodebuildis required in order to install the testing app onto the IOS simulator, however the current GitLab Runner we have is on UNIX system which doesn't support to installxcodebuild. (We need to create a separate runner in an AWS EC2 Mac Instance in future) - For Android: The Android Emulators require the KVM support which is also not available for our current GitLab runner env.
Hence, we need to have QAs to conduct those E2E tests manually before we release the packages to our consumers.
- For IOS:
Sometimes, the test can get stuck and not proceeding in android device, in that case, you can simply terminate the current testing process and restart to resolve it.
6. Running Storybook
Our stories for components/screens are stored in ./stories
- To start the metro server that is boot up the RN storybook application, you can run
Note: make sure there is no other metro servers ongoing which is occupying the same ip address and port.yarn run start-storybook - Then you can run the app on the virtual device as you did previously like:
yarn run android # or yarn run ios
7. Raising the MR
Make sure you have run yarn run pre-release and yarn run release before raising the MR.
Best practices
1. Creating Vega Component
- In general, all component classes (including the native one like
View,Text) should be wrapped withVegaContainer, and rename the wrapped one with nameVegaXXX(takeVegaTextfor example). For vega components, you need to create an internal component class/function first, then wrapped it withVegaNodeContainer(takevega-iconfor example) - While wrapping vega components, you should make sure the class properties is penetrated into the
render()method to make sure the wrapped component is consuming the class controlled style fromVegaNodeproperly (example regarding how to do that).
2. Class v.s. StyleSheet
StyleSheetshould be avoided if it can be converted to a class registered withinVegaClassRenderer.- For general class that can be used by multiple component, you should define them into
src/stylesheet/general.ts
Troubleshooting
Unable to resolve module
@heartlandone/vegaafter opening the demo app. This is sometimes due to the symlink is not properly picked up by metro and what you can do is starting the metro withreset-cacheflag enabled by:react-native start --reset-cacheor you encounter this error during the metro release build for e2e tests, you can clean up the cache by running
watchman watch-del '/Users/<username>/workspace/tiger' ; watchman watch-project '/Users/<username>/workspace/tiger'Encounter errors that can only be reproduced in release build bundle. Sometimes, the error is not able to reproduce in the debug (development) build, hence we need to manually create a release build to reproduce the issue.
- Here We would like to suggest to use
xcode(IOS platform) as it can easily print out the JS console output into xcode console as the debug tools is not available in release build. - In order to make the release build by
xcode, you need to- open the xcworkspace of
VegaReactNativebyopen ios/VegaReactNative.xcworkspace/ - Open
xcodetab and selectProduct > Scheme > Edit Scheme - Change the
Build ConfigurationfromDebugtoReleasein Run section - Re-run the workspace and now the app should be built with release configuration
- open the xcworkspace of
- Here We would like to suggest to use
3 years ago