logo
Navigate back to the homepage

API Driven Development for Building Components

Kamlesh Chandnani
June 30th, 2020 · 4 min read

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';
2
3const ModalContext = React.createContext();
4
5export 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};
12
13export 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 element
3
4 // we want to create a DOM element when the ModalProvider is first rendered
5 React.useEffect(() => {
6 document.body.appendChild(modalElementRef.current);
7
8 const modalElement = modalElementRef.current;
9
10 return () => {
11 // Remove the DOM element once ModalProvider is unmounted
12 document.body.removeChild(modalElement);
13 };
14 }, []);
15
16 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.current
26 );
27};

So far so good?

Step-3: Let’s document the usage of this component

1import React from 'react';
2import Modal from './Modal';
3
4const [open, setOpen] = React.useState(false);
5
6<React.Fragment>
7 <button type="button" onClick={() => setOpen(true)}>
8 Open Modal
9 </button>
10 <Modal variant="ERROR" isOpen={open} onClose={() => setOpen(false)} animationType="fade">
11 <Modal.Card width="300px" height="200px">
12 Modal Card
13 </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';
3
4const [open, setOpen] = React.useState(false);
5
6<React.Fragment>
7 <button type="button" onClick={() => setOpen(true)}>
8 Open Modal
9 </button>
10 <Modal variant="ERROR" isOpen={open} onClose={() => setOpen(false)} animationType="fade">
11 <Modal.Card width="300px" height="200px">
12 Modal Card
13 </Modal.Card>
14 </Modal>
15</React.Fragment>

Step-2: Parity with our checklist.

1import React from 'react';
2import Modal from './Modal';
3
4/**
5 * Limitations:
6 * 1. Every Component has to maintain the state of Modal
7 * 2. If I have to create modal over modal it becomes very difficult since Modal is a controlled input meaning the consumers are controlling it
8 * 3. If I have to open/close specific Modal in the whole tree it becomes clumsy because again Controlled Modal
9 * 4. If I have to open a Modal from a function which doesn't render JSX then I cannot do it with this implementation
10*/
11
12const [open, setOpen] = React.useState(false);
13
14<React.Fragment>
15 <button type="button" onClick={() => setOpen(true)}>
16 Open Modal
17 </button>
18 <Modal variant="ERROR" isOpen={open} onClose={() => setOpen(false)} animationType="fade">
19 <Modal.Card width="300px" height="200px">
20 Modal Card
21 </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();
4
5/**
6 * TODO
7 * 1. context
8 * 2. openModal - to open modal from anywhere in the tree
9 * 3. currentModalId
10 * 4. closeModal(modalId) - to close any modal in the tree
11 * 5. component to render inside a modal - Card/Panel
12 */
13
14const 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);
21
22return (
23 <React.Fragment>
24 <button
25 type="button"
26 onClick={() => {
27 modalContext.openModal({
28 variant: modalVariants,
29 animationType: modalAnimationType,
30 component: ModalContent,
31 });
32 }}
33 >
34 Open Modal
35 </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.js
2
3/* A lot of code is not shown here for simplicity */
4
5import ModalContext from './ModalContext';
6
7const modalVariant = ['SUCCESS', 'ERROR', 'INFO', 'WARNING'];
8const modalAnimationType = ['fade', 'slide-left', 'slide-right'];
9
10const 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 id
14 const [modals, setModals] = React.useState([]);
15
16 React.useEffect(() => {
17 document.body.appendChild(modalElementRef.current);
18
19 const modalElement = modalElementRef.current;
20
21 return () => {
22 document.body.removeChild(modalElement);
23 };
24 }, []);
25
26 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 }
30
31 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 }, []);
40
41 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 }, []);
46
47 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 );
57
58 const modalContextValue = { openModal, closeModal, currentModalId: modalIdRef.current };
59
60 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};
69
70export 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

Join the Newsletter

Subscribe to get the latest write-ups about my learnings from JavaScript, React, Design Systems and Life

No spam, pinky promise!

More articles from Kamlesh Chandnani

Versioning JavaScript Apps

Strategies around how to version JavaScript applications

May 26th, 2020 · 7 min read

Unpolished Thoughts from Deep Work

Notes I took while listening to Deep Work

May 12th, 2020 · 6 min read
Link to https://github.com/kamleshchandnaniLink to https://twitter.com/@_kamlesh_Link to https://www.youtube.com/playlist?list=PLpATFO7gaFGgwZRziAoScNoAUyyR_irFMLink to https://linkedin.com/in/kamleshchandnani