Skip to content

Commit

Permalink
New useWindowDimensions hook to replace most Dimensions usage
Browse files Browse the repository at this point in the history
Summary:
Automatically provides and subscribes to dimension updates - super easy usage:
```
function MyComponent(props: Props) {
  const {width, height, scale, fontScale} = useWindowDimensions();
  return <Text ...
};
```

Only window for now - it's what people want 99% of the time, so we'll just shovel out a pit of success for them...

There are still cases where `Dimensions` is needed outside of React component render functions, like in GraphQL variables, so we need to keep the existing module.

Reviewed By: zackargyle

Differential Revision: D16525189

fbshipit-source-id: 0a049fb3be8d92888a8a69e3898d337b93422a09
  • Loading branch information
sahrens authored and facebook-github-bot committed Jul 29, 2019
1 parent 62591ac commit 5ec382d
Show file tree
Hide file tree
Showing 6 changed files with 126 additions and 239 deletions.
145 changes: 2 additions & 143 deletions Libraries/FBReactNativeSpec/FBReactNativeSpec/FBReactNativeSpec.h
Original file line number Diff line number Diff line change
Expand Up @@ -687,107 +687,13 @@ namespace facebook {
} // namespace react
} // namespace facebook

namespace JS {
namespace NativeDeviceInfo {
struct DisplayMetricsIOS {

struct Builder {
struct Input {
RCTRequired<double> width;
RCTRequired<double> height;
RCTRequired<double> scale;
RCTRequired<double> fontScale;
};

/** Initialize with a set of values */
Builder(const Input i);
/** Initialize with an existing DisplayMetricsIOS */
Builder(DisplayMetricsIOS i);
/** Builds the object. Generally used only by the infrastructure. */
NSDictionary *buildUnsafeRawValue() const { return _factory(); };
private:
NSDictionary *(^_factory)(void);
};

static DisplayMetricsIOS fromUnsafeRawValue(NSDictionary *const v) { return {v}; }
NSDictionary *unsafeRawValue() const { return _v; }
private:
DisplayMetricsIOS(NSDictionary *const v) : _v(v) {}
NSDictionary *_v;
};
}
}

namespace JS {
namespace NativeDeviceInfo {
struct DisplayMetricsAndroid {

struct Builder {
struct Input {
RCTRequired<double> width;
RCTRequired<double> height;
RCTRequired<double> scale;
RCTRequired<double> fontScale;
RCTRequired<double> densityDpi;
};

/** Initialize with a set of values */
Builder(const Input i);
/** Initialize with an existing DisplayMetricsAndroid */
Builder(DisplayMetricsAndroid i);
/** Builds the object. Generally used only by the infrastructure. */
NSDictionary *buildUnsafeRawValue() const { return _factory(); };
private:
NSDictionary *(^_factory)(void);
};

static DisplayMetricsAndroid fromUnsafeRawValue(NSDictionary *const v) { return {v}; }
NSDictionary *unsafeRawValue() const { return _v; }
private:
DisplayMetricsAndroid(NSDictionary *const v) : _v(v) {}
NSDictionary *_v;
};
}
}

namespace JS {
namespace NativeDeviceInfo {
struct ConstantsDimensions {

struct Builder {
struct Input {
folly::Optional<JS::NativeDeviceInfo::DisplayMetricsIOS::Builder> window;
folly::Optional<JS::NativeDeviceInfo::DisplayMetricsIOS::Builder> screen;
folly::Optional<JS::NativeDeviceInfo::DisplayMetricsAndroid::Builder> windowPhysicalPixels;
folly::Optional<JS::NativeDeviceInfo::DisplayMetricsAndroid::Builder> screenPhysicalPixels;
};

/** Initialize with a set of values */
Builder(const Input i);
/** Initialize with an existing ConstantsDimensions */
Builder(ConstantsDimensions i);
/** Builds the object. Generally used only by the infrastructure. */
NSDictionary *buildUnsafeRawValue() const { return _factory(); };
private:
NSDictionary *(^_factory)(void);
};

static ConstantsDimensions fromUnsafeRawValue(NSDictionary *const v) { return {v}; }
NSDictionary *unsafeRawValue() const { return _v; }
private:
ConstantsDimensions(NSDictionary *const v) : _v(v) {}
NSDictionary *_v;
};
}
}

