Preloading assets in React-Native expo app never finishes - react-native

I'm writing a simple app with React Native and Expo.
This app has ~10 small to medium sized images that are used in different places within the app.
From what I read, unless I cache these images, they will be required to be downloaded from expo each time.
For this reason, I have noticed that they seem to load in really slowly when testing the app. Upon building and navigating through the app, I find that it takes a few seconds for my images to pop up even after the rest of the page has loaded.
I followed the setup as seen in the starting template.
Here is what my App.js looks like (I am using react-navigation so it varies from the sample file above):
export default class App extends React.Component {
state = {
isLoadingComplete: false
};
componentDidMount() {
StatusBar.setHidden(true);
}
render() {
_loadResourcesAsync = async () => {
return Promise.all([
Asset.loadAsync([
require("./assets/syria.png"),
require("./assets/lebanon.png"),
require("./assets/kenya.png"),
require("./assets/indonesia.png"),
require("./assets/somalia.png"),
require("./assets/india.png"),
require("./assets/america.png"),
require("./assets/albania.png"),
require("./assets/bosnia.png")
])
]);
};
_handleLoadingError = error => {
Alert.alert(error);
};
_handleFinishLoading = () => {
this.setState({ isLoadingComplete: true });
};
if (this.state.isLoadingComplete == false) {
return (
<AppLoading
startAsync={this._loadResourcesAsync}
onError={this._handleLoadingError}
onFinish={this._handleFinishLoading}
/>
);
} else {
return (
<AppContainer
ref={navigatorRef => {
NavigationService.setTopLevelNavigator(navigatorRef);
}}
/>
);
}
}
}
I have excluded my react-navigation code for the sake of brevity.
When I run this, my app gets stuck at Downloading JavaScript bundle 100.00%.
It seems that the _handleFinishLoading never runs. At least, that's the only reason I can see for it to never finish loading.
Given the small amount of images, I don't know how this could take more than a second. Instead it sits at the splash screen forever.
Any ideas on what I might be doing wrong here?

Found the solution:
I made a simple error. The async functions (_loadResourcesAsync, _handleFinishLoading, etc) need to be outside the render method. Moving them below my render method inside of the app class caused this to work as expected.

Related

Next.JS Abort fetching component for route: "/login"

I was developing a useUser Hook for per-page authentication. I have implemented the useUser hook normally and Redirecting works fine accordingly.
But I am getting the above error.
Abort fetching component for route: "/login"
How can I fix useUserHook to solve it??
//useUser.tsx
const useUser = ({ redirectTo, redirectIfFound }: IParams) => {
const { data, error } = useRequest("authed", isAuthed);
const user = data?.data;
const hasUser = user;
useEffect(() => {
if (!redirectTo) return;
if (
// If redirectTo is set, redirect if the user was not found.
(redirectTo && !redirectIfFound && !hasUser) ||
// If redirectIfFound is also set, redirect if the user was found
(redirectIfFound && hasUser)
) {
Router.push(redirectTo);
}
}, [redirectTo, redirectIfFound, hasUser]);
return error ? null : user;
};
//index.tsx
const Home: NextPage = () => {
const user = useUser({ redirectTo: "/login" });
if (user === undefined || user === false) {
return <div>Loading...</div>;
}
return (
<div>
<Head>
<meta name="description" content="Generated by create next app" />
<link rel="icon" href="/favicon.ico" />
</Head>
<div>Home</div>
</div>
);
};
UseRequest Hook returns true and false as return values.
tl;dr
Ensure that you only call router.push() once throughout all potential re-executions of useEffect with the help of state:
const [calledPush, setCalledPush] = useState(false); // <- add this state
// rest of your code [...]
useEffect(() => {
if (!redirectTo) return;
if (
(redirectTo && !redirectIfFound && !hasUser) ||
(redirectIfFound && hasUser)
) {
// check if we have previously called router.push() before redirecting
if (calledPush) {
return; // no need to call router.push() again
}
Router.push(redirectTo);
setCalledPush(true); // <-- toggle 'true' after first redirect
}
}, [redirectTo, redirectIfFound, hasUser]);
return error ? null : user;
};
Background
useEffect potentially gets called multiple times if you have more than one dependency (Also happens with React Strict Mode enabled, but in this case there seems to be no error), and (re-)calling router.push() multiple times within the same Next.js page in different places/throughout different re-renders seems to cause this error in some cases, as the redundant router.push() call(s) will have to be aborted, because the current page-component unmounts due to the successful, previously called router.push().
If we keep track of whether we have already called router.push via the calledPush state as in the code snippet above, we omit all redundant router.push() calls in potential useEffect re-executions, because for all subsequent useEffect executions the state value calledPush will already be updated to true as useEffect gets triggered after re-renders, hence after setCalledPush(true) takes effect.
In my case I have use rotuer.push("/") two times in a single file. That caused the error. Try using one. I think problem will be solved.
This error occurs because useEffect tries to update a component that has already been unmounted and this can introduce memory leaks in which your app uses more memory than it needs to. To prevent this, use the following approach:
useEffect(() => {
//first manually mount the effect
let mounted = true;
//check if component is currently mounted
if(mounted && ...code){
router.push('/index')
}
//cleanup side effects before unmounting
return()=>{mounted=false}
}, [router]);
};
In my current NextJS project, when I make reactStrictMode: false,, the value to false, then it seems like the re-rendering will be gone, and component will be only rendered once.
I don't like react strict mode very much ..

