Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

useSafeAreaInsets emits too many results during device rotation #172

Open
mym0404 opened this issue Jan 26, 2021 · 10 comments
Open

useSafeAreaInsets emits too many results during device rotation #172

mym0404 opened this issue Jan 26, 2021 · 10 comments
Labels
help wanted Extra attention is needed

Comments

@mym0404
Copy link

mym0404 commented Jan 26, 2021

Because of the device performance issue, the repeated emission of the same insets or useless emission from useSafeAreaInsets should be skipped.

When I rotated my device with iPhone X, the console printed out like the following.

AS-IS

[Tue Jan 26 2021 11:40:03.498]  LOG      openFullScreen
[Tue Jan 26 2021 11:40:03.512]  LOG      {"bottom": 34, "left": 0, "right": 0, "top": 44}
[Tue Jan 26 2021 11:40:04.220]  LOG      {"bottom": 21, "left": 44, "right": 0, "top": 0}
[Tue Jan 26 2021 11:40:04.267]  LOG      {"bottom": 21, "left": 44, "right": 0, "top": 0}
[Tue Jan 26 2021 11:40:04.644]  LOG      {"bottom": 21, "left": 44, "right": 0, "top": 0}
[Tue Jan 26 2021 11:40:04.930]  LOG      {"bottom": 0, "left": 0, "right": 0, "top": 0}
[Tue Jan 26 2021 11:40:05.251]  LOG      {"bottom": 0, "left": 44, "right": 0, "top": 0}
[Tue Jan 26 2021 11:40:05.558]  LOG      {"bottom": 21, "left": 44, "right": 44, "top": 0}
[Tue Jan 26 2021 11:40:05.827]  LOG      {"bottom": 21, "left": 44, "right": 44, "top": 0}
[Tue Jan 26 2021 11:40:08.221]  LOG      closeFullScreen
[Tue Jan 26 2021 11:40:08.325]  LOG      {"bottom": 21, "left": 44, "right": 44, "top": 0}
[Tue Jan 26 2021 11:40:08.491]  LOG      {"bottom": 0, "left": 0, "right": 0, "top": 44}
[Tue Jan 26 2021 11:40:08.793]  LOG      {"bottom": 0, "left": 0, "right": 0, "top": 44}
[Tue Jan 26 2021 11:40:09.234]  LOG      {"bottom": 0, "left": 0, "right": 0, "top": 44}
[Tue Jan 26 2021 11:40:09.394]  LOG      {"bottom": 0, "left": 0, "right": 0, "top": 0}
[Tue Jan 26 2021 11:40:09.677]  LOG      {"bottom": 0, "left": 0, "right": 0, "top": 44}
[Tue Jan 26 2021 11:40:09.956]  LOG      {"bottom": 34, "left": 0, "right": 0, "top": 44}
[Tue Jan 26 2021 11:40:10.364]  LOG      {"bottom": 34, "left": 0, "right": 0, "top": 44}

The 6 emissions of the processing are useless and cause performance issues because every layout of components is re-calculated with each inset values.

TO-BE

[Tue Jan 26 2021 11:40:03.498]  LOG      openFullScreen
[Tue Jan 26 2021 11:40:03.512]  LOG      {"bottom": 34, "left": 0, "right": 0, "top": 44}
[Tue Jan 26 2021 11:40:05.827]  LOG      {"bottom": 21, "left": 44, "right": 44, "top": 0}
[Tue Jan 26 2021 11:40:08.221]  LOG      closeFullScreen
[Tue Jan 26 2021 11:40:08.325]  LOG      {"bottom": 21, "left": 44, "right": 44, "top": 0}
[Tue Jan 26 2021 11:40:10.364]  LOG      {"bottom": 34, "left": 0, "right": 0, "top": 44}
@mym0404 mym0404 changed the title useSafeAreaInsets emit too many results when iOS rotation useSafeAreaInsets emits too many results when iOS rotation Jan 26, 2021
@mym0404 mym0404 changed the title useSafeAreaInsets emits too many results when iOS rotation useSafeAreaInsets emits too many results when rotation Jan 26, 2021
@mym0404 mym0404 changed the title useSafeAreaInsets emits too many results when rotation useSafeAreaInsets emits too many results during device rotation Jan 26, 2021
@mym0404
Copy link
Author

