How to unmount screen on blur in a StackNavigator? - react-native

Im using React Navigation x5, I have StackNavigator that's loading bunch of screens. I want to unmount one particular screen component on blur to get the same result of using unmountOnBlur when using a DrawerNavigator.
How can I achieve that?

There are currently three ways to do it since 5.x. You can either
use useFocusEffect hook to trigger an action (recommended), or
use useIsFocused hook, or
listen to the focus event
Example of useFocusEffect
import { useFocusEffect } from '#react-navigation/native';
function Sample(props) {
const [shouldHide, setShouldHide] = React.useState(false);
useFocusEffect(() => {
setShouldHide(false)
return () => {
setShouldHide(true)
}
});
return shouldHide ? null : <InnerComponent {...props} />;
}
Example of useIsFocused hook
import * as React from 'react';
import { Text } from 'react-native';
import { useIsFocused } from '#react-navigation/native';
function Profile() {
// This hook returns `true` if the screen is focused, `false` otherwise
const isFocused = useIsFocused();
return {isFocused ? <Text>Inner component</Text> : null};
}
Note:
Using this hook component may introduce unnecessary component re-renders as a screen comes in and out of focus. ...
Hence we recommend to use this hook only if you need to trigger a re-render.
More information can be found in the documentation Call a function when focused screen changes
Besides that, stack navigators come with a prop called detachInactiveScreens which is enabled by default. Link to the documentation
detachInactiveScreens
Boolean used to indicate whether inactive screens should be detached from the view hierarchy to save memory. Make sure to call enableScreens from react-native-screens to make it work. Defaults to true.
(The instruction on enabling React Native Screens can be found at https://reactnavigation.org/docs/react-native-screens/#setup-when-you-are-using-expo)
Inside the stack navigator's options object, there is also a detachPreviousScreen prop to customise the behaviour of certain screens. This option is usually enabled as well.
detachPreviousScreen
Boolean used to indicate whether to detach the previous screen from the view hierarchy to save memory. Set it to false if you need the previous screen to be seen through the active screen. Only applicable if detachInactiveScreens isn't set to false. Defaults to false for the last screen when mode='modal', otherwise true.

Related

Which React navigation action to use to unmount

I have a react native app with two screens (actually too many): ScreenA and ScreenB. In ScreenA, I am doing some API calls and in ScreenB, I have some animation. I am using stack navigator to visit ScreenB from ScreenA. In ScreenB, I have some animation in my useEffect() hook and also from gestures. When I move away from ScreenB to ScreenA and then back to ScreenB, the state of the screen is not changed. It remains the same. I want the entire animation to reset, the useEffect() to be called again, but only for ScreenB. I want all my other mounted screens to be mounted, to avoid calling APIs and displaying data again but ScreenB to reset to its initial state if I move away from it. How can I achieve this in most efficient way possible? Thanks
You could use useIsFocused in a useEffect and reset the animation with something like
const isFocused = useIsFocused();
useEffect(()=>{
if(isFocused){
// start your animations
}
else{
// stop your animations
}
},[isFocused])

How unmount a hook after going to new screen with navigate

The context is a simple React Native app with React Navigation.
There are 3 screens.
The first simply displays a button to go to second screen using navigation.navigate("SecondScreen").
The Second contains a hook (see code below) that adds a listener to listen the mouse position. This hook adds the listener in a useEffect hook and removes the listener in the useEffect cleanup function. I just added a console.log in the listener function to see when the function is triggered.
This screen contains also a button to navigate to the Third screen, that only shows a text.
If I go from first screen to second screen: listener in hook start running. Good.
If I go back to the first screen using default react navigation 's back button in header. the listener stops. Good.
If I go again to second screen, then listener runs again. Good.
But if I now go from second screen to third screen, the listener is still running. Not Good.
How can I unmount the hook when going to third screen, and mount it again when going back to second screen?
Please read the following before answering :
I know that:
this is due to the fact that react navigation kills second screen when we go back to first screen, and then trigger the cleanup function returned by the useEffect in the hook. And that it doesn't kill second screen when we navigate to third screen, and then doesn't trigger the cleanup function.
the react navigation's hook useFocusEffect could be used to resolve this kind of problem. But it can't be used here because it will involve to replace the useEffect in the hook by the useFocusEffect. And I want my hook to be usable in every context, even if react navigation is not installed. More, I'm using here a custom hook for explanation, but it's the same problem for any hook (for example, the native useWindowDimensions).
Then does anyone know how I could manage this case to avoid to have the listener running on third screen ?
This is the code of the hook sample, that I take from https://github.com/rehooks/window-mouse-position/blob/master/index.js, but any hook could be used.
"use strict";
let { useState, useEffect } = require("react");
function useWindowMousePosition() {
let [WindowMousePosition, setWindowMousePosition] = useState({
x: null,
y: null
});
function handleMouseMove(e) {
console.log("handleMouseMove");
setWindowMousePosition({
x: e.pageX,
y: e.pageY
});
}
useEffect(() => {
window.addEventListener("mousemove", handleMouseMove);
return () => {
window.removeEventListener("mousemove", handleMouseMove);
};
}, []);
return WindowMousePosition;
}
module.exports = useWindowMousePosition;
the react navigation's hook useFocusEffect could be used to resolve this kind of problem. But it can't be used here because it will involve to replace the useEffect in the hook by the useFocusEffect. And I want my hook to be usable in every context, even if react navigation is not installed
So your hook somehow needs to know about the navigation state. If you can't use useFocusEffect, you'll need to pass the information about whether the screen is focused or not (e.g. with an enabled prop).
function useWindowMousePosition({ enabled = true } = {}) {
let [WindowMousePosition, setWindowMousePosition] = useState({
x: null,
y: null
});
useEffect(() => {
if (!enabled) {
return;
}
function handleMouseMove(e) {
console.log("handleMouseMove");
setWindowMousePosition({
x: e.pageX,
y: e.pageY
});
}
window.addEventListener("mousemove", handleMouseMove);
return () => {
window.removeEventListener("mousemove", handleMouseMove);
};
}, [enabled]);
return WindowMousePosition;
}
And then pass enabled based on screen focus:
const isFocused = useIsFocused();
const windowMousePosition = useWindowMousePosition({ enabled: isFocused });
Note that this approach will need the screen to re-render when it's blurred/focused unlike useFocusEffect.

useFocusEffect hook is invoked even screen is in background when update global state on the screen in foreground

I am developing react-native project. This question is mainly about the useFocusEffect hook behaviour in my project.
I have several screens. There is a share data & each screen can update the data. So, I decided to use context to host the data via useState hook. When one screen setMyData(), the up-to-date data is available to all screens.
My data context to host shared data via useState hook:
import React, {useState} from 'react';
const MyDataContext = React.createContext();
export const MyDataProvider = ({children}) => {
const [myData, setMyData] = useState([]);
...
return (
<MyDataContext.Provider
value={{
myData,
setMyData,
}}>
{children}
</MyDataContext.Provider>
);
}
export default MyDataContext;
(My App component is wrapped by the MyDataContext.Provider, I just don't show that part here since it is not the point of my question)
In each of my screen components, I need to process my data once when the screen is on the foreground (visible), and update the global state with updated data. I use useFocusEffect hook. Something like below:
import {useFocusEffect} from '#react-navigation/native';
import MyDataContext from '../context/MyDataContext';
const MyScreenOne = ({navigation})=> {
const {myData, setMyData} = useContext(MyDataContext);
useFocusEffect(() => {
console.log('################# Screen one is FOCUSED! ');
const updatedData = processData([...myData]);
// set my data
setMyData(updatedData);
return () => {
console.log('Screen one was unfocused');
};
}, []);//empty array to force code in hook only run once
return (<View>
<MyButton onPress={()=> navigation.navigate("MyScreenTwo")}>
...
</View>
)
}
...
As you can see I have a button which navigates user to another screen. The other screen has the same structure of having that useFocusEffect hook in which it processes & updates data of global state.
When I run my app, the following happens to me:
At first MyScreenOne is launched. useFocusEffect is invoked, data is processed and updated and set to global state via the setMyData()
Since setMyData() is called in above step, the re-rendering happens, at this time, the useFocusEffect of MyScreenOne is not invoked again, which is good and expected.
Now I press the button which navigates to MyScreenTwo. Same process happens on MyScreenTwo: useFocusEffect is invoked, data is processed and updated and set to global state via the setMyData(). NOTICE: MyScreenOne is now not visible and in background stack/memory.
Since in above step the global state is updated by the second screen, re-rendering happens again to all screens in memory. This time surprisingly the useFocusEffect of MyScreenOne is invoked again, because of that the global state is updated again by the hook in MyScreenOne again, and re-rendering happens again to screens in memory.
As you can imaging , endless re-rendering happens now due to in above step the background screen MyScreenOne's useFocusEffect is invoked surprisingly.
Isn't useFocusEffect supposed to be run only when screen is visible? Why the useFocusEffect hook of background screen is also invoked when re-rendering happens? How to achieve what I need to only run the process data & update data once on each screen when the screen is visible in the meantime the data can be shared by all screens?
useFocusEffect doesn't take a dependency array. You need to pass useCallback as mentioned in the docs:
useFocusEffect(
React.useCallback(() => {
// whatever
}, [])
);
https://reactnavigation.org/docs/use-focus-effect/

Why does Switch in ReactNative show toggle animation although its value prop is not changed? (iOS)

I am experiencing some behavior of a ReactNative Switch that I cannot explain.
I have a very simple switch (see code below). It has a fixed value prop and its onValueChange-callback is only a log. As a switch is a controlled component, I thought the UI is only rerendered when the value prop changes.
Despite my assumptions the switch shows a quick animation of toggling to false and then jumps back to true. Why is that happening? Does it have to do with the iOS specific component that is used by ReactNative?
import React from "react";
import { Switch, View } from "react-native";
export const SwitchCell = () => {
return (
<View>
<Switch onValueChange={() => console.log("Test")} value={true} />
</View>
);
};
If you want to block the event there is a prop for switch called "onChange" where as parameter you will receive the event and the new value, you can execute a check to decide if will be necessary to set the new property o if it won't change.
In case you doesn't want to change the value of switch you have to call the methods "preventDefault()"

React-native / redux - how to re-initialize screen via navigation?

I'm developing a react-native / redux app with a bottom-tab-navigator similar to the example at https://reactnavigation.org/docs/en/tab-based-navigation.html#customizing-the-appearance. My screens all connect to a Redux store and display shared data, however I'd like at least one of these screens to ignore the current data in the store and instead re-initialize this data each time it's navigated to (instead of continuing to display the data in whatever state it was last left in).
The screen has a method to do this, but I can't figure out how to call it after the first time the screen is rendered (e.g. from the constructor or componentDidMount() method). I can't call it from the render() method as this causes a "Cannot update during an existing state transition" error.
I need my navigator to somehow cause my HomeScreen.initializeData() method to be invoked each time the Home icon is pressed, but how do I do this?
HomeScreen.js:
initializeData() {
this.props.resetData(initialValue);
}
const initialValue = ...
(resetData() is a dispatch function that re-initializes the Redux store).
Updating state from render() would create an infinite loop. Also, you don’t want to run your state update every time the component re-render, only when the tab button is pressed. This tells me that the proper place to make your state update is some onPress function on the tab button.
So the question now relies on how to implement some onPress function on a tab button. I believe this answer this question:
Is there an onPress for TabNavigator tab in react-navigation?
So I found an answer, it's a little more complicated than might be expected: As Vinicius has pointed out I need to use the tabBarOnPress navigation option, but I also need to make my dispatch function available to this navigation option.
To do this I found I need to pass a reference to my dispatch function (which is available as a property of my screen) into the navigation option, so I've used navigation params to do this and here's what I've ended up with:
HomeScreen.js:
componentDidMount() {
initializeData(this.props);
this.props.navigation.setParams({ homeProps: this.props });
}
export const initializeData = (homeProps) => {
homeProps.resetData(initialValue);
};
const initialValue = ...
AppNavigator.js:
tabBarOnPress: ({navigation, defaultHandler}) => {
const routeName = navigation.state.routeName;
if (navigation.state.params === undefined) {
// no params available
} else if (routeName === 'Home') {
let homeProps = navigation.getParam('homeProps', null);
initializeData(homeProps);
} else if (routeName === ...
...
}
defaultHandler();
}
Notes:
I'm passing props as a navigation param rather than my dispatch function (which also works) as it's more flexible (e.g. it makes all of my dispatch functions available).
initializeData() is called both during construction of HomeScreen (for the first time the screen is displayed) and from the navigation icon (for subsequent displays of the screen).
It's necessary to check that params is defined within the navigation option as it'll be undefined the first time the screen is displayed (as screen construction has yet to occur). This also makes it necessary to call initializeData() during screen construction.