namespace JS {
namespace NativeDeviceInfo {
struct Constants {

struct Builder {
struct Input {
RCTRequired<JS::NativeDeviceInfo::ConstantsDimensions::Builder> Dimensions;
RCTRequired<id<NSObject>> Dimensions;
folly::Optional<bool> isIPhoneX_deprecated;
};

Expand Down Expand Up @@ -2520,57 +2426,10 @@ inline JS::NativeBlobModule::Constants::Builder::Builder(const Input i) : _facto
inline JS::NativeBlobModule::Constants::Builder::Builder(Constants i) : _factory(^{
return i.unsafeRawValue();
}) {}
inline JS::NativeDeviceInfo::DisplayMetricsIOS::Builder::Builder(const Input i) : _factory(^{
NSMutableDictionary *d = [NSMutableDictionary new];
auto width = i.width.get();
d[@"width"] = @(width);
auto height = i.height.get();
d[@"height"] = @(height);
auto scale = i.scale.get();
d[@"scale"] = @(scale);
auto fontScale = i.fontScale.get();
d[@"fontScale"] = @(fontScale);
return d;
}) {}
inline JS::NativeDeviceInfo::DisplayMetricsIOS::Builder::Builder(DisplayMetricsIOS i) : _factory(^{
return i.unsafeRawValue();
}) {}
inline JS::NativeDeviceInfo::DisplayMetricsAndroid::Builder::Builder(const Input i) : _factory(^{
NSMutableDictionary *d = [NSMutableDictionary new];
auto width = i.width.get();
d[@"width"] = @(width);
auto height = i.height.get();
d[@"height"] = @(height);
auto scale = i.scale.get();
d[@"scale"] = @(scale);
auto fontScale = i.fontScale.get();
d[@"fontScale"] = @(fontScale);
auto densityDpi = i.densityDpi.get();
d[@"densityDpi"] = @(densityDpi);
return d;
}) {}
inline JS::NativeDeviceInfo::DisplayMetricsAndroid::Builder::Builder(DisplayMetricsAndroid i) : _factory(^{
return i.unsafeRawValue();
}) {}
inline JS::NativeDeviceInfo::ConstantsDimensions::Builder::Builder(const Input i) : _factory(^{
NSMutableDictionary *d = [NSMutableDictionary new];
auto window = i.window;
d[@"window"] = window.hasValue() ? window.value().buildUnsafeRawValue() : nil;
auto screen = i.screen;
d[@"screen"] = screen.hasValue() ? screen.value().buildUnsafeRawValue() : nil;
auto windowPhysicalPixels = i.windowPhysicalPixels;
d[@"windowPhysicalPixels"] = windowPhysicalPixels.hasValue() ? windowPhysicalPixels.value().buildUnsafeRawValue() : nil;
auto screenPhysicalPixels = i.screenPhysicalPixels;
d[@"screenPhysicalPixels"] = screenPhysicalPixels.hasValue() ? screenPhysicalPixels.value().buildUnsafeRawValue() : nil;
return d;
}) {}
inline JS::NativeDeviceInfo::ConstantsDimensions::Builder::Builder(ConstantsDimensions i) : _factory(^{
return i.unsafeRawValue();
}) {}
inline JS::NativeDeviceInfo::Constants::Builder::Builder(const Input i) : _factory(^{
NSMutableDictionary *d = [NSMutableDictionary new];
auto Dimensions = i.Dimensions.get();
d[@"Dimensions"] = Dimensions.buildUnsafeRawValue();
d[@"Dimensions"] = Dimensions;
auto isIPhoneX_deprecated = i.isIPhoneX_deprecated;
d[@"isIPhoneX_deprecated"] = isIPhoneX_deprecated.hasValue() ? @((BOOL)isIPhoneX_deprecated.value()) : nil;
return d;
Expand Down
136 changes: 59 additions & 77 deletions Libraries/Utilities/Dimensions.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,90 +10,84 @@

'use strict';

const EventEmitter = require('../vendor/emitter/EventEmitter');
const Platform = require('./Platform');
const RCTDeviceEventEmitter = require('../EventEmitter/RCTDeviceEventEmitter');
import EventEmitter from '../vendor/emitter/EventEmitter';
import RCTDeviceEventEmitter from '../EventEmitter/RCTDeviceEventEmitter';
import NativeDeviceInfo, {
type DisplayMetrics,
type DimensionsPayload,
} from './NativeDeviceInfo';
import invariant from 'invariant';

import NativeDeviceInfo from './NativeDeviceInfo';

const invariant = require('invariant');
type DimensionsValue = {window?: DisplayMetrics, screen?: DisplayMetrics};

const eventEmitter = new EventEmitter();
let dimensionsInitialized = false;
const dimensions = {};
let dimensions: DimensionsValue;

class Dimensions {
/**
* NOTE: `useWindowDimensions` is the preffered API for React components.
*
* Initial dimensions are set before `runApplication` is called so they should
* be available before any other require's are run, but may be updated later.
*
* Note: Although dimensions are available immediately, they may change (e.g
* due to device rotation) so any rendering logic or styles that depend on
* these constants should try to call this function on every render, rather
* than caching the value (for example, using inline styles rather than
* setting a value in a `StyleSheet`).
*
* Example: `const {height, width} = Dimensions.get('window');`
*
* @param {string} dim Name of dimension as defined when calling `set`.
* @returns {Object?} Value for the dimension.
*/
static get(dim: string): Object {
invariant(dimensions[dim], 'No dimension set for key ' + dim);
return dimensions[dim];
}

/**
* This should only be called from native code by sending the
* didUpdateDimensions event.
*
* @param {object} dims Simple string-keyed object of dimensions to set
*/
static set(dims: {[key: string]: any}): void {
static set(dims: $ReadOnly<{[key: string]: any}>): void {
// We calculate the window dimensions in JS so that we don't encounter loss of
// precision in transferring the dimensions (which could be non-integers) over
// the bridge.
if (dims && dims.windowPhysicalPixels) {
// parse/stringify => Clone hack
dims = JSON.parse(JSON.stringify(dims));

const windowPhysicalPixels = dims.windowPhysicalPixels;
dims.window = {
let {screen, window} = dims;
const {windowPhysicalPixels} = dims;
if (windowPhysicalPixels) {
window = {
width: windowPhysicalPixels.width / windowPhysicalPixels.scale,
height: windowPhysicalPixels.height / windowPhysicalPixels.scale,
scale: windowPhysicalPixels.scale,
fontScale: windowPhysicalPixels.fontScale,
};
if (Platform.OS === 'android') {
// Screen and window dimensions are different on android
const screenPhysicalPixels = dims.screenPhysicalPixels;
dims.screen = {
width: screenPhysicalPixels.width / screenPhysicalPixels.scale,
height: screenPhysicalPixels.height / screenPhysicalPixels.scale,
scale: screenPhysicalPixels.scale,
fontScale: screenPhysicalPixels.fontScale,
};

// delete so no callers rely on this existing
delete dims.screenPhysicalPixels;
} else {
dims.screen = dims.window;
}
// delete so no callers rely on this existing
delete dims.windowPhysicalPixels;
}
const {screenPhysicalPixels} = dims;
if (screenPhysicalPixels) {
screen = {
width: screenPhysicalPixels.width / screenPhysicalPixels.scale,
height: screenPhysicalPixels.height / screenPhysicalPixels.scale,
scale: screenPhysicalPixels.scale,
fontScale: screenPhysicalPixels.fontScale,
};
} else if (screen == null) {
screen = window;
}

Object.assign(dimensions, dims);
dimensions = {window, screen};
if (dimensionsInitialized) {
// Don't fire 'change' the first time the dimensions are set.
eventEmitter.emit('change', {
window: dimensions.window,
screen: dimensions.screen,
});
eventEmitter.emit('change', dimensions);
} else {
dimensionsInitialized = true;
}
}

/**
* Initial dimensions are set before `runApplication` is called so they should
* be available before any other require's are run, but may be updated later.
*
* Note: Although dimensions are available immediately, they may change (e.g
* due to device rotation) so any rendering logic or styles that depend on
* these constants should try to call this function on every render, rather
* than caching the value (for example, using inline styles rather than
* setting a value in a `StyleSheet`).
*
* Example: `var {height, width} = Dimensions.get('window');`
*
* @param {string} dim Name of dimension as defined when calling `set`.
* @returns {Object?} Value for the dimension.
*/
static get(dim: string): Object {
invariant(dimensions[dim], 'No dimension set for key ' + dim);
return dimensions[dim];
}

/**
* Add an event handler. Supported events:
*
Expand All @@ -102,7 +96,7 @@ class Dimensions {
* are the same as the return values of `Dimensions.get('window')` and
* `Dimensions.get('screen')`, respectively.
*/
static addEventListener(type: string, handler: Function) {
static addEventListener(type: 'change', handler: Function) {
invariant(
type === 'change',
'Trying to subscribe to unknown event: "%s"',
Expand All @@ -114,7 +108,7 @@ class Dimensions {
/**
* Remove an event handler.
*/
static removeEventListener(type: string, handler: Function) {
static removeEventListener(type: 'change', handler: Function) {
invariant(
type === 'change',
'Trying to remove listener for unknown event: "%s"',
Expand All @@ -124,25 +118,13 @@ class Dimensions {
}
}

let dims: ?{[key: string]: any} =
global.nativeExtensions &&
global.nativeExtensions.DeviceInfo &&
global.nativeExtensions.DeviceInfo.Dimensions;
let nativeExtensionsEnabled = true;
if (!dims) {
dims = NativeDeviceInfo.getConstants().Dimensions;
nativeExtensionsEnabled = false;
}

invariant(
dims,
'Either DeviceInfo native extension or DeviceInfo Native Module must be registered',
);
Dimensions.set(dims);
if (!nativeExtensionsEnabled) {
RCTDeviceEventEmitter.addListener('didUpdateDimensions', function(update) {
// Subscribe before calling getConstants to make sure we don't miss any updates in between.
RCTDeviceEventEmitter.addListener(
'didUpdateDimensions',
(update: DimensionsPayload) => {
Dimensions.set(update);
});
}
},
);
Dimensions.set(NativeDeviceInfo.getConstants().Dimensions);

module.exports = Dimensions;
Loading

5 comments on commit 5ec382d

@brunolemos
Copy link
Contributor

@brunolemos brunolemos commented on 5ec382d Aug 8, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sahrens You’re calling forceUpdate() which is an empty setState() but I thought react don’t update when the state is the same? Anyway, if you committed it’s probably working

@sahrens
Copy link
Contributor Author

@sahrens sahrens commented on 5ec382d Aug 8, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

react don’t update when the state is the same

I believe that's opt-in via PureComponent, shouldComponentUpdate, memo, etc.

@sahrens
Copy link
Contributor Author

@sahrens sahrens commented on 5ec382d Aug 8, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's probably better practice to update state anyway though - I'll update to make it clear.

@brunolemos
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe that's opt-in via PureComponent, shouldComponentUpdate, memo, etc.

No, I was talking specifically about the hook useState, I remember when react made the change so it won't trigger a new update when the value is the same.

Made this quick codesandbox: https://codesandbox.io/embed/elegant-cori-0ixbx
PR: #25990

@muescha
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would expect at this place also the orientation info about the device

Please sign in to comment.