Pass useAnimatedGestureHandler via forwardRef - react-native

I'm about to swap the old React Native Animated library with the new React Native Reanimated one to gain performance issues but I have encountered one problem I could not solve.
In all examples I found online, I saw that the GestureHandler, created with useAnimatedGestureHandler, is in the same component as the Animated.View. In reality that is sometimes not possible.
In my previous app, I just pass the GestureHandler object to the component via forwardRef but it seems React Native Reanimated is not able to do that. I don't know whether I have a syntax error or it is just a bug.
const App = () => {
const handlerRef = useAnimatedRef();
const y = useSharedValue(0);
handlerRef.current = useAnimatedGestureHandler({
onStart: (_, ctx) => {
ctx.startY = y.value;
},
onActive: ({translationX, translationY}, ctx) => {
y.value = translationY;
},
onEnd: () => {},
});
const animatedStyles = useAnimatedStyle(() => ({transform: [{translateY: withSpring(y.value)}]}));
const UsingHandlerDirect = () => (
<PanGestureHandler onGestureEvent={handlerRef.current} >
<Animated.View style={[styles.blueBox, animatedStyles]} />
</PanGestureHandler>
)
const UsingHandlerForwardRef = forwardRef(({animatedStyles}, ref) => (
<PanGestureHandler onGestureEvent={ref?.handlerRef?.current}>
<Animated.View style={[styles.redBox, animatedStyles]} />
</PanGestureHandler>
));
return (
<SafeAreaView>
<View style={styles.container}>
<UsingHandlerForwardRef ref={handlerRef} animatedStyles={animatedStyles}/>
<UsingHandlerDirect />
</View>
</SafeAreaView>
);
}
I have saved the GestureHandler in a useAnimatedRef handlerRef.current = useAnimatedGestureHandler({}) to make things more representable. Then I pass the the ref directly into the PanGestureHandler of the UsingHandlerDirect component. The result is that when I drag the blue box the box will follow the handler. So this version works.
But as soon as I pass the handlerRef to the UsingHandlerForwardRef component non of the gesture events get fired. I would expect that when I drag the red box will also follow the handler but it doesn't
Has someone an idea whether it's me or it's a bug in the library?
Cheers

I have given up on the idea to pass a ref around instead, I created a hook that connects both components with each other via context.
I created a simple hook
import { useSharedValue } from 'react-native-reanimated';
const useAppState = () => {
const sharedXValue = useSharedValue(0);
return {
sharedXValue,
};
};
export default useAppState;
that holds the shared value using useSharedValue from reanimated 2
The child component uses this value in the gestureHandler like that
const gestureHandler = useAnimatedGestureHandler({
onStart: (_, ctx) => {
ctx.startX = sharedXValue.value;
},
onActive: (event, ctx) => {
sharedXValue.value = ctx.startX + event.translationX;
},
onEnd: (_) => {
sharedXValue.value = withSpring(0);
},
});
and the Parent just consumes the hook value
const animatedStyle = useAnimatedStyle(() => {
return {
transform: [
{
translateX: -sharedXValue.value,
},
],
};
});
I have created a workable Snack which contains the 2 components - a Child with a blue box and a Parent with a red box

Related

React Native inactivity logout

