Lesson 12-React Native Advanced Features

Performance Optimization

Virtual DOM and Diff Algorithm Optimization

Virtual DOM Principles and Optimization Strategies

// Optimization strategy example: Reducing unnecessary re-renders
import React, { memo, useCallback } from 'react';

// Use memo to prevent unnecessary re-renders
const ExpensiveComponent = memo(({ data, onClick }) => {
  // Component implementation
});

// Use useCallback to cache function references
const ParentComponent = () => {
  const handleClick = useCallback(() => {
    // Handle click event
  }, []); // Empty dependency array ensures function reference stability

  return <ExpensiveComponent data={data} onClick={handleClick} />;
};

Manual Control of Component Updates

// Use shouldComponentUpdate (class components) or React.memo (function components) for fine-grained control
class MyComponent extends React.Component {
  shouldComponentUpdate(nextProps, nextState) {
    // Update only when specific props or state change
    return nextProps.id !== this.props.id || nextState.value !== this.state.value;
  }
  
  render() {
    // Component implementation
  }
}

FlatList and SectionList Performance Optimization

Basic Optimization Configuration

import React from 'react';
import { FlatList, View, Text } from 'react-native';

const OptimizedFlatList = () => {
  // Use keyExtractor to provide stable keys
  // Use getItemLayout to optimize fixed-height lists
  // Use initialNumToRender to control initial render count
  // Use windowSize to control render window size
  // Use maxToRenderPerBatch to control batch rendering count
  // Use updateCellsBatchingPeriod to control batch update interval

  return (
    <FlatList
      data={data}
      keyExtractor={(item) => item.id.toString()} // Stable key
      getItemLayout={(data, index) => (
        { length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index, index }
      )} // Fixed-height list optimization
      initialNumToRender={10} // Initial render count
      windowSize={5} // Render window size (items rendered outside the screen)
      maxToRenderPerBatch={5} // Maximum items rendered per batch
      updateCellsBatchingPeriod={50} // Batch update interval (ms)
      renderItem={({ item }) => <ListItem item={item} />}
    />
  );
};

Advanced Optimization Techniques

// Use React.memo to optimize list items
const ListItem = memo(({ item }) => {
  // List item implementation
});

// Use separate data sources (e.g., FlashList) instead of FlatList
// Use virtualization techniques for complex list items
// Avoid inline functions and objects in list items
// Use shouldComponentUpdate or React.memo to optimize list item rendering

Memory Leak Detection and Analysis

Common Causes of Memory Leaks

// Common memory leak scenarios
class LeakExample extends React.Component {
  intervalId = null;
  
  componentDidMount() {
    // Forgetting to clear the timer
    this.intervalId = setInterval(() => {
      // Timer task
    }, 1000);
  }
  
  // Missing componentWillUnmount cleanup
}

// Closure reference causing a leak
function createLeak() {
  const bigData = new Array(1000000).fill('data');
  
  return () => {
    // Closure retains reference to bigData
    console.log('Closure with memory leak');
  };
}

Detection and Analysis Tools

// Using React Native Debugger to detect memory leaks
// 1. Install React Native Debugger
// 2. Connect the app
// 3. Use the Memory tab for heap snapshot comparison

// Using Chrome DevTools Memory panel
// 1. Enable remote debugging when running the app
// 2. Use the Heap Snapshot feature
// 3. Compare heap states at different points in time

// Using performance monitoring tools
// 1. Reactotron
// 2. Flipper
// 3. Firebase Performance Monitoring

Image Loading and Caching Optimization (react-native-fast-image)

Basic Optimization Configuration

import FastImage from 'react-native-fast-image';

const OptimizedImage = () => (
  <FastImage
    style={{ width: 200, height: 200 }}
    source={{
      uri: 'https://example.com/image.jpg',
      priority: FastImage.priority.high, // Loading priority
      cache: FastImage.cacheControl.immutable, // Cache strategy
    }}
    resizeMode={FastImage.resizeMode.contain}
  />
);

Advanced Cache Strategies

// Custom cache control
const CustomCacheImage = () => (
  <FastImage
    source={{
      uri: 'https://example.com/image.jpg',
      priority: FastImage.priority.normal,
      cache: FastImage.cacheControl.web, // Use web cache strategy
    }}
  />
);

// Preload images
import { useEffect } from 'react';

const PreloadImages = () => {
  useEffect(() => {
    const preload = async () => {
      await FastImage.preload([
        { uri: 'https://example.com/image1.jpg' },
        { uri: 'https://example.com/image2.jpg' },
      ]);
    };
    
    preload();
  }, []);
  
  return null;
};

