How to detect when a React Native app is closed (not suspended)? - mobile

I have looked everywhere and cannot find an answer to this. How can I detect when a user is trying to close my React Native app (as in the process is running, and they manually manage their apps and force exit it). I would like to add logout functionality when this happens, however cannot find a way to detect it. AppState appears to only detect when the app is brought in and out of the background.

Looks like you can detect the previous state and compare it to the next state. You can't detect that the app is closing vs going into the background, from what I can find online, but you can detect if it was inactive (closed), or in the background.
Example from React Native Docs
import React, {Component} from 'react'
import {AppState, Text} from 'react-native'
class AppStateExample extends Component {
state = {
appState: AppState.currentState
}
componentDidMount() {
AppState.addEventListener('change', this._handleAppStateChange);
}
componentWillUnmount() {
AppState.removeEventListener('change', this._handleAppStateChange);
}
_handleAppStateChange = (nextAppState) => {
if (this.state.appState.match(/inactive|background/) && nextAppState === 'active') {
console.log('App has come to the foreground!')
}
this.setState({appState: nextAppState});
}
render() {
return (
<Text>Current state is: {this.state.appState}</Text>
);
}
}

With #react-native-community/hooks you can use the useAppState hook to check the app state.
When you close the app, it gets a state of unknown once you open it back. When is in background, it says background' or 'inactive'. So when it's unknown` you know the app is opening from being closed, not minimized or in the background.

I suggest to use web socket and it will help you to solve your problem, as following :
react native app
import React from 'react';
const io = require('socket.io-client/dist/socket.io');
const App = ()=>{
React.useEffect(()=>{
// connect to web socket
window.appSocket = io.connect(`<server-origin>/?token=<string>&userAppId=<string>`);
window.appSocket.on('connect',()=>{
// do what you line
})
},[])
...
}
export default App;
express server side
const express = require('express');
const server = express();
const http = require('http').Server(server);
const io = require('socket.io')(http);
io.on('connection',(socket)=>{ // this callback function for each web socket connection
let { token , userAppId } = socket.handshake.query;
if( userAppId ){
console.log(`${userAppId} online`)
socket.on('disconnect',(resean)=>{
console.log(`${userAppId} shut down`)
})
}
});
for more details, you can check socket.io

Just ran into this same issue. The answer that's accepted does not actually do what is asked in the OP.
A hacky solution would be to set a flag on the first screen your application opens when opened fresh and to NOT SHOW THAT SCREEN when the app has just been backgrounded.
Not super elegant and I can't show example code, but it fixes my particular issue.

As a simple method , we can use componentWillUnmount() inside the root component for detect the app is closed. Because Root component Unmount only when app is closed. :)

Related

notifications doesn't close when route changes antd

In my react app I need to close all notifications on current tab when i go to another tad. antd version "^4.20.7". Notifications open and close through antd api.
I know that i can do it directly in the component, but maybe is it some solutions to add this functionality to all notifications in whole app?
You can use the destroy method to get rid of all notification generated.
In your case you would just need to notification.detroy() when another tab is clicked.
To add this functionality to all notification that exists inside your app you would simply need to handle the notification state in redux or equivalent.
Resolved by creating a hook that monitors the change of the route and inserting it in the App component and wrapper class Notification that store notification key. I used close method instead of destroy because when using destroy notification disappear without close animation.
class Notification {
key = '';
constructor() {
notification.config({
closeIcon: <CrossIcon />,
maxCount: 1,
});
}
open(config: ArgsProps) {
this.key = `notification-${Date.now()}`;
notification.open({
...config,
key: this.key,
});
}
close() {
notification.close(this.key);
}
}
function useNotificationClose() {
const {pathname} = useLocation(); // React Router v6 hook
useEffect(() => {
Notification.key && Notification.close();
}, [pathname]);
}
function App() {
useNotificationClose();
return ...your app components
}
Note: this will work when maxCount set to 1 as in my case.

Etherjs "window" object not accessible in Next.js

I have a relatively basic project using solidity and react working as a single page dApp using Create React App. However I am trying to move this now to Nextjs and have hit a hurdle which I assume is something to do with the server side stuff Nextjs does. I have removed all the redundant code and just provide enough to generate the error:
import { ethers, Contract } from 'ethers';
import Project from '../src/artifacts/contracts/Project.sol/Project.json';
const contractAddress = process.env.contract_address;
export default function App() {
const provider = new ethers.providers.Web3Provider(window.ethereum);
console.log(provider.getSigner())
return (
<div className="App">
<p>Hello!</p>
</div>
);
}
This errors with:
window is not defined
I saw someone else suggest loading and setting it via state like so:
const [provider, setProvider] = useState({})
React.useEffect(() => {
setProvider(new ethers.providers.Web3Provider(window.ethereum))
}, []);
const signer = provider.getSigner();
But this returns: TypeError: provider.getSigner is not a function
However if i comment out this code, refresh and let the page load, then uncomment the code and let hot reload refresh the component I get no such error and can successfully console.log the signer.
Pulling my limited supply of hair out trying to resolve this, any help would be appreciated.
I have managed to get this working whilst sticking with my functional components.
Within useEffect I included a statement to check if the windowobject was undefined:
if (typeof window.ethereum !== "undefined" || (typeof window.web3 !== "undefined")) {
// Existing code goes here
}
And then had to make sure that any variables that I wanted to use outside of this if statement were saved to state as well as declared within the statement. Like:
const provider = new ethers.providers.Web3Provider(window.ethereum);
setProvider(provider)
This seemed to solve most of the issues with moving from CRA to Next and I now have the dApp back up and running.

Next js how to fetch localStorage data before client side rendering

I am using react + next.js in my client side and I am getting this weird warning, which I've failed to fix over the previous 3 days.
I assume the Warning arise in because the isUserAuthenticated value is saved on the localStorage.
and localStorage is only available on the client side, and therefore the value is diffrent from the initial data rendered on the server.
after searching on google I read a few post which suggested using componentDidMount() to in order to load the localStorage values before the the client side in rendred.
unfortunelty I've failed to implement this idea, threfore I am asking for help, can someone help me to to solve this problem? thanks in advance
The warning:
react-dom.development.js:67 Warning: Text content did not match.
Server: "false" Client: "true"
at div
at ul
at div
at nav
at Navbar
Navbar.js
const Navbar = () => {
const { isUserAuthenticated, } = useSelector((state) => state.authReducer);
return (
<nav data-testid='navbar'>
<div>
<>{isUserAuthenticated ? <div>true</div> : <div>false</div>}</>
</div>
</nav>
);
};
export default Navbar;
the solution
// TODO recipes crud to navbar
// test author and guest links
import { useSelector } from 'react-redux';
import React from 'react';
const Navbar = () => {
const [isUserAuthenticated, setIsUserAuthenticated] = useState();
useEffect(() => {
setIsUserAuthenticated(store.getState().authReducer.isUserAuthenticated);
}, []);
return (
<nav data-testid='navbar'>
<div>
<>{isUserAuthenticated ? <div>true</div> : <div>false</div>}</>
</div>
</nav>
);
};
export default Navbar;
This error happens when your server returns a different html structure than your client side, this will force React to re-render the entire App instead just attach event listeners (which is much faster).
From the error, looks like your isUserAuthenticated & loggedUserData have different values at client side & server side, Print them, if this is the situation, check the reason for this.
Edit
As you've mentioned, localStorage available only at client side, this means that your code most support it.
At the serverSide (there is no localStorage) your isUserAuthenticated should return false.
At the client side, you should "continue" the server state (which means that you don't load the value from localStorage firstly), then when the app is mounted, load the value.
if you are using classes, use the componentDidMount (which runs only at client side) in order to update the value of isUserAuthenticated.
if you are using hooks, use the useEffect hook (which runs only at client side) in order to update the value of isUserAuthenticated.

How do people handle scroll restoration with react-router v4?

I'm experiencing some problems with scroll positions on the back button (history popstate) when using react-router. React router v4 doesn't handle scroll management out of the box because browsers are implementing some automatic scroll behavior. This is great except when the height of the browser window changes too dramatically from one view to another. I have implemented the ScrollToTop component as described here: https://reacttraining.com/react-router/web/guides/scroll-restoration
This works great. When you click a link and go to a different component, the browser scrolls to the top (like a normal server-rendered website would). The issue only happens when you go back (via the browser back button) to a view with a much taller window height. It seems that (chrome) tries to go to the scroll position of the previous page before react has rendered the content (and browser height). This results in the scroll only going as far down as it can based on the height of the view it's coming from. Picture this scenario:
View1: Long list of movies (window height 3500px).
(movie is clicked)
View2: Details view of the selected movie (window height: 1000px).
(Browser back button is clicked)
Back to view 1, but scroll position can't go further than 1000px, because chrome is trying to set the position before react renders the long movie list.
For some reason this is only a problem in Chrome. Firefox and Safari seem to handle it fine. I wonder if anyone else have had this problem, and how you guys generally handle scroll restoration in React.
Note: all the movies are imported from a sampleMovies.js — so I'm not waiting for an API response in my example.
Note that history.scrollRestoration is just a way of disabling the browser's automatic attempts at scroll restoration, which mostly don't work for single-page apps, so that they don't interfere with whatever the app wants to do. In addition to switching to manual scroll restoration, you need some sort of library that provides integration between the browser's history API, React's rendering, and the scroll position of the window and any scrollable block elements.
After not being able to find such a scroll restoration library for React Router 4, I created one called react-scroll-manager. It supports scrolling to top on navigation to a new location (aka history push) and scroll restoration on back/forward (aka history pop). In addition to scrolling the window, it can scroll any nested element that you wrap in an ElementScroller component. It also supports delayed/asynchronous rendering by using a MutationObserver to watch the window/element content up to a user-specified time limit. This delayed rendering support applies to scroll restoration as well as scrolling to a specific element using a hash link.
npm install react-scroll-manager
import React from 'react';
import { Router } from 'react-router-dom';
import { ScrollManager, WindowScroller, ElementScroller } from 'react-scroll-manager';
import { createBrowserHistory as createHistory } from 'history';
class App extends React.Component {
constructor() {
super();
this.history = createHistory();
}
render() {
return (
<ScrollManager history={this.history}>
<Router history={this.history}>
<WindowScroller>
<ElementScroller scrollKey="nav">
<div className="nav">
...
</div>
</ElementScroller>
<div className="content">
...
</div>
</WindowScroller>
</Router>
</ScrollManager>
);
}
}
Note that an HTML5 browser (10+ for IE) and React 16 are required. HTML5 provides the history API, and the library uses the modern Context and Ref APIs from React 16.
How do you handle your scroll restoration?
Turns out browsers have implementations of the history.scrollRestoration.
Maybe you can use that? Check these links out.
https://developer.mozilla.org/en/docs/Web/API/History#Specifications
https://developers.google.com/web/updates/2015/09/history-api-scroll-restoration
In addition, I found an npm module that might be able to handle scroll restoration in react with ease, but this library only works with react router v3 and below
https://www.npmjs.com/package/react-router-restore-scroll
https://github.com/ryanflorence/react-router-restore-scroll
I hope this can help.
my solution:save window.scrollY for every pathname with a Map (ES6)
scroll-manager.tsx
import { FC, useEffect } from 'react';
import { useLocation } from 'react-router-dom';
export function debounce(fn: (...params: any) => void, wait: number): (...params: any) => void {
let timer: any = null;
return function(...params: any){
clearTimeout(timer);
timer = setTimeout(()=>{
fn(...params)
}, wait);
}
}
export const pathMap = new Map<string, number>();
const Index: FC = () => {
const { pathname } = useLocation();
useEffect(() => {
if (pathMap.has(pathname)) {
window.scrollTo(0, pathMap.get(pathname)!)
} else {
pathMap.set(pathname, 0);
window.scrollTo(0, 0);
}
}, [pathname]);
useEffect(() => {
const fn = debounce(() => {
pathMap.set(pathname, window.scrollY);
}, 200);
window.addEventListener('scroll', fn);
return () => window.removeEventListener('scroll', fn);
}, [pathname]);
return null;
};
export default Index;
App.tsx
function App() {
return (
<Router>
<ScrollManager/>
<Switch>...</Switch>
</Router>
);
}
You can also use pathMap.size === 1 to determine if the user entered the app for the first time
I wound up using localStorage to track the scroll position - not sure this would handle all situations.
In this example, there's a Company page with a set of Stores, and each Store has a set of Display cases. I needed to track the scroll position of the display cases, so saved that to a 'storeScrollTop' key. There were 6 lines of code to add.
company.jsx:
// click on a store link
const handleClickStore = (evt) => {
window.localStorage.removeItem('storeScrollTop') // <-- reset scroll value
const storeId = evt.currentTarget.id
history.push(`/store/${storeId}`)
}
store.jsx:
// initialize store page
React.useEffect(() => {
// fetch displays
getStoreDisplays(storeId).then(objs => setObjs(objs)).then(() => {
// get the 'store' localstorage scrollvalue and scroll to it
const scrollTop = Number(window.localStorage.getItem('storeScrollTop') || '0')
const el = document.getElementsByClassName('contents')[0]
el.scrollTop = scrollTop
})
}, [storeId])
// click on a display link
const handleClickDisplay = (evt) => {
// save the scroll pos for return visit
const el = document.getElementsByClassName('contents')[0]
window.localStorage.setItem('storeScrollTop', String(el.scrollTop))
// goto the display
const displayId = evt.currentTarget.id
history.push(`/display/${displayId}`)
}
The trickiest part was figuring out which element had the correct scrollTop value - I had to inspect things in the console until I found it.
This component scroll to up if page is new but if page seen before restore the scroll.
scroll-to-top.tsx file:
import { useEffect, FC } from 'react';
import { useLocation } from 'react-router-dom';
const pathNameHistory = new Set<string>();
const Index: FC = (): null => {
const { pathname } = useLocation();
useEffect((): void => {
if (!pathNameHistory.has(pathname)) {
window.scrollTo(0, 0);
pathNameHistory.add(pathname);
}
}, [pathname]);
return null;
};
export default Index;
app.tsx file:
<BrowserRouter>
<ScrollToTop />
<App />
</BrowserRouter>
Use this library
https://www.npmjs.com/package/react-scroll-restoration
React Router does not provide out of the box support for scroll restoration and as it currently stands they won't either, because browsers are implementing some automatic scroll behavior

Code splitting causes chunks to fail to load after new deployment for SPA

I have a single page app that I code split on each route. When I deploy a new version of my app the users will usually get an error if a user still has the page open and visits a route they haven't visited before.
Another scenario where this can also happen is if the app has service workers enabled. When the user visits a page after a new deployment, the service worker will serve from the cache. Then if the user tries to visit a page not in their cache, they'll get the chunk loading failure.
Currently I disabled code splitting in my app to avoid this but I've been very curious what's the best way to handle this issue. I've thought about pre-loading all the other routes after the user finishes loading the initial page and I believe this might fix the issue for code splitting on routes. But let's say I want to code split on components then that would mean I have to try to figure out when and how to pre-load all of those components.
So I'm wondering how do people handle this issue for single page apps? Thanks! (I'm currently using create-react-app)
I prefer to let the user refresh rather than refreshing automatically (this prevents the potential for an infinite refresh loop bug).
The following strategy works well for a React app, code split on routes:
Strategy
Set your index.html to never cache. This ensures that the primary file that requests your initial assets is always fresh (and generally it isn't large so not caching it shouldn't be an issue). See MDN Cache Control.
Use consistent chunk hashing for your chunks. This ensures that only the chunks that change will have a different hash. (See webpack.config.js snippet below)
Don't invalidate the cache of your CDN on deploy so the old version won't lose it's chunks when a new version is deployed.
Check the app version when navigating between routes in order to notify the user if they are running on an old version and request that they refresh.
Finally, just in case a ChunkLoadError does occur: add an Error Boundary. (See Error Boundary below)
Snippet from webpack.config.js (Webpack v4)
From Uday Hiwarale:
optimization: {
moduleIds: 'hashed',
splitChunks: {
cacheGroups: {
default: false,
vendors: false,
// vendor chunk
vendor: {
name: 'vendor',
// async + async chunks
chunks: 'all',
// import file path containing node_modules
test: /node_modules/,
priority: 20
},
}
}
Error Boundary
React Docs for Error Boundary
import React, { Component } from 'react'
export default class ErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
// Update state so the next render will show the fallback UI.
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
// You can also log the error to an error reporting service
console.error('Error Boundary Caught:', error, errorInfo);
}
render() {
const {error, hasError} = this.state
if (hasError) {
// You can render any custom fallback UI
return <div>
<div>
{error.name === 'ChunkLoadError' ?
<div>
This application has been updated, please refresh your browser to see the latest content.
</div>
:
<div>
An error has occurred, please refresh and try again.
</div>}
</div>
</div>
}
return this.props.children;
}
}
Note: Make sure to clear the error on an internal navigation event (for example if you're using react-router) or else the error boundary will persist past internal navigation and will only go away on a real navigation or page refresh.
The issue in our create-react-app was that the chunks that the script tags were referencing did not exist so it was throwing the error in our index.html. This is the error we were getting.
Uncaught SyntaxError: Unexpected token < 9.70df465.chunk.js:1
Update
The way we have solved this is by making our app a progressive web app so we could take advantage of service workers.
Turning a create react app into a PWA is easy. CRA Docs on PWA
Then to make sure the user was always on the latest version of the service worker we made sure that anytime there was an updated worker waiting we would tell it to SKIP_WAITING which means the next time the browser is refreshed they will get the most up to date code.
import { Component } from 'react';
import * as serviceWorker from './serviceWorker';
class ServiceWorkerProvider extends Component {
componentDidMount() {
serviceWorker.register({ onUpdate: this.onUpdate });
}
onUpdate = (registration) => {
if (registration.waiting) {
registration.waiting.postMessage({ type: 'SKIP_WAITING' });
}
}
render() {
return null;
}
}
export default ServiceWorkerProvider;
Bellow is the first thing we tried and did run into some infinite looping
The way I got it to work is by adding a window.onerror function above all of our script tags in index.html.
<script>
window.onerror = function (message, source, lineno, colno, error) {
if (error && error.name === 'SyntaxError') {
window.location.reload(true);
}
};
</script>
I wish there was a better way but this is the best way I could come up with and felt like it's a pretty safe solution since create-react-app will not compile or build with any syntax errors, this should be the only situation that we get a syntax error.
We solved this in a slightly ugly, albeit really simple solution. Probably temporary for now, but might help someone.
We have an AsyncComponent that we created to load chunks (i.e. route components). When this component loads a chunk and receives and error, we just do a simple page reload to update index.html and it's reference to the main chunk. The reason it's ugly is because depending on what your page looks like or how it loads, the user could see a brief flash of empty page before the refresh. It can be kind of jarring, but maybe that's also because we don't expect an SPA to refresh spontaneously.
App.js
// import the component for the route just like you would when
// doing async components
const ChunkedRoute = asyncComponent(() => import('components/ChunkedRoute'))
// use it in the route just like you normally would
<Route path="/async/loaded/route" component={ChunkedRoute} />
asyncComponent.js
import React, { Component } from 'react'
const asyncComponent = importComponent => {
return class extends Component {
state = {
component: null,
}
componentDidMount() {
importComponent()
.then(cmp => {
this.setState({ component: cmp.default })
})
.catch(() => {
// if there was an error, just refresh the page
window.location.reload(true)
})
}
render() {
const C = this.state.component
return C ? <C {...this.props} /> : null
}
}
}
export default asyncComponent
I am using AsyncComponent HOC to lazy load chunks, and was facing same issue.
the work around I did is, identifying the error and make a hard reload once.
.catch(error => {
if (error.toString().indexOf('ChunkLoadError') > -1) {
console.log('[ChunkLoadError] Reloading due to error');
window.location.reload(true);
}
});
the full HOC file looks like this,
export default class Async extends React.Component {
componentWillMount = () => {
this.cancelUpdate = false;
this.props.load
.then(c => {
this.C = c;
if (!this.cancelUpdate) {
this.forceUpdate();
}
})
.catch(error => {
if (error.toString().indexOf('ChunkLoadError') > -1) {
console.log('[ChunkLoadError] Reloading due to error');
window.location.reload(true);
}
});
};
componentWillUnmount = () => {
this.cancelUpdate = true;
};
render = () => {
const props = this.props;
return this.C ? (
this.C.default ? (
<this.C.default {...props} />
) : (
<this.C {...props} />
)
) : null;
};
}
I have a solution to the problem!
I was facing an identical situation. In my case, I use Vite in my React projects and every time the rollup generates the bundle chunks, it generates different hashes that are included in the filenames (Example: LoginPage.esm.16232.js). I also use a lot of code splitting in my routes. So every time I deployed to production, the chunk names would change, which would generate blank pages for clients every time they clicked on a link (which pointed to the old chunk) and which could only be resolved when the user refreshed the page (forcing the page to use the new chunks).
My solution was to create an ErrorBoundary wrapper for my React application that would "intercept" the error and display a nice error page explaining the problem and giving the option for the user to reload the page (or reload automatically)

Resources