react-native-reanimated: [3.0.0-rc.10] Cannot read property 'prototype' of undefined when creating a `new Clock` instance

Description

When I try to new Clock() it throws Cannot read property 'prototype' of undefined.

Here’s the hook:

useCollapsible

import React, { useCallback } from 'react';
import type { LayoutChangeEvent } from 'react-native';
import Animated from 'react-native-reanimated';

import { runTiming } from './reanimatedHelpers';
import type { State, Config } from './types';

const { Clock, Value } = Animated;

export function useCollapsible(config?: Config) {
  const [height, setHeight] = React.useState(0);
  const [state, setState] = React.useState<State>(
    config?.state === 'expanded' ? 'expanded' : 'collapsed'
  );

  const { current: clock } = React.useRef(new Clock());
  const { current: progress } = React.useRef(new Value<number>(0));
  const { current: animation } = React.useRef(new Value<number>(0));
  const { current: animatedHeight } = React.useRef(
    runTiming(clock, progress, animation, config?.duration, config?.easing)
  );

  const [mounted, setMounted] = React.useState(
    config?.state === 'expanded' ? true : false
  );

  const handleAnimationEnd = () => {
    if (config?.unmountOnCollapse && !config?.show) setMounted(false);
  };

  React.useEffect(() => {
    if (state === 'collapsed') {
      animation.setValue(0);
      setMounted(false);
      handleAnimationEnd();
    } else {
      animation.setValue(height);
      setMounted(true);
    }
  }, [state, height, animation]);

  const onPress = React.useCallback(() => {
    setState((prev) => (prev === 'collapsed' ? 'expanded' : 'collapsed'));
  }, []);

  const onLayout = React.useCallback(
    (event: LayoutChangeEvent) => {
      const measuredHeight = event.nativeEvent.layout.height;

      if (height !== measuredHeight) {
        setHeight(measuredHeight);
      }
    },
    [height]
  );

  return {
    onLayout,
    onPress,
    animatedHeight,
    height,
    state,
    mounted,
    setMounted,
  };
}

and the runTiming:

import Animated, { EasingNode } from 'react-native-reanimated';

const { Value, set, cond, startClock, clockRunning, timing, stopClock, block } =
  Animated;

export function runTiming(
  clock: Animated.Clock,
  value: Animated.Value<number>,
  dest: Animated.Value<number>,
  duration: number = 250,
  easing: Animated.EasingNodeFunction = EasingNode.out(EasingNode.ease)
) {
  const state = {
    finished: new Value(0),
    position: value,
    time: new Value(0),
    frameTime: new Value(0),
  };

  const config = {
    duration,
    toValue: dest,
    easing,
  };

  return block([
    cond(clockRunning(clock), 0, [
      // If the clock isn't running we reset all the animation params and start the clock
      set(state.finished, 0),
      set(state.time, 0),
      set(state.position, value),
      set(state.frameTime, 0),
      set(config.toValue, dest),
      startClock(clock),
    ]),
    // we run the step here that is going to update position
    timing(clock, state, config),
    // if the animation is over we stop the clock
    cond(state.finished, stopClock(clock)),
    // return the updated position
    state.position,
  ]);
}

CollapsableView:

/* eslint-disable react/prop-types */
import React, { useCallback, useEffect, useState } from "react";
import { StyleSheet, Text, TouchableOpacity, View } from "react-native";
import Animated, {
  Easing,
  interpolate,
  useAnimatedStyle,
  useDerivedValue,
  useSharedValue,
  withTiming
} from "react-native-reanimated";
import Svg from "react-native-svg";
import { AnimatedSection, useCollapsible } from "reanimated-collapsible-helpers";

import ArrowDownIcon from "@components/CollapsibleView/ArrowDownIcon";

const ROTATE_ANGLE = 180;