Animation Performance Optimization (useNativeDriver)

Basic Animation Optimization

import React, { useRef } from 'react';
import { Animated, View, Easing } from 'react-native';

const OptimizedAnimation = () => {
  const fadeAnim = useRef(new Animated.Value(0)).current;

  const fadeIn = () => {
    // Use useNativeDriver: true to move animation to native thread
    Animated.timing(fadeAnim, {
      toValue: 1,
      duration: 1000,
      easing: Easing.ease,
      useNativeDriver: true, // Key optimization point
    }).start();
  };

  return (
    <View>
      <Animated.View
        style={{
          opacity: fadeAnim,
          transform: [{ scale: fadeAnim }], // Note: Not all properties support native driver
        }}
      />
      <Button title="Fade In" onPress={fadeIn} />
    </View>
  );
};

Properties Supporting Native Driver

// Style properties supporting useNativeDriver
const supportedStyles = [
  'opacity', // Opacity
  'transform', // Transform (translate, rotate, scale)
  'backgroundColor', // Background color (Android)
  'width', // Width (iOS)
  'height', // Height (iOS)
];

// Properties not supporting native driver
const unsupportedStyles = [
  'color', // Text color
  'borderRadius', // Border radius
  'shadowOpacity', // Shadow opacity
  'textShadowOffset', // Text shadow offset
];

Native Module Development

Creating Native Modules (Android, iOS)

Android Native Module

// ToastModule.java
package com.example.app;

import android.widget.Toast;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;

public class ToastModule extends ReactContextBaseJavaModule {
    private final ReactApplicationContext reactContext;

    public ToastModule(ReactApplicationContext reactContext) {
        super(reactContext);
        this.reactContext = reactContext;
    }

    @Override
    public String getName() {
        return "ToastModule";
    }

    @ReactMethod
    public void show(String message, int duration) {
        Toast.makeText(reactContext, message, duration).show();
    }
}

iOS Native Module

// ToastModule.m
#import <React/RCTBridgeModule.h>

@interface RCT_EXTERN_MODULE(ToastModule, NSObject)

RCT_EXTERN_METHOD(show:(NSString *)message duration:(NSInteger)duration)

@end

Calling Native APIs (Camera, Sensor)

Android Camera API Call

// CameraModule.java
@ReactMethod
public void takePicture(Promise promise) {
    try {
        // Get camera instance
        Camera camera = Camera.open();
        
        // Set camera parameters
        Camera.Parameters parameters = camera.getParameters();
        parameters.setPictureFormat(ImageFormat.JPEG);
        camera.setParameters(parameters);
        
        // Take picture
        camera.takePicture(null, null, (data, camera) -> {
            // Convert image data to Base64
            String base64Image = Base64.encodeToString(data, Base64.DEFAULT);
            promise.resolve(base64Image);
            
            // Release camera resources
            camera.release();
        });
    } catch (Exception e) {
        promise.reject("CAMERA_ERROR", e);
    }
}

iOS Sensor API Call

// SensorModule.m
#import <CoreMotion/CoreMotion.h>

@implementation SensorModule {
    CMMotionManager *_motionManager;
}

RCT_EXPORT_MODULE();

- (instancetype)init {
    self = [super init];
    if (self) {
        _motionManager = [[CMMotionManager alloc] init];
    }
    return self;
}

RCT_EXPORT_METHOD(startAccelerometerUpdates:(RCTResponseSenderBlock)callback) {
    if (!_motionManager.accelerometerAvailable) {
        callback(@[@"Accelerometer not available"]);
        return;
    }
    
    [_motionManager startAccelerometerUpdatesToQueue:[NSOperationQueue mainQueue]
                                         withHandler:^(CMAccelerometerData *accelerometerData, NSError *error) {
        if (error) {
            callback(@[error.localizedDescription]);
            return;
        }
        
        CMAcceleration acceleration = accelerometerData.acceleration;
        NSDictionary *result = @{
            @"x": @(acceleration.x),
            @"y": @(acceleration.y),
            @"z": @(acceleration.z)
        };
        callback(@[result]);
    }];
}

@end

Native Component Encapsulation (Custom Native UI Components)

Android Custom View

// CustomViewManager.java
package com.example.app;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.view.View;

import com.facebook.react.uimanager.SimpleViewManager;
import com.facebook.react.uimanager.ThemedReactContext;

public class CustomViewManager extends SimpleViewManager<View> {
    @Override
    public String getName() {
        return "CustomView";
    }

    @Override
    protected View createViewInstance(ThemedReactContext reactContext) {
        return new CustomDrawableView(reactContext);
    }

    public static class CustomDrawableView extends View {
        private final Paint paint = new Paint();

