MODAL PROMISES

never gonna give you up

2022-11-24, CCT Office

About me

  • CTO of Corner case technologies
  • 10+ years experience in software development
  • Worked on 25+ MVP's 

Have you ever built a modal?

Have you used a lib while building a modal?

Problems

Modal state in root component

const SubscribeSection = () => {
  const [isOpen, setOpen] = useState(false);
  return (
    <div>
      ...
    </div>
  );
};

Modal state in root component

const SubscribeSection = () => {
  const [isOpen, setOpen] = useState(false);
  return (
    <div>
      <Modal isOpen={isOpen} onClose={() => setOpen(false)}>
        <input type="email" />
        <button type="button" onClick={() => {}}>
          Save
        </button>
      </Modal>
    </div>
  );
};

Modal state in root component

const SubscribeSection = () => {
  const [isOpen, setOpen] = useState(false);
  return (
    <div>
      <Modal isOpen={isOpen} onClose={() => setOpen(false)}>
        <input type="email" />
        <button type="button" onClick={() => {}}>
          Save
        </button>
      </Modal>

      <button type="button" onClick={() => setOpen(true)}>
        Subscribe
      </button>
    </div>
  );
};

Reusability

const SubscribeSection = () => {
  const [isOpen, setOpen] = useState(false);
  return (
    <div>
      <SubscribeModal isOpen={isOpen} onClose={() => setOpen(false)} />
      <button type="button" onClick={() => setOpen(true)}>
        Subscribe
      </button>
    </div>
  );
};

Reusability

const SubscribeModal = ({ isOpen, onClose }) => {
  useEffect(()=>{
    if (isOpen) {
     // Fetch api
    }
  }, [isOpen])	
  return (
    <Modal isOpen={isOpen} onClose={onClose}>
      <input type="email" />
      <button type="button" onClick={() => {}}>
        Save
      </button>
    </Modal>
  );
};

const SubscribeSection = () => {
  const [isOpen, setOpen] = useState(false);
  return (
    <div>
      <SubscribeModal isOpen={isOpen} onClose={() => setOpen(false)} />
      <button type="button" onClick={() => setOpen(true)}>
        Subscribe
      </button>
    </div>
  );
};

Multiple modals = Multiple states

const SubscribeSection = () => {
  const [isSubscribeModalOpen, setSubscribeModalOpen] = useState(false);
  const [isSuccessModalOpen, setSuccessModalOpen] = useState(false);
  return (
    <div>
      <SubscribeModal
        isOpen={isSubscribeModalOpen}
        onClose={() => setSubscribeModalOpen(false)}
        onSave={(email) => {
          setSubscribeModalOpen(false);
          setSuccessModalOpen(true);
  	}}
      />
      <SuccessModal 
      	isOpen={isSuccessModalOpen} 
        onClose={() => setSuccessModalOpen(false)} 
      />
      <button type="button" onClick={() => setSubscribeModalOpen(true)}>
        Subscribe
      </button>
    </div>
  );
};

Multiple modals = Multiple states

const SubscribeSection = () => {
  const [isSubscribeModalOpen, setSubscribeModalOpen] = useState(false);
  const [isSuccessModalOpen, setSuccessModalOpen] = useState(false);
  const [subscriberEmail, setSubscriberEmail] = useState('');

  return (
    <div>
      <SubscribeModal
        isOpen={isSubscribeModalOpen}
        onClose={() => setSubscribeModalOpen(false)}
        onSave={(email) => {
          setSubscriberEmail(email);
          setSubscribeModalOpen(false);
          setSuccessModalOpen(true);
  	}}
      />
      <SuccessModal 
      	isOpen={isSuccessModalOpen} 
        onClose={() => setSuccessModalOpen(false)} 
        email={subscriberEmail}
      />
      <button type="button" onClick={() => setSubscribeModalOpen(true)}>
        Subscribe
      </button>
    </div>
  );
};

