Andrei Calazans

React Native Weekly - W24 2021

☕️☕️ 9 min read

Welcome to the thirteenth edition of React Native Weekly. This is the retrospect of week 24 of 2021.

Reach out to me via Twitter if you have any feedback, and don’t forget to subscribe!

Flipper Update

This commit bumps flipper deps to 0.91 to support XCode 12.5 out of the box (#31562)

With this, we no longer need to pass custom version overrides to use_flipper, as the defaults will be up to date.

Make measure and getRelativeLayoutMetrics asynchronous

These changes 1 and 2 within the UIManager and Scheduler, turn these two functions into lambdas to make them asynchronous.

More Swipeable Card Examples

Andrei Shikov ashikov@fb.com adds more examples for the swipeable card with swipe-out animation. The example also tries to reuse the underlying views by ensuring the card reuses the same React node and changes position based on zIndex.

Andrei Shikov ashikov@fb.com has two cool patterns here in his code:

  • Usage of zIndex to conserve views
  • Usage of StyleSheet.compose
  • Usage of React.memo to reset the Animated Value on prop change: React.useMemo(() => new Animated.Value(0), [props.color])

See diff:

diff --git a/packages/rn-tester/js/examples/SwipeableCardExample/SwipeableCardExample.js b/packages/rn-tester/js/examples/SwipeableCardExample/SwipeableCardExample.js
index f4f935453ac..747ccb01e7a 100644
--- a/packages/rn-tester/js/examples/SwipeableCardExample/SwipeableCardExample.js
+++ b/packages/rn-tester/js/examples/SwipeableCardExample/SwipeableCardExample.js
@@ -15,6 +15,7 @@ import {
   View,
   StyleSheet,
   FlatList,
+  Text,
   useWindowDimensions,
 } from 'react-native';
 
@@ -31,34 +32,88 @@ module.exports = {
       description: ('This example creates a swipeable card using PanResponder. ' +
         'Under the hood, JSResponderHandler should prevent scroll when the card is being swiped.': string),
       render: function(): React.Node {
-        return <SwipeableCard />;
+        return <SwipeableCardExample />;
       },
     },
   ],
 };
 
