React Native - render when app comes into foreground - javascript

I have a simple display which is based on some async data.
My code is as such:
componentDidMount() {
asynFunctionCall(result => {
this.setState({stateVariable: result});
});
}
And I use the state variable stateVariable to decide whether or not to show a <Text> component:
render() {
return (
<View>
{this.state.stateVariable &&
<Text> Special Message </Text>
}
</View>
);
}
What I want is for asyncFunctionCall to be run whenever the app comes to the foreground so that the user can leave the app, do something (which may affect the result of that async call), and come back to the app and have the display updated appropriately.
This seems like a standard app behavior, and I wonder if React Native's lifecycle API has a standard way to support it, but I'm not finding it.
How can I achieve this behavior, with a lifecycle function or otherwise?

Yes. you can use the AppState https://facebook.github.io/react-native/docs/appstate.html API.
I would suggest to add this somewhere at the start of the app and coordinate your views accordingly.

AppState should work mostly for iOS, but for Android background activity is triggered anytime a native module outside of your own code is triggered, e.g capturing a photo. It is way better to detect home or recent app button clicks. This is because fragments within your app from other apps like social media or photos will also trigger background state, which you don't want because they are still in the app adding a photo to a profile from the camera etc. You can easily detect home and recent app button clicks on Android with react-native-home-pressed. This library simply exposes the android button events.
First install the library with npm i react-native-home-pressed --save and then link it react-native link. Then rebuild your app and add the following snippet.
import { DeviceEventEmitter } from 'react-native'
class ExampleComponent extends Component {
componentDidMount() {
this.onHomeButtonPressSub = DeviceEventEmitter.addListener(
'ON_HOME_BUTTON_PRESSED',
() => {
console.log('You tapped the home button!')
})
this.onRecentButtonPressSub = DeviceEventEmitter.addListener(
'ON_RECENT_APP_BUTTON_PRESSED',
() => {
console.log('You tapped the recent app button!')
})
}
componentWillUnmount(): void {
if (this.onRecentButtonPressSub) this.onRecentButtonPressSub.remove()
if (this.onHomeButtonPressSub) this.onHomeButtonPressSub.remove()
}
}

Related

Why am I getting the following React Native warning: Can't perform a React state update on an unmounted component