Implementation

Default prompt modal

A window that prevents the user from interacting with your application until he closes the window.

Prompt modal

const SubscribeSection = () => {
  const onConfirmClick = () => {
    const emailProvider = prompt('Who is your email provider?');
    const userEmail = prompt('What is your email?');
    //continue working with user email
  };
  return (
    <div>
      <button type="button" onClick={onConfirmClick}>
        Confirm
      </button>
    </div>
  );
};

Requirements for our modal

  • The code of modal component is not executed if it's invisible.
  • It should not break the transitions of showing/hiding a modal.

Requirements for our modal

  • The code of modal component is not executed if it's invisible.
  • It should not break the transitions of showing/hiding a modal.
  • Easy multiple modal control
  • The modal should be uncontrolled component
  • Good DX for developers
  • Typescript support

Expected result

const providerModalResult = await showModal({ component: ProviderModal });
const subscribeResult = await showModal({
  component: SubscribeModal,
  props: {
    provider: providerModalResult.values.provider,
  },
});

Modal context

export const ModalProvider = ({ children }: { children: ReactNode }) => { 
  return (
    <ModalContext.Provider value={{ showModal, closeModal }}>
      {children}
    </ModalContext.Provider>
  );

}

Modal context

export const ModalProvider = ({ children }: { children: ReactNode }) => { 
  return (
    <ModalContext.Provider value={{ showModal, closeModal }}>
      {children}
      {state.modals.map((modal) => {
        const Modal = modal.component;
        return (
          <div key={modal.id}>
            <Modal {...modal.props} closeModal={closeModal} />
          </div>
        );
      })}

    </ModalContext.Provider>
  );

}

Modal context

type ModalProps = {
  closeModal: (param?: PromiseResolvePayload<'CLOSE'>) => void;
};

Modal context

type ModalProps = {
  closeModal: (param?: PromiseResolvePayload<'CLOSE'>) => void;
};

type ModalContextType = {
  showModal<P extends ModalProps>(options: {
    component: React.FunctionComponent<P>;
    props?: Omit<P, 'closeModal'>;
    closeable?: boolean;
  });
};

Modal context

type ModalProps = {
  closeModal: (param?: PromiseResolvePayload<'CLOSE'>) => void;
};

type ModalContextType = {
  showModal<P extends ModalProps>(options: {
    component: React.FunctionComponent<P>;
    props?: Omit<P, 'closeModal'>;
    closeable?: boolean;
  });
  closeModal(data?: PromiseResolvePayload): void;
};

Modal context

export const ModalProvider = ({ children }: { children: ReactNode }) => {

  const [state, setState] = useState({
    modals: [],
  });
  
}

Modal context

export const ModalProvider = ({ children }: { children: ReactNode }) => {
  const [state, setState] = useState<{
    modals: {
      id: number;
    }[];
  }>({
    modals: [],
  });
}

Modal context

export const ModalProvider = ({ children }: { children: ReactNode }) => {
  const [state, setState] = useState<{
    modals: {
      id: number;
      component: React.FunctionComponent;
    }[];
  }>({
    modals: [],
  });
}

Modal context

export const ModalProvider = ({ children }: { children: ReactNode }) => {
  const [state, setState] = useState<{
    modals: {
      id: number;
      component: React.FunctionComponent;
      props?: { [key: string]: unknown };
    }[];
  }>({
    modals: [],
  });
}

Modal context

export const ModalProvider = ({ children }: { children: ReactNode }) => {
  const [state, setState] = useState<{
    modals: {
      id: number;
      component: React.FunctionComponent;
      props?: { [key: string]: unknown };
      resolve: (data: PromiseResolvePayload) => void;
    }[];
  }>({
    modals: [],
  });
}

Modal context

export const ModalProvider = ({ children }: { children: ReactNode }) => {
  const showModal = useCallback(({ component, props }) => {
    return new Promise((resolve) => {
      // Add modal to state
    });
  }, []);
}