mym0404 commented Jan 26, 2021

Android Galaxy S8

[Tue Jan 26 2021 11:50:31.245]  LOG      openFullScreen
[Tue Jan 26 2021 11:50:31.766]  LOG      {"bottom": 0, "left": 0, "right": 0, "top": 24}
[Tue Jan 26 2021 11:50:32.763]  LOG      {"bottom": 0, "left": 0, "right": 0, "top": 24}
[Tue Jan 26 2021 11:50:33.890]  LOG      {"bottom": 0, "left": 0, "right": 0, "top": 0}
[Tue Jan 26 2021 11:50:38.119]  LOG      closeFullScreen
[Tue Jan 26 2021 11:50:38.215]  LOG      {"bottom": 0, "left": 0, "right": 0, "top": 0}
[Tue Jan 26 2021 11:50:39.119]  LOG      {"bottom": 0, "left": 0, "right": 0, "top": 0}
[Tue Jan 26 2021 11:50:39.678]  LOG      {"bottom": 0, "left": 0, "right": 0, "top": 24}

@janicduplessis
Copy link
Collaborator

janicduplessis commented Jan 27, 2021

Hey! Where did you log this from? There is code that is supposed to prevent extra renders if the value did not change. Maybe something else could cause the extra logs? Could you try logging here https://github.com/th3rdwave/react-native-safe-area-context/blob/master/src/SafeAreaContext.tsx#L69?

Could also be caused by nested safe area views, maybe post a simplified version of how you are using the library.

@mym0404
Copy link
Author

mym0404 commented Jan 27, 2021

@janicduplessis sure I will check it

@denotter
Copy link

denotter commented Apr 1, 2021

Having the same issue here. Did anyone find a solution yet?

1 similar comment
@dohooo
Copy link

dohooo commented Sep 29, 2021

Having the same issue here. Did anyone find a solution yet?

@mym0404
Copy link
Author

mym0404 commented Oct 4, 2021

In my case, I just made native module for me.
There are several files and verbose codes but If you need you can use it.
These codes are written with Swift Native Module.
If you need to learn how to use Swift for Native Module in RN, look my sample project, documentation.

This module reduces some ignorable event emission but not 100%

SafeArea.swift

@objc(SafeAreaModule)
class SafeAreaModule: RCTEventEmitter{
  static var shared: SafeAreaModule? = nil
  static func viewSafeAreaInsetsDidChange(){
    guard let shared = SafeAreaModule.shared else { return }
    guard shared.getWindow() != nil else { return }
    shared.sendEvent(shared.getSafeAreaInsets())
  }
  
  // MARK: - RCTEventEmitter
  override func supportedEvents() -> [String]! {
    return ["viewSafeAreaInsetsDidChange"]
  }
  
  private func sendEvent(_ insets: Dictionary<String, Int>){
    guard bridge != nil else { return }
    self.sendEvent(withName: "viewSafeAreaInsetsDidChange", body: insets)
  }
  
  @objc
  override func constantsToExport() -> [AnyHashable: Any]!{
    return [:]
  }
  
  @objc
  override static func requiresMainQueueSetup() -> Bool {
    return true
  }
  
  override init() {
    super.init()
    SafeAreaModule.shared = self
  }
  
  @objc func getSafeAreaInsets(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock){
    DispatchQueue.main.async { [weak self] in
      guard let self = self else {
        reject("self not found", "self not found", nil)
        return
      }
      resolve(self.getSafeAreaInsets())
    }
  }
  
