Android Release Builds Fail (RN 0.74+)
Description
iOS is fine and Android debug builds are fine. Android release builds fail. (React Native 0.74.1)
Get the error [TypeError: undefined is not a function]
Commenting out the Canvas and all elements within it makes it not crash so deff something related to Skia.
This was working fine on React Native 0.73.8 and Skia 0.1.229
Version
1.2.3
Steps to reproduce
Build release version of Android app
Snack, code example, screenshot, or link to a repository
import React, { forwardRef, useEffect, useImperativeHandle, useRef, useState, useMemo } from 'react'; import { StyleSheet, TouchableWithoutFeedback, useWindowDimensions, View, ScrollView, Alert } from 'react-native'; import { Canvas, Group, LinearGradient, RoundedRect, vec, mix } from '@shopify/react-native-skia'; import { Easing, SharedValue, withTiming, useDerivedValue, useSharedValue } from 'react-native-reanimated'; import * as Haptics from 'expo-haptics'; import { useSafeAreaInsets } from 'react-native-safe-area-context';
export interface ButtonGroupButton { label?: string; icon?: string; onPress?: () => void; multiPress?: boolean; }
export interface ButtonGroupRef { setButton: (idx: number | string, force?: boolean, action?: () => void) => void; next: () => void; previous: () => void; }
export interface ButtonGroupIdx { previous: number; next: number; current: number; }
export interface ButtonGroupProps {
justification?: 'between' | 'even';
buttons: Record<string, ButtonGroupButton>;
width?: number;
height?: number;
radius?: number;
padding?: {
x?: number;
y?: number;
};
disableHaptics?: boolean;
duration?: number;
state?: SharedValue<ButtonGroupIdx>;
transition?: SharedValue
export const ButtonGroup = forwardRef<ButtonGroupRef, ButtonGroupProps>( (props: ButtonGroupProps, ref) => { const { buttons = {}, preButtonPress, onButtonPress, justification = 'even', disableHaptics, padding, duration = 750, selectOnInit = false, mask = false, // eslint-disable-next-line react-hooks/rules-of-hooks state = useSharedValue<ButtonGroupIdx>({ previous: 0, next: 0, current: 0 }), // eslint-disable-next-line react-hooks/rules-of-hooks transition = useSharedValue(0) } = props; const xPadding = padding?.x ?? 0; const yPadding = padding?.y ?? 0; const buttonKeys = Object.keys(buttons);
const { width: windowWidth } = useWindowDimensions();
const { top } = useSafeAreaInsets();
const width = useMemo(() => (props?.width ?? 64), [props.width]);
const height = useMemo(() => (props?.height ?? 64), [props.height]);
const radius = useMemo(() => (props?.radius ?? 16), [props.radius]);
const [scrollable, setScrollable] = useState<boolean>(false);
const [groupWidth, setGroupWidth] = useState<number>(windowWidth);
const [buttonPadding, setButtonPadding] = useState<number>(0);
const [buttonIdx, setButtonIdx] = useState<number>(0);
const [loading, setLoading] = useState<boolean>(false);
const animateTimer = useRef<NodeJS.Timeout>(null);
const [animating, setAnimating] = useState<boolean>(false);
useImperativeHandle(ref, () => ({
setButton: (idx: number | string, force = true, action?: () => void) =>
onButtonSelect(idx, force, action),
next: () => {
onButtonSelect(state.value.next + 1);
},
previous: () => {
onButtonSelect(state.value.next - 1);
}
}));
useEffect(() => {
const overflow = buttonKeys.length * width > groupWidth;
setScrollable(overflow);
setButtonPadding(
overflow
? 0
: (groupWidth - xPadding * 2 - buttonKeys.length * width) /
buttonKeys.length /
(justification === 'between' ? 1 + (buttonKeys.length - 2) / buttonKeys.length : 2)
);
}, [buttons, groupWidth, justification]);
useEffect(() => {
Alert.alert(`Canvas: ${!!Canvas}`, `Canvas`);
}, [Canvas]);
useEffect(() => {
setAnimating(false);
setLoading(false);
onButtonSelect(0, selectOnInit);
}, []);
const onButtonSelect = async (idx: number | string, force = false, action?: () => void) => {
let index;
if (typeof idx === 'string') {
const valIdx = buttonKeys.indexOf(idx);
index = valIdx === -1 ? 0 : valIdx;
} else index = idx;
const btn = Object.values(buttons)[index];
setButtonIdx(index);
if ((state.value.next === index || btn?.multiPress) && !force)
return !!btn?.multiPress ? btn?.onPress?.() : null;
if (!disableHaptics) Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
setLoading(true);
await preButtonPress?.(index, buttonKeys[index]);
// @ts-ignore - Nullish Coalesce Void Functions
Promise.resolve(action?.() ?? btn?.onPress?.())
.then(() => {
setAnimating(true);
clearTimeout(animateTimer.current);
animateTimer.current = setTimeout(
() => {
setAnimating(false);
},
force ? 0 : duration - 300
);
state.value = {
previous: state.value.current,
current: state.value.next,
next: index
};
transition.value = 0;
transition.value = withTiming(1, {
duration,
easing: Easing.inOut(Easing.cubic)
});
})
.catch(() => {
setButtonIdx(state.value.previous);
})
.finally(() => {
setLoading(false);
onButtonPress?.(index, buttonKeys[index]);
});
};
const transform = useDerivedValue(() => {
const { current, next } = state.value;
const offset = width + buttonPadding * 2;
return [
{
translateX: mix(transition.value, current * offset, next * offset)
}
];
}, [state, transition, buttonPadding]);
return (
<>
{!!mask && (
<View
className={'bg-background absolute left-0 top-0 z-10 h-20'}
style={{
marginTop: -10 - top,
width: windowWidth
}}
/>
)}
<View
style={{
borderRadius: radius,
pointerEvents: loading ? 'none' : 'auto'
}}
className={'bg-background-secondary z-10 w-full shadow-md shadow-black/25'}
onLayout={({ nativeEvent }) => setGroupWidth(nativeEvent.layout.width)}
>
<View className={'w-full overflow-hidden'} style={{ borderRadius: radius }}>
<ScrollView
className={'w-full overflow-visible'}
alwaysBounceVertical={false}
horizontal={scrollable}
showsHorizontalScrollIndicator={false}
>
<View
className={'w-full flex-row'}
style={{ paddingVertical: yPadding, paddingHorizontal: xPadding }}
>
<Canvas style={StyleSheet.absoluteFill}>
<Group transform={transform}>
<RoundedRect
x={justification === 'between' && !scrollable ? 0 : buttonPadding + xPadding}
y={yPadding}
height={height}
width={width}
r={radius}
>
<LinearGradient
colors={['#31CBD1', '#61E0A1']}
start={vec(0, 0)}
end={vec(width, height)}
/>
</RoundedRect>
</Group>
</Canvas>
{Object.entries(buttons).map(([value, btn], index) => (
<TouchableWithoutFeedback
key={value + index}
onPress={() => onButtonSelect(index)}
>
<View
className={'items-center justify-center'}
style={{
width,
height,
borderRadius: radius,
marginRight:
justification === 'between' &&
!scrollable &&
index === buttonKeys.length - 1
? 0
: buttonPadding,
marginLeft:
justification === 'between' && !scrollable && index === 0
? 0
: buttonPadding
}}
>
{loading && buttonIdx === index ? (
<></>
) : (
<>
</>
)}
</View>
</TouchableWithoutFeedback>
))}
</View>
</ScrollView>
</View>
</View>
</>
);
} );
More info: Is working fine on Android versions 11 (API 30) and below
Additional stack:
2024-05-17 22:24:40.995 24513-24622 ReactNativeJS pid-24513 E [TypeError: undefined is not a function]
2024-05-17 22:24:40.996 24513-24623 unknown:ReactNative pid-24513 E TypeError: undefined is not a function, js engine: hermes, stack:
clearContainer@1:2869768
Eg@1:2835736
Th@1:2850621
Nh@1:2850245
Ch@1:2847149
Xc@1:2806156
J@1:2867011
R@1:2867353
anonymous@1:284808
_callTimer@1:283759
_callReactNativeMicrotasksPass@1:283903
callReactNativeMicrotasks@1:285898
__callReactNativeMicrotasks@1:147100
anonymous@1:146184
__guard@1:146938
flushedQueue@1:146095
callFunctionReturnFlushedQueue@1:145951
Upon even more testing and downgrading to React Native 0.73.8 (while keeping Skia at 1.2.3) the Android release builds work again. Seems to be some issue with 0.74+
@wcandillon any updates here? seems to be an issue with the react-reconciler and Skia in React Native 0.74+ in Android release builds API 31 and above
do you have a reproducible example? A project you could share on Github?
Issue found, seems to be coming from react-native-skottie
@enchorb did you file issue to RN-skottie? I faced this issue today and been pulling my hair as the stack only shows [TypeError: undefined is not a function], and no where pointing at skottie. Not until i found your issue, and replacing all my skottie back with rn-lottie and the issue's fix.
I'm facing the same issue.🥲
"react-native": "0.74.5",
"@shopify/react-native-skia": "1.3.9",
"react-native-skottie": "2.1.4",
"react-native-reanimated": "3.12.1",
I have the same issue, but I'm not convinced that Skottie is the issue, because in my case, Skottie components/animations work fine, it's when I render a Skia canvas in a completely different component that it crashes. Much like the original report, it only happens in release builds and only on Android.
The relevant section from adb logcat
10-21 23:06:36.276 4830 6858 W ReactNativeJS: Error: null 10-21 23:24:05.820 7583 7583 I ReactNative: [GESTURE HANDLER] Initialize gesture handler for root view com.facebook.react.ReactRootView{ccfa845 V.E...... ......ID 0,0-1440,2976 #b} 10-21 23:24:12.709 7583 7925 E ReactNativeJS: TypeError: undefined is not a function
I'm running:
"expo": "^51.0.38",
// ...
"react-native": "0.74.5",
// ...
"react-native-reanimated": "~3.15.5",
// ...
"react-native-skia": "^0.0.1",
"react-native-skottie": "^2.1.4",
The Skia canvas that I'm rendering uses Group (with clippingPath), Image, RoundedRect and LinearGradient.
Update: Nevermind, I removed Skottie and the error disappeared.