GestureDetector gesture handler app crash when calling external function - react-native

i tried to use GestureDetector of react-native-gesture-handler
import React from 'react';
import { Directions, Gesture, GestureDetector } from 'react-native-gesture-handler';
import Animated, { useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated';
/**
* Component used as Home Page
*/
const HomePage: React.FC = () => {
const position = useSharedValue(0);
const trigger = () => {
console.log('fdfs')
}
const flingGesture = Gesture.Fling()
.direction(Directions.RIGHT)
.onStart((e) => {
position.value = withTiming(position.value + 10, { duration: 100 });
console.log(e)
// trigger()
});
const flingGestureLeft = Gesture.Fling()
.direction(Directions.LEFT)
.onStart((e) => {
position.value = withTiming(position.value - 10, { duration: 100 });
// trigger()
});
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ translateX: position.value }],
}));
return (
<GestureDetector gesture={Gesture.Simultaneous(flingGestureLeft, flingGesture)}>
<Animated.View style={[{ width: 100, height: 30, backgroundColor: 'red' }, animatedStyle]} />
</GestureDetector>
);
}
export default HomePage;
this work without problem when i fling my bloc to left or right, but when i tried to call an exeternal function like the trigger(), my app crash. Is a bug of the gesture detector or there is something to add?

The reanimated, gesture handler hooks and callbacks works on the UI thread and the trigger function you defined is by default on the JS thread, so you can not use it directly.
There are two solutions to this:
add 'worklet' in the trigger function as below
const trigger = () => {
'worklet'
console.log('fdfs')
}
Or wrap your function with 'runOnJS' from reanimated as below
import { runOnJS } from 'react-native-reanimated';
const flingGesture = Gesture.Fling()
.direction(Directions.RIGHT)
.onStart((e) => {
position.value = withTiming(position.value + 10, { duration: 100 });
console.log(e)
runOnJS(trigger)()
});
Note:- syntax for runOnJS is like 'runOnJS(functionName)(params).
So if your function takes two params (Ex. 1st number and 2nd string), you would call it like this:- runOnJS(trigger)(1, 'dummyString')
For more details you can read the docs from reanimated and gesture-handler.

Related

Warning: React has detected a change in the order of Hooks

I have run into this error in my code, and don't really know how to solve it, can anyone help me?
I get the following error message:
ERROR Warning: React has detected a change in the order of Hooks called by ScreenA. This will lead to bugs and errors if not fixed. For more information, read the Rules of Hooks: https://reactjs.org/link/rules-of-hooks
import React, { useCallback, useEffect, useState } from "react";
import { View, Text, StyleSheet, Pressable } from "react-native";
import { useNavigation } from '#react-navigation/native';
import { DancingScript_400Regular } from "#expo-google-fonts/dancing-script";
import * as SplashScreen from 'expo-splash-screen';
import * as Font from 'expo-font';
export default function ScreenA({ route }) {
const [appIsReady, setAppIsReady] = useState(false);
useEffect(() => {
async function prepare() {
try {
// Keep the splash screen visible while we fetch resources
await SplashScreen.preventAutoHideAsync();
// Pre-load fonts, make any API calls you need to do here
await Font.loadAsync({ DancingScript_400Regular });
// Artificially delay for two seconds to simulate a slow loading
// experience. Please remove this if you copy and paste the code!
await new Promise(resolve => setTimeout(resolve, 2000));
} catch (e) {
console.warn(e);
} finally {
// Tell the application to render
setAppIsReady(true);
}
}
prepare();
}, []);
const onLayoutRootView = useCallback(async () => {
if (appIsReady) {
// This tells the splash screen to hide immediately! If we call this after
// `setAppIsReady`, then we may see a blank screen while the app is
// loading its initial state and rendering its first pixels. So instead,
// we hide the splash screen once we know the root view has already
// performed layout.
await SplashScreen.hideAsync();
}
}, [appIsReady]);
if (!appIsReady) {
return null;
}
const navigation = useNavigation();
const onPressHandler = () => {
// navigation.navigate('Screen_B', { itemName: 'Item from Screen A', itemID: 12 });
}
return (
<View style={styles.body} onLayout={onLayoutRootView}>
<Text style={styles.text}>
Screen A
</Text>
<Pressable
onPress={onPressHandler}
style={({ pressed }) => ({ backgroundColor: pressed ? '#ddd' : '#0f0' })}
>
<Text style={styles.text}>
Go To Screen B
</Text>
</Pressable>
<Text style={styles.text}>{route.params?.Message}</Text>
</View>
)
}
const styles = StyleSheet.create({
body: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
text: {
fontSize: 40,
margin: 10,
fontFamily: 'DancingScript_400Regular'
}
})
I have read the rules of hooks: https://reactjs.org/docs/hooks-rules.html
The output is correct, but i want to fix this error before i add more additions to the app
You need to move useNavigation use before early returns.
Instead, always use Hooks at the top level of your React function, before any early returns.
The key is you need to call all the hooks in the exact same order on every component lifecycle update, which means you can't use hooks with conditional operators or loop statements such as:
if (customValue) useHook();
// or
for (let i = 0; i< customValue; i++) useHook();
// or
if (customValue) return;
useHook();
So moving const navigation = useNavigation(); before if (!appIsReady) {return null;}, should solve your problem:
export default function ScreenA({ route }) {
const [appIsReady, setAppIsReady] = useState(false);
const navigation = useNavigation();
// ...
}

