Implementing dynamic code splitting using React.lazy and React.Suspense was quite straight forward and the docs expound on the topic in more depth but a very simple implementation could be achieved as follows:

const OtherComponent = React.lazy(() => import('./OtherComponent'));



function MyComponent() {

return (

<div>

<Suspense fallback={<div>Loading...</div>}>

<OtherComponent />

</Suspense>

</div>

);

}

The code above uses React.lazy which takes a function as the only input and this function calls a dynamic import which then returns a Promise that resolves to a module containing a React component.

Ok, now let’s get into the details of what we built. In the code snippet below we are just setting things up. We set the default intersected state to false, create a ref, which will later be used as the target element in which to observe intersections in relationship to a root element. The root element in-turn defaults to the browser viewport if not specified or if null .

state = {

hasIntersected: false

}; targetContainerRef = React.createRef(); options = {

root: this.props.root || null,

rootMargin: this.props.margin || "0px",

threshold: this.props.threshold || 0

}; observer;

Next we setup up our subscriptions which will call our callback this.load when the intersection happens:

componentDidMount() {

this.observer = new IntersectionObserver(this.load, this.options);

this.observer.observe(this.targetContainerRef.current);

} componentWillUnmount() {

this.observer.unobserve(this.targetContainerRef.current);

}

Finally, we tell the observer what to actually do when an intersection event is observed. Our first condition tells the observer to stop listening once an intersection has happened. This is what we want for dynamic module loading.

The second condition is useful when you want to trigger a callback each and every time an intersection event is fired. We’ve used this for infinite loading of images and search results as this allows us to make additional calls to backend services, for instance each time a user has scrolled to the bottom of a list. The only thing to keep in mind for the second scenario is that you might need to track the direction of scroll as you may only want to trigger a callback if the intersection happens in a specific direction.

load = (entries) => { const { onIntersection, continueObserving } = this.props; if (!continueObserving && !this.state.hasIntersected) {

const entry =

entries.find(

entry => entry.target === this.targetContainerRef.current

); if (entry && entry.isIntersecting) {

this.setState({ hasIntersected: true });

onIntersection && onIntersection(entries);

this.observer.unobserve(this.targetContainerRef.current);

}

} else if (continueObserving && onIntersection) {

onIntersection(entries);

}

};

Next we need to actually use the IntersectionObserver component in conjunction with React.Suspense. We can achieve that with the following:

<IntersectionObserver>

<DynamicModule

placeholder={<Placeholder />}

component={() => import("./path/to/asyncComponent")}

/>

</IntersectionObserver>

The above code wraps a component that calls the component function prop when it mounts. Our <IntersectionObserver /> component internally only renders its children when the intersection happens. When this occurs the

<DynamicModule /> component internally does the following when it mounts:

componentDidMount() {

this.setState({

Component: lazy(this.props.component),

initializing: true

});

}

The above taps into the React.lazy api which then pulls in our component at runtime and this happens only when the user scrolls this component into view! To play around with these components and see a bit more in detail how it’s all working please check out the codesandbox below.

In addition to the above example, if you wanted to see how infinite scrolling could work with the <IntersectionObserver /> component you can simply append it to the bottom of your scrollable container and pass it an onIntersection() callback and a continueObserving prop set to true if you wanted to fire a callback each time the <IntersectionObserver /> intersects your root element.

By implementing the above components we were able to defer work, which in-turn sped up our initial page load times since we were shipping significantly smaller bundles. In addition, it improved the perceived performance of the application since we were only loading individual component modules when they were actually needed, and since those modules depended on external services for data, the loading of the module’s chunks and the async responses from the apis worked really well together. To the user, it seem one-in the same and they were unable to perceive the the difference between the load of the modules and the loading state of the api resolution. It was a win-win for us and more importantly for our users!

I hope the above makes sense and please feel free to provide any feedback for my benefit and the benefit of the community at large. Happy coding! 🙌 🙌 🙌