  private func getSafeAreaInsets() -> Dictionary<String, Int>{
    var ret = ["top": 0, "bottom": 0, "left": 0, "right": 0]
    let window = getWindow()
    
    if let insets = window?.safeAreaInsets{
      ret["top"] = Int(insets.top)
      ret["bottom"] = Int(insets.bottom)
      ret["left"] = Int(insets.left)
      ret["right"] = Int(insets.right)
    }
    
    return ret
  }
  
  private func getWindow() -> UIWindow?{
    if #available(iOS 13.0, *) {
      return UIApplication.shared.windows[0]
    }else{
      return UIApplication.shared.keyWindow
    }
  }
}

MainViewController.swift

import UIKit

class MainViewController : UIViewController{}

// MARK: -Lifecycle
extension MainViewController{
  override func viewDidLoad() {
    super.viewDidLoad()
  }
  
  override func viewSafeAreaInsetsDidChange() {
    SafeAreaModule.viewSafeAreaInsetsDidChange()
  }
  override func viewDidLayoutSubviews() {
  }
}

Appdelegate.m

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
#if DEBUG && TARGET_OS_SIMULATOR
  InitializeFlipper(application);
#endif
  ...
  
  RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions];
  
  NSDictionary *appProperties = [RNFBMessagingModule addCustomPropsToUserProps:nil withLaunchOptions:launchOptions];
  
  RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge
                                                   moduleName:@"MathKing"
                                            initialProperties:appProperties];
  
  rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:1.0f blue:1.0f alpha:1];
  
  self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
  UIViewController *rootViewController = [MainViewController new];
  rootViewController.view = rootView;
  self.window.rootViewController = rootViewController;
  [self.window makeKeyAndVisible];
  
  return YES;
}

Modules.m

@interface RCT_EXTERN_MODULE(SafeAreaModule, RCTEventEmitter)
RCT_EXTERN_METHOD(
                  getSafeAreaInsets: (RCTPromiseResolveBlock)resolve
                  reject: (RCTPromiseRejectBlock)reject
                  )
@end

SafeAreaModuleIOS.ts

import { NativeEventEmitter, NativeModules } from 'react-native';

import { EdgeInsets } from 'react-native-safe-area-context';

const COMPONENT_NAME = 'SafeAreaModule';

const NativeSafeAreaModule = NativeModules[COMPONENT_NAME];
const EventEmitter = new NativeEventEmitter(NativeSafeAreaModule);

type Listener = (insets: EdgeInsets) => void;
class SafeAreaModuleIOS {
  private subscription;
  private listeners: Listener[] = [];

  constructor() {
    this.subscription = EventEmitter.addListener('viewSafeAreaInsetsDidChange', (event: EdgeInsets) => {
      this.listeners.forEach((listener: Listener) => {
        listener(event);
      });
    });
  }

  addListener(listener: Listener) {
    this.listeners.push(listener);
  }

  removeListener(listener: Listener) {
    const idx = this.listeners.indexOf(listener);
    if (idx === -1) {
      return;
    }
    this.listeners.splice(idx, 1);
  }

  async getSafeAreaInsets(): Promise<EdgeInsets> {
    return NativeSafeAreaModule.getSafeAreaInsets();
  }
}

export default new SafeAreaModuleIOS();

useSafeAreaInsets.ios.ts

import { useCallback, useEffect, useState } from 'react';

import { EdgeInsets } from 'react-native-safe-area-context';
import SafeAreaModuleIOS from './SafeAreaModuleIOS';
import { SentryWrapper } from '../log/SentryWrapper';
import { useMount } from '../../hooks/useMount';

export default function useSafeAreaInsets() {
  const [insets, setInsets] = useState<EdgeInsets>({ top: 0, bottom: 0, left: 0, right: 0 });
  const listener = useCallback((insets: EdgeInsets) => {
    setInsets(insets);
  }, []);

  useMount(() => {
    SafeAreaModuleIOS.getSafeAreaInsets()
      .then(setInsets)
      .catch((e) => {
        SentryWrapper.throw('safe area not found', e);
      });
  });

  useEffect(() => {
    SafeAreaModuleIOS.addListener(listener);
    return () => {
      SafeAreaModuleIOS.removeListener(listener);
    };
  }, [listener]);

  return insets;
}

