While coming back from react navigation componentWillMount doesn't get called - react-native

I have used react-navigation and on clicking hardware back button in android, I come back to previous component but componentWillMount doesn't get called. How do I ensure that componentWillMount is called?

componentWillMount will not trigger when you entering new screen / back to the screen.
my solution is using event navigator handler
https://wix.github.io/react-native-navigation/#/screen-api?id=listen-visibility-events-in-onnavigatorevent-handler
you can implement your 'componentWillMount' codes while 'willAppear' event id triggered, see this implementation:
export default class ExampleScreen extends Component {
constructor(props) {
super(props);
this.props.navigator.setOnNavigatorEvent(this.onNavigatorEvent.bind(this));
}
onNavigatorEvent(event) {
switch(event.id) {
case '`willAppear`':
// { implement your code on componentWillMount }
break;
case 'didAppear':
break;
case 'willDisappear':
break;
case 'didDisappear':
break;
case 'willCommitPreview':
break;
}
}
}

Does this answer from #bumbur help you? It defines a global variable that tracks if nav state has changed. You could insert a piece of code to see if you're in the specific tab that you are interested in. With that you could trigger a call to componentWillMount() ?
If you don't want to use redux, this is how you can store globally
information about current route, so you can both detect a tab change
and also tell which tab is now active.
https://stackoverflow.com/a/44027538/7388644
export default () => <MyTabNav
ref={(ref) => { this.nav = ref; }}
onNavigationStateChange={(prevState, currentState) => {
const getCurrentRouteName = (navigationState) => {
if (!navigationState) return null;
const route = navigationState.routes[navigationState.index];
if (route.routes) return getCurrentRouteName(route);
return route.routeName;
};
global.currentRoute = getCurrentRouteName(currentState);
}}
/>;

Related

Re-render component everytime screen is opened react native

