browniefed / react-native-ticker

React Native Number Ticker
https://www.npmjs.com/package/react-native-ticker
460 stars 46 forks source link

cannot read property 'prototype' of undefined #41

Open tristanheilman opened 10 months ago

tristanheilman commented 10 months ago

Hit an issue today after upgrading react-native-reanimated from 2.15.1 -> 3.5.4

Redbox error while developing on iOS & Android showed an issue in this packages index.js file for cannot read property 'prototype' of undefined react native ticker The issue pointed to the widthAnim value/function line.

I dug into this a bit and made some changes to the files to utilize better logic for running the animations and ultimately fixed the issue I was experiencing. I also solved a problem with the string length changing and preventing the last character(s) from animating.

Ex. Initial value rendered is $0.00 but changes after API fetches data to $52.50. The component would only animate the first three characters in the new string, leaving the 0 stale.

Ex. Initial value rendered is $0.00 but changes after API fetches data to $134.42. The component would only animate the first three characters in the new string, leaving the final 42 characters stale with no animation.

My changes fixes the Redbox bug as well as improves on the current animations running when the string value changes.

I created a patch file that includes all of this if anybody else experiences the issue.

diff --git a/node_modules/react-native-ticker/index.js b/node_modules/react-native-ticker/index.js
index a865f7b..817b863 100644
--- a/node_modules/react-native-ticker/index.js
+++ b/node_modules/react-native-ticker/index.js
@@ -11,7 +11,7 @@ var __rest = (this && this.__rest) || function (s, e) {
 };
 import React, { useRef, useEffect, useState, Children } from "react";
 import { StyleSheet, Text, View, I18nManager, } from "react-native";
