I'd like to refresh the data on the screen in a react native app whenever the screen appears - like in a ViewWillAppear method. I tried using the componentWillMount method but it looks like it fires once before it appears, and doesn't fire again when the view is navigated to again.
Looking at this example https://reactnavigation.org/docs/guides/screen-tracking, it looks like I can add a listener on the onNavigationStateChange method on the root navigation, but I'd like to keep the logic in the screen as it gets confusing if I move the data fetch logic for that one scree outside to the root navigator.
I've tried to follow the example and set that method to my stacknavigation but it doesn't seem to trigger.
<RootNavigation ref={nav => { this.navigator = nav; }}
onNavigationStateChange={(prevState, currentState, action) => {
// maybe here I can fire redux event or something to let screens
// know that they are getting focus - however it doesn't fire so
// I might be implementing this incorrectly
const currentScreen = getCurrentRouteName(currentState);
const prevScreen = getCurrentRouteName(prevState);
if (prevScreen !== currentScreen) {
console.log('navigating to this screen', currentScreen);
}
}}
/>
So here's how I did it using the onNavigateStateChange.
<RootNavigation
ref={nav => { this.navigator = nav; }}
uriPrefix={prefix}
onNavigationStateChange={(prevState, currentState) => {
const currentScreen = this.getCurrentRouteName(currentState);
const prevScreen = this.getCurrentRouteName(prevState);
if (prevScreen !== currentScreen) {
this.props.emitActiveScreen(currentScreen);
{/*console.log('onNavigationStateChange', currentScreen);*/}
}
}}
/>
And in your screen you can check to see if your view will appear, note MyPage is the route name from your navigation object.
componentWillReceiveProps(nextProps) {
if ((nextProps.activeScreen === 'MyPage' && nextProps.activeScreen !== this.props.activeScreen)) {
// do your on view will appear logic here
}
}
Here is my navigator reducer.
function getCurrentRouteName(navState) {
if (!navState) {
return null;
}
const navigationState = (navState && navState.toJS && navState.toJS()) || navState;
const route = navigationState.routes[navigationState.index];
// dive into nested navigators
if (route.routes) {
return getCurrentRouteName(route);
}
return route.routeName;
}
export default function NavigatorReducer(state, action) {
// Initial state
if (!state) {
return fromJS(AppNavigator.router.getStateForAction(action, state));
}
// Is this a navigation action that we should act upon?
if (includes(NavigationActions, action.type)) {
// lets find currentScreen before this action based on state
const currentScreen = getCurrentRouteName(state);
const nextState = AppNavigator.router.getStateForAction(action, state.toJS());
// determine what the new screen will be after this action was performed
const nextScreen = getCurrentRouteName(nextState);
if (nextScreen !== currentScreen) {
nextState.currentRoute = nextScreen;
console.log(`screen changed, punk: ${currentScreen} -> ${nextScreen}`);
}
return fromJS(nextState);
}
return state;
}
And then we have to connect the module/route to the redux store (sceneIsActive is the important bit):
export default connect(
state => ({
counter: state.getIn(['counter', 'value']),
loading: state.getIn(['counter', 'loading']),
sceneIsActive: state.getIn(['navigatorState', 'currentRoute']) === 'Counter',
}),
dispatch => {
return {
navigate: bindActionCreators(NavigationActions.navigate, dispatch),
counterStateActions: bindActionCreators(CounterStateActions, dispatch),
};
},
)(CounterView);
And then inside your component, you can watch for/trigger code when the scene becomes active:
componentWillReceiveProps(nextProps) {
if (nextProps.sceneIsActive && (this.props.sceneIsActive !== nextProps.sceneIsActive)) {
console.log('counter view is now active, do something about it', this.props.sceneIsActive, nextProps.sceneIsActive);
doSomethingWhenScreenBecomesActive();
}
}
Know that componentWillReceiveProps does not run when initially mounted. So don't forget to call your doSomethingWhenScreenBecomesActive there as well.
You can use focus/blur event listeners(demonstrated here and discussed here).
Related
I'm using React Native with Redux. Currently i'm having two screens that uses same redux state. Two screen are on a stack navigation. There is a use effect call in the 1st screen that trigger when the redux state changed. same kind a use effect call is also in the 2nd screen that trigger when same redux state changed. The problem is when i navigate from screen 1 to screen 2 and changed the state, it triggers both use effects.
This is 1st Screen. I'm navigating to 2nd screen using navigation.navigate('LoginScreen')
export default function StartScreen({route, navigation, props}) {
const {loginStatus,user} = useSelector(state => state.auth);
const initialRender = useRef(true);
useEffect(() => {
console.log('use effect');
if (!initialRender.current) {
if (loginStatus === 'success') {
hideDialog();
navigation.reset({
index: 0,
routes: [{name: 'Dashboard'}],
});
} else if(loginStatus === 'failed') {
alert('invalid QR code!')
hideDialog();
dispatch(clearLoginStatus())
}
} else {
initialRender.current = false;
}
}, [loginStatus]);
return (
<Background maxWidth="85%">
<VectorHeading
img={require('../assets/startvector.png')}
marginBottom={50}
/>
<Header>xxxxxxx</Header>
<Paragraph>
The easiest way to start with your amazing application.
</Paragraph>
<Button mode="contained" onPress={() => navigation.navigate('QRScanner')}>
Scan QR
</Button>
<Button mode="contained" onPress={() => navigation.navigate('LoginScreen')}>
Login
</Button>
<Loader visible={visible} hideDialog={hideDialog} />
</Background>
);
.........
This is 2nd Screen
export default function LoginScreen({navigation, props}) {
....
const {isLoggedIn, loginStatus,user} = useSelector(state => state.auth);
const initialRender = useRef(true);
useEffect(() => {
console.log('use effect');
if (!initialRender.current) {
if (loginStatus === 'success') {
hideDialog();
forward();
} else if(loginStatus === 'failed') {
alert('invalid credentials!')
hideDialog();
dispatch(clearLoginStatus())
}
} else {
initialRender.current = false;
}
}, [loginStatus]);
.........
When "loginStatus" changed it rendering useefect functions in both screens and show both alerts that setted inside useeffect.
Any help will be really appreciated.
Try adding another useEffect to clean up when component unmounts,
useEffect(() => {
return () => {
console.log("cleaned up");
};
}, []);
OneSignal on notification open event fires after the home screen got launched then it navigates to the desired screen. I want to detect if the app was launched on pressing the notification prior the home screen get rendered so I can navigate to the Second screen directly and avoid unnecessarily calling of apis.
"react-native-onesignal": "^3.9.3"
"react-navigation": "^4.0.0"
code
const _opened = openResult => {
const { additionalData, body } = openResult.notification.payload;
// how to navigate or set the initial screen depending on the payload
}
useEffect(() => {
onesignal.init();
onesignal.addEventListener('received', _received);
onesignal.addEventListener('opened', _opened);
SplashScreen.hide();
return () => {
// unsubscriber
onesignal.removeEventListener('received', _received);
onesignal.removeEventListener('opened', _opened);
}
}, []);
Debug
your question is how to navigate or set the initial screen depending on the opened notification payload?
1) - set the initial screen depending on the opened notification payload.
according to class Lifecycle useEffect runs after the component output has been rendered, so listener in useEffect not listen until the component amounting, and this the reason of logs in home screen shown before logs in useEffect, see this explanation.
//this the problem (NavigationContainer called before useEffect).
function App() {
useEffect(() => {}); //called second.
return <NavigationContainer>; //called first.
}
//this the solution (useEffect called Before NavigationContainer).
function App() {
const [ready, setReady] = useState(false);
//called second.
useEffect(() => {
//listen here
setReady(true);
SplashScreen.hide();
});
//called first
//no function or apis run before useEffect here it just view.
if(!ready) return <></>;// or <LoadingView/>
//called third.
return <NavigationContainer>;
}
your code may be like this.
function App() {
const [ready, setReady] = useState(false);
const openedNotificationRef = useRef(null);
const _opened = openResult => {
openedNotificationRef.current = openResult.notification.payload;
}
const getInitialRouteName = () => {
if (openedNotificationRef.current) {
return "second"; //or what you want depending on the notification.
}
return "home";
}
useEffect(() => {
onesignal.addEventListener('opened', _opened);
//setTimeout(fn, 0) mean function cannot run until the stack on the main thread is empty.
//this ensure _opened is executed if app is opened from notification
setTimeout(() => {
setReady(true);
}, 0)
});
if(!ready) return <LoadingView/>
return (
<NavigationContainer initialRouteName={getInitialRouteName()}>
</NavigationContainer>
);
}
2) - navigate depending on the opened notification payload.
first you need to kown that
A navigator needs to be rendered to be able to handle actions If you
try to navigate without rendering a navigator or before the navigator
finishes mounting, it will throw and crash your app if not handled. So
you'll need to add an additional check to decide what to do until your
app mounts.
read docs
function App() {
const navigationRef = React.useRef(null);
const openedNotificationRef = useRef(null);
const _opened = openResult => {
openedNotificationRef.current = openResult.notification.payload;
//remove loading screen and start with what you want.
const routes = [
{name : 'home'}, //recommended add this to handle navigation go back
{name : 'orders'}, //recommended add this to handle navigation go back
{name : 'order', params : {id : payload.id}},
]
navigationRef.current.dispatch(
CommonActions.reset({
routes : routes,
index: routes.length - 1,
})
)
}
useEffect(() => {
//don't subscribe to `opened` here
//unsubscribe
return () => {
onesignal.removeEventListener('opened', _opened);
}
}, []);
//subscribe to `opened` after navigation is ready to can use navigate
const onReady = () => {
onesignal.addEventListener('opened', _opened);
//setTimeout(fn, 0) mean function cannot run until the stack on the main thread is empty.
//this ensure _opened is executed if app is opened from notification
setTimeout(() => {
if (!openedNotificationRef.current) {
//remove loading screen and start with home
navigationRef.current.dispatch(
CommonActions.reset({
routes : [{name : 'home'}],
index: 0,
})
)
}
}, 0)
};
return (
<NavigationContainer
ref={navigationRef}
onReady={onReady}
initialRouteName={"justLoadingScreen"}>
</NavigationContainer>
);
}
refrences for setTimeout, CommonActions.
Screen C:
I have an edit function screen which user allow to delete.
Screen A:
The home page, I have a ready function which is onRefresh function that allows to the page refresh.
From screen C the user delete the function then go to screen A
is that possible to refresh the specific function ?
example.
this.props.navigation.navigate('Home', {
specific function here
})
My home page code looks like this
constructor(props) {
super(props);
}
componentWillUnmount = () => {
}
componentWillMount() {
this.getOrderListData(1)
}
onRefresh = () => {
this.setState({
onRefreshLoading: true
}, () => {
this.getOrderListData(this.state.page)
})
}
If I understand this case, you want to trigger onRefresh in Screen A, when coming back from Screen C.
Not sure regarding the function, but you can just pass a param to the Home screen, that Home screen would check on load and refresh if it's specified.
navigation.navigate('Home', {
refreshOnLoad: true
})
...
function HomeScreen({ route, navigation }) {
const { refreshOnLoad } = route.params;
if (refreshOnLoad) {
...
}
I would have a common state for the App (Redux or MobX tree), that would be updated in such a case
Another way to refresh ScreenA after you navigate it from ScreenC is to add a listener to focus event on navigation, for example in the constructor of ScreenA have a listener like this
construction(props){
...
this.focusListener = props.navigation.addListener('focus', this.onRefresh);
...
}
Dont forget to remove it on componentWillUnmount() like this
componentWillUnmount = () => {
this.focusListener();
}
Hope this helps. You can read more about this here.
React Navigation 2x example
In react-navigation 2x you have to listen for didFocus event. For example have it like this in your constructor
construction(props){
...
this.focusListener = props.navigation.addListener('didFocus', this.onRefresh);
...
}
And then unsubscribe to the event like this
componentWillUnmount = () => {
this.focusListener.remove();
}
I solved the problem, Another way to do it.
Screen A:
constructor(props) {
super(props);
this.onRefresh = this.onRefresh.bind(this);
}
onRefresh = () => {
this.setState({
onRefreshLoading: true,
orderList:[]
}, () => {
this.getOrderListData(1)
})
}
this.props.navigation.navigate('Screen B', {
onRefresh: this.onRefresh,
})
Screen B passing params to Screen C
const { state, setParams, navigate } = this.props.navigation;
const params = state.params || {};
this.props.navigation.navigate('Screen C', {
onRefresh: params.onRefresh
})
Screen C :
this.props.navigation.navigate('ScreenA')
const { state, setParams, navigate } = this.props.navigation;
const params = state.params || {};
this.props.navigation.state.params.onRefresh()
Trying to use a AsyncStorage variable to conditionally render content.
My app uses createBottomTabNavigator from react-navigation. I have a tab called Settings that must conditionally render content based on wether a user is logged in or not (checking AsyncStorage). The following code works on first render but another tab can update AsyncStorage value, returning back to Settings tab it still renders initial content.
Which approach can i use to achieve this, i'm also trying to use shouldComponentUpdate but i'm not sure how it works.
import React, {Component} from 'react';
class Settings extends React.Component{
constructor(props){
super(props);
this.state = {
isLoggedIn:false
};
}
//I want to use this method but not sure how.
shouldComponentUpdate(nextProps, nextState){
// return this.state.isLoggedIn != nextState;
}
componentDidMount(){
console.log("componentWillUpdate..");
this.getLocalStorage();
}
getLocalStorage = async () => {
try {
const value = await AsyncStorage.getItem('username');
if(value !== null) {
this.setState({isLoggedIn:true});
}
} catch(e) {
// error reading value
}
}
render() {
if(this.state.isLoggedIn)
{
return(
<View>
<Text style={styles.title_header}>Logged In</Text>
</View>
);
}
else{
return(
<View>
<Text style={styles.title_header}>Logged Out</Text>
</View>
);
}
}
}
export default Settings;
})
Use NavigationEvents. Add event listeners to your Settings components.
onWillFocus - event listener
onDidFocus - event listener
onWillBlur - event listener
onDidBlur - event listener
for example, the following will get fired when the next screen is focused.
focusSubscription = null;
onWillFocus = payload => {
// get values from storage here
};
componentDidMount = () => {
this.focusSubscription = this.props.navigation.addListener(
'willFocus',
this.onWillFocus
);
};
componentWillUnmount = () => {
this.focusSubscription && this.focusSubscription.remove();
this.focusSubscription = null;
};
The problem comes from react-navigation createBottomTabNavigator. On first visit, the component is mounted and so componentDidMount is called and everything is great.
However, when you switch tab, the component is not unmounted, which means that when you come back to the tab there won't be any new call to componentDidMount.
What you should do is add a listener to the willFocus event to know when the user switches back to the tab.
componentDidMount() {
this.listener = this.props.navigation.addListener('willFocus', () => {
AsyncStorage.getItem('username').then((value) => {
if (value !== null) {
this.setState({ isLoggedIn: true });
}
catch(e) {
// error reading value
}
});
});
}
Don't forget to remove the listener when the component is unmounted:
componentWillUnmount() {
this.listener.remove();
}
I'm using react-navigation / TabNavigator, is there a way to change the state of a tab from another tab without using Redux or mobx?
Yes you can. It is a little complicated, a little hacky and probably has some side-effects but in theory you can do it. I have created a working example snack here.
In react-navigation you can set parameters for other screens using route's key.
When dispatching SetParams, the router will produce a new state that
has changed the params of a particular route, as identified by the key
params - object - required - New params to be merged into existing route params
key - string - required - Route key that should get the new params
Example
import { NavigationActions } from 'react-navigation'
const setParamsAction = NavigationActions.setParams({
params: { title: 'Hello' },
key: 'screen-123',
})
this.props.navigation.dispatch(setParamsAction)
For this to work you need to know key prop for the screen you want to pass parameter. Now this is the place we get messy. We can combine onNavigationStateChange and screenProps props to get the current stacks keys and then pass them as a property to the screen we are currently in.
Important Note: Because onNavigationStateChange is not fired when the app first launched this.state.keys will be an empty array. Because of that you need to do a initial navigate action.
Example
class App extends Component {
constructor(props) {
super(props);
this.state = {
keys: []
};
}
onNavigationChange = (prevState, currentState) => {
this.setState({
keys: currentState.routes
});
}
render() {
return(
<Navigation
onNavigationStateChange={this.onNavigationChange}
screenProps={{keys: this.state.keys}}
/>
);
}
}
And now we can use keys prop to get the key of the screen we need and then we can pass the required parameter.
class Tab1 extends Component {
onTextPress = () => {
if(this.props.screenProps.keys.length > 0) {
const Tab2Key = this.props.screenProps.keys.find((key) => (key.routeName === 'Tab2')).key;
const setParamsAction = NavigationActions.setParams({
params: { title: 'Some Value From Tab1' },
key: Tab2Key,
});
this.props.navigation.dispatch(setParamsAction);
}
}
render() {
const { params } = this.props.navigation.state;
return(
<View style={styles.container}>
<Text style={styles.paragraph} onPress={this.onTextPress}>{`I'm Tab1 Component`}</Text>
</View>
)
}
}
class Tab2 extends Component {
render() {
const { params } = this.props.navigation.state;
return(
<View style={styles.container}>
<Text style={styles.paragraph}>{`I'm Tab2 Component`}</Text>
<Text style={styles.paragraph}>{ params ? params.title : 'no-params-yet'}</Text>
</View>
)
}
}
Now that you can get new parameter from the navigation, you can use it as is in your screen or you can update your state in componentWillReceiveProps.
componentWillReceiveProps(nextProps) {
const { params } = nextProps.navigation.state;
if(this.props.navigation.state.params && params && this.props.navigation.state.params.title !== params.title) {
this.setState({ myStateTitle: params.title});
}
}
UPDATE
Now react-navigation supports listeners which you can use to detect focus or blur state of screen.
addListener - Subscribe to updates to navigation lifecycle
React Navigation emits events to screen components that subscribe to
them:
willBlur - the screen will be unfocused
willFocus - the screen will focus
didFocus - the screen focused (if there was a transition, the transition completed)
didBlur - the screen unfocused (if there was a transition, the transition completed)
Example from the docs
const didBlurSubscription = this.props.navigation.addListener(
'didBlur',
payload => {
console.debug('didBlur', payload);
}
);
// Remove the listener when you are done
didBlurSubscription.remove();
// Payload
{
action: { type: 'Navigation/COMPLETE_TRANSITION', key: 'StackRouterRoot' },
context: 'id-1518521010538-2:Navigation/COMPLETE_TRANSITION_Root',
lastState: undefined,
state: undefined,
type: 'didBlur',
};
If i understand what you want Its how i figure out to refresh prevous navigation screen. In my example I refresh images witch i took captured from camera:
Screen A
onPressCamera() {
const { navigate } = this.props.navigation;
navigate('CameraScreen', {
refreshImages: function (data) {
this.setState({images: this.state.images.concat(data)});
}.bind(this),
});
}
Screen B
takePicture() {
const {params = {}} = this.props.navigation.state;
this.camera.capture()
.then((data) => {
params.refreshImages([data]);
})
.catch(err => console.error(err));
}