I'm kinda new to React Native. I'm using the getFamily() on my screen MyFamily but when I go to another screen there change the value of the Family and come back to my MyFamily screen then I don't see the changes.
I tried doing it with the useEffect but still nothing happens, also the log doesn't happen. How can I solve this?
export default function MyFamily({ navigation, props, person, inheritors }) {
console.log(getFamily());
let [family, setFamily] = useState(getFamily());
useEffect(() => {
console.log(getFamily());
setFamily(getFamily());
}, [getFamily]);
In the screen where I set the Family again I do this:
And I know that's correct because the Json that is shown shows the updated value.
import { setFamily } from '../../utilities/family';
setFamily(responseJson.family);
This is the way family is formulated:
let family = '';
export default family;
export function getFamily() {
return family;
}
export function setFamily(f) {
family = f;
}
React doesn't actually know that the value returned from the getFamily function changes each render. In the useState function, it's only used in the initial state, and the useEffect function never gets re-run because the getFamily function itself doesn't ever change and re-trigger the useEffect. You have to change the getFamily() function to use a state that's stored in a parent component and pass it into the MyFamily component as a prop.
e.g.
// the parent component that renders the MyFamily screen
function Router() {
const [family, setFamily] = useState('')
return (
<Navigator>
<Screen component={<MyFamily family={family} setFamily={setFamily} />
<Screen component={<OtherComponent family={family} setFamily={setFamily} />
</Navigator>
}
)
}
And then from MyFamily:
function MyFamily({ family }) {
console.log(family); // this should be updated
}
and from OtherComponent:
function OtherComponent({ setFamily }) {
return (<Button onClick={() => setFamily('newFamily')>Change family</Button>)
}

React native UI is not getting rendered after callback from native event emitter. Even callback having state change

I want to navigate the user to another screen in react native project after native app widget click in android. I was able to catch event using native event emitter in my MainView.js and there i changed state of one of my component and it got changed but UI is not getting rendered after this state change. It is showing blank screen and there is not error on the console. Thanks in advance for any help!!
export default class MainView extends React.Component {
constructor(props) {
super(props);
this.state = {text: 'Hi, This is main screen for app widget!!!'};
}
componentDidMount() {
const eventEmitter = new NativeEventEmitter();
this.listener = eventEmitter.addListener('MyCustomEvent', (event) => {
console.log('MyCustomEvent -->', event);
console.log('MyCustomEvent ArticleId -->', event.ArticleId);
if (event.ArticleId === data.articleId) {
console.log('data ArticleId true', data.articleId);
//navigation.push('Article Details', data);
this.setState({
text: data.articleDes,
});
// setText(data.articleDes);
console.log('text -->', this.state.text);
} else {
// setText('No such article found.');
console.log('text -->', this.state.text);
}
});
}
componentWillUnmount() {
this.eventListener.remove(); //Removes the listener
}
render() {
return (
<View style={{flex: 1}}>
<Text>{this.state.text}</Text>
<Button
title="click"
onPress={() => this.props.navigation.push('Article Details', data)}
/>
</View>
);
}
}
CustomActivity source code which is launched from appwidget click. From this activity's oncreate, I'm emitting events to react-native main view.
int articleId = 0;
if (getIntent() != null) {
articleId = getIntent().getIntExtra("articleId", 0);
Log.e("articleid", "" + articleId);
}
// Put data to map
WritableMap payload = Arguments.createMap();
payload.putInt("ArticleId", articleId);
// Emitting event from java code
ReactContext context = getReactNativeHost().getReactInstanceManager().getCurrentReactContext();
if ( context != null && context.hasActiveCatalystInstance()) {
Log.e("react context", "not null");
(getReactNativeHost().getReactInstanceManager().getCurrentReactContext())
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit("MyCustomEvent", payload);
}
That is not how to use NativeEventEmitter. You need to initialise the NativeEventEmitter with the native module you are emitting events from:
import { NativeEventEmitter, NativeModules } from 'react-native';
const { myNativeModule } = NativeModules;
componentDidMount() {
...
const eventEmitter = new NativeEventEmitter(myNativeModule);
this.eventListener = eventEmitter.addListener('myEvent', (event) => {
console.log(event.eventProperty) // "someValue"
});
...
}
componentWillUnmount() {
this.eventListener.remove(); //Removes the listener
}
Read more about NativeModules here: https://reactnative.dev/docs/native-modules-android
This sound familiar with an issue I am experiencing on IOS. The code is similar, but I cannot guarantee that the underlying structure in Android works in the same way. Anyways, I am sending an event message from IOS-Native (written in swift in xCode) to React-native file using the NativeEventEmitter. After the initial render, the value just wont update, and as I understand this issue is not limited to this type of Event. After some googling I found out that everything you read from state inside that event-callback has a reference to only the first render, and will not update on future renders.
Solution; use useRef so you keep a reference to the the updated value. useRef keeps the value across renders and event-callbacks. This is not something I have found out myself, please look at https://medium.com/geographit/accessing-react-state-in-event-listeners-with-usestate-and-useref-hooks-8cceee73c559 and React useState hook event handler using initial state for, they are the one that deserves the credit.

React native flatList scrollToItem not scrolling in componentDidMount

I have a really weird issue, I have two components (both navigation routes) which share the same higher-order component that keeps the posts as state.
One component can link to the other by passing a post slug to the route as a parameter, this component, in turn, scrolls a flaList to the correct index:
findIndexBySlug = memoizeOne(
(postsArray, selectedPostSlug) => postsArray.findIndex(
post => post.slug === selectedPostSlug,
),
);
constructor(props) {
super(props);
this.shouldScrollToPost = this.shouldScrollToPost.bind(this);
this.scrollToIndex = this.scrollToIndex.bind(this);
this.flatListRef = React.createRef();
}
componentDidMount() {
this.shouldScrollToPost();
}
componentDidUpdate() {
this.shouldScrollToPost();
}
shouldScrollToPost() {
const { navigation } = this.props;
const selectedPostSlug = navigation.getParam('selectedPostSlug');
console.log('shouldscrollto', selectedPostSlug);
if (selectedPostSlug) {
const { posts } = this.props;
const { postsArray } = posts;
const selectedPostIndex = this.findIndexBySlug(
postsArray, selectedPostSlug,
);
this.scrollToIndex(selectedPostIndex);
}
}
scrollToIndex(index) {
if (this.flatListRef.current) {
console.log('scrolling to ', index)
this.flatListRef.current.scrollToIndex({ index, animated: false });
}
}
In both the first mount (componentDidMount) and subsequent calls (componentDidUpdate) all the console.log fire (including the one that checks for the flatListref) but, when called the first time in componentDidMount no scrolling occurs, in componentDidUpdate it does actually scroll!
Why?
It's driving me insane, even read the Dan Abramov post about the availability of refs (componentDidMount called BEFORE ref callback) and the flatList is the only rendered component (in fact the ref is always available).
Any help greatly appreciated.
Thanks

Implementing UISplitViewController using React Navigation

I need to implement tablet support in the React Native app I am working on. We decided that going with UISplitViewController makes the most sense for us, but React Navigation (https://reactnavigation.org) does not have any support for it.
I tried solving this problem in a straightforward way by putting 2 navigators side-by-side in a view and modifying their getStateForAction to open certain screens in the details view:
export interface Props {
MasterNavigator: NavigationContainer;
DetailNavigator: NavigationContainer;
detailRouteNames: string[];
}
export class MasterDetailView extends React.Component<Props> {
masterNavigator: any;
detailNavigator: any;
componentDidMount() {
const { MasterNavigator, DetailNavigator, detailRouteNames } = this.props;
const defaultMasterGetStateForAction = MasterNavigator.router.getStateForAction;
MasterNavigator.router.getStateForAction = (action: any, state: any) => {
if (action.type === NavigationActions.NAVIGATE && detailRouteNames.indexOf(action.routeName) !== -1) {
action.params.isRootScreen = true;
this.detailNavigator.dispatch(NavigationActions.reset({ index: 0, actions: [action] }));
return state;
}
return defaultMasterGetStateForAction(action, state);
};
const defaultDetailGetStateForAction = DetailNavigator.router.getStateForAction;
DetailNavigator.router.getStateForAction = (action: any, state: any) => {
if (action.type === NavigationActions.BACK && state.routes.length === 1) {
this.masterNavigator.dispatch(NavigationActions.back());
return null;
}
return defaultDetailGetStateForAction(action, state);
};
}
render() {
const { MasterNavigator, DetailNavigator } = this.props;
return (
<View style={styles.view}>
<View style={styles.masterContainer}>
<MasterNavigator ref={(mn: any) => this.masterNavigator = mn}/>
</View>
<View style={styles.divider}/>
<View style={styles.detailContainer}>
<DetailNavigator ref={(dn: any) => this.detailNavigator = dn}/>
</View>
</View>
);
}
}
The master navigator is a TabNavigator with StackNavigators in each tab, the detail navigator is a StackNavigator. Here's how it looks:
This approach kind of works, but back button on Android behaves incorrectly. I want it to navigate back in the details navigator, then in the master navigator. It is possible to make it work with a couple of hacks, but then it becomes impossible to navigate back from the app using the back button (for some reason, nothing happens when I click it). I could try fix this by overriding the behavior of back button and dispatching back action to the master/detail navigators or closing the app, but when dispatching an action to the navigator, there is no way to know whether it responded to it or not (particularly with the master navigator, which is a TabNavigator), so it seems like I am stuck.
Are there some special considerations when using navigators this way? Is React Navigation even suitable for this use case? If not, what are the other ways to emulate UISplitViewController in React Native apps?
The problem with incorrect back button behavior was my fault. One of the screens had a back button handler which always returned true, thus consuming all back button presses (duh!). I fixed this and added the following back button handler to the MasterDetailView:
private onPressBack = (): boolean => {
const action = NavigationActions.back();
return this.detailNavigator.dispatch(action) || this.masterNavigator.dispatch(action);
};
And yes, there is a way to find out whether a navigator consumed an action: the navigator.dispatch(action) returns true if the event was consumed and false otherwise.
Once the problem with back button is fixed, this setup emulates the UISplitViewController pretty well.

How to avoid navigating to other screen multiple times

When press on any button on my React Native App to navigate to a different screen multiple times, then it will redirected to the next screen multiple times.
My sample code is:
// This is my button click event
myMethod()
{
this.props.navigation.navigate("ScreenName")
}
I am using react-navigation to navigate through my app.
How can I fix this behaviour?
I think there are a few ways this could be done. Perhaps recording when the navigation has occurred and preventing it from navigating multiple times.
You may also want to consider resetting hasNavigated after an amount of time etc as well.
// Somewhere outside of the myMethod scope
let hasNavigated = false
// This is my button click event
myMethod()
{
if (!hasNavigated) {
this.props.navigation.navigate("ScreenName")
hasNavigated = true
}
}
This react-navigation issue contains a discussion about this very topic, where two solutions were proposed.
The first, is to use a debouncing function such as Lodash's debounce that would prevent the navigation from happening more than once in a given time.
The second approach, which is the one I used, is to check on a navigation action, whether it is trying to navigate to the same route with the same params, and if so to drop it.
However, the second approach can only be done if you're handling the state of the navigation yourself, for example by using something like Redux.
Also see: Redux integration.
One of solution is custom custom components with adds debounce to onPress:
class DebounceTouchableOpacity extends Component {
constructor(props) {
super(props);
this.debounce = false;
}
_onPress = () => {
if (typeof this.props.onPress !== "function" || this.debounce)
return;
this.debounce = true;
this.props.onPress();
this.timeoutId = setTimeout(() => {
this.debounce = false;
}, 2000);
};
componentWillUnmount() {
this.timeoutId && clearTimeout(this.timeoutId)
}
render() {
const {children, onPress, ...rest} = this.props;
return (
<TouchableOpacity {...rest} onPress={this._onPress}>
{children}
</TouchableOpacity>
);
}
}
another: wrap onPress function into wrapper with similar behavior
const debounceOnPress = (onPress, time) => {
let skipCall = false;
return (...args) => {
if (skipCall) {
return
} else {
skipCall = true;
setTimeout(() => {
skipCall = false;
}, time)
onPress(...args)
}
}
}