useURL hook expo-linking for background app

The expo-linking React Native package has a hook named useURL that isn't working for me when the app is in the background. From the docs it Returns the initial URL followed by any subsequent changes to the URL. The problem I'm having with my managed expo app is that the hook doesn't work when the app is already open in the background. Here is the hook:
export default function App() {
const isLoadingComplete = useCachedResources();
const url = Linking.useURL();
useEffect(() => {
Alert.alert(url ? url.substring(20) : 'null');
}, [url]);
if (!isLoadingComplete) {
return null;
} else {
return (
...
);
}
}
If I open the URL exp://exp.host/#myprofile/myproject?a=bwhen the app is closed, I get an Alert as expected. If the app is in the background, the Alert doesn't go off. I've tested on an iOS emulator and a physical Android. Any solutions? Note that similar problems happen with Linking.addEventListener().

How to properly test if a Toast has been shown in react native using native base?

I am trying to write a test that checks if the screen is showing a Toast with an error message. The test passes, but there is a warning:
console.error
Warning: You called act(async () => ...) without await.
This could lead to unexpected testing behaviour, interleaving multiple act calls
and mixing their scopes. You should - await act(async () => ...);
The screen is working fine, I am just learning how to write tests. This is my test:
it('shows error correctly', async () => {
mockAxios.get.mockRejectedValueOnce(new Error('Async error'))
const { queryByText } = renderWithRedux(<DiscoverScreen />)
await waitFor(() => {
expect(queryByText(ErrorMessages.GeneralErrorToast)).not.toBeNull()
})
await waitForElementToBeRemoved(() => queryByText(ErrorMessages.GeneralErrorToast), { timeout: 5000 })
})
What am I not doing right? Definitely there is an issue with react native testing, because there are problems for certain async querying, especially when you have several of them. I found that here: https://github.com/callstack/react-native-testing-library/issues/379#issuecomment-720734366
I am using native base for showing the Toast, which is using Animated I think. Should I use jest.useFakeTimers() and how?
After researching how the Toast in native base works (this could be done when you open the source code in github - https://github.com/GeekyAnts/NativeBase/blob/master/src/basic/ToastContainer.js), I found that it uses Animated.timing.
So I had to find out how to deal with react native animations in tests. That article had a solution that worked for me: https://medium.com/#joncardasis/react-native-how-to-test-components-implementing-animated-with-jest-8cabb5fc2730
After I added the code in my jest setup file, this is how my test looks:
global.withAnimatedTimeTravelEnabled(async () => {
const { queryByText } = renderedComponent
await waitFor(() => {
expect(queryByText(ErrorMessages.GeneralErrorToast)).not.toBeNull()
})
global.timeTravel(Constants.ErrorToastDelay)
expect(queryByText(ErrorMessages.GeneralErrorToast)).toBeNull()
})
It works and now the test passes with no warnings!
One little adjustment in my jest configuration was also missing. I had to add this:
"testEnvironment": "jsdom"
I hope this could help someone else, too!

React Native Arabic (RTL) without forceRTL

In RN my bilingual app (English - Arabic), I have used I18nManager (views) and I18n (for translations)
When I am changing app language to Arabic, the whole app gets reloaded again from the splash-screen using this code:
I18nManager.forceRTL(true)
Ideally, it should not restart the app from start and it should continue with the current screen with Arabic data.
Currently, it is not happening, only translation elements are getting converted using I18n.t('keyword') but for views Arabic alignment, it's not proper.
Still looking for a better solution, let me know if anyone achieved it.
Thanks
Sopo !!
you should put this code in the top component in your project
import RNRestart from "react-native-restart";
I18nManager.forceRTL(true);
if (!I18nManager.isRTL) RNRestart.Restart();
If you guys wants to store stack state after reloading(because there is no other option without reloading) and want stack state back you can follow this link also you can check my code.
Link: React navigation state persist
Any Component
AsyncStorage.setItem('navigation_state', JSON.stringify(navigation.dangerouslyGetState()));
My App.js
const App = () => {
const [initialState, setInitialState] = useState();
const [isReady, setIsReady] = useState(false);
useEffect(() => {
restoreState();
}, []);
const restoreState = async () => {
try {
const savedStateString = await AsyncStorage.getItem('navigation_state');
const state = savedStateString ? JSON.parse(savedStateString) : undefined;
if (state !== undefined) {
AsyncStorage.removeItem('navigation_state');
setInitialState(state);
}
} finally {
setIsReady(true);
}
};
if (!isReady) {
return null;
}
return (
<Provider store={store}>
<NavigationContainer
initialState={initialState}
ref={rootNavigationRef}>
<Root>
<AppNavigator />
</Root>
</NavigationContainer>
</Provider>
);
};
I working on a project which has two languages, Arabic and English.i use redux for handling app language. I put all styles on redux and handle app style with redux. and when user change language all styles on my app change to that language . also all text handled with redux too. with this way, my app does not reload and app language changed immediately.
If your app is an android hybrid app, you can try this:
import com.facebook.react.modules.i18nmanager.I18nUtil;
I18nUtil i18nUtil = I18nUtil.getInstance();
i18nUtil.forceRTL(context, forceRtl);
i18nUtil.allowRTL(context, true);
value 'forceRtl' is a boolean.
for iOS,I think you can find the same method.
In Expo use
import {Updates} from "expo"
Updates.reload()

React Native startup optimization

I am looking for a way to optimize the startup time of a pure react native mobile app.
As a JavaScript framework, is that possible to bundle the JavaScript files into separated files, say something like common.js and app.js. I was searching via Google with keywords something like "react native webpack" stuff but it seems like all these libraries are deprecated or out of date, such as react-native-webpack-server, react-native-webpack-starter-kit etc.
I am wondering if anybody here is also looking for a way to optimize the JavaScript bundle in react native. Or, maybe these third party bundle approach has been overcame by Facebook standard bundle?
You could dynamically load your component, in this way your bundle.js will contain only the fraction of js needed and as you navigate you will request the other different parts / fractions.
Rather than do the traditional way: import App from './containers/App/App'; you could do something like this:
class ImportedComponent extends Component {
state = {
component: null
}
componentWillMount() {
this.props.load()
.then((mod) => this.setState(() => ({
component: mod.default
})))
}
render() {
return this.props.children(this.state.component)
}
}
const App = (props) => (
<ImportedComponent load={() => import('./containers/App/App')}>
{(Component) => Component === null ? <h6 className="loading-message">Loading...</h6> : <Component {...props}/>}
</ImportedComponent>
)
or you can lazy load your component itself. Let's say for example that I have Moment JS and I don't want to load it until it's needed. so what I could do:
1) Create a state and set it to null.
constructor(props){
super(props);
this.state = {
lazyLoadedComponent: () => null
}
}
2) Use async componentDidMount with await, try and catch and update the state lazyLoadedComponent on componentDidMount
async componentDidMount(){
try {
const Moment = await import('react-moment');
this.setState({ lazyLoadedComponent: (data)=>{
return React.createElement(Moment.default, {format:'MM/DD/YY'}, data)
}
});
}
catch(err) {
this.setState({ lazyLoadedComponent: <div>{`Failed to load component: ${err}`}</div> });
}
}
3) Call the component on the render:
{this.state.lazyLoadedComponent(value.createdOn)}
By following these 2 examples you should, hopefully, be looking at a bundle.js under 250KB.
As a possible solution you can use ram-bundle format, that metro bundler provides.
In this case you will not load the entire js-bundle - you will load only part, that you need at a startup (in a lot of application are a lot of places, which user may not even see, and this feature allow you load such parts, only when they are required). So you can simplify your entry point and load only small piece of your bundle.
You can look at react-native-bundle-splitter. This library well integrated with almost all popular navigation libraries and allows you to postpone a loading of specific routes. For example, if you have a login screen, you can load at start up only this screen, and all others load in background or start the loading of them, only when user can see them.