All of us build components in our day to day life. We either build components which are specific for some particular use cases or we build generic components which can be reused at a later point in time.
The problem is with generic components because we don’t know what generic is. We don’t know what a good abstraction is and all of us always want to avoid bad abstractions. But the problem is we identify all these problems once we are done with the implementation and then realize “Damn, this is a bad abstraction this is not what the end result I’d expected” and then we re-iterate and the loop goes on and on. I’ve myself be in the same boat multiple times where I realize the whole implementation is incorrect but at very later point of time and then get nightmares 🤦♂️. We talk about building Design Systems but we rarely talk about the techniques that can be used to build design systems.
This post is about my experience where I was working on a Modal component once and then after few days I realized that I was going in the wrong direction but at the same time I had spent a lot of time building it and then I had no other option but to rewrite 😕. But in that process I discovered a technique and I’ve been using the same after that.
Let’s see all the above in practice by building a Modal component the wrong and then somewhat the right way
If you’re a video person then you can watch me doing this hands on 👨🏻💻
Requirements Spec for a Modal Component
- Modal can be opened or closed from anywhere in the component tree.
- We should be able to open multiple modals stacking over each other.
- We should be able to check whether any modal is opened anywhere in the react component tree.
- Support some entry and exit animations.
- Modal have some frame components which we can use and render our own components inside it.
- Modal has a scrim(backdrop) which can be of different types.
How do we generally start?
We start by thinking about the behavior of Modal and how we can achieve that. So we start by looking at our spec and then figure out what all things we’ll use
- Hooks
- Portals - Portals provide a first-class way to render children into a DOM node that exists outside the DOM hierarchy of the parent component
- Context API- Since we need modal to exist as a global thing in the component tree
- Refs
Now the next obvious step we usually do is we jump onto the implementation to get that basic initial draft of Modal working. Let’s do that
Step-1: Create ModalContext.js
1import React from 'react';23const ModalContext = React.createContext();45export const useModalContext = () => {6 const modalContext = React.useContext(ModalContext);7 if (modalContext === undefined) {8 throw new Error('useModalContext must be used within a ModalConextProvider');9 }10 return modalContext;11};1213export default ModalContext;
Step-2: Next to render the Modal container we’ll create ModalProvider.js
1const ModalProvider = ({isOpen, onClose, variant, animationType, children, ...props}) => {2 const modalElementRef = React.useRef(document.createElement('div')); // we'll render Modal in this DOM element34 // we want to create a DOM element when the ModalProvider is first rendered5 React.useEffect(() => {6 document.body.appendChild(modalElementRef.current);78 const modalElement = modalElementRef.current;910 return () => {11 // Remove the DOM element once ModalProvider is unmounted12 document.body.removeChild(modalElement);13 };14 }, []);1516 return ReactDOM.createPortal(17 <ModalContext.Provider value={{ isOpen, onClose }}>18 {isOpen ? (19 <React.Fragment>20 <ModalScrim variant={variant} animationType={animationType} {...props} />21 {children}22 </React.Fragment>23 ) : null}24 </ModalContext.Provider>,25 modalElementRef.current26 );27};
So far so good?
Step-3: Let’s document the usage of this component
1import React from 'react';2import Modal from './Modal';34const [open, setOpen] = React.useState(false);56<React.Fragment>7 <button type="button" onClick={() => setOpen(true)}>8 Open Modal9 </button>10 <Modal variant="ERROR" isOpen={open} onClose={() => setOpen(false)} animationType="fade">11 <Modal.Card width="300px" height="200px">12 Modal Card13 </Modal.Card>14 </Modal>15</React.Fragment>
Do you see any issues here? Let’s tally with our original requirements spec
- Modal can be opened or closed from anywhere in the component tree. 🤔(partially)
- We should be able to open multiple modals stacking over each other. 🤔
- We should be able to check whether any modal is opened anywhere in the react component tree. 🤔
- Support some entry and exit animations. ✅
- Modal have some frame components which we can use and render our own components inside it. ✅
- Modal has a scrim(backdrop) which can be of different types. ✅
So we were able to check off most of the things from our spec 🎉. But hold you breath. There are few things which we aren’t able to accomplish yet. What do we do? Usually we’ll go back to our implementation and re-iterate. But let me tell you there’s a major architectural issue with our current API to accommodate the things we want
- Modal can be opened or closed from anywhere in the component tree. — What if we want to open a Modal from a function which let’s say triggers API call but doesn’t has any UI being rendered 😮
- We should be able to open multiple modals stacking over each other. — Right now we could do that but it’ll be a hacky way and making things work for the sake of working 🤕
- We should be able to check whether any modal is opened anywhere in the react component tree. — We could make this work by changing few things and storing identifier in the ModalProvider and then passing that in provider value. 😐
We can surely go back and iterate to accomplish on the remaining things that’s not the problem. The issue that I want to highlight here is that we came to know about these issues with our API much later in our development lifecycle. 😢. What if we could just tweak our development workflow?
API first, develop later
Assume the same spec we had earlier and instead of directly jumping into the implementation what if we could have written the Usage API first? Let’s see how
Step-1: Write the Usage first to get a glimpse of the API
1import React from 'react';2import Modal from './Modal';34const [open, setOpen] = React.useState(false);56<React.Fragment>7 <button type="button" onClick={() => setOpen(true)}>8 Open Modal9 </button>10 <Modal variant="ERROR" isOpen={open} onClose={() => setOpen(false)} animationType="fade">11 <Modal.Card width="300px" height="200px">12 Modal Card13 </Modal.Card>14 </Modal>15</React.Fragment>
Step-2: Parity with our checklist.
1import React from 'react';2import Modal from './Modal';34/**5 * Limitations:6 * 1. Every Component has to maintain the state of Modal7 * 2. If I have to create modal over modal it becomes very difficult since Modal is a controlled input meaning the consumers are controlling it8 * 3. If I have to open/close specific Modal in the whole tree it becomes clumsy because again Controlled Modal9 * 4. If I have to open a Modal from a function which doesn't render JSX then I cannot do it with this implementation10*/1112const [open, setOpen] = React.useState(false);1314<React.Fragment>15 <button type="button" onClick={() => setOpen(true)}>16 Open Modal17 </button>18 <Modal variant="ERROR" isOpen={open} onClose={() => setOpen(false)} animationType="fade">19 <Modal.Card width="300px" height="200px">20 Modal Card21 </Modal.Card>22 </Modal>23</React.Fragment>
Step-3: Re-iterate since we couldn’t accomplish a lot of things which means the API needs to change. Step-4: Let’s see how we can modify the API
1import React from 'react';2import { ModalProvider, ModalCard, ModalPanel, useModalContext } from '../Modal';3const modalContext = useModalContext();45/**6 * TODO7 * 1. context8 * 2. openModal - to open modal from anywhere in the tree9 * 3. currentModalId10 * 4. closeModal(modalId) - to close any modal in the tree11 * 5. component to render inside a modal - Card/Panel12 */1314const ModalContent = () => (15 <ModalCard width="300px" height="200px" onClose={() => modalContext.closeModal()}>16 <Text>This is modal title</Text>17 <Text>This is modal Content</Text>18 <Text>This is modal Footer</Text>19 </ModalCard>20);2122return (23 <React.Fragment>24 <button25 type="button"26 onClick={() => {27 modalContext.openModal({28 variant: modalVariants,29 animationType: modalAnimationType,30 component: ModalContent,31 });32 }}33 >34 Open Modal35 </button>36 </React.Fragment>37)
Step-5: Parity with our spec
- Modal can be opened or closed from anywhere in the component tree. ✅
- Modal can be opened or closed from any function as well even if the function doesn’t render any UI ✅
- We should be able to open multiple modals stacking over each other. ✅
- We should be able to check whether any modal is opened anywhere in the react component tree. ✅
- Support some entry and exit animations. ✅
- Modal have some frame components which we can use and render our own components inside it. ✅
- Modal has a scrim(backdrop) which can be of different types. ✅
Bingo 🥁
Step-6: Since we now have evaluated how our API will look like for our consumers and we have checked off everything on our spec we can now go ahead and implement it in action. Our context was fine we just need to modify our provider
1// ModalProvider.js23/* A lot of code is not shown here for simplicity */45import ModalContext from './ModalContext';67const modalVariant = ['SUCCESS', 'ERROR', 'INFO', 'WARNING'];8const modalAnimationType = ['fade', 'slide-left', 'slide-right'];910const ModalProvider = ({ children }) => {11 const modalElementRef = React.useRef(document.createElement('div'));12 modalElementRef.current.id = 'modal';13 const modalIdRef = React.useRef(0); // this will hold the current modal id14 const [modals, setModals] = React.useState([]);1516 React.useEffect(() => {17 document.body.appendChild(modalElementRef.current);1819 const modalElement = modalElementRef.current;2021 return () => {22 document.body.removeChild(modalElement);23 };24 }, []);2526 const openModal = React.useCallback(function openModal({ variant, animationType, component }) {27 if (!modalVariant.includes(variant)) {28 throw new Error(`Variant must be one of these ${modalVariant.toString()}`);29 }3031 if (!modalAnimationType.includes(animationType)) {32 throw new Error(`Variant must be one of these ${modalAnimationType.toString()}`);33 }34 modalIdRef.current += 1;35 setModals((modalsState) => [36 ...modalsState,37 { modalId: modalIdRef.current, variant, animationType, component },38 ]);39 }, []);4041 const closeModal = React.useCallback(function closeModal({ modalId = undefined } = {}) {42 const closeModalId = modalId || modalIdRef.current;43 modalIdRef.current -= 1;44 setModals((modalsState) => modalsState.filter((modal) => modal.modalId !== closeModalId));45 }, []);4647 const renderModals = () => (48 <React.Fragment>49 {modals.map(({ modalId, variant, animationType, component: Component }) => (50 <React.Fragment key={modalId}>51 <ModalScrim variant={variant} animationType={animationType} />52 <Component />53 </React.Fragment>54 ))}55 </React.Fragment>56 );5758 const modalContextValue = { openModal, closeModal, currentModalId: modalIdRef.current };5960 return (61 <React.Fragment>62 <ModalContext.Provider value={modalContextValue}>63 {children}64 {ReactDOM.createPortal(renderModals(), modalElementRef.current)}65 </ModalContext.Provider>66 </React.Fragment>67 );68};6970export default ModalProvider;
You can find the complete implementation on GitHub
You can see it in action it’s deployed here
TL;DR 📝
- Analyze the Requirements Spec thoroughly.
- Don’t jump into implementation.
- Think about how your consumers will use it.
- Start by creating API first by documenting in plain file or storybook.
- Since it’s right in front of your face you exactly know where it’ll work and whether it satisfies the use cases you’re trying to solve for.
- Once the API is finalized we can then jump into implementation.
- This is also called as Red-Green development since we start with errors on the screen and then keep modifying your code until our stories execute and our component is rendered exactly the way we want.
Phew! That was a wild ride ⚡️. I hope you learned something out of this.
If you have some techniques that you have found while working on Design Systems then you can write it to me or you can DM me on Twitter. I would love to hear them!
If you like this then don’t forget to
🐤 Share
🔔 Subscribe
➡️ Follow me on Twitter