Hello i want to make an inactivity log out for my app, so if the user doesn't do anything on the app for 3 minutes, the app will comeback to the login screen.
I'm using expo, react native navigation V6, and functional components.
i haven't been able to figure how to do it. please help.
I think i was able to do that on an app that i made 1 year ago, i think this code can you help you.
const ManageExpenses = ({ route, navigation }) => {
const [time, setTime] = useState(0);
useEffect(() => {
let mounted = true;
if (mounted) {
tick();
}
return () => mounted = false;
}, []);
function tick() {
let timer = setInterval(() => {
setTime((prevTime) => (prevTime = prevTime + 1));
}, 1000);
}
if (time >= 10) {
navigation.goBack();
}
function pressHandler() {
setTime(0)
}
return (
<Pressable onPress={pressHandler} style={styles.container}>
</Pressable>
);
export default ManageExpenses;
const styles = StyleSheet.create({
container: {
flex: 1,
}
})
I created a Pressable component around the entire screen and it redefine the time state when the user press the screen. I hope this will help you!

Expo-notifications trigger all useEffects in the application

I created the entire flow for expo-notifications, although I encounter one problem. Once I receive the notification, the UI of the specific type is re-rendered and - which is the core of the problem - all the useEffects with fetch get triggered in the application; it seems that it re-renders the entire application. Even disabling the update of the specific part of the UI (that I want to update) still causes that a notification makes the app to re-render.
I tried to find the cause of that, but no progress so far. Did anyone of you ever encountered this kind of problem? Why the app gets re-rendered entirely?
The function registerForPushNotificationsAsync is copy-pasted from their docs.
Here is my notification provider - I get notification correctly, but idk what causes the re-render and trigger all the useEffects:
const NotificationsProvider = () => {
const authenticationStatus = useSelector(authStatus);
const dispatch = useDispatch();
const [expoPushToken, setExpoPushToken] = useState("");
const [notification, setNotification] = useState<Notifications.Notification | null>(null);
useEffect(() => {
if (authenticationStatus === AUTHENTICATION_MESSAGES.AUTHENTICATION_SUCCESS) {
registerForPushNotificationsAsync()
.then((token) => setExpoPushToken(token))
.catch((error) => console.error(error));
const subscription = Notifications.addNotificationReceivedListener((receivedNotification) => {
setNotification(receivedNotification);
const { id, title } = receivedNotification.request.content.data;
console.log(receivedNotification.request.content.data);
dispatch(
addAsync(
[
{
id: id,
title: title,
},
],
1 * 1000
)
);
});
APP.tsx
const App = () => {
const [fontsLoaded] = useFonts({
Roboto_400Regular,
Roboto_500Medium,
});
return fontsLoaded ? (
<Provider store={store}>
<PaperProvider theme={theme}>
<NotificationsProvider />
</PaperProvider>
</Provider>
) : (
<AppLoading />
);
};

How to hide element when device keyboard active using hooks?

I wanted to convert a hide element when keyboard active HOC I found to the newer react-native version using hooks (useEffect), the original solution using the older react lifecycle hooks looks like this - https://stackoverflow.com/a/60500043/1829251
So I created a useHideWhenKeyboardOpen function that wraps the child element and should hide that child if the device keyboard is active using useEffect. But on render the child element useHideWhenKeyboardOpen isn't displayed regardless of keyboard displayed.
When I've debugged the app I see the following error which I didn't fully understand,because the useHideWhenKeyboardOpen function does return a <BaseComponent>:
ExceptionsManager.js:179 Warning: Functions are not valid as a React
child. This may happen if you return a Component instead of from render. Or maybe you meant to call this function rather than
return it.
in RCTView (at View.js:34)
Question:
How can you attach keyboard displayed listener to a component in the render?
Example useHideWhenKeyboardOpen function:
import React, { useEffect, useState } from 'react';
import { Keyboard } from 'react-native';
// Wrapper component which hides child node when the device keyboard is open.
const useHideWhenKeyboardOpen = (BaseComponent: any) => (props: any) => {
// todo: finish refactoring.....
const [isKeyboadVisible, setIsKeyboadVisible] = useState(false);
const _keyboardDidShow = () => {
setIsKeyboadVisible(true);
};
const _keyboardDidHide = () => {
setIsKeyboadVisible(false);
};
/**
* Add callbacks to keyboard display events, cleanup in useeffect return.
*/
useEffect(() => {
console.log('isKeyboadVisible: ' + isKeyboadVisible);
Keyboard.addListener('keyboardDidShow', _keyboardDidShow);
Keyboard.addListener('keyboardDidHide', _keyboardDidHide);
return () => {
Keyboard.removeCurrentListener();
};
}, [_keyboardDidHide, _keyboardDidShow]);
return isKeyboadVisible ? null : <BaseComponent {...props}></BaseComponent>;
};
export default useHideWhenKeyboardOpen;
Example Usage:
return(
.
.
.
{useHideWhenKeyboardOpen(
<View style={[styles.buttonContainer]}>
<Button
icon={<Icon name="save" size={16} color="white" />}
title={strings.STOCKS_FEED.submit}
iconRight={true}
onPress={() => {
toggleSettings();
}}
style={styles.submitButton}
raised={true}
/>
</View>,
)}
)
Mindset shift will help: think of hooks as data source rather than JSX factory:
const isKeyboardShown = useKeyboardStatus();
...
{!isKeyboardShown && (...
Accordingly your hook will just return current status(your current version look rather as a HOC):
const useHideWhenKeyboardOpen = () => {
const [isKeyboadVisible, setIsKeyboadVisible] = useState(false);
const _keyboardDidShow = useCallback(() => {
setIsKeyboadVisible(true);
}, []);
const _keyboardDidHide = useCallback(() => {
setIsKeyboadVisible(false);
}, []);
useEffect(() => {
Keyboard.addListener('keyboardDidShow', _keyboardDidShow);
Keyboard.addListener('keyboardDidHide', _keyboardDidHide);
return () => {
Keyboard.addListener('keyboardDidShow', _keyboardDidShow);
Keyboard.addListener('keyboardDidHide', _keyboardDidHide);
};
}, [_keyboardDidHide, _keyboardDidShow]);
return isKeyboadVisible;
};
Note usage of useCallback. Without it your hook will unsubscribe from Keyboard and subscribe again on every render(since _keyboardDidHide would be referentially different each time and would trigger useEffect). And that's definitely redundant.

React Native onLayout with React Hooks

I want to measure the size of a React Native View every time it renders, and save it to state. If element layout didn't change the effect should not run.
It's easy to do with a class based component, where onLayout can be used. But what do I do in a functional component where I use React Hooks?
I've read about useLayoutEffect. If that's the way to go, do you have an example of how to use it?
I made this custom hook called useDimensions. This is how far I've got:
const useDimensions = () => {
const ref = useRef(null);
const [dimensions, setDimensions] = useState({});
useLayoutEffect(
() => {
setDimensions(/* Get size of element here? How? */);
},
[ref.current],
);
return [ref, dimensions];
};
And I use the hook and add the ref to the view that I want to measure the dimensions of.
const [ref, dimensions] = useDimensions();
return (
<View ref={ref}>
...
</View>
);
I've tried to debug ref.current but didn't find anything useful there. I've also tried measure() inside the effect hook:
ref.current.measure((size) => {
setDimensions(size); // size is always 0
});
If you could like a more self-contained version of this here is a custom hook version for React Native:
const useComponentSize = () => {
const [size, setSize] = useState(null);
const onLayout = useCallback(event => {
const { width, height } = event.nativeEvent.layout;
setSize({ width, height });
}, []);
return [size, onLayout];
};
const Component = () => {
const [size, onLayout] = useComponentSize();
return <View onLayout={onLayout} />;
};
You had the right idea, it just needed a couple of tweaks... mainly, handing in the element ref and using elementRef (not elementRef.current) in the useEffect dependency array.
(Regarding useEffect vs useLayoutEffect, as you're only measuring rather than mutating the DOM then I believe useEffect is the way to go, but you can swap it out like-for-like if you need to)
const useDimensions = elementRef => {
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
useEffect(() => {
const el = elementRef.current;
setDimensions({ width: el.clientWidth, height: el.clientHeight });
}, [elementRef]);
return [dimensions];
};
Use it like this:
function App() {
const divRef = useRef(null);
const [dimensions] = useDimensions(divRef);
return (
<div ref={divRef} className="App">
<div>
width: {dimensions.width}, height: {dimensions.height}
</div>
</div>
);
}
Working codesandbox here
Edited to Add React Native version:
For React Native you can use useState with onLayout like this:
const App=()=>{
const [dimensions, setDimensions] = useState({width:0, height:0})
return (
<View onLayout={(event) => {
const {x, y, width, height} = event.nativeEvent.layout;
setDimensions({width:width, height:height});
}}>
<Text}>
height: {dimensions.height} width: {dimensions.width}
</Text>
</View>
);
}
As a refinement to
matto1990's answer, and to answer Kerkness's question - here's an example custom hook that supplies the x, y position as well as the layout size:
const useComponentLayout = () => {
const [layout, setLayout] = React.useState(null);
const onLayout = React.useCallback(event => {
const layout = event.nativeEvent.layout;
setLayout(layout);
}, [])
return [layout, onLayout]
}
const Component = () => {
const [{ height, width, x, y }, onLayout] = useComponentSize();
return <View onLayout={onLayout} />;
};

How to update the header while the component is still rendered using React Navigation?

I'm writing a React Native app and I'm using React Navigation (V2) with it. I want to update the navigationOptions and add a new button, after my component has updated. Here is the code with which I tried it:
static navigationOptions = ({ navigation }) => {
const options = {
headerTitle: SCREEN_TEXT_MENU_HEADER,
headerStyle: {
borderBottomWidth: 0,
marginBottom: -5
}
};
if (navigation.getParam("drawer", true)) {
options["headerLeft"] = (
<HeaderIconButton
onClick={() => {
navigation.openDrawer();
}}
icon={require("../../assets/icons/burgerMenu.png")}
/>
);
}
if (navigation.getParam("renderBillButton", false)) {
options["headerRight"] = (
<HeaderIconButton
onClick={() => {
navigation.navigate("BillScreen");
}}
type="primary"
icon={require("../../assets/icons/euro.png")}
/>
);
}
return options;
};
componentDidUpdate = prevProps => {
const { navigation, orders } = this.props;
if (prevProps.orders.length !== orders.length) {
navigation.setParams({
renderBillButton: orders.length > 0
});
}
};
The problem with this approach is, that the navigationOptions do not get reset after componentDidUpdate(). How can I dynamically adjust the header with React Navigation?
You can use this.props.navigation.setParams() function to update the navigation state params.
Reference: https://reactnavigation.org/docs/en/headers.html#updating-navigationoptions-with-setparams
Okay here is what went wrong: I also had to call the same code within componentDidMount(), otherwise it would not affect the page upon loading. So in addition to the code of my question I added:
componentDidMount = () => {
const { navigation, order } = this.props;
navigation.setParams({
renderBillButton: orders.length > 0
});
}