I'm working on porting an app to React-Native to test it out. When I pop back to a previous view in the navigator stack (hit the back button) I'd like to run some code. Is there a viewWillAppear method? I see on the Navigator there is a "onDidFocus()" callback which sounds like it might be the right thing.. but there doesn't appear to be anything like that on NavigatorIOS
I find a way to simulate viewDidAppear and viewDidDisappear in UIKit,
but i'm not sure if it's a "right" way.
componentDidMount: function() {
// your code here
var currentRoute = this.props.navigator.navigationContext.currentRoute;
this.props.navigator.navigationContext.addListener('didfocus', (event) => {
//didfocus emit in componentDidMount
if (currentRoute === event.data.route) {
console.log("me didAppear");
} else {
console.log("me didDisappear, other didAppear");
}
console.log(event.data.route);
});
},
For people who are using hooks and react navigation version 5.x, I think you can do this to expect similar behavior of viewDidAppear:
import React, {useCallback } from "react";
import { useFocusEffect } from "#react-navigation/native";
const SomeComponent = () => {
useFocusEffect(
useCallback(() => {
//View did appear
}, [])
);
//Other codes
}
For more information, refer https://reactnavigation.org/docs/use-focus-effect/
Here is a solution to simulate viewDidAppear with latest React Navigation version:
componentDidMount() {
var currentRoute = this.props.navigation.state.routeName;
this.props.navigation.addListener('didFocus', (event) => {
if (currentRoute === event.state.routeName) {
// VIEW DID APPEAR
}
});
}
Thanks Jichao Wu for the idea :)
If you are using React Navigation, use this:
componentDidMount(){
this.props.navigation.addListener('focus', () => {
// put your code here
});
}
Basically you are adding a focus event when component is first mounted. It will be called whenever (including the first time too) the component is focused. Ideally you'd also need to remove listener on unmount by capturing the value returned from addListener call and call that returned value (which is actually the unsubscribe function).
I've created a custom button with onLeftButtonPress to handled the back to run code as per https://github.com/facebook/react-native/issues/26
The way to get around it is to either set your custom back button on the left side, or to implement - viewWillDisappear: in iOS.
You can use ComponentWillMount or if you're leaving the view you can use ComponentWillUnmount which will run some code on exit.
Related
TL;DR: My UI freezes for .5-1s when I try to render a component that does a API fetch within a useEffect().
I have ComponentX which is a component that fetches data from an API in a useEffect() via a redux dispatch. I'm using RTK to build my redux store.
function ComponentX() {
const dispatch = useAppDispatch();
useEffect(() => {
dispatch(fetchListData()); // fetch list data is a redux thunk.
}, [dispatch]);
...
return <FlatList data={data} /> // pseudo code
}
as you can see the fetch will happen everytime the component is rendered.
Now I have ComponentX in App along with another component called ComponentY.
Here's a rudamentary implementation on how my app determines which component to show. Pretend each component has a button that executes the onClick
function App() {
const [componentToRender, setComponentToRender] = useState("x");
if (componentToRender === "x") {
return <ComponentX onClick={() => setComponentToRender("y")}/>
} else {
return <ComponentY onClick={() => setComponentToRender("x")}/>
}
}
Now the issue happens when I try to move from ComponentY to ComponentX. When I click the "back" button on ComponentY the UI will freeze for .5-1s then show ComponentX. Removing the dispatch(fetchListData()); from the useEffect fixes the issue but obviously I can't do that since I need the data from the API.
Another fascinating thing is that I tried wrapping the dispatch in an if statement assuming that it would prevent a data fetch thus resolving the "lag" when shouldReload is false. The UI still froze before rendering ComponentX.
useEffect(() => {
if (shouldReload) { // assume this is false
console.log("reloading");
dispatch(fetchListData());
}
}, [dispatch, shouldReload]);
Any idea what's going on here?
EDIT:
I've done a little more pruning of code trying to simplify things. What I found that removing redux from the equation fixes the issue. By simply doing below, the lag disappears. This leads me to believe it has something to do with Redux/RTK.
const [listData, setListData] = useState([]);
useEffect(() => {
getListData().then(setListData)
}, []);
Sometimes running the code after interactions/animations completed solves the issue.
useEffect(() => {
InteractionManager.runAfterInteractions(() => {
dispatch(fetchListData());
});
}, [dispatch]);
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.
I have an async function and a useEffect that fetches data once.
const [data, setData] = useState([]);
async function fetchData() {
fetch(`${baseURL}api/v1/data/${userId}`)
.then((response) => response.json())
.then((response) => {
try {
if (response.length > 0) {
setData(response);
} else {
setData([]);
// console.log(response);
}
} catch (err) {
console.log('no response');
alert(err);
}
});
}
useEffect(() => {
fetchData();
}, [userId, data]);
I could remove the array on the use effect but it will always run the function if I do that.
So when I open the screen, it will fetch the latest data. However, if I want to add a new data from a different screen, it wont trigger the async nor the useEffect function. How should I tell RN that there is a new data? Would AsyncStorage work? to update a data from one screen and apply the data here? I am open for suggestions on how to proceed.
What I meant by a different screen: A register screen and a view screen. In this case, I already opened the View Screen before I open the register screen so view screen is already rendered.
In React Navigation and most of the navigation libraries, screens don't get unmounted from the stack when it's navigated to another screen. For example if you have a list of something and then you press to "+" button to navigate to the "new item" screen to add a new one, when you press back button, since the previous "list" screen was not unmounted from the stack, useEffect won't be triggered, and you won't get the new data.
There are a couple of solutions for this case:
You can hold your data in a global state, and when you update an item from another screen, after a successful API call, you can also update the global state. You can look for React Context, MobX or Redux for this.
You can pass parent's state with a callback from one screen to another if they are not that apart from each other. So that in the "new data" screen, you can call that callback function to change the parent screen's state too.
Third, and IMO the best way is using a hook called useFocusEffect by React Navigation itself: https://reactnavigation.org/docs/use-focus-effect
I hope these will help.
I wanted to reload data from the server ever time a certain tab is tapped on. useEffect was however not doing this by itself as I expected it would. I found useFocusEffect however
which seems to do what I want.
import { useFocusEffect } from "#react-navigation/native";
useFocusEffect(
React.useCallback(() => {
request(uid);
return () => {
alert("Screen was unfocused");
// Do something when the screen is unfocused
// Useful for cleanup functions
};
}, [])
);
So, this works great in that every time I tap on that tab it fetches the data like I want. However, in the example code it has the 'useful for cleanup functions'. Is there something I should be doing there? I am basically fetching a list of users.
I am really confused how to solve the issue of canceling an async process when moving to a new tab. If you start an async request on a page but, then navigate to a new tab before it's complete, you will get the warning: "Can't call setState (or forceUpdate) on an unmounted component"
However, changing screens via the tab navigator will never fire the willunmount so, there is no real place to cancel any operations.
Stack Navigator and switch navigator fire this and I can cancel any operations just fine. I literally am about to build my own bottom nav to get around this.
This sample is way to hacky IMHO:
YES, I've tried the this.isMounted approach (BTW you now will get the isMounted(...) is deprecated warning if you use that) Yes, I've used the willupdate method but, PureComponent is suppose to remove that "hack".
This really feels like a bug to me and I am at lost to how to have a bottom Navigation AND have a page with some fetch results.
// Hacky Example in async method
try {
let response = await fetch(
'https://your/rest/endpoint/with/json'
);
if (response.ok) {
if (!_isMounted) {
console.log('oops! ' + SCREEN_NAME + ' was unmounted before async');
return; // just bail if component is no longer mounted
}
let responseJson = await response.json();
`
If you're using Redux:
You could move all of your Async Code into an Action ...
If you're executing your async code in an action instead of executing it inside your component's life ... it's ok if the user decided to move to a different tab and the action has not been resolved yet ((because it's running in a different context than the component's lifecycle)) ... once the action is done and your component is still active >> then it'll receive a new set of props to update itself ...
And regarding setting state on an unmounted component ... you could use this template for any class-based component that has a state:
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
// other fields...
isUnmounted: false,
};
}
componentWillUnmount() {
this.setState({ isUnmounted: true });
}
setComponentState = (values) => {
if (!this.state.isUnmounted) this.setState(values);
};
}
Redux is not wrong but, for my case more complicated than required.
So, if anyone is struggling with React-Navigation Tab Navigator and async fetch. I was able to achieve the pattern I was after (firing an event to be able to cancel async events).
You CAN add a Listener when the screen is navigated to or from (blur)
React Navigation emits events to screen components that subscribe to them:
willFocus - the screen will focus
didFocus - the screen focused (if there was a transition, the transition completed)
willBlur - the screen will be unfocused
didBlur - the screen unfocused (if there was a transition, the transition completed)
basically I did this:
async componentDidMount() {
console.log(SCREEN_NAME + ' Component Did Mount');
this.props.navigation.addListener('willBlur', (route) => {
_isMounted = false;
this.axiosCancelSource.cancel('Component unmounted.');
});
// didFocus will fire on 1st Mount as well
this.props.navigation.addListener('didFocus', async (route) => {
_isMounted = true;
this.axiosCancelSource = axios.CancelToken.source();
await this._getTourList();
});
console.log(SCREEN_NAME + ' Component Did Mount Complete');
}
React-Navtive Navigation props web site