Issue sending & receiving streams between two clients in LiveKit's React Native SDK

I'm trying to build on the example app provided by livekit, so far I've implemented everything like the example app and I've been successful with connecting to a room on example website, I recieve audio from website, but I don't read the video stream, and I also can't send audio or video at all.
Steps to reproduce the behavior:
add the following to index.js
import { registerRootComponent } from "expo";
import { registerGlobals } from "livekit-react-native";
import App from "./App";
registerRootComponent(App);
registerGlobals();
Rendering the following component in App.tsx
import { Participant, Room, Track } from "livekit-client";
import {
useRoom,
useParticipant,
AudioSession,
VideoView,
} from "livekit-react-native";
import { useEffect, useState } from "react";
import { Text, ListRenderItem, StyleSheet, FlatList, View } from "react-native";
import { ParticipantView } from "./ParticipantView";
import { RoomControls } from "./RoomControls";
import type { TrackPublication } from "livekit-client";
const App = () => {
// Create a room state
const [, setIsConnected] = useState(false);
const [room] = useState(
() =>
new Room({
publishDefaults: { simulcast: false },
adaptiveStream: true,
})
);
// Get the participants from the room
const { participants } = useRoom(room);
const url = "[hard-coded-url]";
const token =
"[hard-coded-token";
useEffect(() => {
let connect = async () => {
// If you wish to configure audio, uncomment the following:
await AudioSession.configureAudio({
android: {
preferredOutputList: ["speaker"],
},
ios: {
defaultOutput: "speaker",
},
});
await AudioSession.startAudioSession();
await room.connect(url, token, {});
await room.localParticipant.setCameraEnabled(true);
await room.localParticipant.setMicrophoneEnabled(true);
await room.localParticipant.enableCameraAndMicrophone();
console.log("connected to ", url);
setIsConnected(true);
};
connect();
return () => {
room.disconnect();
AudioSession.stopAudioSession();
};
}, [url, token, room]);
// Setup views.
const stageView = participants.length > 0 && (
<ParticipantView participant={participants[0]} style={styles.stage} />
);
const renderParticipant: ListRenderItem<Participant> = ({ item }) => {
return (
<ParticipantView participant={item} style={styles.otherParticipantView} />
);
};
const otherParticipantsView = participants.length > 0 && (
<FlatList
data={participants}
renderItem={renderParticipant}
keyExtractor={(item) => item.sid}
horizontal={true}
style={styles.otherParticipantsList}
/>
);
const { cameraPublication, microphonePublication } = useParticipant(
room.localParticipant
);
return (
<View style={styles.container}>
{stageView}
{otherParticipantsView}
<RoomControls
micEnabled={isTrackEnabled(microphonePublication)}
setMicEnabled={(enabled: boolean) => {
room.localParticipant.setMicrophoneEnabled(enabled);
}}
cameraEnabled={isTrackEnabled(cameraPublication)}
setCameraEnabled={(enabled: boolean) => {
room.localParticipant.setCameraEnabled(enabled);
}}
onDisconnectClick={() => {
// navigation.pop();
console.log("disconnected");
}}
/>
</View>
);
};
function isTrackEnabled(pub?: TrackPublication): boolean {
return !(pub?.isMuted ?? true);
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: "center",
justifyContent: "center",
},
stage: {
flex: 1,
width: "100%",
},
otherParticipantsList: {
width: "100%",
height: 150,
flexGrow: 0,
},
otherParticipantView: {
width: 150,
height: 150,
},
});
export default App;
the components used here are mostly the same as what's in the example, I've removed the screensharing logic and the messages
5. I run the app using an expo development build
6. it will log that it's connected, you'll be able to hear sound from the remote participant, but not see any video or send any sound.
7. if i try to add
await room.localParticipant.enableCameraAndMicrophone();
in the useEffect, I get the following error:
Possible Unhandled Promise Rejection (id: 0):
Error: Not implemented.
getSettings#http://192.168.1.150:8081/index.bundle?platform=ios&dev=true&hot=false:103733:24
#http://192.168.1.150:8081/index.bundle?platform=ios&dev=true&hot=false:120307:109
generatorResume#[native code]
asyncGeneratorStep#http://192.168.1.150:8081/index.bundle?platform=ios&dev=true&hot=false:21908:26
_next#http://192.168.1.150:8081/index.bundle?platform=ios&dev=true&hot=false:21927:29
#http://192.168.1.150:8081/index.bundle?platform=ios&dev=true&hot=false:21932:14
tryCallTwo#http://192.168.1.150:8081/index.bundle?platform=ios&dev=true&hot=false:26656:9
doResolve#http://192.168.1.150:8081/index.bundle?platform=ios&dev=true&hot=false:26788:25
Promise#http://192.168.1.150:8081/index.bundle?platform=ios&dev=true&hot=false:26675:14
#http://192.168.1.150:8081/index.bundle?platform=ios&dev=true&hot=false:21924:25
#http://192.168.1.150:8081/index.bundle?platform=ios&dev=true&hot=false:120173:52
generatorResume#[native code]
asyncGeneratorStep#http://192.168.1.150:8081/index.bundle?platform=ios&dev=true&hot=false:21908:26
_next#http://192.168.1.150:8081/index.bundle?platform=ios&dev=true&hot=false:21927:29
tryCallOne#http://192.168.1.150:8081/index.bundle?platform=ios&dev=true&hot=false:26648:16
#http://192.168.1.150:8081/index.bundle?platform=ios&dev=true&hot=false:26729:27
#http://192.168.1.150:8081/index.bundle?platform=ios&dev=true&hot=false:27687:26
_callTimer#http://192.168.1.150:8081/index.bundle?platform=ios&dev=true&hot=false:27602:17
_callReactNativeMicrotasksPass#http://192.168.1.150:8081/index.bundle?platform=ios&dev=true&hot=false:27635:17
callReactNativeMicrotasks#http://192.168.1.150:8081/index.bundle?platform=ios&dev=true&hot=false:27799:44
__callReactNativeMicrotasks#http://192.168.1.150:8081/index.bundle?platform=ios&dev=true&hot=false:21006:46
#http://192.168.1.150:8081/index.bundle?platform=ios&dev=true&hot=false:20806:45
__guard#http://192.168.1.150:8081/index.bundle?platform=ios&dev=true&hot=false:20986:15
flushedQueue#http://192.168.1.150:8081/index.bundle?platform=ios&dev=true&hot=false:20805:21
flushedQueue#[native code]
Expected behavior
This should both receive & send video and audio streams between the two clients