        public CustomDrawableView(Context context) {
            super(context);
            paint.setColor(Color.RED);
            paint.setStyle(Paint.Style.FILL);
        }

        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
            canvas.drawCircle(100, 100, 50, paint);
        }
    }
}

iOS Custom UIView

// CustomViewManager.m
#import <React/RCTViewManager.h>

@interface CustomViewManager : RCTViewManager
@end

@implementation CustomViewManager

RCT_EXPORT_MODULE()

- (UIView *)view {
    return [[CustomDrawableView alloc] init];
}

@end

// CustomDrawableView.m
#import "CustomDrawableView.h"

@implementation CustomDrawableView {
    CAShapeLayer *_circleLayer;
}

- (instancetype)init {
    self = [super init];
    if (self) {
        _circleLayer = [CAShapeLayer layer];
        _circleLayer.fillColor = [UIColor redColor].CGColor;
        
        UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0, 0, 100, 100)];
        _circleLayer.path = path.CGPath;
        
        [self.layer addSublayer:_circleLayer];
    }
    return self;
}

- (void)layoutSubviews {
    [super layoutSubviews];
    _circleLayer.frame = CGRectMake(50, 50, 100, 100);
}

@end

Using TurboModules

Android TurboModules

// ToastModuleSpec.java
package com.example.app;

import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.turbomodule.core.interfaces.TurboModule;

public interface ToastModuleSpec extends TurboModule {
    void show(String message, int duration, Promise promise);
}

// ToastModule.java
package com.example.app;

import android.widget.Toast;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.module.annotations.ReactModule;

@ReactModule(name = ToastModuleSpec.NAME)
public class ToastModule extends NativeToastModuleSpec implements ToastModuleSpec {
    public ToastModule(ReactApplicationContext reactContext) {
        super(reactContext);
    }

    @Override
    public void show(String message, int duration, Promise promise) {
        try {
            Toast.makeText(getReactApplicationContext(), message, duration).show();
            promise.resolve(null);
        } catch (Exception e) {
            promise.reject("TOAST_ERROR", e);
        }
    }
}

iOS TurboModules

// ToastModule.h
#import <React/RCTTurboModule.h>

@interface ToastModule : NSObject <RCTTurboModule>
- (void)show:(NSString *)message duration:(NSInteger)duration resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject;
@end

// ToastModule.mm
#import "ToastModule.h"
#import <React/RCTConvert.h>

@implementation ToastModule

RCT_EXPORT_MODULE()

- (void)show:(NSString *)message duration:(NSInteger)duration resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
    dispatch_async(dispatch_get_main_queue(), ^{
        UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Toast"
                                                                       message:message
                                                                preferredStyle:UIAlertControllerStyleAlert];
        UIAlertAction *ok = [UIAlertAction actionWithTitle:@"OK"
                                                     style:UIAlertActionStyleDefault
                                                   handler:nil];
        [alert addAction:ok];
        [[UIApplication sharedApplication].keyWindow.rootViewController presentViewController:alert
                                                                                     animated:YES
                                                                                   completion:nil];
        resolve(nil);
    });
}

@end

Performance Optimization for Native Modules

Reducing Bridge Calls

// Bad practice: Frequent bridge calls
const badExample = () => {
  dataArray.forEach(item => {
    NativeModules.ToastModule.show(item.message); // Crosses bridge each loop
  });
};

// Good practice: Batch processing
const goodExample = () => {
  const messages = dataArray.map(item => item.message);
  NativeModules.ToastModule.showBatch(messages); // Single bridge call
};

Using Batch Processing and Caching

// Android batch processing example
@ReactMethod
public void showBatch(ReadableArray messages) {
    for (int i = 0; i < messages.size(); i++) {
        String message = messages.getString(i);
        Toast.makeText(reactContext, message, Toast.LENGTH_SHORT).show();
    }
}
// iOS batch processing example
RCT_EXPORT_METHOD(showBatch:(NSArray *)messages) {
    for (NSString *message in messages) {
        UIAlertController *alert = [UIAlertController 
            alertControllerWithTitle:@"Batch Toast"
                             message:message
                      preferredStyle:UIAlertControllerStyleAlert];
        UIAlertAction *ok = [UIAlertAction 
            actionWithTitle:@"OK" 
                      style:UIAlertActionStyleDefault 
                    handler:nil];
        [alert addAction:ok];
        [[UIApplication sharedApplication].keyWindow.rootViewController 
            presentViewController:alert 
                         animated:YES 
                       completion:nil];
    }
}

Animations and Gestures

Advanced Usage of Animated API (Interpolation, Combined Animations)

