From 0d9d14025997c16d46bb081e601d155d49a0c99a Mon Sep 17 00:00:00 2001 From: Chef Cheems Date: Thu, 1 Jul 2021 08:08:13 +0300 Subject: [PATCH] feat: Reactive modals --- .../src/widgets/Modal/ModalContext.tsx | 19 ++++++++- .../src/widgets/Modal/index.stories.tsx | 42 ++++++++++++++++++- .../src/widgets/Modal/useModal.ts | 36 +++++++++++++--- 3 files changed, 89 insertions(+), 8 deletions(-) diff --git a/packages/pancake-uikit/src/widgets/Modal/ModalContext.tsx b/packages/pancake-uikit/src/widgets/Modal/ModalContext.tsx index b3ec164fe..7ac69faa0 100644 --- a/packages/pancake-uikit/src/widgets/Modal/ModalContext.tsx +++ b/packages/pancake-uikit/src/widgets/Modal/ModalContext.tsx @@ -4,7 +4,11 @@ import Overlay from "../../components/Overlay/Overlay"; import { Handler } from "./types"; interface ModalsContext { - onPresent: (node: React.ReactNode, key?: string) => void; + isOpen: boolean; + nodeId: string; + modalNode: React.ReactNode; + setModalNode: React.Dispatch>; + onPresent: (node: React.ReactNode, newNodeId: string) => void; onDismiss: Handler; setCloseOnOverlayClick: React.Dispatch>; } @@ -23,6 +27,10 @@ const ModalWrapper = styled.div` `; export const Context = createContext({ + isOpen: false, + nodeId: "", + modalNode: null, + setModalNode: () => null, onPresent: () => null, onDismiss: () => null, setCloseOnOverlayClick: () => true, @@ -31,16 +39,19 @@ export const Context = createContext({ const ModalProvider: React.FC = ({ children }) => { const [isOpen, setIsOpen] = useState(false); const [modalNode, setModalNode] = useState(); + const [nodeId, setNodeId] = useState(""); const [closeOnOverlayClick, setCloseOnOverlayClick] = useState(true); - const handlePresent = (node: React.ReactNode) => { + const handlePresent = (node: React.ReactNode, newNodeId: string) => { setModalNode(node); setIsOpen(true); + setNodeId(newNodeId); }; const handleDismiss = () => { setModalNode(undefined); setIsOpen(false); + setNodeId(""); }; const handleOverlayDismiss = () => { @@ -52,6 +63,10 @@ const ModalProvider: React.FC = ({ children }) => { return ( { const [onPresent1] = useModal(); return ; }; + +export const ReactingToOusideChanges: React.FC = () => { + const [counter, setCounter] = useState(0); + useEffect(() => { + const intervalId = setInterval(() => { + setCounter((prev) => prev + 1); + }, 500); + return () => clearInterval(intervalId); + }, []); + const ReactiveModal: React.FC = ({ title, count, onDismiss }) => { + return ( + +

Counter: {count}

+ +
+ ); + }; + + const [onPresent1] = useModal( + , + true, + true, + "reactiveModal" + ); + + const [onPresent2] = useModal( + + ); + return ( +
+

Counter: {counter}

+ + +
+ ); +}; diff --git a/packages/pancake-uikit/src/widgets/Modal/useModal.ts b/packages/pancake-uikit/src/widgets/Modal/useModal.ts index 653d06414..7df6f8674 100644 --- a/packages/pancake-uikit/src/widgets/Modal/useModal.ts +++ b/packages/pancake-uikit/src/widgets/Modal/useModal.ts @@ -1,12 +1,38 @@ -import { useCallback, useContext, useEffect } from "react"; +import React, { useCallback, useContext, useEffect } from "react"; +import get from "lodash/get"; import { Context } from "./ModalContext"; import { Handler } from "./types"; -const useModal = (modal: React.ReactNode, closeOnOverlayClick = true): [Handler, Handler] => { - const { onPresent, onDismiss, setCloseOnOverlayClick } = useContext(Context); +const useModal = ( + modal: React.ReactNode, + closeOnOverlayClick = true, + updateOnPropsChange = false, + modalId = "defaultNodeId" +): [Handler, Handler] => { + const { isOpen, nodeId, modalNode, setModalNode, onPresent, onDismiss, setCloseOnOverlayClick } = useContext(Context); const onPresentCallback = useCallback(() => { - onPresent(modal); - }, [modal, onPresent]); + onPresent(modal, modalId); + }, [modal, modalId, onPresent]); + + // Updates the "modal" component if props are changed + // Use carefully since it might result in unnecessary rerenders + // Typically if modal is staic there is no need for updates, use when you expect props to change + useEffect(() => { + // NodeId is needed in case there are 2 useModal hooks on the same page and one has updateOnPropsChange + if (updateOnPropsChange && isOpen && nodeId === modalId) { + const modalProps = get(modal, "props"); + const oldModalProps = get(modalNode, "props"); + // Note: I tried to use lodash isEqual to compare props but it is giving false-negatives too easily + // For example ConfirmSwapModal in exchange has ~500 lines prop object that stringifies to same string + // and online diff checker says both objects are identical but lodash isEqual thinks they are different + // Do not try to replace JSON.stringify with isEqual, high risk of infinite rerenders + // TODO: Find a good way to handle modal updates, this whole flow is just backwards-compatible workaround, + // would be great to simplify the logic here + if (modalProps && oldModalProps && JSON.stringify(modalProps) !== JSON.stringify(oldModalProps)) { + setModalNode(modal); + } + } + }, [updateOnPropsChange, nodeId, modalId, isOpen, modal, modalNode, setModalNode]); useEffect(() => { setCloseOnOverlayClick(closeOnOverlayClick);