npm package: @qlik/react-native-simple-grid
Description: A react native straight table component designed for performance
Author: Qlik
Latest version: 1.4.1

# @qlik-trial/react-native-simple-grid

A straight table component designed for performance

## Installation

```sh
npm install @qlik-trial/react-native-simple-grid
```

## Architecture

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):
  ```objc
  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 `@objcMembers` to expose all properties to Objective-C++:
  ```swift
  @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
      }
    }
  }
  ```
- `@objcMembers` on the class makes individual `@objc` annotations redundant
- Uses `JSONSerialization` + `JSONDecoder` to convert `NSDictionary` → 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:**
```objectivec++
#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 `@ReactProp` annotations - compatible with both Paper and Fabric
- Each prop method is called individually when that prop changes:
  ```java
  @ReactProp(name = "rows")
  public void setRows(View view, @Nullable ReadableMap source) {
    // Process rows prop
  }
  ```
- Handles `rowsJSON` string format (matching iOS Codegen pattern):
  ```java
  // 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 `RecyclerView` for performance
- `DataProvider` manages columns and rows
- Implements wipe-on-column-change strategy (same as iOS):
  ```java
  if(tableView.dataProvider.dataColumns.size() != dataColumns.size()) {
    tableView.resetTable();  // Wipe state on column count change
  }
  ```

#### 4. **Rendering**
- Uses Android's `RecyclerView` with custom `ViewHolder` pattern
- 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:
```java
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:
```java
@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):
```java
@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:

1. `cols` prop updates with new table's columns → `cols` didSet fires
2. Old `dataRows` still present from previous table
3. Attempting to render with mismatched columns/rows → **crash**

**Solution**: Wipe all state when column count changes:

```swift
// 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:

```tsx
<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:

```swift
// 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:

```typescript
// 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:

```objc++
// 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](CONTRIBUTING.md) to learn how to contribute to the repository and the development workflow.

## License

MIT