const CollapsibleView = ({
  children = [],
  title = "",
  touchableWrapperStyle = {},
  initExpanded = false,
  expanded = null,
  unmountOnCollapse = false,
  duration = 200,
  arrowStyling = {},
  noArrow = false,
  activeOpacityFeedback = 0.9,
  TouchableComponent = TouchableOpacity,
  titleProps = {},
  titleStyle = {},
  touchableWrapperProps = {},
  nested = 0,
  lastCommentIndex = -1,
  length = 0,
  collapsedTitle = "",
  isParentLast = false,
  parentCommentLength = 0
}) => {
  const [show, setShow] = useState<boolean | null | undefined>(initExpanded);

  const { animatedHeight, onPress, onLayout, state, mounted, setMounted } =
    useCollapsible({
      state: initExpanded ? "expanded" : "collapsed",
      unmountOnCollapse,
      show
    });

  const rotateAnim = useSharedValue(0);

  if (!mounted && expanded) {
    setMounted(true);
  }

  const rotation = useDerivedValue(() => {
    return interpolate(rotateAnim.value, [180, 360], [180, 360]);
  });

  const arrowAnimatedStyle = useAnimatedStyle(() => {
    return {
      marginTop: 10,
      marginHorizontal: 4,
      transform: [
        {
          rotate: `${rotation.value}deg`
        }
      ]
    };
  });

  const handleArrowRotate = useCallback(
    (open = null) => {
      const _open = open === null ? show : open;
      if (!_open) {
        rotateAnim.value = withTiming(0, { duration, easing: Easing.ease });
      } else {
        rotateAnim.value = withTiming(ROTATE_ANGLE, {
          duration,
          easing: Easing.ease
        });
      }
    },
    [duration, rotateAnim, show]
  );

  const handleToggleShow = () => {
    if (!mounted) {
      if (!show) {
        onPress();
        setMounted(true);
      }
    } else {
      onPress();
      setShow(!show);
    }
  };

  useEffect(() => {
    // this part is to trigger collapsible animation only after he has been fully mounted so animation would
    // not be interrupted.
    if (mounted) {
      setShow(true);
    }
  }, [mounted]);

  useEffect(
    () => {
      // on mounting set the rotation angle
      rotateAnim.value = show ? 0 : ROTATE_ANGLE;
    },
    // eslint-disable-next-line react-hooks/exhaustive-deps
    []
  );

  useEffect(() => {
    if (mounted) {
      handleArrowRotate(show);
    }
    if (show !== expanded) {
      setShow(expanded);
    }
  }, [expanded, handleArrowRotate, mounted, show]);

  return (
    <View style={styles.container}>
      <TouchableComponent
        style={[touchableWrapperStyle]}
        onPress={handleToggleShow}
        activeOpacity={activeOpacityFeedback}
        {...touchableWrapperProps}
      >
        <View
          // eslint-disable-next-line react-native/no-inline-styles
          style={{
            flexDirection: "row",
            alignItems: "center",
            ...titleStyle,
            marginLeft: nested === 1 ? 34 : 62
          }}
          {...titleProps}
        >
          <View
            // eslint-disable-next-line react-native/no-inline-styles
            style={{
              height: state === "expanded" ? "160%" : "70%",
              marginBottom: 12,
              borderLeftColor: "#d9d9d9",
              borderLeftWidth: nested === 1 ? 2 : 2
            }}
          />
          {nested > 1 &&
          lastCommentIndex > -1 &&
          parentCommentLength === 1 &&
          !isParentLast ? (
            <Svg height="160%" width={2} style={styles.nestedTwoLine} />
          ) : null}

          {nested === 2 &&
          parentCommentLength === length - 1 &&
          length > 1 &&
          !isParentLast ? (
            <Svg height="160%" width={2} style={styles.nestedTwoLine} />
          ) : null}

          {nested === 2 && parentCommentLength > 1 && !isParentLast ? (
            <Svg height="160%" width={2} style={styles.nestedTwoLine} />
          ) : null}

          {nested === 2 && parentCommentLength > length && length > 1 && !isParentLast ? (
            <Svg height="160%" width={2} style={styles.nestedTwoLine} />
          ) : null}

          {nested === 2 && length > 1 && parentCommentLength > length && !isParentLast ? (
            <Svg height="160%" width={2} style={styles.nestedTwoLine} />
          ) : null}

          {state !== "expanded" ? (
            <Svg height={2} width={10} style={styles.horizontalLine} />
          ) : null}

          {noArrow ? null : (
            <Animated.View style={arrowAnimatedStyle}>
              <ArrowDownIcon {...arrowStyling} />
            </Animated.View>
          )}
          <Text style={styles.title}>
            {state === "collapsed" ? title : collapsedTitle}
          </Text>
        </View>
      </TouchableComponent>
      {mounted ? (
        <AnimatedSection
          animatedHeight={animatedHeight}
          onLayout={onLayout}
          state={state}
          style={styles.animatedSection}
        >
          {children}
        </AnimatedSection>
      ) : null}
    </View>
  );
};

export default CollapsibleView;

const styles = StyleSheet.create({
  container: {
    flex: 1
  },
  title: {
    color: "#9C9C9C",
    fontSize: 16,
    marginTop: 10
  },
  nestedTwoLine: {
    marginBottom: 10,
    backgroundColor: "#d9d9d9",
    position: "absolute",
    left: -28
  },
  horizontalLine: {
    backgroundColor: "#d9d9d9",
    marginTop: 10,
    marginLeft: -2
  },
  animatedSection: {
    width: "100%"
  }
});

Simulator Screen Shot - iPhone 14 Plus - 2023-01-12 at 19 59 27

This used to work fine on 2.14.x

Steps to reproduce

code sample above

Snack or a link to a repository

none

Reanimated version

3.0.0-rc.10

React Native version

0.71.0

Platforms

Android, iOS

JavaScript runtime

Hermes

Workflow

React Native (without Expo)

Architecture

Paper (Old Architecture)

Build type

Debug mode

Device

iOS simulator

Device model

No response

Acknowledgements

Yes

About this issue

  • Original URL
  • State: closed
  • Created a year ago
  • Comments: 16 (6 by maintainers)

Most upvoted comments

This may not be the exact same issue but we found that we had missed a dependency that needed to be updated - react-native-tab-view in our case, see https://github.com/software-mansion/react-native-reanimated/issues/4420#issuecomment-1734857719

@piaskowyk thanks for the clarification! I would love some help on how to transition since I do not know what are equivalent replacements of these functions.

This is actually not a bug. In last year we dropped the support for Reanimated 1 and in Reanimated 3 we removed source code of Reanimated 1 from package. Reanimated 2 still contains API v1.

  • Reanimated 1 - API v1
  • Reanimated 2 - API v1 + v2
  • Reanimated 3 - API v2

I recommend to rewrite parts with v1 API to v2 API. If you want some guidance I can help you.