White page after fresh build using React Route-based code splitting - reactjs

The app is using react and React Route-based code splitting: https://reactjs.org/docs/code-splitting.html#route-based-code-splitting
The app is working fine. A user is on the homepage.
Then I do a change in the code and build the app again.
User is clicking on a link, and he is landing on a white page.
Of course, the bundle has changed, and loading the new page (thanks to React.lazy) will drop an error.
Uncaught SyntaxError: Unexpected token <
How can I prevent that and show for example: "Site has been updated, please reload" instead of a white page?

This is built off Alan's comment, which doesn't quite solve the problem of the original question. I faced a similar issue where a build done on a server changed all the file names of the bundles I was loading using React.lazy() and a user who didn't refresh their page would be looking for bundles that no longer exists, resulting in the error he describes.
Again, this is mostly based off Alan's code but solves the problem nicely...
export default function lazyReloadOnFail(fn) {
return new Promise(resolve => {
fn()
.then(resolve)
.catch(() => {
window.location.reload();
});
});
}
const Report = React.lazy(() => lazyReloadOnFail(() => import('./views/Reports/Report')));

Solution is:
Did you know that the import(...) function that we use on lazy is just a function that returns a Promise? Which basically means that you can chain it just like any other Promise.
function retry(fn, retriesLeft = 5, interval = 1000) {
return new Promise((resolve, reject) => {
fn()
.then(resolve)
.catch((error) => {
setTimeout(() => {
if (retriesLeft === 1) {
// reject('maximum retries exceeded');
reject(error);
return;
}
// Passing on "reject" is the important part
retry(fn, retriesLeft - 1, interval).then(resolve, reject);
}, interval);
});
});
}
Now we just need to apply it to our lazy import.
// Code split without retry login
const ProductList = lazy(() => import("./path/to/productlist"));
// Code split with retry login
const ProductList = lazy(() => retry(() => import("./path/to/productlist")));
If the browser fails to download the module, it'll try again 5 times with a 1 second delay between each attempt. If even after 5 tries it import it, then an error is thrown.
Thanks to Guilherme Oenning from: https://dev.to/goenning/how-to-retry-when-react-lazy-fails-mb5

Related

How should I be using playwright's toHaveScreenshot() within a cucumber test in a React Typescript project?

I'm wanting to implement visual regression testing into a ReactJS app. I already have playwright setup called through cucumber for some other BDD UI tests and wanted to make use of the built in toHaveScreenShot method for visual regression. However, whenever I run the test it throws this error:
Error: toHaveScreenshot() must be called during the test
Here's the test script definition:
package.json excerpt
"test:e2e": "cucumber-js --require cucumber.conf.js --require features/step_definitions/**/*.js --format #cucumber/pretty-formatter",
Here's an example of the code:
cucumber.conf.js
const {
Before,
BeforeAll,
AfterAll,
After,
setDefaultTimeout,
} = require("#cucumber/cucumber");
const { chromium } = require("playwright");
// in milliseconds
setDefaultTimeout(60000);
// launch the browser
BeforeAll(async function () {
global.browser = await chromium.launch({
headless: false,
slowMo: 1000,
});
});
// close the browser
AfterAll(async function () {
await global.browser.close();
});
// Create a new browser context and page per scenario
Before(async function () {
global.context = await global.browser.newContext();
global.page = await global.context.newPage();
});
// Cleanup after each scenario
After(async function () {
await global.page.close();
await global.context.close();
});
homepage.feature
Feature: Homepage
A simple homepage
Scenario: Someone visiting the homepage
Given a new visitor to the site
When they load the homepage
Then they see the page
homepage.js
const { Given, When, Then } = require("#cucumber/cucumber");
const { expect } = require("#playwright/test");
Given("a new visitor to the site", function () {});
When("they load the homepage", async () => {
await page.goto("http://localhost:3000/");
});
Then("they see the page", async () => {
const locator = page.locator('img[alt="An image you expect to see"]');
await expect(locator).toBeVisible();
await expect(locator).toHaveScreenshot();
});
I think the error is complaining that I'm not writing my tests in the usual test() method, but I've not come across anything similar in searches and don't know how to give this context, assuming that is the problem, while using Cucumber.
Can anyone suggest a solution? I'm at a bit of a loss.

Is there a way to avoid blanks in react loadable components

I am using the react Loadable Components Package to lazily load components in my react app as shown in the snippet below:
import loadable from '#loadable/component'
const OtherComponent = loadable(() => import('./OtherComponent'))
function MyComponent() {
return (
<div>
<OtherComponent />
</div>
)
}
The site is hosted on firebase hosting.
I noticed that sometimes the lazily imported component won't load until the page is refreshed manually.
On checking the logs I can see errors like
Chunk Failed to load
Once I refresh the page, the page gets loaded and the Chunk Failed to Load error is gone.
Is there a way to avoid this?
You can have a retry(...) function. It wont make the problem completely disappear, but at least it will add some resilience upon failures:
export function retry(
fn,
{retries = 4, interval = 500, exponentialBackoff = true} = {}
) {
return new Promise((resolve, reject) => {
fn()
.then(resolve)
.catch(error => {
setTimeout(() => {
if (retries === 1) {
reject(error)
return
}
console.warn(`ChunkLoad failed. Will retry ${retries - 1} more times. Retrying...`)
// Passing on "reject" is the important part
retry(fn, {
retries: retries - 1,
interval: exponentialBackoff ? interval * 2 : interval
}).then(resolve, reject)
}, interval)
})
})
}
Than you can use it like this (with the default options):
loadable(() => retry(() => import(...)))

load splash screen before nextjs

