DEV Community

Marko Marinovic for Blank

Posted on • Edited on

Preloading React components

In previous post I wrote about code-splitting and how it improves application performance.
This works great, but what about user experience? A loader is displayed each time the app needs to load additional code to run. This can get annoying, especially on slower connections. What we can do to improve this is to assume the user's next action. Is user scrolling thought the blog list and hovering over a specific post? If yes, then a user is likely to click on that post to get more info.
Making this assumption allows us to preload content for the post, rendering preloaded content on the actual click.

Preloading implementation

I created a simple function lazyWithPreload to help in this case. It's a wrapper around React.lazy with additional preloading logic that returns special PreloadableComponent.

Code for lazyWithPreload and PreloadableComponent is available down here:

import { ComponentType } from 'react';

export type PreloadableComponent<T extends ComponentType<any>> = T & {
  preload: () => Promise<void>;
};
Enter fullscreen mode Exit fullscreen mode
import { lazy, ComponentType, createElement } from 'react';
import { PreloadableComponent } from 'shared/types/component';

const lazyWithPreload = <T extends ComponentType<any>>(
  factory: () => Promise<{ default: T }>
) => {
  let LoadedComponent: T | undefined;
  let factoryPromise: Promise<void> | undefined;

  const LazyComponent = lazy(factory);

  const loadComponent = () =>
    factory().then(module => {
      LoadedComponent = module.default;
    });

  const Component = (props =>
    createElement(
      LoadedComponent || LazyComponent,
      props
    )) as PreloadableComponent<T>;

  Component.preload = () => factoryPromise || loadComponent();

  return Component;
};

export default lazyWithPreload;
Enter fullscreen mode Exit fullscreen mode

lazyWithPreload take a single argument, factory and returns a special component that acts in two different ways. When preload is initiated, factory gets called, loading the component.
Loaded component is stored and rendered when the app renders PreloadableComponent. Another case is when component is not preloaded via preload, then PreloadableComponent acts like a regular React.lazy component.

Using it with blog list

The idea is to preload content for a post on post title hover. IBlogPost has a property PreloadableContent which utilizes lazyWithPreload.

import { IBlogPost } from 'shared/types/models/blog';
import lazyWithPreload from 'shared/components/lazy-with-preload';

const post: IBlogPost = {
  id: 2,
  title: 'Whole year of reading (2019)',
  description: 'Complete list of my 2019 reads.',
  date: '2020-01-10',
  slug: 'whole-year-of-reading-2019',
  PreloadableContent: lazyWithPreload(() => import('./content.mdx')),
};

export default post;
Enter fullscreen mode Exit fullscreen mode

BlogListItem displays preview for single post in the list. Hovering on post title link initializes the content preload. Now the content is loaded and loader will not appear
when navigating to the post details.

import React from 'react';
import { Link } from '@reach/router';
import { IBlogPost } from 'shared/types/models/blog';
import { StyledContent } from './BlogListItemStyles';

interface IProps {
  post: IBlogPost;
}

const BlogListItem = ({ post }: IProps) => {
  const { title, description, date, slug, PreloadableContent } = post;
  const preloadPost = () => PreloadableContent.preload();

  return (
    <StyledContent>
      <Link to={`/${slug}`} onMouseEnter={preloadPost}>
        {title}
      </Link>
      <span>{date}</span>
      <p>{description}</p>
    </StyledContent>
  );
};

export default BlogListItem;
Enter fullscreen mode Exit fullscreen mode

Happy coding 🙌

Top comments (0)