React.Lazy(() => import not working on Firefox, with HTTPS - reactjs

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.

Related

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(...)))

Next.js - Footer / Header loading via API only once, server side

I'm building a Next.js headless application, where I'm getting the data via API calls to an Umbraco backend. Im using getServerSideProps to load the data for each of my pages, which then is passed as "data" into the functional component and into the page.
The issue I have is that I have separate endpoints for the header / footer portion of the website, and is shared across all pages. Thus, it is a shame, and bad practice to do 3 calls per page (header, data, footer).
What could be done, in order to get header / footer once, then keep it across multiple pages, while maintaining SSR? (important). I've tried using cookies, but they cannot hold so much data. Below is some code:
Page Fetching data:
export async function getServerSideProps({ locale }) {
const footer = await Fetcher(footerEndPoint, locale);
return {
props: {
locale: locale,
footer: footer
}
}
}
Layout
const Layout = (props) => {
const { children, footer } = props;
return (
<>
<Header />
<main>
{children}
</main>
<Footer footer={footer} />
</>
);
};
export default Layout;
I see three options to achieve SSR-only data fetching once for things that won't ever change between page transitions:
1. getInitialProps() in _app.ts
You can just use getInitialProps() in _app.tsx. This runs on the server first and you can just cache the response value in a variable. Next time getInitialProps() is executed, it will just serve the cached value instead of firing another request. To make this work client-side, you have to rehydrate the cache variable in an useEffect:
// pages/_app.tsx
let navigationPropsCache
function MyApp({ Component, pageProps, navigationProps }) {
useEffect(
()=>{
navigationPropsCache = navigationProps
},
[]
)
return <>
<Navigation items={navigationProps}/>
<Component {...pageProps} />
</>
}
MyApp.getInitialProps = async () => {
if(navigationPropsCache) {
return {navigationProps: navigationPropsCache}
}
const res = await fetch("http://localhost:3000/api/navigation")
const navigationProps = await res.json()
navigationPropsCache = navigationProps
return {navigationProps}
}
Note that getInitialProps() is a deprecated feature since next 9.3. Not sure how long this will be supported in the the future. See: https://nextjs.org/docs/api-reference/data-fetching/getInitialProps
See https://github.com/breytex/firat500/tree/trivial-getInitialProps for full code example.
2. Use a custom next server implementation
This solution is based on two ideas:
Use a custom server.ts to intercept the nextjs SSR feature. Fetch all the data you need, render the navbar and footer serverside, inject the component HTML into the SSR result.
Rehydrate the DOM based on stringified versions of the fetched data you also attached to the DOM as a <script>.
// server/index.ts
server.all("*", async (req, res) => {
const html = await app.renderToHTML(req, res, req.path, req.query);
const navigationProps = await getNavigationProps()
const navigationHtml = renderToString(React.createElement(Navigation, {items: navigationProps}))
const finalHtml = html
.replace("</body>", `<script>window.navigationProps = ${JSON.stringify(navigationProps)};</script></body>`)
.replace("{{navigation-placeholder}}", navigationHtml)
return res.send(finalHtml);
});
// components/Navigation.tsx
export const Navigation: React.FC<Props> = ({items})=>{
const [finalItems, setFinalItems] = useState(items ?? [])
useEffect(
()=>{
setFinalItems((window as any).navigationProps)
},
[]
)
if(!Array.isArray(finalItems) || finalItems.length === 0) return <div>{"{{navigation-placeholder}}"}</div>
return (
<div style={{display:"flex", maxWidth: "500px", justifyContent: "space-between", marginTop: "100px"}}>
{finalItems.map(item => <NavigationItem {...item}/>)}
</div>
)
}
I'd consider this a pretty dirty example for now, but you could build something powerful based on this.
See full code here: https://github.com/breytex/firat500/tree/next-link-navigation
3. Use react-ssr-prepass to exec all data fetching server side
This uses a custom made fetch wrapper which has some kind of cache
The React component tree is traversed server side, and all data fetching functions are executed. This populates the cache.
The state of the cache is sent to the client and rehydrates the client side cache
On DOM rehydration all data is served from that cache, so no request is sent a second time
This example is a little bit longer and based on the outstanding work of the urql project: https://github.com/FormidableLabs/next-urql/blob/master/src/with-urql-client.tsx
See full example here: https://github.com/breytex/firat500/tree/prepass
Conclusion:
I'd personally would go with option #1 as long as its feasible.
#3 looks like an approach with a good developer experience, suitable for bigger teams. #2 needs some love to actually be useful :D

React testing library, how to test history.push

In a React App, I have a button that goes to a new url programatically:
<ComposeButton
onClick={() => {
history.push('some-link'))
}}
>
In my test, I render my component as mentioned in the react-testing-library documentation for React Router:
const renderComponent = (page: Pages) => {
const history = createMemoryHistory()
return renderWithState(
<MockedProvider
mocks={mocks}
addTypename={false}
defaultOptions={{
watchQuery: { fetchPolicy: 'no-cache' },
query: { fetchPolicy: 'no-cache' }
}}
>
<Router history={history}>
<ComponentToRender />
</Router>
</MockedProvider>
)
}
How can I wait for this change page in react-testing-library?
it('sends invitations', async () => {
const { getByTestId, queryByTestId, debug, getByText } = renderComponent(
Pages.contacts
)
await new Promise((resolve) => setTimeout(resolve))
const writeMessageBtn = getByTestId('write-message-btn')
waitFor(() => fireEvent.click(writeMessageBtn))
await new Promise((resolve) => setTimeout(resolve))
waitFor(() => debug()) // <- expect to see new page
getByText('EV_305_NEW_INVITATION_SEND') // <- this is in the second page, never get here
})
I can never see the content of the new page (after clicking the button) when using debug
I'm not sure this will make everything work, but there are a few issues with the code sample you shared I can highlight:
You are rendering a Router but no Routes. Use MemoryRouter instead and provide the same routes as your actual application - at least provide the route you are pushing into history
You are not using react-testing-library correctly. Instead of using getByTestId, use findByTestId. e.g. const writeMessageBtn = await findByTestId('write-message-btn'). The difference is that get* queries are synchronous while find* are async. You don't need to wait for arbitrary timeouts, the testing library will try to find the element for a few seconds.
The same applies to the other places where you use get*

React jest how to test Redirect based on localStorage item

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.
});
});
});

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

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

Resources