I have a NextJS website and I want to add a Splash Screen for before website is loaded
but because the Splash Screen is also in the NextJS code, it will loading when nextjs rendered on the server and the JS downloaded and executed on the client. in fact, it's useless because it will execute after the page is ready!
how can I do the Splash Screen before react completely loaded and executed ?
I also use nginx for proxy_pass
use this code
useEffect(() => {
const handleStart = () => { setPageLoading(true); };
const handleComplete = () => {
setPageLoading(false);
};
router.events.on('routeChangeStart', handleStart);
router.events.on('routeChangeComplete', handleComplete);
router.events.on('routeChangeError', handleComplete);
}, [router]);
and use pageLoding for show splash
For loading screen:
import React from 'react'
import useSWR from 'swr'
const fetcher = (url) => fetch(url).then((res) => res.json());
// your main function
export default function Profile() {
//for relative and absolute paths
const { data, error } = useSWR('/api/user', fetcher)
if (error) return <div>failed to load</div>
//for the loading you can create your custom component and insert instead of div, like this you keep same styling
if (!data) return <div>loading...</div>
if (data) return <div>hello {data.name}!</div>
}
Don't use useEffect hook, use this lib, better code and functionnality.
You have multiple possibility
You can start your Next Application on a page that contain a simple shimmer(splash screen), then replace the URL by url that contain SSR, and finally remove loading page for indexing with robot.txt. You can read more about this tips there.
You can insert inside on ... tag a CSS to show a loader(your splash screen). Then when the SSR function will be in loading the loader will be hide but when the browser download index.html(first file loaded, that contain style tag) the loader will be show before the browser download another assets(images, js, etc) and load it. You can read more about this tips there
The first tips will show loader fast than the second tip but require more steep(create another page, replace url, remove loader page for indexing)
You can do it by using DOMContentLoaded event, here an example:
In your _app.tsx and outside your function:
if (typeof window !== "undefined") {
document.addEventListener("DOMContentLoaded", () => {
// your code here ..
}
}

In web Firestore get() callback is executing twice when page is reloaded

I'm using Ionic with React and when the page is reloaded, Firestore call is returning duplicated data, since the callback executes twice.
Here's the code on the Page:
// View is already rendered
useIonViewDidEnter(() => {
fetchData()
});
function fetchData() {
getPendingCalls(userId).then((calls: any) => {
if (calls) {
// Show calls
setPendingCalls(calls)
}
})
}
And here's my firebaseService with the Firebase logic:
export function getPendingCalls(userId: string) {
return new Promise((resolve, reject) => {
db.collection(collection.calls)
.where(call.created_by, "==", userId)
.where(call.state, "==", callState.pending)
.orderBy(call.date)
.get()
.then(function(querySnapshot) {
const calls: Array<Call> = []
// ... some more stuff
resolve(calls)
})
.catch(function(error) {
console.log("Error getting pending calls: ", error);
resolve(null)
});
})
}
Any ideas why the code inside then() is executing twice?
This only happens when the page is reloaded, if I go to another page and come back it works fine, also when I land in that page.
Thanks!
[SOLUTION]
After a few days I reached to the solution thanks to one guy on Firebase community in Slack.
What happened was that even though the useIonViewDidEnter was calling once, it made some kind of conflict with the promise callback. So what I've been suggested to do was to use useEffect with no parameters instead. This is how it looks now:
// View is already rendered
useEffect(() => {
fetchData()
}, [])
Hope this helps to someone else :)

Promise.then is not working as expected

I am writing auth for my react app and the function that fetches the token can be seen below
axios.post("mywebsite.com/api/token-auth", {
username: this.state.username,
password: this.state.password
})
.then((fulfilled) => {
userData = fulfilled.data;
AsyncStorage.setItem("userData ", JSON.stringify(userData))
.then(
() => {
this.scaleAnimationDialog.dismiss();
// Set timeout is added to makesure the animation closes properly before navigating.
setTimeout(
() => this.props.navigation.navigate("Home", userData), 500
);
}
)
.catch(
() => {
alert("Something went wrong, please try again");
this.scaleAnimationDialog.dismiss();
}
)
})
.catch((error) => {
this.scaleAnimationDialog.dismiss();
alert("Authentication failed please try again");
});
When I try to login with valid credentials, it redirects to Home Screen as expected but if I refresh the app, it again goes to the LoginScreen as the AsyncStorage doesn't have key 'userData',
When the app fires up, LoadingScreen is the first component to load and which decides what screen to load. I implemented this as per react-navigation documentation and the code is as follows
_bootstrapAsync = async () => {
const userDataString = await AsyncStorage.getItem('userData');
const userDataObject = JSON.parse(userDataString);
// This will switch to the App screen or Auth screen and this loading
// screen will be unmounted and thrown away.
this.props.navigation.navigate(userDataString ? "Home":"AuthNavigator", userDataObject);
};
What am I doing wrong here. I am new to using promises, Please keep the answer more descriptive.
I also tried async await with try/catch but in that case the code never went past await and the loading popup never goes off. Please point out if my code is wrong and also suggest the appropriate solution.
Thanks!
How are you checking for the key in AsyncStorage?
Do it before the component renders, using the lifecycle method componentWillMount.
Something like this:
componentWillMount() {
AsyncStorage.getItem('userData')
.then(userData => {
this.setState({ userData });
})
.catch((err) => {
// Handle error here (maybe fetch user?)
})
}
Then, inside the render method render accordingly this.state.userData.
Hope this makes sense :)
After some digging, I found out there is this issue with AsyncStorage for lot of other people, thanks to #Filipe for pointing to the right thread. After a lot of struggle I've decided to just use localstorage like sqlite or something and found this library https://github.com/sunnylqm/react-native-storage. It implements AsyncStorage internally and provides a wrapper to set and get data. Somehow this seems to work for me. If anyone is facing the same issue as me. Give that library a try.

Resources