State-of-the-Art Modal Strategies for High-Performance React Apps
Introduction
As a frontend developer, you've likely encountered the need for modals while crafting sleek components for your applications. In this blog, I'm going to take you through an efficient approach to tackle this common requirement.
A modal, also referred to as a dialog, serves as a crucial interface element, primarily designed to draw attention or provide supplementary information within a webpage.
Thanks for reading The Busy Beavr! Subscribe for free to receive new posts and support my work.
While there are numerous strategies to manage modals, I'll be sharing my preferred methodology. Although I’m going to use React + ChakraUI Modals, the techniques I will discuss can be applied to other popular frameworks as underlying state management strategy and on-demand load works same for all.
The focus of this demonstration revolves around optimising performance in three key areas:
Modal invocation without global stores: We'll explore a technique that avoids reliance on global stores like Redux, Recoil, and the likes.
Lazy loading and state lifting: I'll guide you through the art of lazy loading and strategic placement of modal components by lifting state up.
Modal with multi-views: Lastly, I'll showcase how to incorporate multiple views (or pages) within a single modal. Let's elevate your modal game and enhance your users’ experiences!
Discussion #1: Usage of Single & Shared Modals
Creation of Modal
Although one can implement their own set of Modals by using Portals, I’m going with a component library called ChakraUI which provides me basic building blocks for a Modal.
There are many other cool libraries that provide UI components, you can check them out based on styling stack that you use.
Here’s what my basic Chakra modal looks like:
// SimpleModal.tsx
import {
Button,
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalFooter,
ModalBody,
ModalCloseButton,
} from '@chakra-ui/react'
export const SimpleModal= ({ isOpen, onClose }) => {
return (
<Modal isOpen={isOpen} onClose={onClose}>
<ModalOverlay />
<ModalContent>
<ModalHeader>Modal Title</ModalHeader>
<ModalCloseButton />
<ModalBody>Modal Body</ModalBody>
<ModalFooter>Modal Footer</ModalFooter>
</ModalContent>
</Modal>
)
}
Case 1: Usage of modal at one place
To use this modal at a single component, the most preferred way is to pass show state and close function as a prop into the modal component, as shown below:
// App.tsx
import {
Button,
useDisclosure,
} from '@chakra-ui/react'
import { SimpleModal } from '@modals/SimpleModal';
export const App = ({ isOpen, onClose }) => {
const [showSimpleModal, setShowSimpleModal] = useState(false)
return (
<>
<Button onClick={() => setShowSimpleModal(true)}>Open Simple Modal</Button>
<SimpleModal isOpen={showSimpleModal} onClose={() => showSimpleModal(false)} />
</>
)
}
Case 2: Usage of shared modals
_Example: Triggering log-out confirmation modal from sidebar and navbar. _
This is where many people try to use global state for modal toggling and will move the component directly to the main layout, which is unnecessary.
Instead, we can use the React concept called Lifting State Up. TLDR; it encourages us to move common state to the closer parent and pass the state variables and functions as props.
Let’s see the code that reflects the example I have given above. I’m assuming the component layout looks like this:
So the DashboardLayout.tsx
is the closer parent for both Navbar.tsx
and Sidebar.tsx
. Then, instead of repeating the modal code in individual components like Case 1 or using global state, we can actually move the modal importing and state handling logic to the closer parent. Now, you can do one of the following:
Pass the modal toggling functions as a prop to both the components that use the modal.
Simply pass the setState method of the useState hook as a prop.
// Solution#1
// DashboardLayout.tsx
import { Button, useDisclosure } from '@chakra-ui/react'
import { LogoutConfirmModal } from '@modals/LogoutConfirmationModal'
export const DashboardLayout = ({ isOpen, onClose }) => {
// ...
const [showLogoutConfirmation, setShowLogoutConfirmation] = useState(false)
const openLogoutConfirmationModal = () => setShowLogoutConfirmation(true)
const closeLogoutConfirmationModal = () => setShowLogoutConfirmation(false)
return (
<>
// ...
<div className="...">
<Navbar
onOpenLogoutConfirmationModal={openLogoutConfirmationModal}
onCloseLogoutConfirmationModal={closeLogoutConfirmationModal}
/>
<Sidebar
onOpenLogoutConfirmationModal={openLogoutConfirmationModal}
onCloseLogoutConfirmationModal={closeLogoutConfirmationModal}
/>
</div>
<LogoutConfirmModal
isOpen={showLogoutConfirmation}
onClose={onCloseLogoutConfirmationModal}
/>
</>
)
}
// Solution#2
// DashboardLayout.tsx
import { Button, useDisclosure } from '@chakra-ui/react'
import { LogoutConfirmModal } from '@modals/LogoutConfirmationModal'
export const DashboardLayout = ({ isOpen, onClose }) => {
// ...
const [showLogoutConfirmation, setShowLogoutConfirmation] = useState(false)
return (
<>
// ...
<div className="...">
<Navbar
setLogoutConfirmationModal={setShowLogoutConfirmation}
/>
<Sidebar
setLogoutConfirmationModal={setShowLogoutConfirmation}
/>
</div>
<LogoutConfirmModal
isOpen={showLogoutConfirmation}
onClose={() => setShowLogoutConfirmation(false)}
/>
</>
)
}
One can argue that we can also move the all shared modals directly to the root level. While that’s true, I prefer to keep them to the closer component for two main reasons:
You will end up increasing the initial page load by importing all your modals at the root level if you use static imports. You can lazy load them in background, but loading them at root level is completely unnecessary as you can use that space for other required imports. Refer to Discussion 2 to learn more about lazy loading.
Most of the modals are not simple, and may contain forms which will mutations or queries (API calls), so moving all this logic to the layout level looks large code and will end up with unmaintainable code.
Discussion 2: Importing (Loading) Modals
I always prefer lazy loading (or dynamic loading) for modals. Why? Because they’re not viewed until the invocation, and because on first load of the webpage it won’t invoke, since the modal state variable sets to false.
You can opt to React lazy loading, or other frameworks specific to the lazy loading approach, by looking into the respective documentation since every frontend framework provides lazy loading facility.
You can use this load time to bring other key elements that make an impression on first load like all the UI layouts to avoid CLS (Cumulative Layout Shifts).
But wait, the counter-part of me had a doubt 🤔 — what if I wanted show the modal on the first load, similar to the onboarding modals (e.g. intro modals, notification modals, referral modals)? Then the answer is simple; you should load that on first load since it’s going to effect the initial impression.
Discussion 3: Multi views in a single modal
Cool, now we are at one step away from finishing this modal tour, which brings us to handling multiple pages/views on certain events.
Example: You have a multi-page form in a single modal
The tempting approach is to go for global state which keeps track of the current view and updates the global state upon event triggering. To implement this global state solution, developers can use one of these 2 approaches:
Create multiple modals instead of single modal with multiple views and toggle between them based on the global state value. This will give you a flicker whenever a view changes.
Create a single modal with view changes by passing view state to the modal as a prop. Now, you might be wondering if this good idea 😏 It is, but we don’t actually need a global state for this.
Here’s an example of an approach I took with the LinkedIn Application form modal which looks like this:
// ApplicationFormModal.tsx
import { useState } from "react";
import {
Modal,
ModalContent,
ModalOverlay,
} from "@chakra-ui/react";
import DetailsView from "./views/DetailsView";
import ResumeUploadView from "./views/ResumeUploadView";
import QAView from "./views/QAView";
export enum ApplicationFormViews {
DETAILS_VIEW,
RESUME_UPLOAD_VIEW,
QAVIEW
}
const Content = (props) => {
switch (props.view) {
case DepositModalViews.CurrencySelector:
return <DetailsView {...props} />;
case DepositModalViews.BtcDeposit:
return <ResumeUploadView {...props} />;
case DepositModalViews.CadDeposit:
return <QAView {...props} />;
default:
return null;
}
};
const ApplicationFormModal = ({ isOpen, onClose }) => {
const [currentView, setCurrentView] = useState(
ApplicationFormViews.DETAILS_VIEW,
);
return (
<Modal
onClose={onClose}
isOpen={isOpen}
isCentered
blockScrollOnMount
>
<ModalOverlay bg="blackAlpha.500" backdropFilter="blur(0.3px)" />
<ModalContent
borderRadius={"cxl"}
height={"auto"}
>
<Content onClose={onClose} set={setCurrentView} view={currentView} />
</ModalContent>
</Modal>
);
};
export default ApplicationFormModal;
Wind-up
That’s my approach on modal triggering without affecting the bundle size with lazy loading and by not using additional global store config just to trigger modals. By implementing these efficient modal strategies, you're poised to take your frontend development skills to new heights. Remember, modals play a crucial role in enhancing user experiences, and with the techniques discussed here, you'll be well-equipped to handle them seamlessly.
Whether you're working with React, ChakraUI, or other frameworks, the principles remain applicable. So go ahead, dive in, and elevate your modal game. Your users will thank you for it!
Here's to crafting exceptional interfaces and delivering top-notch experiences. I would love to get your feedback and for you to share opinions.
Learn like a newbie, apply like a pro!
Top comments (0)