@qlik/react-native-simple-grid
v1.4.1
Published
A react native straight table component designed for performance
Readme
@qlik-trial/react-native-simple-grid
A straight table component designed for performance
Installation
npm install @qlik-trial/react-native-simple-gridArchitecture
This component is built using React Native's new architecture (Fabric) for optimal performance on both iOS and Android. Below is an overview of how data flows from JavaScript to native rendering.
iOS Architecture Flow
JavaScript (React)
↓
TypeScript Codegen Spec (SimpleGridNativeComponent.ts)
↓
React Native Codegen → C++ Props Structs
↓
Objective-C++ Bridge (ReactNativeStraightTableComponentView.mm)
↓
Swift View Layer (ContainerView.swift)
↓
Native Rendering (UIKit)Android Architecture Flow
JavaScript (React)
↓
TypeScript Codegen Spec (SimpleGridNativeComponent.ts)
↓
React Native Codegen → C++ Props/Events
↓
ViewManager with @ReactProp (ReactNativeStraightTableViewManager.java)
↓
TableView (Java)
↓
Native Rendering (Android Views/RecyclerView)iOS Detailed Flow
1. JavaScript → Native (Codegen Bridge)
- Props are defined as TypeScript interfaces in
src/specs/SimpleGridNativeComponent.ts - React Native Codegen generates C++ structs from these interfaces at build time
- When props change in React, they are serialized and sent across the bridge as C++ structs
2. Objective-C++ Layer (ios/ReactNativeStraightTableComponentView.mm)
- The
updateProps()method is called automatically by Fabric whenever props change - Receives C++ props:
const ReactNativeStraightTableViewProps &newViewProps - Converts C++ structs to
NSDictionary(Objective-C compatible format):NSMutableDictionary *theme = [NSMutableDictionary new]; theme[@"headerBackgroundColor"] = RCTNSStringFromString(newViewProps.theme.headerBackgroundColor); _view.theme = theme; - Assigns dictionaries to the Swift view instance
3. Swift Layer (ios/ContainerView.swift)
- Class marked with
@objcMembersto expose all properties to Objective-C++:@objc @objcMembers public class ContainerView: UIView { public var theme: NSDictionary = [:] { didSet { // Deserialize NSDictionary → Swift struct let json = try JSONSerialization.data(withJSONObject: theme) let decoded = try JSONDecoder().decode(TableTheme.self, from: json) // Pass to rendering logic } } } @objcMemberson the class makes individual@objcannotations redundant- Uses
JSONSerialization+JSONDecoderto convertNSDictionary→ type-safe Swift structs - Property observers (
didSet) are automatically triggered when values are set from Objective-C++ - Decoded structs are passed to rendering components
4. Rendering
- Swift files (
TableViewFactory.swift,DataCollectionView.swift, etc.) use the decoded structs to render the table with proper styling and data
Why This Architecture?
- @objc Boundary: Swift and Objective-C++ can only share Objective-C-compatible types (
NSDictionary,NSString,NSNumber) - Type Safety: JSON decoding provides type-safe Swift structs instead of manually extracting dictionary values
- Fabric Requirement: The Objective-C++ bridge layer is required by React Native's new architecture
- Performance: Fabric's synchronous rendering eliminates the old bridge's asynchronous overhead
Bridging Between Layers
Swift to Objective-C++ Connection:
#import "react_native_simple_grid-Swift.h" // Auto-generated bridging header
@implementation ReactNativeStraightTableComponentView {
ContainerView *_view; // Swift class accessible from Objective-C++
}
- (instancetype)initWithFrame:(CGRect)frame {
_view = [[ContainerView alloc] initWithFrame:frame]; // Creates Swift instance
// ...
}The bridging header (react_native_simple_grid-Swift.h) is auto-generated by Xcode and exposes all Swift classes marked with @objc or public to Objective-C++.
Android Detailed Flow
Cross-Platform:
- TypeScript Spec:
src/specs/SimpleGridNativeComponent.ts- Defines the props interface for codegen (both platforms) - React Component:
src/components/SimpleGrid.tsx- Unified component for iOS and Android
Android:
- ViewManager:
android/src/main/java/.../ReactNativeStraightTableViewManager.java- Handles props with @ReactProp - TableView:
android/src/main/java/.../TableView.java- Main rendering component with RecyclerView - Data Layer:
android/src/main/java/.../DataProvider.java,RowFactory.java,DataColumn.java
2. ViewManager Layer (android/src/main/java/.../ReactNativeStraightTableViewManager.java)
- Uses traditional
@ReactPropannotations - compatible with both Paper and Fabric - Each prop method is called individually when that prop changes:
@ReactProp(name = "rows") public void setRows(View view, @Nullable ReadableMap source) { // Process rows prop } - Handles
rowsJSONstring format (matching iOS Codegen pattern):// Parse JSON string to ReadableArray org.json.JSONArray jsonArray = new org.json.JSONArray(rowsJSON); // Convert to WritableArray for processing
3. TableView Layer (android/src/main/java/.../TableView.java)
- Main rendering component using
RecyclerViewfor performance DataProvidermanages columns and rows- Implements wipe-on-column-change strategy (same as iOS):
if(tableView.dataProvider.dataColumns.size() != dataColumns.size()) { tableView.resetTable(); // Wipe state on column count change }
4. Rendering
- Uses Android's
RecyclerViewwith customViewHolderpattern - Cell types: text, images, mini-charts, URLs
- Frozen first column implemented with separate
RecyclerView
Android-Specific Implementation Details
JSON String Handling:
The rowsJSON prop (a JSON-serialized array) requires manual conversion:
private boolean processRows(TableView tableView, ReadableMap rows) {
if (rows.hasKey("rowsJSON")) {
String rowsJSON = rows.getString("rowsJSON");
org.json.JSONArray jsonArray = new org.json.JSONArray(rowsJSON);
// Manual conversion: JSONArray → WritableArray
WritableArray writableArray = Arguments.createArray();
for (int i = 0; i < jsonArray.length(); i++) {
JSONObject jsonObject = jsonArray.getJSONObject(i);
WritableMap writableMap = convertJsonToMap(jsonObject);
writableArray.pushMap(writableMap);
}
// Pass to RowFactory for processing
}
}Prop Ordering & Initialization: Props arrive individually and asynchronously. The table only initializes when all required props are present:
@ReactProp(name = "rows")
public void setRows(View view, ReadableMap source) {
tableView.setRows(...);
// Only initialize if columns, rows, and styles are all present
if(tableView.isInitialized() &&
tableView.cellContentStyle != null &&
tableView.headerContentStyle != null) {
tableView.initialize();
}
}Race Condition Prevention: When switching between tables (different column configurations):
@ReactProp(name = "cols")
public void setCols(View view, ReadableMap source) {
List<DataColumn> dataColumns = processColumns(tableView, source);
if(tableView.dataProvider.dataColumns != null) {
if(tableView.dataProvider.dataColumns.size() != dataColumns.size()) {
tableView.resetTable(); // Wipe everything on column count change
tableView.setDataColumns(dataColumns);
} else {
tableView.setDataColumns(dataColumns);
}
}
}Platform Differences Summary
| Aspect | iOS | Android |
|--------|-----|---------|
| Bridge Pattern | Obj-C++ → NSDictionary → Swift | @ReactProp → Direct Java |
| Type Safety | JSONDecoder to Swift structs | ReadableMap to Java objects |
| Prop Updates | Batch via didSet | Individual method calls |
| Row Data Format | rowsJSON string parsed in Swift | rowsJSON string parsed in Java |
| Initialization | Guards in didSet | Guards in @ReactProp + isInitialized() check |
| Race Prevention | dataColumns = nil in Swift | resetTable() in Java |
| Rendering | UICollectionView | RecyclerView |
Both platforms now share the same TypeScript codegen spec and implement the same wipe-on-column-change strategy for race condition prevention.
Critical Implementation Patterns
Fabric Prop Update Behavior
In React Native's new architecture (Fabric), props update immediately and individually rather than being batched. Each prop's didSet handler fires as soon as the prop is received, which can create race conditions if not handled carefully.
Race Condition Prevention: Wipe-on-Column-Change
When switching between different tables (different column configurations), a critical race condition can occur:
colsprop updates with new table's columns →colsdidSet fires- Old
dataRowsstill present from previous table - Attempting to render with mismatched columns/rows → crash
Solution: Wipe all state when column count changes:
// In ContainerView.swift
if dataColumns != nil && decodedCols.header?.count != dataColumns?.count {
// Column count changed - wipe everything
dataColumns = nil
dataRows = nil
totals = nil
// Store new columns but don't render yet
dataColumns = decodedCols.header
return // Wait for fresh rows
}This pattern ensures the table only renders when it has a complete, consistent data set.
React Component Identity
Use a stable key prop based on table identity to ensure proper React lifecycle:
<SimpleGrid
key={layout?.qInfo?.qId || 'default'}
// ...other props
/>This forces React to remount the component when switching between different tables, complementing the native wipe strategy.
Selection Clearing Behavior
The clearSelections prop gates when selections are cleared:
// Only clear when explicitly requested
if decodedRows.reset == true &&
clearSelections != nil &&
clearSelections!.compare("yes") == .orderedSame {
selectionsEngine.clear()
}This allows selections to persist during normal data updates while still supporting explicit clearing when clearSelections="yes" is set.
Event Serialization
Complex objects in events are serialized as JSON strings due to Codegen limitations:
// Event structure
onSearchColumn?: (event: {
target: Double;
column: string; // JSON serialized DataColumn
}) => void;Wrapper functions in SimpleGrid.ios.tsx automatically parse these JSON strings before passing to consumers.
Dictionary Completeness Pattern
When passing dictionaries from Objective-C++ to Swift, always include all fields even if empty:
// Always include all fields, use @"" for empty values
representation[@"urlLabel"] = !col.representation.urlLabel.empty()
? RCTNSStringFromString(col.representation.urlLabel)
: @"";This ensures Swift's JSONDecoder receives all expected fields, preventing missing data in decoded structs.
Contributing
See the contributing guide to learn how to contribute to the repository and the development workflow.
License
MIT