@zbranevichfinstek
Copy link

Does anyone have any other ideas on how to avoid this?

@jacobp100 jacobp100 added the help wanted Extra attention is needed label Jan 19, 2023
@skam22
Copy link

skam22 commented Mar 14, 2023

@zbranevichfinstek

a custom debounced useSafeAreaInsets hook works nicely:

import {useEffect, useState} from 'react';
import {useWindowDimensions} from 'react-native';
import {
  EdgeInsets,
  initialWindowMetrics,
  useSafeAreaInsets,
} from 'react-native-safe-area-context';

const _debounceDelay = 200; // 200ms

export const useDebouncedSafeAreaInsets = () => {
  const _insets = useSafeAreaInsets();
  const [insets, setInsets] = useState<EdgeInsets>(_insets);

  useEffect(() => {
    let timer: number | null = null;

    timer = setTimeout(() => {
      setInsets(_insets);
    }, _debounceDelay);

    return () => {
      if (timer) {
        clearTimeout(timer);
        timer = null;
      }
    };
  }, [_insets]);

  return {...insets};
};

@swey
Copy link

swey commented Sep 27, 2024

a custom debounced useSafeAreaInsets hook works nicely:

Thank you for the inspiration. But didn't work for me because the nested useSafeAreaInsets() still causes the component to re-render on every change. I ended up creating a pseudo component which can be nested on top level.

import { EventEmitter } from 'events';
import React, { Fragment, useEffect, useState } from 'react';
import { EdgeInsets, useSafeAreaInsets } from 'react-native-safe-area-context';

const eventEmitter = new EventEmitter();

export function DebouncedSafeAreaInsetsProvider({ debounceDelay }: { debounceDelay?: number }) {
	const insets = useSafeAreaInsets();

	useEffect(() => {
		const timeout: NodeJS.Timeout = setTimeout(() => {
			eventEmitter.emit('delayedInsets', insets);
		}, debounceDelay ?? 200);

		return () => {
			if (timeout) {
				clearTimeout(timeout);
			}
		};
	}, [insets]);

	return React.createElement(Fragment);
}

export function useDebouncedSafeAreaInsets(): EdgeInsets {
	const [insets, setInsets] = useState<EdgeInsets>({ top: 0, bottom: 0, left: 0, right: 0 });

	useEffect(() => {
		const listener = (delayedInsets: EdgeInsets) => {
			setInsets(delayedInsets);
		};

		eventEmitter.on('delayedInsets', listener);

		return () => {
			eventEmitter.removeListener('delayedInsets', listener);
		};
	}, []);

	return insets;
}

Feel free to further optimize it :)

@tcK1
Copy link

tcK1 commented Oct 1, 2024

Not sure if this helps the issue, but here are the logs added to /src/SafeAreaContext.tsx

When rotation there seems to trigger a "intermediate" stage with only the bottom inset.

 LOG  [setFrame] curFrame {"height": 956, "width": 440, "x": 0, "y": 0}
 LOG  [setInsets] nextInsets {"bottom": 21, "left": 62, "right": 0, "top": 0}
 LOG  [setFrame] nextFrame {"height": 956, "width": 440, "x": 258, "y": -258}
 LOG  [setInsets] nextInsets {"bottom": 21, "left": 0, "right": 0, "top": 0}
 LOG  [setInsets] nextInsets {"bottom": 21, "left": 62, "right": 62, "top": 0}
 LOG  [setFrame] nextFrame {"height": 440, "width": 956, "x": 0, "y": 0}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
help wanted Extra attention is needed
Projects
None yet
Development

No branches or pull requests

9 participants