Modal context

export const ModalProvider = ({ children }: { children: ReactNode }) => {
  const closeModal = useCallback((data = { action: 'CLOSE' }) => {
    // Remove last modal from state
  }, []);
}

Modal body

interface ProviderModalProps extends ModalProps {
  closeModal: (props?: { action: 'CLOSE' }) => void;
}

Modal body

interface ProviderModalProps extends ModalProps {
  closeModal: (props?: 
  	{ action: 'CLOSE' } | 
  	{ action: 'SELECT_PROVIDER'; values: { provider: string } }
 ) => void;
}

Modal body

const ProviderModal = ({ closeModal }: ProviderModalProps) => {
  return (
    <div>I am a modal</div>
  );
};

Modal body

const ProviderModal = ({ closeModal }: ProviderModalProps) => {
  return (
    <div>
      <button type="button" onClick={() => closeModal({action: 'CLOSE'})}>
        Cancel
      </button>
    </div>
  );
};

Modal body

const ProviderModal = ({ closeModal }: ProviderModalProps) => {
  return (
    <div>
      <button type="button" onClick={() => closeModal({action: 'CLOSE'})}>
        Cancel
      </button>
      <button 
        type="button" 
        onClick={() => {
          closeModal({ 
            action: 'SELECT_PROVIDER', 
            values: { provider: 'google' } 
          });
      	}}>
        Google
      </button>	
    </div>
  );
};

Let's use our modal

const { showModal } = useModal();

Let's use our modal

const { showModal } = useModal();
const providerModalResult = await showModal({ component: ProviderModal });

Let's use our modal

const { showModal } = useModal();
const providerModalResult = await showModal({ component: ProviderModal });
if (providerModalResult.action === 'SELECT_PROVIDER') {
  const result = await showModal({
    component: SubscribeModal,
    props: {
      provider: providerModalResult.values.provider,
    },
  });
}
interface ProviderModalProps extends ModalProps {
  closeModal: (props?: 
  	{ action: 'CLOSE' } | 
  	{ action: 'SELECT_PROVIDER'; values: { provider: string } }
 ) => void;
}

const onSelectProvider = (provider: string) => {
  closeModal({ action: 'SELECT_PROVIDER' });
};
// 🚨 TS2345: Property 'values' is missing in type '{ action: "SELECT_PROVIDER"; }' but required in type '{ action: "SELECT_PROVIDER"; values: { provider: string; }; }'

TS support

interface SubscribeModalProps extends ModalProps {
  closeModal: (props?: { action: 'CLOSE' }}) => void;
  provider: string;
}

TS support

interface SubscribeModalProps extends ModalProps {
  closeModal: (props?: { action: 'CLOSE' }}) => void;
  provider: string;
}

const result = await showModal({
  component: SubscribeModal,
  props: {
    selectedProvider: 'google',
  },
 });

// 🚨 TS2322: Type '{ selectedProvider: string; }' is not assignable to type 'Omit<SubscribeModalProps, "closeModal">'. Object literal may only specify known properties, and 'selectedProvider' does not exist in type 'Omit<SubscribeModalProps, "closeModal">'

TS support

  • Nice modal react by eBay - https://opensource.ebay.com/nice-modal-react/
  • React modal promise - https://github.com/cudr/react-modal-promise
  • React promise components - https://www.npmjs.com/package/@foundernest/react-promise-components

Alternatives

const result = await showModal({
  component: VideoPlayerModal,
  props: {
    videos: [{ id: faq.id, title: faq.question, url: faq.video_url }],
    initialId: faq.id,
  },
});


const VideoPlayerModal = (props: VideoPlayerModalProps) => {
  return (
   <Modal isVisible onBackButtonPress={props.closeModal}>
    ...
   </Modal>
  )
}

React native

Thank you!

Made with Slides.com