@figliolia/react-lazy
v1.1.3
Published
Lazy components that dynamically load on mount
Downloads
30
Maintainers
Readme
React Lazy
Lazy components that support preloading and dynamically load on mount.
Installation
npm i -S @figliolia/react-lazy
#or
yarn add @figliolia/react-lazy
Basic Usage
import type { ErrorInfo } from "react";
import { CreateLazyComponent } from "@figliolia/react-lazy";
const MyLazyComponent = CreateLazyComponent({
fallback: <div>Loading</div>,
errorBoundary: <div>Whoops!</div>,
loader: () => import("./path/to/Component"),
onError: (error: Error, errorInfo: ErrorInfo) => {}
});
// Optionally Preload your component using:
void MyLazyComponent.preload()
// Render your component without preloading and it'll
// load on mount
export const MyApp () => {
return (
<main>
<MyLazyComponent {...lazyComponentProps} />
{/* your other component markup */}
</main>
);
}
CreateLazyComponent()
Parameters
loader
: An async function that resolves to { default: ComponentType<T> }
. This function is passed to React.lazy()
.
fallback
: (optional) A ReactNode
to display while to the lazy component is suspended
errorBoundary
: (optional) A ReactNode
to display if the lazy component throws an error
onError
: (optional) A callback to execute if the lazy component throws an error.
Returns
A LazyComponent
instance
Advanced Usage - Preloading
This feature is an additional optimization that allows you to optimistically load dynamic components ahead of when they're actually needed.
Consider a case where you have a user logging into your application for the first time. When they press your login button, you'll probably send a request to your server to validate the user's credentials - then, if they are valid, you'll redirect the user into your app.
While the credentials are being sent to the server, it may also be prudent to securely preload
some of the components that are sitting behind your authentication. A working example may look something like the following:
import { useRef, useCallback } from "react";
import { LazyHeader, LazyContent, LazyFooter } from "./your-app-components";
export const LoginScreen = () => {
const preloaded = useRef(false);
const preloadComponents = useCallback(async () => {
if(preloaded.current) {
return;
}
try {
await Promise.all([
LazyHeader.preload(),
LazyContent.preload(),
LazyFooter.preload(),
]);
preloaded.current = true;
} catch() {
// silence
}
}, [preloaded])
const onSubmit = useCallback(async (e) => {
e.preventDefault();
void preloadComponents();
try {
await fetch("/api/auth", {
method: "POST",
body: JSON.stringify(/* Login Credentials */)
});
redirect("to/your/app");
} catch(e) {
// handle error
}
}, [preloadComponents]);
return (
<form onSubmit={onSubmit}>
{/* login inputs */}
<button type="submit" value="Login" />
</form>
);
}
Using this technique, we can utilize the time that an API request is already in-flight to cache component assets in the browser. This way when authentication completes the redirect to our main application content is instantaneous.
Advanced Usage - Priority Queue
For very large applications, it's expected that many components will be lazy loaded. For instances such as these, it may make sense to yield the main thread back to the end user and pause loading JavaScript temporarily.
This pattern has been in use at companies like Facebook for several years. Check out Facebook's blog post on how they handle large numbers of asynchronous tasks.
This library comes with a solution for cases such as these:
import { PriorityQueue, LazyComponentFactory } from "@figliolia/react-lazy"
export const LoadingQueue = new PriorityQueue(
5 /* milliseconds to yield to the main thread */
);
export const CreateLazyComponent = LazyComponentFactory(LoadingQueue);
The LoadingQueue
in the example above is a priority queue that'll handle loading lazy components created using CreateLazyComponent()
. The LoadingQueue
will detect when a user is attempting to interact with the page and pause loading for a specified number of milliseconds to allow events to dispatch on the main thread.
In addition to user-input awareness, the CreateLazyComponent
function generated by LazyComponentFactory
now comes with the ability to prioritize loading certain components against others using a PriorityLevel
:
import { PriorityQueue, LazyComponentFactory, PriorityLevel } from "@figliolia/react-lazy"
export const LoadingQueue = new PriorityQueue(
5 /* milliseconds to yield to the main thread */
);
export const CreateLazyComponent = LazyComponentFactory(LoadingQueue);
const MyHighPriorityComponent = CreateLazyComponent({
priority: PriorityLevel.Immediate,
loader: () => import("./HighPriorityComponent")
});
const MyLowPriorityComponent = CreateLazyComponent({
priority: PriorityLevel.Background,
loader: () => import("./LowPriorityComponent")
});
By default Priority.Immediate
components will be loaded ahead of Priority.Background
components.
A good use of Priority.Immediate
would be for components that are core to your UI's most basic functionality, such as headers and sidebars. Similarly, for components that are conditionally used such as modals or drawer menus Priority.Background
is more appropriate.
Utilitizing this pattern in large applications can have a great effect on your core metrics such as FMP
and TTI
.
In addition to loading components, the LoadingQueue
in the example above can also be used for just about any asynchronous task. You can use it to load a large javascript library in the background or even preload images that might be below the fold:
import { PriorityQueue, PriorityLevel } from "@figliolia/react-lazy";
export const LoadingQueue = new PriorityQueue();
LoadingQueue.enqueue(
PriorityLevel.Background,
() => import("expensive-node-module").then(() => {
// do something with your expensive module when
// it loads
})
)
LoadingQueue.enqueue(
PriorityLevel.Background,
() => new Promise(resolve => {
const image = new Image();
image.onload = resolve;
image.onerror = resolve;
image.src = "path/to/large-image.jpeg";
})
)
Your expensive JS module and large image will now load optimistically, but behind any Components or tasks with Priority.Immediate
.