I'm trying to build a React Native app, but am still kind of new to the React/RN ecosystem, so I may just be misunderstanding something obvious with the problem I'm having.
I have an app where a lot of the pages are structured as follows:
<View>
<NavComponent />
<View>
{/* Page component structure/logic here */}
</View>
</View>
NavComponent loads a toggleable nav menu with TouchableOpacity elements like the following:
Go to Screen #1
Go to Screen #2
Go to Screen #3
The problem I'm having (and maybe this isn't a problem so much as just how React/RN works) is that if I start on screen #1, open the nav, go to screen #2, open the nav again, and then go back to screen #1, even though screen #1 shows up again, the actual rendering function for screen #1 doesn't seem to be called again, and since NavComponent is part of the rendering of each screen, when I try to open the nav from screen #1 again, I get the following warning:
Warning: Can't perform a React state update on an unmounted component.
This is a no-op, but it indicates a memory leak in your application.
To fix, cancel all subscriptions and asynchronous tasks in %s.%s, a
useEffect cleanup function ...
Again, maybe my approach with the app is flawed to begin with, but essentially, when I go from one screen to another from the nav, I always want the new screen to re-render from scratch (including the initial Ajax call for data).
Here's a more fleshed-out example of the render function for a screen (they all follow this same basic pattern):
const screen1 = ({ navigation }) => {
const [serverData, setServerData] = useState(null);
useEffect(() => {
// getPageData is a custom method I added to axios.
axios.getPageData('/api/url/here', (data) => {
setServerData(data);
});
}, []);
if (serverData) {
const { meta, user, data } = serverData;
return (
<View>
<NavComponent />
<View style={styles.container}>
{/* Page component structure/logic here */}
</View>
</View>
);
}
return null;
};
If, for example, I added a console.log to the beginning of the render function above, it's called the first time the screen is loaded, but if I go to screen #2 and then come back to screen #1 via the nav component, the console.log isn't output again. Why?
And for what it's worth, I'm using the standard navigation.navigate('ScreenName') in NavComponent to go from screen to screen.
Any advice on how to fix the warning (and/or just better design the app) so that I can have that nav on every page would be greatly appreciated. Thank you.
your api call is resulting in the warning, in the react/native ecosystem, when a component is removed from the tree, the developer needs to cancel all subscriptions (listeners to events) and async tasks(fetching data from the web), those function need to be canceld by the developer as react/native cant do that for you.
to handle that in a class based component, you need to impelment componentWillUnmount and remove the subscriptions there
class MyClass extends Component {
componentWillUnmount() {
// remove listeners and cancel requests
}
but in a modern hook component , you need to return a cleanup function, a function to return in useEffect that will be called by react to cancel any subscriptions you have made, in your case, just return a cleanup function should remove that warning for you
const [mounted, setIsMounted] useState(false)
useEffect(() => {
// getPageData is a custom method I added to axios.
setIsMounted(true)
axios.getPageData('/api/url/here', (data) => {
if(isMounted)
setServerData(data);
});
return () => {
setIsMounted(false)
}
}, []);

How to create a widget with react for integration in other react app?

I would like to know if it is possible to write a widget with React and distribute it as a CDN to integrate it into another application react?
The idea:
I have several applications in writing with react and I would like to have a banner common to all applications without having to rewrite it in each of them. The goal is to facilitate the updates of this banner.
My widget named toolBar works perfectly when I do npm start.
I have build my toolBar and add the script generate in the folder build into an other app named myAppTest.
My toolBar work and the other app to. However, the css and toolBar images are not loaded properly when launching myAppTest.
What is the best way pleasure ?
If by "integrating a widget with a react application" you mean a stand-alone react application (your widget) that needs to get data and pass data back to another application (react application in your case) then it's totally doable.
I even posted an article with a tutorial on how to do just that.
Basically the gist of it is that most of us learned that each react application has an entry point that run this line:
ReactDOM.render(<App/>, myContainer);
As in fire and forget approach.
But actually we can run ReactDOM.render as much as we want, it won't re-mount our application but instead will trigger the diffing for the tree.
If the React element was previously rendered into container, this will perform an update on it and only mutate the DOM as necessary to reflect the latest React element.
So what we can do is wrap it in a function and expose it globally so other code on the page can run it.
A pattern i use is to accept props with that function and it will pass it on to the <App/> via ReactDOM.render.
For example:
window.CoolWidget = {
mount: (props, container) => {
ReactDOM.render(<CoolWidget {...props} />, container);
},
unmount: (container) => {
ReactDOM.unmountComponentAtNode(container);
}
}
And the consumers of your widget can run it:
window.CoolWidget.mount({widgetProp: someValue, onLogin: function(){...}}, myContainer)
The best thing with this approach is that they can convert this code to a react component (or maybe even you can do it for them!):
class CoolWidgetWrapper extends PureComponent {
// create a ref so we can pass the element to mount and unmount
widgetRef = React.createRef();
componentDidMount() {
// initial render with props
window.CoolWidget.mount(this.props, this.widgetRef.current);
}
componentDidUpdate(prevProps) {
if(prevProps !== this.props){
window.CoolWidget.mount(this.props, this.widgetRef.current);
}
}
componentWillUnmount(){
window.CoolWidget.unmount(this.widgetRef.current);
}
render() {
return <div ref={this.widgetRef}></div>
}
}
And this is how they will use it:
<CoolWidgetWrapper someProp={someValue} onLogin={someFunc} />
Hope that helps

What is the best way to render a screen change?

I am making an app that changes screen layout on an event detection, i.e. When the mobile detects a BLE signal, it should load the interface according to the id received. What should be the best way to do it? (setState for a variable)
DeviceEventEmitter.addListener(someevent, () => {
this.setState({some variable})
});
.....
render() {
if (this.state.some variable) {
return();
}else {
return (
regular interface
);
}
}
this is how I do it currently, I cannot navigate to different page, as this page contains listeners for two events, exit and entry. Also if i use react native router flux with API calls and loading huge data, would it affect speed?

React-Native BackHandler always closes App in Android (hardware back)

I'm building a App with react-native v0.44.0 using redux v5.0.5 and react-navigation v1.0.0-beta.11. The routing is done with nested navigators, one main StackNavigator and DrawerNavigator.
I'm handling all navigation events in a navigation reducer, also the hardware back press on Android using BackHandler. Now comes the weird part (for me), I've implemented the BackHandler event handlers like so:
import { BackHandler, Modal, View } from 'react-native';
import { NavigationActions } from 'react-navigation';
import { HARDWARE_BACK_PRESS } from '../helpers/NavTypes';
constructor(props) {
super(props);
this.handleBack = this.handleBack.bind(this);
}
componentWillMount() {
BackHandler.addEventListener(HARDWARE_BACK_PRESS, this.handleBack);
}
componentWillUnmount() {
BackHandler.removeEventListener(HARDWARE_BACK_PRESS, this.handleBack);
}
handleBack() {
const navAction = NavigationActions.back();
this.props.navigation.dispatch(navAction);
return true;
}
In my navigation reducer I'm handling the Navigation/BACK action type and keep track of my state. Now, when I'm pressing the hardware back button on my Android device or in the emulator, I can see thanks to redux-logger and the React Native debugger that the navigation action is dispatched correctly and the previous shown screen appears, but the app closes anyway. This happens also when I alter the handleBack method to something like this:
handleBack() {
return true;
}
Every time the hardware back button is pressed, the App still closes. I did some step-debugging innode_modules/react-native/Libraries/Utilities/BackHandler.android.js, inside of RCTDeviceEventEmitter.addListener I can see that my event listeners are registered and invokeDefault is set to true in the loop. addListener is exited but the App still closes. Does anyone know if there is some point there react-navigation and redux are overriding the behaviour of the hardware back button at some top level I'm not aware of?
I've setup a second plain RN project without react-navigation and redux, implementing the same BackHandler event listeners (also returning true) and the app won't close. So, right now this leaves me a bit puzzled.
I'm using react-navigation and I also handle the os back button. It works fine for me.
May be you can try this out. Note that handleBack must return true if you are performing any other task than closing the app. If not it will close the app immediately.
componentWillMount() {
BackHandler.addEventListener(HARDWARE_BACK_PRESS, () => { return this.handleBack.bind(this)() });
}
I had the same problem, returning true wouldn't update. Running react-native run-android again after the changes fixed the problem for me.

prevent react-router from detecting location change

In my React/Redux based application, I have implemented logout like following
In reducers/index.js where I do combineReducers, I have created an app level reducer called appReducer. There I check for LOGOUT action and then return undefined.
All this works fine. What I want to do is, that for LOGOUT action, I also want to clear localStorage and redirect to login page. Please note that I want to redirect native browser way, not using react-router. If I do window.location = '/'. First its detected by react-router and I see login page for a bit and then it refreshes which is a bit odd.
Is there a way to prevent react-router from being notified on location change!?
You cannot prevent it completely but you can control it with the access to
history.listen function.
With React-Router 4 you can wrap top level components using the HOC withRouter.
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(App));
This allows accessing this.props.history and controlling it
class App extends Component {
constructor(props) {
super(props);
this.props.history.listen((location, action) => {
//here you can control the location change
});
}
render() {
return (
</div>
);
}
}
And you also have the listenBefore event on the history object and you can use this event to control the navigation and add your own custom navigation logic:
history.listenBefore( (location, done) => doSomething(location).then(done) )

Categories