Interpolation Animation

import React, { useRef } from 'react';
import { Animated, View, Easing } from 'react-native';

const InterpolationExample = () => {
  const animValue = useRef(new Animated.Value(0)).current;

  const startAnimation = () => {
    Animated.timing(animValue, {
      toValue: 1,
      duration: 1000,
      easing: Easing.bezier(0.25, 0.1, 0.25, 1),
      useNativeDriver: true,
    }).start();
  };

  // Use interpolation to map 0-1 values to different ranges
  const rotateInterpolate = animValue.interpolate({
    inputRange: [0, 1],
    outputRange: ['0deg', '360deg'],
  });

  const scaleInterpolate = animValue.interpolate({
    inputRange: [0, 0.5, 1],
    outputRange: [1, 1.5, 1],
  });

  return (
    <View>
      <Animated.View
        style={{
          transform: [
            { rotate: rotateInterpolate },
            { scale: scaleInterpolate },
          ],
        }}
      />
      <Button title="Start Animation" onPress={startAnimation} />
    </View>
  );
};

Combined Animation

import React, { useRef } from 'react';
import { Animated, View, Easing } from 'react-native';

const CombinedAnimation = () => {
  const fadeAnim = useRef(new Animated.Value(0)).current;
  const translateXAnim = useRef(new Animated.Value(0)).current;

  const startCombinedAnimation = () => {
    Animated.parallel([
      // Fade-in animation
      Animated.timing(fadeAnim, {
        toValue: 1,
        duration: 1000,
        useNativeDriver: true,
      }),
      // Translate animation
      Animated.timing(translateXAnim, {
        toValue: 100,
        duration: 1500,
        easing: Easing.elastic(1),
        useNativeDriver: true,
      }),
    ]).start();
  };

  return (
    <View>
      <Animated.View
        style={{
          opacity: fadeAnim,
          transform: [{ translateX: translateXAnim }],
        }}
      />
      <Button title="Start Combined Animation" onPress={startCombinedAnimation} />
    </View>
  );
};

Gesture Handling (PanResponder, react-native-gesture-handler)

PanResponder Basic Implementation

import React, { useRef } from 'react';
import { View, PanResponder, Text } from 'react-native';

const PanResponderExample = () => {
  const position = useRef({ x: 0, y: 0 }).current;

  const panResponder = useRef(
    PanResponder.create({
      onStartShouldSetPanResponder: () => true,
      onMoveShouldSetPanResponder: () => true,
      onPanResponderGrant: () => {
        // Gesture start
      },
      onPanResponderMove: (evt, gestureState) => {
        position.x += gestureState.dx;
        position.y += gestureState.dy;
      },
      onPanResponderRelease: () => {
        // Gesture end
      },
    })
  ).current;

  return (
    <View
      {...panResponder.panHandlers}
      style={{
        width: 100,
        height: 100,
        backgroundColor: 'red',
        position: 'absolute',
        left: position.x,
        top: position.y,
      }}
    />
  );
};

Advanced Usage of react-native-gesture-handler

import React from 'react';
import { View, Text } from 'react-native';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import Animated, { useSharedValue, withSpring } from 'react-native-reanimated';

const GestureHandlerExample = () => {
  const translateX = useSharedValue(0);
  const translateY = useSharedValue(0);

  const panGesture = Gesture.Pan()
    .onUpdate((event) => {
      translateX.value = event.translationX;
      translateY.value = event.translationY;
    })
    .onEnd(() => {
      // Add spring animation
      translateX.value = withSpring(0);
      translateY.value = withSpring(0);
    });

  return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
      <GestureDetector gesture={panGesture}>
        <Animated.View
          style={{
            width: 100,
            height: 100,
            backgroundColor: 'blue',
            transform: [
              { translateX: translateX.value },
              { translateY: translateY.value },
            ],
          }}
        />
      </GestureDetector>
    </View>
  );
};

Physics-Based Animations and Spring Animations

Spring Animation Implementation

import React, { useRef } from 'react';
import { Animated, View, Button, Easing } from 'react-native';

const SpringAnimation = () => {
  const animValue = useRef(new Animated.Value(0)).current;

  const startSpringAnimation = () => {
    animValue.setValue(0); // Reset value
    Animated.spring(animValue, {
      toValue: 1,
      friction: 4, // Control spring oscillation
      tension: 30, // Control spring speed
      useNativeDriver: true,
    }).start();
  };

  return (
    <View>
      <Animated.View
        style={{
          transform: [{ scale: animValue }],
        }}
      />
      <Button title="Start Spring Animation" onPress={startSpringAnimation} />
    </View>
  );
};

