ReactJs/NextJs - Conditionally render mutually exclusive components without compromising load time

Viewed 2138

I am trying to improve loading speed of a react web app.

I have two component imports - one for mobile and one for desktop (Bad design? I think so):

import Posts from '../components/post/posts';
import PostsMobile from '../components/post/postsMobile';

This was easy for development because I did not have to try hard to make the same component compatible for desktop and mobile.

Then to check screen size and load the appropriate component, I do this:

const largeScreen = useMediaQuery(theme => theme.breakpoints.up('sm'));
 ...
 {largeScreen? (
 <Posts />
) :
(
<PostsMobile />
)
}

You can resize the browser here to see the two components load: Link to home page showing the two components

  1. Does <PostsMobile /> get imported only when react sees that its needed OR does it automatically get imported in the beginning no matter what?

  2. Is there a better way to conditionally render mutually exclusive components without compromising load time?

5 Answers

Classic conditional rendering, that's the appropriate way to do it, only one component will be added to the DOM in any case. As a side note it's not the best idea to have two different components for mobile vs desktop view, generally your html structure should be the same and you should use CSS for any layout changes (as per Google's suggestions - https://web.dev/responsive-web-design-basics/)

Check out @artsy/fresnel, they have a very straightforward example showing how to configure Next.js and Gatsby.js to achieve screen-width dependent control in SSR environments

  • Note: it hardly increases overhead size; I am using it in my portfolio currently in conjunction with tailwindcss and it has proven to be a fantastic tool. Easy to configure and its implementation is straightforward. Here is an example of conditionally rendering the same svg icon four times as a function of screen size to customize styles accordingly (xs (mobile), sm, md, greater than md (desktop))

(1) Create a window-width file in your components directory to configure @artsy/fresnel for global sharing

  • components/window-width.jsx or components/window-width.tsx
import { createMedia } from '@artsy/fresnel';

const PortfolioMedia= createMedia({
  breakpoints: {
    xs: 0,
    sm: 768,
    md: 1000,
    lg: 1200,
  },
})

// Generate CSS to be injected in the head using a styles tag (pages/_document.jsx or pages/_document.tsx)
export const mediaStyles = PortfolioMedia.createMediaStyle();
export const { Media, MediaContextProvider } = PortfolioMedia;

// https://github.com/artsy/fresnel/tree/master/examples/nextjs
// ...
const PortfolioMedia = createMedia({
    breakpoints: {
        xs: 0,
        sm: 640,
        md: 768,
        lg: 1024,
        xl: 1280
    }
});
// ...

(2) Wrap pages/index.jsx or pages/index.tsx with the MediaContextProvider component

// ...

import { MediaContextProvider } from 'components/window-width';


interface IndexProps {
    allPosts: Post[];
    allAbout: AboutType[];
    allBlog: BlogType[];
}

const Index = ({ allPosts, allAbout, allBlog }: IndexProps) => {
    const morePosts = allPosts.slice(0);
    const moreAbout = allAbout.slice(0);
    const moreBlog = allBlog.slice(0);
    return (
        <Fragment>
            <MediaContextProvider>
                <Lead />
                <Head>
                    <title>{`${CLIENT_NAME} landing page`}</title>
                </Head>
                <div className='max-w-cardGridMobile md:max-w-cardGrid my-portfolioH2F grid mx-auto content-center justify-center items-center text-center'>
                    {morePosts.length > 0 && <Cards posts={morePosts} />}
                </div>
                <div className='max-w-full my-portfolioH2F block mx-auto content-center justify-center items-center text-left'>
                    {moreAbout.length > 0 && <AboutCoalesced abouts={allAbout} />}
                </div>
                <div className='max-w-full my-portfolioH2F block mx-auto content-center justify-center items-center text-left'>
                    {moreBlog.length > 0 && <BlogCoalesced blogs={allBlog} />}
                </div>
                <Footer />
            </MediaContextProvider>
        </Fragment>
    );
};

export default Index;

// ...

(3) Finally, inject generated mediaStyles CSS into a style tag of type text/css in Next's Head

  • pages/_document.jsx or pages/_document.tsx
import Document, {
    Html,
    Head,
    Main,
    NextScript,
    DocumentContext
} from 'next/document';
import { mediaStyles } from 'components/window-width';

export default class MyDocument extends Document {
    static async getInitialProps(ctx: DocumentContext) {
        const initialProps = await Document.getInitialProps(ctx);
        return { ...initialProps };
    }
    render() {
        return (
            <Html lang='en-US'>
                <Head>
                    <meta charSet='utf-8' />
                    <link rel='stylesheet' href='https://use.typekit.net/cub6off.css' />
                    <style type='text/css' dangerouslySetInnerHTML={{ __html: mediaStyles }} />
                </Head>
                <body className='root'>
                    <script src='./noflash.js' />
                    <Main />
                    <NextScript />
                </body>
            </Html>
        );
    }
}

(4) Profit; Configuration complete

(5) Bonus example - conditionally rendering ArIcon as a function of device-size

  • components/lead-arIcon.tsx
import { ArIcon } from 'components/svg-icons';
import Link from 'next/link';
import { Media } from 'components/window-width';
import { Fragment } from 'react';
import DarkMode from 'components/lead-dark-mode';

const ArIconConditional = (): JSX.Element => {
    const arIconXs: JSX.Element = (
        <Media at='xs'>
            <Link href='/'>
                <a
                    className='container block pl-portfolio pt-portfolio justify-between mx-auto w-full min-w-full '
                    id='top'
                    aria-label='top'
                >
                    <ArIcon width='18vw' height='18vw' />
                </a>
            </Link>
        </Media>
    );

    const arIconSm: JSX.Element = (
        <Media at='sm'>
            <Link href='/'>
                <a
                    className='container block pl-portfolio pt-portfolio justify-between mx-auto w-full min-w-full '
                    id='top'
                    aria-label='top'
                >
                    <ArIcon width='15vw' height='15vw' />
                </a>
            </Link>
        </Media>
    );

    const arIconMd: JSX.Element = (
        <Media at='md'>
            <Link href='/'>
                <a
                    className='container block pl-portfolio pt-portfolio justify-between mx-auto w-full min-w-full '
                    id='top'
                    aria-label='top'
                >
                    <ArIcon width='12.5vw' height='12.5vw' />
                </a>
            </Link>
        </Media>
    );

    const arIconDesktop: JSX.Element = (
        <Media greaterThan='md'>
            <Link href='/'>
                <a
                    className='container block pl-portfolio pt-portfolio justify-between mx-auto w-full min-w-full'
                    id='top'
                    aria-label='top'
                >
                    <ArIcon
                        width='10vw'
                        height='10vw'
                        classNames={[
                            ` antialised w-svgIcon max-w-svgIcon transform transition-all`,
                            '  stroke-current',
                            ` fill-primary`
                        ]}
                    />
                </a>
            </Link>
        </Media>
    );

    const DarkModeToggler = (): JSX.Element => (
        <div className='pt-portfolio text-customTitle transition-all transform -translate-y-mdmxSocial col-span-4 text-right -translate-x-portfolioPadding'>
            <DarkMode />
        </div>
    );

    const ArIconsCoalesced = (): JSX.Element => (
        <Fragment>
            <div className='relative block justify-between lg:w-auto lg:static lg:block lg:justify-start transition-all w-full min-w-full col-span-2'>
                {arIconXs}
                {arIconSm}
                {arIconMd}
                {arIconDesktop}
            </div>
        </Fragment>
    );
    return (
        <Fragment>
            <div className='select-none relative z-1 justify-between pt-portfolioDivider navbar-expand-lg grid grid-cols-6 min-w-full w-full container overflow-y-hidden overflow-x-hidden transform'>
                <ArIconsCoalesced />
                <DarkModeToggler />
            </div>
        </Fragment>
    );
};

export default ArIconConditional;

Alt Text

You could try to lazy load the components.

posts;

componentDidMount() {
   if (largeScreen) {
      import('../components/post/posts').then(({ default: Posts }) => {
                                      // ^^^^ make sure it has a default export
         this.posts = Posts;
         this.forceUpdate();
      });
   } else {
     // here load the other component for lower screens
   }
}

Then, inside render:

const Posts = this.posts;

return largeScreen? (
   <Posts />
) : (
   <PostsMobile />
);

Note: You will have to also add a resize listener, so if the screen reaches certain width, the another component will load and will get rendered.

Note2: If you don't care about SSR - you could either try React.lazy with Suspense: https://en.reactjs.org/docs/code-splitting.html#reactlazy

In my opinion there are very valid usages of Desktop vs. Mobile components, for example, although CSS versions are definitely almost always preferred, it might not make sense for drag and drop components that don't work on mobiles, mobile-only hamburger menus, etc.

React's lazy loading is the way to go (unless you need SSR):

import React, { Suspense } from 'react';

const Posts = React.lazy(() => import('../components/post/posts');
const PostsMobile = React.lazy(() => import('../components/post/postsMobile');

function MainComponent() {
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>}>
        { largeScreen? ( <Posts />) : <PostsMobile /> }
      </Suspense>
    </div>
  );
}

This will guarantee that the component is only loaded when it is first rendered. Bonus tip: if you change the loading div to an animated loading placeholder, the UI of your application may be more pleasant.

None of the answers were compatible with SSR using Nextjs so I ended up using Dynamic Import feature. Looks very powerful but simple.

https://nextjs.org/docs/advanced-features/dynamic-import

import dynamic from 'next/dynamic'

const Posts = dynamic(() => import('../components/post/posts'),
{ loading: () => <LinearProgress /> });

const PostsMobile = dynamic(() => import('../components/post/postsMobile'),
{ loading: () => <LinearProgress /> });

This saved me a few milliseconds

enter image description here

I am not sure if there is a better option so I hope people will comment.

Related