-import Animated, { EasingNode } from "react-native-reanimated";
+import Animated, { EasingNode, useAnimatedStyle, useDerivedValue, useSharedValue, withTiming } from "react-native-reanimated";
 const styles = StyleSheet.create({
     row: {
         flexDirection: I18nManager.isRTL ? "row-reverse" : "row",
@@ -35,62 +35,44 @@ const numberRange = range(10).map((p) => p + "");
 const numAdditional = [",", "."];
 const numberItems = [...numberRange, ...numAdditional];
 const isNumber = (v) => !isNaN(parseInt(v));
-const getPosition = ({ text, items, height, }) => {
-    const index = items.findIndex((p) => p === text);
-    return index * height * -1;
-};
+
 export const Tick = (_a) => {
     var props = __rest(_a, []);
     //@ts-ignore
     return <TickItem {...props}/>;
 };
-const useInitRef = (cb) => {
-    const ref = useRef();
-    if (!ref.current) {
-        ref.current = cb();
-    }
-    return ref.current;
-};
+
 const TickItem = ({ children, duration, textStyle, textProps, measureMap, rotateItems, }) => {
     const measurement = measureMap[children];
-    const position = getPosition({
-        text: children,
-        height: measurement.height,
-        items: rotateItems,
-    });
-    const widthAnim = useInitRef(() => new Animated.Value(measurement.width));
-    const stylePos = useInitRef(() => new Animated.Value(position));
+    const position = useSharedValue(0);
+    
     useEffect(() => {
-        if (stylePos) {
-            Animated.timing(stylePos, {
-                toValue: position,
-                duration,
-                easing: EasingNode.linear,
-            }).start();
-            Animated.timing(widthAnim, {
-                toValue: measurement.width,
-                duration: 25,
-                easing: EasingNode.linear,
-            }).start();
+        position.value = rotateItems.findIndex((p) => p === children) * measurement.height * -1;
+    }, [children])
+
+    const randomizer = Math.floor(Math.random() * 4)
+    const widthAnim = useAnimatedStyle(() => {
+        return {
+          height: withTiming(measurement.height, {duration: 50}),
+          width: withTiming(measurement.width, {duration: 50})
         }
-    }, [position, measurement]);
-    return (<Animated.View style={[
-            {
-                height: measurement.height,
-                width: widthAnim,
-                overflow: "hidden",
-            },
-        ]}>
-      <Animated.View style={[
-            {
-                transform: [{ translateY: stylePos }],
-            },
-        ]}>
-        {rotateItems.map((v) => (<Text key={v} {...textProps} style={[textStyle, { height: measurement.height }]}>
-            {v}
-          </Text>))}
-      </Animated.View>
-    </Animated.View>);
+    })
+    const stylePos = useAnimatedStyle(() => {
+        return {
+            transform: [{ translateY: withTiming(position.value, {duration: 1000 + randomizer * 200})}],
+        }
+    })
+
+
+    return (
+        <Animated.View style={[{overflow: "hidden"}, widthAnim]}>
+            <Animated.View style={stylePos}>
+                {rotateItems.map((v) => (<Text key={v} {...textProps} style={[textStyle, { height: measurement.height }]}>
+                    {v}
+                </Text>))}
+            </Animated.View>
+        </Animated.View>
+    );
 };
 const Ticker = ({ duration = 250, containerStyle, textStyle, textProps, children }) => {
     const [measured, setMeasured] = useState(false);
diff --git a/node_modules/react-native-ticker/index.tsx b/node_modules/react-native-ticker/index.tsx
index 1eb0eda..7ff8a52 100644
--- a/node_modules/react-native-ticker/index.tsx
+++ b/node_modules/react-native-ticker/index.tsx
@@ -1,4 +1,4 @@
-import React, { useRef, useEffect, useState, Children } from "react";
+import React, {useRef, useEffect, useState, Children} from 'react';
 import {
   StyleSheet,
   Text,
@@ -7,16 +7,20 @@ import {
   TextStyle,
   TextProps,
   I18nManager,
-} from "react-native";
-import Animated, { EasingNode } from "react-native-reanimated";
+} from 'react-native';
+import Animated, {
+  useAnimatedStyle,
+  useSharedValue,
+  withTiming,
+} from 'react-native-reanimated';

 const styles = StyleSheet.create({
   row: {
-    flexDirection: I18nManager.isRTL ? "row-reverse" : "row",
-    overflow: "hidden",
+    flexDirection: I18nManager.isRTL ? 'row-reverse' : 'row',
+    overflow: 'hidden',
   },
   hide: {
-    position: "absolute",
+    position: 'absolute',
     top: 0,
     left: 0,
     opacity: 0,
@@ -29,10 +33,10 @@ const uniq = (values: string[]) => {
   });
 };

-const range = (length: number) => Array.from({ length }, (x, i) => i);
-const splitText = (text = "") => (text + "").split("");
-const numberRange = range(10).map((p) => p + "");
-const numAdditional = [",", "."];
+const range = (length: number) => Array.from({length}, (x, i) => i);
+const splitText = (text = '') => (text + '').split('');
+const numberRange = range(10).map((p) => p + '');
+const numAdditional = [',', '.'];
 const numberItems = [...numberRange, ...numAdditional];
 const isNumber = (v: string) => !isNaN(parseInt(v));

@@ -67,22 +71,13 @@ interface TickProps {
   measureMap: MeasureMap;
 }

-type MeasureMap = Record<string, { width: number; height: number }>;
+type MeasureMap = Record<string, {width: number; height: number}>;

-export const Tick = ({ ...props }: Partial<TickProps>) => {
+export const Tick = ({...props}: Partial<TickProps>) => {
   //@ts-ignore
   return <TickItem {...props} />;
 };

-const useInitRef = (cb: () => Animated.Value<number>) => {
-  const ref = useRef<Animated.Value<number>>();
-  if (!ref.current) {
-    ref.current = cb();
-  }
-
-  return ref.current;
-};
-
 const TickItem = ({
   children,
   duration,
@@ -92,54 +87,40 @@ const TickItem = ({
   rotateItems,
 }: TickProps) => {
   const measurement = measureMap[children];
-
-  const position = getPosition({
-    text: children,
-    height: measurement.height,
-    items: rotateItems,
-  });
-
-  const widthAnim = useInitRef(() => new Animated.Value(measurement.width));
-  const stylePos = useInitRef(() => new Animated.Value(position));
+  const position = useSharedValue(0);

   useEffect(() => {
-    if (stylePos) {
-      Animated.timing(stylePos, {
-        toValue: position,
-        duration,
-        easing: EasingNode.linear,
-      }).start();
-      Animated.timing(widthAnim, {
-        toValue: measurement.width,
-        duration: 25,
-        easing: EasingNode.linear,
-      }).start();
-    }
-  }, [position, measurement]);
-
-  return (
-    <Animated.View
-      style={[
+    position.value =
+      rotateItems.findIndex((p) => p === children) * measurement.height * -1;
+  }, [children]);
+
+  const randomizer = Math.floor(Math.random() * 4);
+  const widthAnim = useAnimatedStyle(() => {
+    return {
+      height: withTiming(measurement.height, {duration: 50}),
+      width: withTiming(measurement.width, {duration: 50}),
+    };
+  });
+  const stylePos = useAnimatedStyle(() => {
+    return {
+      transform: [
         {
-          height: measurement.height,
-          width: widthAnim,
-          overflow: "hidden",
+          translateY: withTiming(position.value, {
+            duration: 1000 + randomizer * duration,
+          }),
         },
-      ]}
-    >
-      <Animated.View
-        style={[
-          {
-            transform: [{ translateY: stylePos }],
-          },
-        ]}
-      >
+      ],
+    };
+  });
+
+  return (
+    <Animated.View style={[{overflow: 'hidden'}, widthAnim]}>
+      <Animated.View style={stylePos}>
         {rotateItems.map((v) => (
           <Text
             key={v}
             {...textProps}
-            style={[textStyle, { height: measurement.height }]}
-          >
+            style={[textStyle, {height: measurement.height}]}>
             {v}
           </Text>
         ))}
@@ -148,18 +129,25 @@ const TickItem = ({
   );
 };

-const Ticker = ({ duration = 250, containerStyle, textStyle, textProps, children }: Props) => {
+const Ticker = ({
+  duration = 250,
+  containerStyle,
+  textStyle,
+  textProps,
+  children,
+}: Props) => {
   const [measured, setMeasured] = useState<boolean>(false);

   const measureMap = useRef<MeasureMap>({});
   const measureStrings: string[] = Children.map(children as any, (child) => {
-    if (typeof child === "string" || typeof child === "number") {
+    if (typeof child === 'string' || typeof child === 'number') {
       return splitText(`${child}`);
     } else if (child) {
       return child?.props && child?.props?.rotateItems;
     }
   }).reduce((acc, val) => acc.concat(val), []);

+  console.log('MEASURE: ', measureStrings);
   const hasNumbers = measureStrings.find((v) => isNumber(v)) !== undefined;
   const rotateItems = uniq([
     ...(hasNumbers ? numberItems : []),
@@ -183,7 +171,7 @@ const Ticker = ({ duration = 250, containerStyle, textStyle, textProps, children
     <View style={[styles.row, containerStyle]}>
       {measured === true &&
         Children.map(children, (child) => {
-          if (typeof child === "string" || typeof child === "number") {
+          if (typeof child === 'string' || typeof child === 'number') {
             return splitText(`${child}`).map((text, index) => {
               let items = isNumber(text) ? numberItems : [text];
               return (
@@ -193,13 +181,13 @@ const Ticker = ({ duration = 250, containerStyle, textStyle, textProps, children
                   textStyle={textStyle}
                   textProps={textProps}
                   rotateItems={items}
-                  measureMap={measureMap.current}
-                >
+                  measureMap={measureMap.current}>
                   {text}
                 </TickItem>
               );
             });
           } else {
+            console.log('RETURNED CLONED ELEMENT');
             //@ts-ignore
             return React.cloneElement(child, {
               duration,
@@ -210,13 +198,13 @@ const Ticker = ({ duration = 250, containerStyle, textStyle, textProps, children
           }
         })}
       {rotateItems.map((v) => {
+        console.log('ROTATE ITEMS');
         return (
           <Text
             key={v}
             {...textProps}
             style={[textStyle, styles.hide]}
-            onLayout={(e) => handleMeasure(e, v)}
-          >
+            onLayout={(e) => handleMeasure(e, v)}>
             {v}
           </Text>
         );
@@ -225,8 +213,8 @@ const Ticker = ({ duration = 250, containerStyle, textStyle, textProps, children
   );
 };

-Ticker.defaultProps = {
-  duration: 250,
-};
+// Ticker.defaultProps = {
+//   duration: 250,
+// };

 export default Ticker;
arichayo commented 9 months ago

Did you find a fix?

arichayo commented 9 months ago

I figured it out its using old deprecated methods from react-native-reanimated so i updated those and it now works.

Heres my PR: https://github.com/browniefed/react-native-ticker/pull/43

sranvare07 commented 9 months ago

Hey @arichayo, I was also facing this same issue so I upgraded to the latest version that you guys pushed but the UI looks weird. Please check attached screenshot. IMG_20231221_104034