-function SwipeableCard() {
-  const movementX = React.useRef(new Animated.Value(0)).current;
+function SwipeableCardExample() {
+  const cardColors = ['red', 'blue', 'pink', 'aquamarine'];
+  const [currentIndex, setCurrentIndex] = React.useState(0);
 
-  const panResponder = React.useRef(
-    PanResponder.create({
-      onMoveShouldSetPanResponderCapture: (e, gestureState) => {
-        const {dx} = gestureState;
-        return Math.abs(dx) > 5;
-      },
-      onPanResponderMove: (e, gestureState) => {
-        Animated.event([null, {dx: movementX}], {
+  const nextIndex = currentIndex + 1;
+
+  const isFirstCardOnTop = currentIndex % 2 !== 0;
+
+  const incrementCurrent = () => setCurrentIndex(currentIndex + 1);
+
+  const getCardColor = index => cardColors[index % cardColors.length];
+
+  /*
+   * The cards try to reuse the views. Instead of always rebuilding the current card on top
+   * the order is configured by zIndex. This way, the native side reuses the same views for bottom
+   * and top after swiping out.
+   */
+  return (
+    <>
+      <SwipeableCard
+        zIndex={isFirstCardOnTop ? 2 : 1}
+        color={
+          isFirstCardOnTop
+            ? getCardColor(currentIndex)
+            : getCardColor(nextIndex)
+        }
+        onSwipedOut={incrementCurrent}
+      />
+      <SwipeableCard
+        zIndex={isFirstCardOnTop ? 1 : 2}
+        color={
+          isFirstCardOnTop
+            ? getCardColor(nextIndex)
+            : getCardColor(currentIndex)
+        }
+        onSwipedOut={incrementCurrent}
+      />
+    </>
+  );
+}
+
+function SwipeableCard(props: {
+  zIndex: number,
+  color: string,
+  onSwipedOut: () => void,
+}) {
+  // eslint-disable-next-line react-hooks/exhaustive-deps
+  const movementX = React.useMemo(() => new Animated.Value(0), [props.color]);
+
+  const panResponder = React.useMemo(
+    () =>
+      PanResponder.create({
+        onMoveShouldSetPanResponderCapture: (e, gestureState) => {
+          const {dx} = gestureState;
+          return Math.abs(dx) > 5;
+        },
+        onPanResponderMove: Animated.event([null, {dx: movementX}], {
           useNativeDriver: false,
-        })(e, gestureState);
-      },
-      onPanResponderEnd: (e, gestureState) => {
-        Animated.timing(movementX, {
-          toValue: 0,
-          useNativeDriver: true,
-        }).start();
-      },
-    }),
-  ).current;
+        }),
+        onPanResponderEnd: (e, gestureState) => {
+          const {dx} = gestureState;
+          if (Math.abs(dx) > 120) {
+            Animated.timing(movementX, {
+              toValue: dx > 0 ? 1000 : -1000,
+              useNativeDriver: true,
+            }).start(props.onSwipedOut);
+          } else {
+            Animated.timing(movementX, {
+              toValue: 0,
+              useNativeDriver: true,
+            }).start();
+          }
+        },
+      }),
+    [movementX, props.onSwipedOut],
+  );
 
   const {width} = useWindowDimensions();
   const rotation = movementX.interpolate({
@@ -68,14 +123,14 @@ function SwipeableCard() {
   });
 
   return (
-    <View style={styles.container}>
+    <View style={StyleSheet.compose(styles.container, {zIndex: props.zIndex})}>
       <Animated.View
         {...panResponder.panHandlers}
         style={{
           transform: [{translateX: movementX}, {rotateZ: rotation}],
           flex: 1,
         }}>
-        <Card />
+        <Card color={props.color} />
       </Animated.View>
     </View>
   );
@@ -83,33 +138,70 @@ function SwipeableCard() {
 
 const cardData = Array(5);
 
-function Card(props) {
+function Card(props: {color: string}) {
   const renderItem = ({item, index}) => (
-    <View style={index % 2 === 0 ? styles.cardSectionA : styles.cardSectionB} />
+    <CardSection color={props.color} index={index} />
   );
+
+  const separatorComponent = () => <View style={styles.separator} />;
+
+  const listRef = React.useRef<?React.ElementRef<typeof FlatList>>();
+
+  React.useEffect(() => {
+    listRef.current?.scrollToOffset({offset: 0, animated: false});
+  }, [props.color]);
+
   return (
     <View style={styles.card}>
-      <FlatList style={{flex: 1}} data={cardData} renderItem={renderItem} />
+      <FlatList
+        style={{flex: 1}}
+        data={cardData}
+        renderItem={renderItem}
+        ItemSeparatorComponent={separatorComponent}
+        ref={listRef}
+      />
+    </View>
+  );
+}
+
+function CardSection(props: {index: number, color: string}) {
+  return (
+    <View
+      style={StyleSheet.compose(styles.sectionBg, {
+        backgroundColor: props.color,
+      })}>
+      <Text style={styles.sectionText}>Section #{props.index}</Text>
     </View>
   );
 }
 ...

For those who don’t know StyleSheet.compose:

  /**
   * Combines two styles such that `style2` will override any styles in `style1`.
   * If either style is falsy, the other one is returned without allocating an
   * array, saving allocations and maintaining reference equality for
   * PureComponent checks.
   */
  compose<T: DangerouslyImpreciseStyleProp>(
    style1: ?T,
    style2: ?T,
  ): ?T | $ReadOnlyArray<T> {
    if (style1 != null && style2 != null) {
      return ([style1, style2]: $ReadOnlyArray<T>);
    } else {
      return style1 != null ? style1 : style2;
    }
  },

Pressable Foreground Ripple Support

Nishan Bende nishanbende@gmail.com implemented support for Android’s ripple effect for the Pressable component in #31632.


+            <Pressable
+              android_ripple={{
+                borderless: false,
+                foreground: true,
+              }}>

My only complain is it introduced snake_case to the prop naming convention, how did they not catch this lol 😅

Ship new C++ Differ in code

If you are following you will have seen the continuous updates 1 and 2 to the new Differ functions that updates the ShadowTree.

Well now they removed the older code and officialized the usage of the newer one.

Accessible colors for DynamicColorIOS (#31651)

Summary: From Birkir Gudjonsson birkir.gudjonsson@gmail.com, this commit allows you to harvest the UIAccessibilityContrastHigh trait from iOS to show accessible colors when high contrast mode is enabled.

// usage

PlatformColorIOS({
  light: '#eeeeee',
  dark: '#333333',
  highContrastLight: '#ffffff',
  highContrastDark: '#000000',
});

// {
//   "dynamic": {
//     "light": "#eeeeee",
//     "dark": "#333333",
//     "highContrastLight": "#ffffff",
//     "highContrastDark": "#000000",
//   }
// }

This is how apple’s own dynamic system colors work under the hood (https://developer.apple.com/design/human-interface-guidelines/ios/visual-design/color/#dynamic-system-colors)

Switch: useMergeRef for forwarding ref

This is a showcase of when you can use useMergeRef.

[Internal] - Replace useImperativeHandle usage with new useMergeRef which will keep both the forwarded and internal ref handle up to date (in case the instance ever changes). That being said, this change was not motivated in fear of a stale ref but more an intention to show that useImperativeHandle's use case is more about creating a selective API and useMergeRef is better suited for publishing ref updates.

Fix setSnapToOffsets crashing on Android if snapToOffsets is null (#31681)

Maxime Bertheau maxime@frontapp.com, in this commit fixes a NullException crash from Android.

Use default priority for text input on change

This revert actually gave us a quick look into how synchronous access of the TextInput onChange value will look like in implementation:


diff --git a/ReactCommon/react/renderer/components/textinput/iostextinput/TextInputEventEmitter.cpp b/ReactCommon/react/renderer/components/textinput/iostextinput/TextInputEventEmitter.cpp
index 8d4284771d2..6342a6c1ff2 100644
--- a/ReactCommon/react/renderer/components/textinput/iostextinput/TextInputEventEmitter.cpp
+++ b/ReactCommon/react/renderer/components/textinput/iostextinput/TextInputEventEmitter.cpp
@@ -72,8 +72,7 @@ void TextInputEventEmitter::onBlur(
 
 void TextInputEventEmitter::onChange(
     TextInputMetrics const &textInputMetrics) const {
-  dispatchTextInputEvent(
-      "change", textInputMetrics, EventPriority::SynchronousUnbatched);
+  dispatchTextInputEvent("change", textInputMetrics);
 }

Set Project Wide Kotlin Version To 1.4.21

This commit sets project wide Kotlin version to 1.4.21, supported in Buck https://github.com/facebook/buck/tree/dev/third-party/java/kotlin.

They expect to see more Kotlin code in React Native. 🙌

Support user-defined PlatformColors on iOS (#31258)

Contribution from Joel Arvidsson joel.arvidsson@klarna.com.

In summary it gives iOS feature parity with Android in the sense that one can use user-defined native colors, something even the docs claim is possible. It’s useful as it enables accessibility features such as high contrast colors and makes implementing dark mode simple. For an example on how it can be used, see https://github.com/klarna-incubator/platform-colors

BridgeDevSupportManager Clean Up

There has been a continous effort to clean up the code that handle bridge during development.

67a486e5771 Move Sampling profiler from DevSupportManagerBase to BridgeDevSupportManager
61dda3242dd Refactor: Delete ExceptionLogger interface in DevSupportManagerBase
1d14df217ec Refactor: Stop having DevSupportManager conform to PackagerCommandListener
675e480fb9d Refactor: Make DevSupportManager not conform to DevInternalSettings.Listener
39d9122445b Delete unnecessary DevSupportManager constructor
7c6eb1fecb1 Move handleReloadJS() from DevSupportManagerBase to BridgeDevSupportManager
2c88459e24a Refactor: Introduce methods to show/hide DevLoadingView in DevSupportManagerBase
30340890dc3 Move loadSplitBundleFromServer from DevSupportManagerBase to BridgeDevSupportManager
3feaecd4737 Rename DevSupportManagerImpl to BridgeDevSupportManager

Add .hprof to .gitignore

Added .hprof file is generated when building a release apk and it’s size is > 300 MB.

diff --git a/template/_gitignore b/template/_gitignore
index ad572e632bc..cc53454ef4e 100644
--- a/template/_gitignore
+++ b/template/_gitignore
@@ -28,6 +28,7 @@ build/
 .gradle
 local.properties
 *.iml
+*.hprof

Delete StyleSheetValidation

This commit deletes StyleSheetValidation because it is prop-types in disguise.

Statically Define ReactNativeStyleAttributes

This rewrites ReactNativeStyleAttributes so that it is statically defined.

This means it will no longer require a handful of modules that defines prop-types only to use their keys.

Functionally, this should be equivalent to what was there before.

Beyond these two changes above some more clean up around prop types were done, which signals to me they are cleaning up in preparation of code-gen and TurboModules since these rely on auto-generated types.

React Native is hiring

Eli White posted on June 10th that React Native is hiring a Front End engineer to join the team improving their public JS APIs. If you are driven to make React Native work great across platforms, or things like KeyboardAvoidingView frustrate you — this is the role for you! Grinning face

Apply at here

The Best React Native UI Toolkit

There was a post on Hackernoon about React Native UI toolkits

In summary, use react-native-elements until you can slowly phase into your own custom design.

Flutter Versus React Native Part 2

In this post I build two apps with each Framework and compare both code and UI results. This is an interesting look into how each framework looks aesthetically.

Since I wrote it, ping me if you want to discuss it.

That Is It!

That’s it for this week. If you want to see more checkout previous week’s posts here. Subscribe to get notified when new posts are out = )