Reanimated 2 reusable animation in custom hook

How can I create a reusable React hook with animation style with Reanimated 2? I have an animation that is working on one element, but if I try to use the same animation on multiple elements on same screen only the first one registered is animating. It is too much animation code to duplicate it everywhere I need this animation, so how can I share this between multiple components on the same screen? And tips for making the animation simpler is also much appreciated.
import {useEffect} from 'react';
import {
cancelAnimation,
Easing,
useAnimatedStyle,
useSharedValue,
withRepeat,
withSequence,
withTiming,
} from 'react-native-reanimated';
const usePulseAnimation = ({shouldAnimate}: {shouldAnimate: boolean}) => {
const titleOpacity = useSharedValue(1);
const isAnimating = useSharedValue(false);
useEffect(() => {
if (shouldAnimate && !isAnimating.value) {
isAnimating.value = true;
titleOpacity.value = withRepeat(
withSequence(
withTiming(0.2, {duration: 700, easing: Easing.inOut(Easing.ease)}),
withTiming(
1,
{duration: 700, easing: Easing.inOut(Easing.ease)},
() => {
if (!shouldAnimate) {
cancelAnimation(titleOpacity);
}
},
),
),
-1,
false,
() => {
if (titleOpacity.value < 1) {
titleOpacity.value = withSequence(
withTiming(0.2, {
duration: 700,
easing: Easing.inOut(Easing.ease),
}),
withTiming(
1,
{duration: 700, easing: Easing.inOut(Easing.ease)},
() => {
isAnimating.value = false;
},
),
);
} else {
titleOpacity.value = withTiming(
1,
{
duration: 700,
easing: Easing.inOut(Easing.ease),
},
() => {
isAnimating.value = false;
},
);
}
},
);
} else {
isAnimating.value = false;
cancelAnimation(titleOpacity);
}
}, [shouldAnimate, isAnimating, titleOpacity]);
const pulseAnimationStyle = useAnimatedStyle(() => {
return {
opacity: titleOpacity.value,
};
});
return {pulseAnimationStyle, isAnimating: isAnimating.value};
};
export default usePulseAnimation;
And I am using it like this inside a component:
const {pulseAnimationStyle} = usePulseAnimation({
shouldAnimate: true,
});
return (
<Animated.View
style={[
{backgroundColor: 'white', height: 100, width: 100},
pulseAnimationStyle,
]}
/>
);
The approach that I've taken is to write my Animations as wrapper components.
This way you can build up a library of these animation components and then simply wrap whatever needs to be animated.
e.g.
//Wrapper component type:
export type ShakeProps = {
// Animation:
children: React.ReactNode;
repeat?: boolean;
repeatEvery?: number;
}
// Wrapper component:
const Shake: FC<ShakeProps> = ({
children,
repeat = false,
repeatEvery = 5000,
}) => {
const shiftY = useSharedValue(0);
const animatedStyles = useAnimatedStyle(() => ({
//Animation properties...
}));
const shake = () => {
//Update shared values...
}
// Loop every X seconds:
const repeatAnimation = () => {
shake();
setTimeout(() => {
repeatAnimation();
}, repeatEvery);
}
// Start Animations on component Init:
useEffect(() => {
// Run animation continously:
if(repeat){
repeatAnimation();
}
// OR ~ call once:
else{
shake();
}
}, []);
return (
<Animated.View style={[animatedStyles]}>
{children}
</Animated.View>
)
}
export default Shake;
Wrapper Component Usage:
import Shake from "../../util/animated-components/shake";
const Screen: FC = () => {
return (
<Shake repeat={true} repeatEvery={5000}>
{/* Whatever needs to be animated!...e.g. */}
<Text>Hello World!</Text>
</Shake>
)
}
From their docs:
CAUTION
Animated styles cannot be shared between views.
To work around this you can generate multiple useAnimatedStyle in top-level loop (number of iterations must be static, see React's Rules of Hooks for more information).
https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

Question on switching threads in react native reanimated

So I have this simple animation where if you drag an element it will return back to (0, 0) on animation end,
import React from "react"
import { SafeAreaView } from "react-native"
import { PanGestureHandler } from "react-native-gesture-handler"
import Animated, {
Easing,
runOnJS,
useAnimatedGestureHandler,
useAnimatedStyle,
useSharedValue,
withSpring,
withTiming
} from "react-native-reanimated"
const Comp: React.FC = () => {
const x = useSharedValue(0)
const y = useSharedValue(0)
const translateAnim = useAnimatedStyle(() => {
return {
transform: [{ translateX: x.value }, { translateY: y.value }]
}
})
const drag = useAnimatedGestureHandler({
onStart: (e, ctx: { startX: number; startY: number }) => {
ctx.startX = x.value
ctx.startY = y.value
},
onActive: (e, ctx) => {
runOnJS(move)(
ctx.startX,
ctx.startY,
e.translationX,
e.translationY
)
},
onEnd: (e, ctx) => {
runOnJS(end)()
}
})
function move(
startX: number,
startY: number,
translateX: number,
translateY: number
) {
x.value = withTiming(startX + translateX, {
duration: 0,
easing: Easing.linear
})
y.value = withTiming(startY + translateY, {
duration: 0,
easing: Easing.linear
})
}
function end() {
x.value = withSpring(0)
y.value = withSpring(0)
}
return (
<SafeAreaView>
<PanGestureHandler onGestureEvent={drag}>
<Animated.View
style={[
{ backgroundColor: "red", height: 100, width: 100 },
translateAnim
]}></Animated.View>
</PanGestureHandler>
</SafeAreaView>
)
}
export default Comp
So as you can see I have move and end run in JS thread using runOnJs() function.
Does that mean withTiming will also run on JS thread or withTiming always runs on UI thread?
Also as you can see x.value = withTiming(...). Should I wrap this in runOnUI() function? Or to be precise when we set the animation value does it have to be run on UI thread?
I'm a bit late to your question, but for the sake of other answer-seekers:
When you assign an animation to your shared value, reanimated will automatically run that animation on the UI thread. So the answer to both of your questions is no, you don't have to do anything.
Note that the optional callback you pass to the animation will also be run on the UI thread.
See the documentation for this:
When using one of the hooks listed in the Reanimated API Reference, we automatically detect that the provided method is a worklet and do not require the directive to be specified. The method provided to the hook will be turned into a worklet and executed on the UI thread automatically.

Geolocation clearWatch(watchId) does not stop location tracking (React Native)

I'm trying to create simple example of location tracker and I'm stuck with following case. My basic goal is to toggle location watch by pressing start/end button. I'm doing separation of concerns by implementing custom react hook which is then used in App component:
useWatchLocation.js
import {useEffect, useRef, useState} from "react"
import {PermissionsAndroid} from "react-native"
import Geolocation from "react-native-geolocation-service"
const watchCurrentLocation = async (successCallback, errorCallback) => {
if (!(await PermissionsAndroid.check(PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION))) {
errorCallback("Permissions for location are not granted!")
}
return Geolocation.watchPosition(successCallback, errorCallback, {
timeout: 3000,
maximumAge: 500,
enableHighAccuracy: true,
distanceFilter: 0,
useSignificantChanges: false,
})
}
const stopWatchingLocation = (watchId) => {
Geolocation.clearWatch(watchId)
// Geolocation.stopObserving()
}
export default useWatchLocation = () => {
const [location, setLocation] = useState()
const [lastError, setLastError] = useState()
const [locationToggle, setLocationToggle] = useState(false)
const watchId = useRef(null)
const startLocationWatch = () => {
watchId.current = watchCurrentLocation(
(position) => {
setLocation(position)
},
(error) => {
setLastError(error)
}
)
}
const cancelLocationWatch = () => {
stopWatchingLocation(watchId.current)
setLocation(null)
setLastError(null)
}
const setLocationWatch = (flag) => {
setLocationToggle(flag)
}
// execution after render when locationToggle is changed
useEffect(() => {
if (locationToggle) {
startLocationWatch()
} else cancelLocationWatch()
return cancelLocationWatch()
}, [locationToggle])
// mount / unmount
useEffect(() => {
cancelLocationWatch()
}, [])
return { location, lastError, setLocationWatch }
}
App.js
import React from "react"
import {Button, Text, View} from "react-native"
import useWatchLocation from "./hooks/useWatchLocation"
export default App = () => {
const { location, lastError, setLocationWatch } = useWatchLocation()
return (
<View style={{ margin: 20 }}>
<View style={{ margin: 20, alignItems: "center" }}>
<Text>{location && `Time: ${new Date(location.timestamp).toLocaleTimeString()}`}</Text>
<Text>{location && `Latitude: ${location.coords.latitude}`}</Text>
<Text>{location && `Longitude: ${location.coords.longitude}`}</Text>
<Text>{lastError && `Error: ${lastError}`}</Text>
</View>
<View style={{ marginTop: 20, width: "100%", flexDirection: "row", justifyContent: "space-evenly" }}>
<Button onPress={() => {setLocationWatch(true)}} title="START" />
<Button onPress={() => {setLocationWatch(false)}} title="STOP" />
</View>
</View>
)
}
I have searched multiple examples which are online and code above should work. But the problem is when stop button is pressed location still keeps getting updated even though I invoke Geolocation.clearWatch(watchId).
I wrapped Geolocation calls to handle location permission and other possible debug stuff. It seems like watchId value that is saved using useRef hook inside useWatchLocation is invalid. My guess is based on attempting to call Geolocation.stopObserving() right after Geolocation.clearWatch(watchId). Subscription stops but I get warning:
Called stopObserving with existing subscriptions.
So I assume that original subscription was not cleared.
What am I missing/doing wrong?
EDIT: I figured out solution. But since isMounted pattern is generally considered antipattern: Does anyone have a better solution?
Ok, problem solved with isMounted pattern. isMounted.current is set at locationToggle effect to true and inside cancelLocationWatch to false:
const isMounted = useRef(null)
...
useEffect(() => {
if (locationToggle) {
isMounted.current = true // <--
startLocationWatch()
} else cancelLocationWatch()
return () => cancelLocationWatch()
}, [locationToggle])
...
const cancelLocationWatch = () => {
stopWatchingLocation(watchId.current)
setLocation(null)
setLastError(null)
isMounted.current = false // <--
}
And checked at mount / unmount effect, success and error callback:
const startLocationWatch = () => {
watchId.current = watchCurrentLocation(
(position) => {
if (isMounted.current) { // <--
setLocation(position)
}
},
(error) => {
if (isMounted.current) { // <--
setLastError(error)
}
}
)
}