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(...)))
Related
I have a working useEffect to call my google analytics when changing page.
This works fine when changing pages but when you initially load for the first time or refresh it does not call the router.events.on
this is my code
useEffect(() => {
if (cookies === true) {
router.events.on("routeChangeComplete", () => {
ReactGA.pageview(window.location.pathname);
});
return () => {
router.events.off("routeChangeComplete", () => {
ReactGA.pageview(window.location.pathname);
});
};
}
}, [router.events]);
I thought of using an other useEffect to initially call the reactGA but then when changing page it would be called twice, which is not good.
any idea on how to make this work on the initial page load?
That's expected behaviour - router.events is only triggered on client-side page navigations initiated by the Next.js router.
You can call ReactGA.pageview on a separate useEffect to handle initial page loads.
useEffect(() => {
if (cookies === true) {
ReactGA.pageview(window.location.pathname);
}
}, []);
I'm using ketting for my React REST client.
The library provides hooks to access some data, In my case, this is this hook :
import { useResource } from 'react-ketting';
...
const { loading, error, data } = useResource('https://api.example/article/5');
I have multiples resources and I want to loop on them:
items.map(item => {
const { loading, error, data, resourceState } = useResource(item);
myItems.push({
title: data.name,
onClick: () => go(resourceState.links.get('self').href),
});
});
But React doesn't accept looping the useResource hook like this.
So I found a dirty solution that I'm not proud of...
import React from 'react';
import { useCollection, useResource } from 'react-ketting';
let myItems = [];
const Collection = ({ resource, go }) => {
const { items } = useCollection(resource);
myItems = [];
return (
<div>
{items.map(item => (
<CollectionItem go={go} resource={item} />
))}
<ElementsUser elements={myItems} />
</div>
);
};
const CollectionItem = ({ resource, go }) => {
const { data, resourceState } = useResource(resource);
myItems.push({
title: data.name,
onClick: () => go(resourceState.links.get('self').href),
});
return null;
};
Do you have any tips to deal with that problem?
And is it possible that it causes a :
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 a useEffect cleanup function.
Using 1 component per Resource is exactly the pattern that's recommended. I'd even say it's a best practice. Note that a call to useResource will not result in an API call if there was a cached resource state.
If you find that you are seeing 1 request per iteration, you might want to make sure that your collection returns every member as an embedded resource. If you use the useCollection hook, the request will include a Prefer: transclude=item header to give a hint to the server that embedding might be desired.
Also, react-ketting will do cleanups on unmount.
I have a bit of an odd Issue. I'm using React.lazy to load stylesheets based on what page a user is on. Since they cancel each other out I only want one of them to be loaded / I want them to be split. This works in all browsers and both with HTTPS and HTTP. Except in Firefox with HTTPS. My certificates are self-signed, so firefox is having some issues with them, but I added exceptions for them.
React version: >16.11
Firefox Version: 78.0.2
I'm using an implementation of React Lazy that lets me retry on failure, but it doesn't work either way:
/**
* Tries lazy loading, retries 4 times on failure with a pause of 1 second inbetween
*/
const retryLazyLoad = (fn: () => Promise<any>, retriesLeft = 5, interval = 1000): Promise<{ default: ComponentType<any>; }> => {
// Return function as promise
return new Promise((resolve, reject) => {
fn()
.then(resolve)
.catch((error) => {
console.log(error);
setTimeout(() => {
// On error either reject or retry
if(retriesLeft === 1) {
// reject('maximum retries exceeded');
console.log("Maximum retries for lazy loading themes exceeded!");
reject(error);
return;
}
// Passing on "reject" is the important part
retryLazyLoad(fn, retriesLeft - 1, interval).then(resolve, reject);
}, interval);
});
});
}
/**
* The theme components only imports it's theme CSS-file. These components are lazy
* loaded, to enable "code splitting" (in order to avoid the themes being bundled together)
*/
const RgaTheme = React.lazy(() => retryLazyLoad(() => import('../../Pages/Content/Graph/RgaTheme')));
const RlmTheme = React.lazy(() => retryLazyLoad(() => import('../../Pages/Content/List/RlmTheme')));
const ThemeSelector: React.FunctionComponent<IThemeSeletorProps> = (props) => {
return (
<React.Fragment>
{/* Conditionally render theme, based on the current seleced module*/}
<React.Suspense fallback={<ProgressSpinner />}>
{props.module === "graph" ? <RgaTheme /> : <RlmTheme />}
</React.Suspense>
{/* Render children immediately */}
{props.children}
</React.Fragment>
);
};
This code can be boiled down to:
React.lazy(() => import('SomeOtherComponentThatImportsCSS')));
React Lazy runs into an exception when retrieving the css chunk 4.
The requests are not even shown in firefox,
but in firefox developer edition.
The requests being seen and marked as 200 and the fact that it works in all other browsers leads me to believe there is an issue between firefox + https and React.lazy.
I have a component which checks for a local-storage key and based on that it decides whether to render component or redirect to login screen.
I want to test this case using jest and enzyme but i am not able to force code to use mock localstorage and not actual browser locastorage.
Right now code it trying to read localstorage and it always gets null value.
I have already spent 2-3 hours and followed many stackobverflow question but most of them are trying to mock localstorage and checking if it sets and reads values from fake localstorage.
I think my case is different because i want to fake localstorage but that output should affect component decision.
Below is my component code
// Below console.log prints null when i run test, which i think should print { "googleId" : null} , isnt it ?
console.log(localStorage.getItem("auth"));
let storageUser = JSON.parse(localStorage.getItem("auth"));
if (!storageUser || !storageUser.googleId){
return <Redirect to="/login" />
}
return (
<Home user = {user} />
)
}
and my test code
it("Renders Home page if googleId is set in localStorage", () => {
const localStorage = jest.fn();
global.localStorage.getItem = key => '{ "googleId" : null}';
// Below code prints { "googleId" : null}
console.log(localStorage.getItem("auth"));
expect(wrapper.find(Home).length).toEqual(1);
});
I'd recommend using jest-localstorage-mock - the setup process shouldn't be tough.
By including the package in your tests, you have access to a mocked LS that can be manipulated with ease, for instance:
localStorage.__STORE__["auth"] = your_desired_object;
Here is a brief example that might fit your issue - and also it's a demonstration of testing the different conditionals in cases like this:
beforeEach(() => {
// According to the documentation: "values stored in tests will also be available in other tests unless you run"
localStorage.clear();
});
describe("when `auth` does not exist in the localStorage", () => {
it("redirects the user to the `login` page", () => {
// ...
});
});
describe("when `auth` exists in the localStorage", () => {
describe("when `googleId` is not defined", () => {
it("redirects the user to the `login` page", () => {
// ...
});
});
describe("when `googleId` is defined", () => {
it("renders `Home`", () => {
// Ensures if the `Home` page is rendered if the last circumstance occurs.
});
});
});
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