Physics-Based Animation Effects

import React, { useRef } from 'react';
import { Animated, View, Button } from 'react-native';

const PhysicsAnimation = () => {
  const animValue = useRef(new Animated.Value(0)).current;

  const startPhysicsAnimation = () => {
    animValue.setValue(0);
    Animated.timing(animValue, {
      toValue: 1,
      duration: 1000,
      easing: (t) => t * t * (3 - 2 * t), // Custom easing function to simulate physics effect
      useNativeDriver: true,
    }).start();
  };

  return (
    <View>
      <Animated.View
        style={{
          opacity: animValue,
          transform: [
            {
              translateY: animValue.interpolate({
                inputRange: [0, 1],
                outputRange: [100, 0],
              }),
            },
          ],
        }}
      />
      <Button title="Start Physics Animation" onPress={startPhysicsAnimation} />
    </View>
  );
};

Interactive Animations (Drag, Scale)

Drag and Scale Combination

import React, { useRef } from 'react';
import { View, PanResponder, Animated } from 'react-native';

const DragAndScaleExample = () => {
  const pan = useRef(new Animated.ValueXY()).current;
  const scale = useRef(new Animated.Value(1)).current;

  const panResponder = useRef(
    PanResponder.create({
      onStartShouldSetPanResponder: () => true,
      onMoveShouldSetPanResponder: () => true,
      onPanResponderGrant: () => {
        // Fix current scale when starting drag
        pan.setOffset({ x: pan.x._value, y: pan.y._value });
        pan.setValue({ x: 0, y: 0 });
      },
      onPanResponderMove: (evt, gestureState) => {
        pan.x.setValue(gestureState.dx);
        pan.y.setValue(gestureState.dy);
      },
      onPanResponderRelease: () => {
        // Clear offset when drag ends
        pan.flattenOffset();
      },
    })
  ).current;

  const handleScale = (scaleValue) => {
    scale.setValue(scaleValue);
  };

  return (
    <View>
      <Animated.View
        {...panResponder.panHandlers}
        style={{
          transform: [
            { translateX: pan.x },
            { translateY: pan.y },
            { scale: scale },
          ],
        }}
      />
      {/* Scale control buttons */}
      <Button title="Scale +" onPress={() => handleScale(scale._value + 0.1)} />
      <Button title="Scale -" onPress={() => handleScale(Math.max(0.5, scale._value - 0.1))} />
    </View>
  );
};

Animation Performance Optimization

Using Native Driver

import React, { useRef } from 'react';
import { Animated, View, Easing } from 'react-native';

const NativeDriverExample = () => {
  const animValue = useRef(new Animated.Value(0)).current;

  const startAnimation = () => {
    // Key optimization: Use useNativeDriver
    Animated.timing(animValue, {
      toValue: 1,
      duration: 1000,
      easing: Easing.bezier(0.25, 0.1, 0.25, 1),
      useNativeDriver: true, // Move animation to native thread
    }).start();
  };

  return (
    <View>
      <Animated.View
        style={{
          opacity: animValue,
          transform: [{ rotate: animValue.interpolate({
            inputRange: [0, 1],
            outputRange: ['0deg', '360deg'],
          }) }],
        }}
      />
      <Button title="Start Optimized Animation" onPress={startAnimation} />
    </View>
  );
};

Avoiding Layout Jank

import React, { useRef, useState } from 'react';
import { Animated, View, Text, Button } from 'react-native';

const AvoidLayoutJank = () => {
  const animValue = useRef(new Animated.Value(0)).current;
  const [text, setText] = useState('Initial Text');

  // Bad practice: Changing layout properties during animation
  const badAnimation = () => {
    Animated.timing(animValue, {
      toValue: 1,
      duration: 1000,
      useNativeDriver: false, // Note: Intentionally using false to demonstrate issue
    }).start(() => {
      // Changing layout-related properties after animation (causes layout jank)
      setText('Updated Text');
    });
  };

  // Good practice: Separate animation and layout changes
  const goodAnimation = () => {
    Animated.timing(animValue, {
      toValue: 1,
      duration: 1000,
      useNativeDriver: true, // Use native driver
    }).start();
    
    // Change text separately after animation (no conflict with animation properties)
    setTimeout(() => {
      setText('Updated Text');
    }, 1000);
  };

  return (
    <View>
      <Animated.View
        style={{
          opacity: animValue,
          transform: [{ scale: animValue }],
        }}
      />
      <Button title="Bad Animation (Layout Jank)" onPress={badAnimation} />
      <Button title="Good Animation (No Layout Jank)" onPress={goodAnimation} />
    </View>
  );
};
Share your love