diff --git a/web/app.react.js b/web/app.react.js
--- a/web/app.react.js
+++ b/web/app.react.js
@@ -35,6 +35,7 @@
 import Chat from './chat/chat.react';
 import InputStateContainer from './input/input-state-container.react';
 import LoadingIndicator from './loading-indicator.react';
+import { ModalProvider, useModalContext } from './modals/modal-provider.react';
 import DisconnectedBar from './redux/disconnected-bar';
 import DisconnectedBarVisibilityHandler from './redux/disconnected-bar-visibility-handler';
 import FocusHandler from './redux/focus-handler.react';
@@ -83,15 +84,9 @@
   +activeThreadCurrentlyUnread: boolean,
   // Redux dispatch functions
   +dispatch: Dispatch,
-};
-type State = {
   +modal: ?React.Node,
 };
-class App extends React.PureComponent<Props, State> {
-  state: State = {
-    modal: null,
-  };
-
+class App extends React.PureComponent<Props> {
   componentDidMount() {
     const {
       navInfo,
@@ -136,16 +131,14 @@
     if (this.props.loggedIn) {
       content = this.renderMainContent();
     } else {
-      content = (
-        <Splash setModal={this.setModal} currentModal={this.state.modal} />
-      );
+      content = <Splash />;
     }
     return (
       <DndProvider backend={HTML5Backend}>
         <FocusHandler />
         <VisibilityHandler />
         {content}
-        {this.state.modal}
+        {this.props.modal}
       </DndProvider>
     );
   }
@@ -153,11 +146,9 @@
   renderMainContent() {
     let mainContent;
     if (this.props.navInfo.tab === 'calendar') {
-      mainContent = (
-        <Calendar setModal={this.setModal} url={this.props.location.pathname} />
-      );
+      mainContent = <Calendar url={this.props.location.pathname} />;
     } else if (this.props.navInfo.tab === 'chat') {
-      mainContent = <Chat setModal={this.setModal} />;
+      mainContent = <Chat />;
     }
 
     return (
@@ -177,23 +168,15 @@
             </div>
           </div>
         </header>
-        <InputStateContainer setModal={this.setModal}>
+        <InputStateContainer>
           <div className={css['main-content-container']}>
             <div className={css['main-content']}>{mainContent}</div>
           </div>
         </InputStateContainer>
-        <LeftLayoutAside setModal={this.setModal} />
+        <LeftLayoutAside />
       </div>
     );
   }
-
-  setModal = (modal: ?React.Node) => {
-    this.setState({ modal });
-  };
-
-  clearModal() {
-    this.setModal(null);
-  }
 }
 
 const fetchEntriesLoadingStatusSelector = createLoadingStatusSelector(
@@ -230,6 +213,7 @@
     );
 
     const dispatch = useDispatch();
+    const modalContext = useModalContext();
 
     return (
       <App
@@ -240,9 +224,18 @@
         mostRecentReadThread={mostRecentReadThread}
         activeThreadCurrentlyUnread={activeThreadCurrentlyUnread}
         dispatch={dispatch}
+        modal={modalContext.modal}
       />
     );
   },
 );
 
-export default ConnectedApp;
+function AppWithProvider(props: BaseProps): React.Node {
+  return (
+    <ModalProvider>
+      <ConnectedApp {...props} />
+    </ModalProvider>
+  );
+}
+
+export default AppWithProvider;
diff --git a/web/calendar/calendar.react.js b/web/calendar/calendar.react.js
--- a/web/calendar/calendar.react.js
+++ b/web/calendar/calendar.react.js
@@ -44,7 +44,6 @@
 import FilterPanel from './filter-panel.react';
 
 type BaseProps = {
-  +setModal: (modal: ?React.Node) => void,
   +url: string,
 };
 type Props = {
@@ -149,7 +148,6 @@
           <Day
             dayString={dayString}
             entryInfos={entries}
-            setModal={this.props.setModal}
             key={curDayOfMonth}
             startingTabIndex={tabIndex}
           />,
@@ -166,7 +164,7 @@
     let calendarContentStyle = null;
     let filterButtonStyle = null;
     if (this.state.filterPanelOpen) {
-      filterPanel = <FilterPanel setModal={this.props.setModal} />;
+      filterPanel = <FilterPanel />;
       calendarContentStyle = { marginLeft: '300px' };
       filterButtonStyle = { backgroundColor: 'rgba(0,0,0,0.67)' };
     }
diff --git a/web/calendar/day.react.js b/web/calendar/day.react.js
--- a/web/calendar/day.react.js
+++ b/web/calendar/day.react.js
@@ -23,6 +23,7 @@
 
 import LogInFirstModal from '../modals/account/log-in-first-modal.react';
 import HistoryModal from '../modals/history/history-modal.react';
+import { useModalContext } from '../modals/modal-provider.react';
 import { useSelector } from '../redux/redux-utils';
 import { htmlTargetFromEvent } from '../vector-utils';
 import { AddVector, HistoryVector } from '../vectors.react';
@@ -34,7 +35,6 @@
 type BaseProps = {
   +dayString: string,
   +entryInfos: $ReadOnlyArray<EntryInfo>,
-  +setModal: (modal: ?React.Node) => void,
   +startingTabIndex: number,
 };
 type Props = {
@@ -45,6 +45,7 @@
   +nextLocalID: number,
   +timeZone: ?string,
   +dispatch: Dispatch,
+  +setModal: (modal: ?React.Node) => void,
 };
 type State = {
   +pickerOpen: boolean,
@@ -107,7 +108,6 @@
           <Entry
             entryInfo={entryInfo}
             focusOnFirstEntryNewerThan={this.focusOnFirstEntryNewerThan}
-            setModal={this.props.setModal}
             tabIndex={this.props.startingTabIndex + i}
             key={key}
             innerRef={this.entryRef}
@@ -216,12 +216,7 @@
 
   createNewEntry = (threadID: string) => {
     if (!this.props.loggedIn) {
-      this.props.setModal(
-        <LogInFirstModal
-          inOrderTo="edit this calendar"
-          setModal={this.props.setModal}
-        />,
-      );
+      this.props.setModal(<LogInFirstModal inOrderTo="edit this calendar" />);
       return;
     }
     const viewerID = this.props.viewerID;
@@ -240,11 +235,7 @@
   onHistory = (event: SyntheticEvent<HTMLAnchorElement>) => {
     event.preventDefault();
     this.props.setModal(
-      <HistoryModal
-        mode="day"
-        dayString={this.props.dayString}
-        onClose={this.clearModal}
-      />,
+      <HistoryModal mode="day" dayString={this.props.dayString} />,
     );
   };
 
@@ -258,10 +249,6 @@
       entry.focus();
     }
   };
-
-  clearModal = () => {
-    this.props.setModal(null);
-  };
 }
 
 const ConnectedDay: React.ComponentType<BaseProps> = React.memo<BaseProps>(
@@ -275,6 +262,7 @@
     const nextLocalID = useSelector(state => state.nextLocalID);
     const timeZone = useSelector(state => state.timeZone);
     const dispatch = useDispatch();
+    const modalContext = useModalContext();
 
     return (
       <Day
@@ -285,6 +273,7 @@
         nextLocalID={nextLocalID}
         timeZone={timeZone}
         dispatch={dispatch}
+        setModal={modalContext.setModal}
       />
     );
   },
diff --git a/web/calendar/entry.react.js b/web/calendar/entry.react.js
--- a/web/calendar/entry.react.js
+++ b/web/calendar/entry.react.js
@@ -45,6 +45,7 @@
 import LogInFirstModal from '../modals/account/log-in-first-modal.react';
 import ConcurrentModificationModal from '../modals/concurrent-modification-modal.react';
 import HistoryModal from '../modals/history/history-modal.react';
+import { useModalContext } from '../modals/modal-provider.react';
 import { useSelector } from '../redux/redux-utils';
 import { nonThreadCalendarQuery } from '../selectors/nav-selectors';
 import { HistoryVector, DeleteVector } from '../vectors.react';
@@ -54,7 +55,6 @@
   +innerRef: (key: string, me: Entry) => void,
   +entryInfo: EntryInfo,
   +focusOnFirstEntryNewerThan: (time: number) => void,
-  +setModal: (modal: ?React.Node) => void,
   +tabIndex: number,
 };
 type Props = {
@@ -68,6 +68,8 @@
   +createEntry: (info: CreateEntryInfo) => Promise<CreateEntryPayload>,
   +saveEntry: (info: SaveEntryInfo) => Promise<SaveEntryResult>,
   +deleteEntry: (info: DeleteEntryInfo) => Promise<DeleteEntryResult>,
+  +setModal: (modal: ?React.Node) => void,
+  +clearModal: () => void,
 };
 type State = {
   +focused: boolean,
@@ -257,12 +259,7 @@
 
   onChange: (event: SyntheticEvent<HTMLTextAreaElement>) => void = event => {
     if (!this.props.loggedIn) {
-      this.props.setModal(
-        <LogInFirstModal
-          inOrderTo="edit this calendar"
-          setModal={this.props.setModal}
-        />,
-      );
+      this.props.setModal(<LogInFirstModal inOrderTo="edit this calendar" />);
       return;
     }
     const target = event.target;
@@ -389,13 +386,10 @@
             type: concurrentModificationResetActionType,
             payload: { id: entryID, dbText: e.payload.db },
           });
-          this.clearModal();
+          this.props.clearModal();
         };
         this.props.setModal(
-          <ConcurrentModificationModal
-            onClose={this.clearModal}
-            onRefresh={onRefresh}
-          />,
+          <ConcurrentModificationModal onRefresh={onRefresh} />,
         );
       }
       throw e;
@@ -405,12 +399,7 @@
   onDelete: (event: SyntheticEvent<HTMLAnchorElement>) => void = event => {
     event.preventDefault();
     if (!this.props.loggedIn) {
-      this.props.setModal(
-        <LogInFirstModal
-          inOrderTo="edit this calendar"
-          setModal={this.props.setModal}
-        />,
-      );
+      this.props.setModal(<LogInFirstModal inOrderTo="edit this calendar" />);
       return;
     }
     this.dispatchDelete(this.props.entryInfo.id, true);
@@ -459,15 +448,10 @@
           this.props.entryInfo.month,
           this.props.entryInfo.day,
         )}
-        onClose={this.clearModal}
         currentEntryID={this.props.entryInfo.id}
       />,
     );
   };
-
-  clearModal: () => void = () => {
-    this.props.setModal(null);
-  };
 }
 
 export type InnerEntry = Entry;
@@ -492,6 +476,8 @@
     const dispatchActionPromise = useDispatchActionPromise();
     const dispatch = useDispatch();
 
+    const modalContext = useModalContext();
+
     return (
       <Entry
         {...props}
@@ -504,6 +490,8 @@
         deleteEntry={callDeleteEntry}
         dispatchActionPromise={dispatchActionPromise}
         dispatch={dispatch}
+        setModal={modalContext.setModal}
+        clearModal={modalContext.clearModal}
       />
     );
   },
diff --git a/web/calendar/filter-panel.react.js b/web/calendar/filter-panel.react.js
--- a/web/calendar/filter-panel.react.js
+++ b/web/calendar/filter-panel.react.js
@@ -26,6 +26,7 @@
 } from 'lib/types/filter-types';
 import type { Dispatch } from 'lib/types/redux-types';
 
+import { useModalContext } from '../modals/modal-provider.react';
 import ThreadSettingsModal from '../modals/threads/thread-settings-modal.react';
 import { useSelector } from '../redux/redux-utils';
 import {
@@ -35,16 +36,13 @@
 import { MagnifyingGlass } from '../vectors.react';
 import css from './filter-panel.css';
 
-type BaseProps = {
-  +setModal: (modal: ?React.Node) => void,
-};
 type Props = {
-  ...BaseProps,
   +filterThreadInfos: () => $ReadOnlyArray<FilterThreadInfo>,
   +filterThreadSearchIndex: () => SearchIndex,
   +filteredThreadIDs: ?$ReadOnlySet<string>,
   +includeDeleted: boolean,
   +dispatch: Dispatch,
+  +setModal: (modal: ?React.Node) => void,
 };
 type State = {
   +query: string,
@@ -204,9 +202,7 @@
   }
 
   onClickSettings = (threadID: string) => {
-    this.props.setModal(
-      <ThreadSettingsModal threadID={threadID} onClose={this.clearModal} />,
-    );
+    this.props.setModal(<ThreadSettingsModal threadID={threadID} />);
   };
 
   onChangeQuery = (event: SyntheticEvent<HTMLInputElement>) => {
@@ -238,10 +234,6 @@
       },
     });
   };
-
-  clearModal = () => {
-    this.props.setModal(null);
-  };
 }
 
 type ItemProps = {
@@ -369,22 +361,23 @@
   };
 }
 
-const ConnectedFilterPanel: React.ComponentType<BaseProps> = React.memo<BaseProps>(
-  function ConnectedFilterPanel(props) {
+const ConnectedFilterPanel: React.ComponentType<{}> = React.memo<{}>(
+  function ConnectedFilterPanel(): React.Node {
     const filteredThreadIDs = useSelector(filteredThreadIDsSelector);
     const filterThreadInfos = useSelector(webFilterThreadInfos);
     const filterThreadSearchIndex = useSelector(webFilterThreadSearchIndex);
     const includeDeleted = useSelector(includeDeletedSelector);
     const dispatch = useDispatch();
+    const modalContext = useModalContext();
 
     return (
       <FilterPanel
-        {...props}
         filteredThreadIDs={filteredThreadIDs}
         filterThreadInfos={filterThreadInfos}
         filterThreadSearchIndex={filterThreadSearchIndex}
         includeDeleted={includeDeleted}
         dispatch={dispatch}
+        setModal={modalContext.setModal}
       />
     );
   },
diff --git a/web/chat/chat-message-list.react.js b/web/chat/chat-message-list.react.js
--- a/web/chat/chat-message-list.react.js
+++ b/web/chat/chat-message-list.react.js
@@ -47,11 +47,7 @@
 import RelationshipPrompt from './relationship-prompt/relationship-prompt';
 import ThreadTopBar from './thread-top-bar.react';
 
-type BaseProps = {
-  +setModal: (modal: ?React.Node) => void,
-};
 type PassedProps = {
-  ...BaseProps,
   // Redux state
   +activeChatThreadID: ?string,
   +threadInfo: ?ThreadInfo,
@@ -191,7 +187,7 @@
         </div>
       );
     }
-    const { threadInfo, setModal } = this.props;
+    const { threadInfo } = this.props;
     invariant(threadInfo, 'ThreadInfo should be set if messageListData is');
     return (
       <Message
@@ -199,7 +195,6 @@
         threadInfo={threadInfo}
         setMouseOverMessagePosition={this.setMouseOverMessagePosition}
         mouseOverMessagePosition={this.state.mouseOverMessagePosition}
-        setModal={setModal}
         timeZone={this.props.timeZone}
         key={ChatMessageList.keyExtractor(item)}
       />
@@ -383,9 +378,8 @@
 
 registerFetchKey(fetchMessagesBeforeCursorActionTypes);
 registerFetchKey(fetchMostRecentMessagesActionTypes);
-
-const ConnectedChatMessageList: React.ComponentType<BaseProps> = React.memo<BaseProps>(
-  function ConnectedChatMessageList(props) {
+const ConnectedChatMessageList: React.ComponentType<{}> = React.memo<{}>(
+  function ConnectedChatMessageList(): React.Node {
     const userAgent = useSelector(state => state.userAgent);
     const supportsReverseFlex = React.useMemo(() => {
       const browser = detectBrowser(userAgent);
@@ -475,7 +469,6 @@
     return (
       <MessageListContext.Provider value={messageListContext}>
         <ChatMessageList
-          {...props}
           activeChatThreadID={activeChatThreadID}
           threadInfo={threadInfo}
           messageListData={messageListData}
diff --git a/web/chat/chat-tabs.react.js b/web/chat/chat-tabs.react.js
--- a/web/chat/chat-tabs.react.js
+++ b/web/chat/chat-tabs.react.js
@@ -11,10 +11,7 @@
 import ChatThreadTab from './chat-thread-tab.react';
 import { ThreadListContext } from './thread-list-provider';
 
-type Props = {
-  +setModal: (modal: ?React.Node) => void,
-};
-function ChatTabs(props: Props): React.Node {
+function ChatTabs(): React.Node {
   let backgroundTitle = 'Background';
   const unreadBackgroundCountVal = useSelector(unreadBackgroundCount);
   if (unreadBackgroundCountVal) {
@@ -52,7 +49,7 @@
         />
       </div>
       <div className={css.threadList}>
-        <ChatThreadList setModal={props.setModal} />
+        <ChatThreadList />
       </div>
     </div>
   );
diff --git a/web/chat/chat-thread-list-item.react.js b/web/chat/chat-thread-list-item.react.js
--- a/web/chat/chat-thread-list-item.react.js
+++ b/web/chat/chat-thread-list-item.react.js
@@ -21,10 +21,9 @@
 
 type Props = {
   +item: ChatThreadItem,
-  +setModal: (modal: ?React.Node) => void,
 };
 function ChatThreadListItem(props: Props): React.Node {
-  const { item, setModal } = props;
+  const { item } = props;
   const {
     threadInfo,
     lastUpdatedTimeIncludingSidebars,
@@ -105,7 +104,6 @@
           threadInfo={item.threadInfo}
           unread={sidebarItem.unread}
           showingSidebarsInline={sidebarItem.showingSidebarsInline}
-          setModal={setModal}
           key="seeMore"
         />
       );
diff --git a/web/chat/chat-thread-list-see-more-sidebars.react.js b/web/chat/chat-thread-list-see-more-sidebars.react.js
--- a/web/chat/chat-thread-list-see-more-sidebars.react.js
+++ b/web/chat/chat-thread-list-see-more-sidebars.react.js
@@ -6,21 +6,20 @@
 import type { ThreadInfo } from 'lib/types/thread-types';
 
 import SidebarListModal from '../modals/chat/sidebar-list-modal.react';
+import { useModalContext } from '../modals/modal-provider.react';
 import css from './chat-thread-list.css';
 
 type Props = {
   +threadInfo: ThreadInfo,
   +unread: boolean,
   +showingSidebarsInline: boolean,
-  +setModal: (modal: ?React.Node) => void,
 };
 function ChatThreadListSeeMoreSidebars(props: Props): React.Node {
-  const { unread, showingSidebarsInline, setModal, threadInfo } = props;
+  const { unread, showingSidebarsInline, threadInfo } = props;
+  const { setModal } = useModalContext();
+
   const onClick = React.useCallback(
-    () =>
-      setModal(
-        <SidebarListModal setModal={setModal} threadInfo={threadInfo} />,
-      ),
+    () => setModal(<SidebarListModal threadInfo={threadInfo} />),
     [setModal, threadInfo],
   );
   const buttonText = showingSidebarsInline ? 'See more...' : 'See sidebars...';
diff --git a/web/chat/chat-thread-list.react.js b/web/chat/chat-thread-list.react.js
--- a/web/chat/chat-thread-list.react.js
+++ b/web/chat/chat-thread-list.react.js
@@ -10,12 +10,7 @@
 import { ThreadListContext } from './thread-list-provider';
 import ThreadListSearch from './thread-list-search.react';
 
-type Props = {
-  +setModal: (modal: ?React.Node) => void,
-};
-
-function ChatThreadList(props: Props): React.Node {
-  const { setModal } = props;
+function ChatThreadList(): React.Node {
   const threadListContext = React.useContext(ThreadListContext);
   invariant(
     threadListContext,
@@ -31,17 +26,13 @@
 
   const threadComponents: React.Node[] = React.useMemo(() => {
     const threads = threadList.map(item => (
-      <ChatThreadListItem
-        item={item}
-        key={item.threadInfo.id}
-        setModal={setModal}
-      />
+      <ChatThreadListItem item={item} key={item.threadInfo.id} />
     ));
     if (threads.length === 0 && isBackground) {
       threads.push(<EmptyItem key="emptyItem" />);
     }
     return threads;
-  }, [threadList, isBackground, setModal]);
+  }, [threadList, isBackground]);
 
   return (
     <div className={css.threadListContainer}>
diff --git a/web/chat/chat.react.js b/web/chat/chat.react.js
--- a/web/chat/chat.react.js
+++ b/web/chat/chat.react.js
@@ -6,16 +6,13 @@
 import ChatTabs from './chat-tabs.react';
 import { ThreadListProvider } from './thread-list-provider';
 
-type Props = {
-  +setModal: (modal: ?React.Node) => void,
-};
-function Chat(props: Props): React.Node {
+function Chat(): React.Node {
   return (
     <>
       <ThreadListProvider>
-        <ChatTabs setModal={props.setModal} />
+        <ChatTabs />
       </ThreadListProvider>
-      <ChatMessageList setModal={props.setModal} />
+      <ChatMessageList />
     </>
   );
 }
diff --git a/web/chat/message.react.js b/web/chat/message.react.js
--- a/web/chat/message.react.js
+++ b/web/chat/message.react.js
@@ -24,7 +24,6 @@
     messagePositionInfo: MessagePositionInfo,
   ) => void,
   +mouseOverMessagePosition: ?OnMessagePositionWithContainerInfo,
-  +setModal: (modal: ?React.Node) => void,
   +timeZone: ?string,
 };
 function Message(props: Props): React.Node {
@@ -58,7 +57,6 @@
         threadInfo={props.threadInfo}
         setMouseOverMessagePosition={props.setMouseOverMessagePosition}
         mouseOverMessagePosition={props.mouseOverMessagePosition}
-        setModal={props.setModal}
       />
     );
   } else {
diff --git a/web/chat/multimedia-message.react.js b/web/chat/multimedia-message.react.js
--- a/web/chat/multimedia-message.react.js
+++ b/web/chat/multimedia-message.react.js
@@ -24,7 +24,6 @@
     messagePositionInfo: MessagePositionInfo,
   ) => void,
   +mouseOverMessagePosition: ?OnMessagePositionWithContainerInfo,
-  +setModal: (modal: ?React.Node) => void,
 };
 type Props = {
   ...BaseProps,
@@ -33,7 +32,7 @@
 };
 class MultimediaMessage extends React.PureComponent<Props> {
   render() {
-    const { item, setModal, inputState } = this.props;
+    const { item, inputState } = this.props;
     invariant(
       item.messageInfo.type === messageTypes.IMAGES ||
         item.messageInfo.type === messageTypes.MULTIMEDIA,
@@ -52,7 +51,6 @@
         <Multimedia
           uri={singleMedia.uri}
           pendingUpload={pendingUpload}
-          setModal={setModal}
           multimediaCSSClass={css.multimedia}
           multimediaImageCSSClass={css.multimediaImage}
           key={singleMedia.id}
diff --git a/web/input/input-state-container.react.js b/web/input/input-state-container.react.js
--- a/web/input/input-state-container.react.js
+++ b/web/input/input-state-container.react.js
@@ -73,13 +73,13 @@
 
 import { validateFile, preloadImage } from '../media/media-utils';
 import InvalidUploadModal from '../modals/chat/invalid-upload.react';
+import { useModalContext } from '../modals/modal-provider.react';
 import { useSelector } from '../redux/redux-utils';
 import { nonThreadCalendarQuery } from '../selectors/nav-selectors';
 import { type PendingMultimediaUpload, InputStateContext } from './input-state';
 
 type BaseProps = {
   +children: React.Node,
-  +setModal: (modal: ?React.Node) => void,
 };
 type Props = {
   ...BaseProps,
@@ -108,6 +108,7 @@
     text: string,
   ) => Promise<SendMessageResult>,
   +newThread: (request: ClientNewThreadRequest) => Promise<NewThreadResult>,
+  +setModal: (modal: ?React.Node) => void,
 };
 type State = {
   +pendingUploads: {
@@ -518,7 +519,7 @@
     );
 
     if (appendResults.some(({ result }) => !result.success)) {
-      setModal(<InvalidUploadModal setModal={setModal} />);
+      setModal(<InvalidUploadModal />);
 
       const time = Date.now() - selectionTime;
       const reports = [];
@@ -1245,6 +1246,7 @@
     const callNewThread = useServerCall(newThread);
     const dispatch = useDispatch();
     const dispatchActionPromise = useDispatchActionPromise();
+    const modalContext = useModalContext();
 
     return (
       <InputStateContainer
@@ -1262,6 +1264,7 @@
         newThread={callNewThread}
         dispatch={dispatch}
         dispatchActionPromise={dispatchActionPromise}
+        setModal={modalContext.setModal}
       />
     );
   },
diff --git a/web/media/multimedia-modal.react.js b/web/media/multimedia-modal.react.js
--- a/web/media/multimedia-modal.react.js
+++ b/web/media/multimedia-modal.react.js
@@ -4,12 +4,18 @@
 import * as React from 'react';
 import { XCircle as XCircleIcon } from 'react-feather';
 
+import { useModalContext } from '../modals/modal-provider.react';
 import css from './media.css';
 
-type Props = {
+type BaseProps = {
   +uri: string,
-  +setModal: (modal: ?React.Node) => void,
 };
+
+type Props = {
+  ...BaseProps,
+  +clearModal: (modal: ?React.Node) => void,
+};
+
 class MultimediaModal extends React.PureComponent<Props> {
   overlay: ?HTMLDivElement;
 
@@ -29,7 +35,7 @@
       >
         <img src={this.props.uri} />
         <XCircleIcon
-          onClick={this.close}
+          onClick={this.props.clearModal}
           className={css.closeMultimediaModal}
         />
       </div>
@@ -44,7 +50,7 @@
     event: SyntheticEvent<HTMLDivElement>,
   ) => void = event => {
     if (event.target === this.overlay) {
-      this.close();
+      this.props.clearModal();
     }
   };
 
@@ -52,13 +58,15 @@
     event: SyntheticKeyboardEvent<HTMLDivElement>,
   ) => void = event => {
     if (event.keyCode === 27) {
-      this.close();
+      this.props.clearModal();
     }
   };
+}
 
-  close: () => void = () => {
-    this.props.setModal(null);
-  };
+function ConnectedMultiMediaModal(props: BaseProps): React.Node {
+  const modalContext = useModalContext();
+
+  return <MultimediaModal {...props} clearModal={modalContext.clearModal} />;
 }
 
-export default MultimediaModal;
+export default ConnectedMultiMediaModal;
diff --git a/web/media/multimedia.react.js b/web/media/multimedia.react.js
--- a/web/media/multimedia.react.js
+++ b/web/media/multimedia.react.js
@@ -11,17 +11,22 @@
 } from 'react-feather';
 
 import { type PendingMultimediaUpload } from '../input/input-state';
+import { useModalContext } from '../modals/modal-provider.react';
 import css from './media.css';
 import MultimediaModal from './multimedia-modal.react';
 
+type BaseProps = {
+  +uri: string,
+  +pendingUpload?: ?PendingMultimediaUpload,
+  +remove?: (uploadID: string) => void,
+  +multimediaCSSClass: string,
+  +multimediaImageCSSClass: string,
+};
 type Props = {
-  uri: string,
-  pendingUpload?: ?PendingMultimediaUpload,
-  remove?: (uploadID: string) => void,
-  setModal?: (modal: ?React.Node) => void,
-  multimediaCSSClass: string,
-  multimediaImageCSSClass: string,
+  ...BaseProps,
+  +setModal: (modal: ?React.Node) => void,
 };
+
 class Multimedia extends React.PureComponent<Props> {
   componentDidUpdate(prevProps: Props) {
     const { uri, pendingUpload } = this.props;
@@ -108,8 +113,14 @@
 
     const { setModal, uri } = this.props;
     invariant(setModal, 'should be set');
-    setModal(<MultimediaModal uri={uri} setModal={setModal} />);
+    setModal(<MultimediaModal uri={uri} />);
   };
 }
 
-export default Multimedia;
+function ConnectedMultimediaContainer(props: BaseProps): React.Node {
+  const modalContext = useModalContext();
+
+  return <Multimedia {...props} setModal={modalContext.setModal} />;
+}
+
+export default ConnectedMultimediaContainer;
diff --git a/web/modals/account/log-in-first-modal.react.js b/web/modals/account/log-in-first-modal.react.js
--- a/web/modals/account/log-in-first-modal.react.js
+++ b/web/modals/account/log-in-first-modal.react.js
@@ -2,18 +2,25 @@
 
 import * as React from 'react';
 
+import { useModalContext } from '../../modals/modal-provider.react';
 import css from '../../style.css';
 import Modal from '../modal.react';
 import LogInModal from './log-in-modal.react';
 
-type Props = {
+type BaseProps = {
   +inOrderTo: string,
+};
+
+type Props = {
+  ...BaseProps,
   +setModal: (modal: ?React.Node) => void,
+  +clearModal: () => void,
 };
+
 class LogInFirstModal extends React.PureComponent<Props> {
   render(): React.Node {
     return (
-      <Modal name="Log in" onClose={this.clearModal}>
+      <Modal name="Log in" onClose={this.props.clearModal}>
         <div className={css['modal-body']}>
           <p>
             {`In order to ${this.props.inOrderTo}, you'll first need to `}
@@ -31,14 +38,22 @@
     );
   }
 
-  clearModal: () => void = () => {
-    this.props.setModal(null);
-  };
-
   onClickLogIn: (event: SyntheticEvent<HTMLAnchorElement>) => void = event => {
     event.preventDefault();
-    this.props.setModal(<LogInModal setModal={this.props.setModal} />);
+    this.props.setModal(<LogInModal />);
   };
 }
 
-export default LogInFirstModal;
+function ConnectedLoginFirstModal(props: BaseProps): React.Node {
+  const modalContext = useModalContext();
+
+  return (
+    <LogInFirstModal
+      {...props}
+      setModal={modalContext.setModal}
+      clearModal={modalContext.clearModal}
+    />
+  );
+}
+
+export default ConnectedLoginFirstModal;
diff --git a/web/modals/account/log-in-modal.react.js b/web/modals/account/log-in-modal.react.js
--- a/web/modals/account/log-in-modal.react.js
+++ b/web/modals/account/log-in-modal.react.js
@@ -25,18 +25,16 @@
 import { useSelector } from '../../redux/redux-utils';
 import { webLogInExtraInfoSelector } from '../../selectors/account-selectors';
 import Input from '../input.react';
+import { useModalContext } from '../modal-provider.react';
 import Modal from '../modal.react';
 import css from './user-settings-modal.css';
 
-type BaseProps = {
-  +setModal: (modal: ?React.Node) => void,
-};
 type Props = {
-  ...BaseProps,
   +inputDisabled: boolean,
   +logInExtraInfo: () => LogInExtraInfo,
   +dispatchActionPromise: DispatchActionPromise,
   +logIn: (logInInfo: LogInInfo) => Promise<LogInResult>,
+  +clearModal: () => void,
 };
 type State = {
   +username: string,
@@ -63,7 +61,7 @@
 
   render() {
     return (
-      <Modal name="Log in" onClose={this.clearModal}>
+      <Modal name="Log in" onClose={this.props.clearModal}>
         <div className={css['modal-body']}>
           <form method="POST">
             <div>
@@ -175,7 +173,7 @@
         password: this.state.password,
         ...extraInfo,
       });
-      this.clearModal();
+      this.props.clearModal();
       return result;
     } catch (e) {
       if (e.message === 'invalid_parameters') {
@@ -216,28 +214,24 @@
       throw e;
     }
   }
-
-  clearModal = () => {
-    this.props.setModal(null);
-  };
 }
 
 const loadingStatusSelector = createLoadingStatusSelector(logInActionTypes);
-
-const ConnectedLoginModal: React.ComponentType<BaseProps> = React.memo<BaseProps>(
-  function ConnectedLoginModal(props) {
+const ConnectedLoginModal: React.ComponentType<{}> = React.memo<{}>(
+  function ConnectedLoginModal(): React.Node {
     const inputDisabled = useSelector(loadingStatusSelector) === 'loading';
     const loginExtraInfo = useSelector(webLogInExtraInfoSelector);
     const callLogIn = useServerCall(logIn);
     const dispatchActionPromise = useDispatchActionPromise();
+    const modalContext = useModalContext();
 
     return (
       <LogInModal
-        {...props}
         inputDisabled={inputDisabled}
         logInExtraInfo={loginExtraInfo}
         logIn={callLogIn}
         dispatchActionPromise={dispatchActionPromise}
+        clearModal={modalContext.clearModal}
       />
     );
   },
diff --git a/web/modals/account/user-settings-modal.react.js b/web/modals/account/user-settings-modal.react.js
--- a/web/modals/account/user-settings-modal.react.js
+++ b/web/modals/account/user-settings-modal.react.js
@@ -29,6 +29,7 @@
 import Button from '../../components/button.react';
 import { useSelector } from '../../redux/redux-utils';
 import Input from '../input.react';
+import { useModalContext } from '../modal-provider.react';
 import Modal from '../modal.react';
 import css from './user-settings-modal.css';
 
@@ -58,11 +59,7 @@
   };
 }
 
-type BaseProps = {
-  +setModal: (modal: ?React.Node) => void,
-};
 type Props = {
-  ...BaseProps,
   +currentUserInfo: ?CurrentUserInfo,
   +preRequestUserState: PreRequestUserState,
   +inputDisabled: boolean,
@@ -73,6 +70,7 @@
   ) => Promise<LogOutResult>,
   +changeUserPassword: (passwordUpdate: PasswordUpdate) => Promise<void>,
   +logOut: (preRequestUserState: PreRequestUserState) => Promise<LogOutResult>,
+  +clearModal: () => void,
 };
 type State = {
   +newPassword: string,
@@ -115,7 +113,7 @@
 
   logOut = async () => {
     await this.props.logOut(this.props.preRequestUserState);
-    this.clearModal();
+    this.props.clearModal();
   };
 
   render() {
@@ -206,7 +204,7 @@
     }
 
     return (
-      <Modal name="Edit account" onClose={this.clearModal} size="large">
+      <Modal name="Edit account" onClose={this.props.clearModal} size="large">
         <ul className={css['tab-panel']}>
           <Tab
             name="General"
@@ -328,7 +326,7 @@
         },
         currentPassword: this.state.currentPassword,
       });
-      this.clearModal();
+      this.props.clearModal();
     } catch (e) {
       if (e.message === 'invalid_credentials') {
         this.setState(
@@ -377,7 +375,7 @@
         this.state.currentPassword,
         this.props.preRequestUserState,
       );
-      this.clearModal();
+      this.props.clearModal();
       return response;
     } catch (e) {
       const errorMessage =
@@ -400,10 +398,6 @@
       throw e;
     }
   }
-
-  clearModal = () => {
-    this.props.setModal(null);
-  };
 }
 
 const deleteAccountLoadingStatusSelector = createLoadingStatusSelector(
@@ -412,9 +406,8 @@
 const changeUserPasswordLoadingStatusSelector = createLoadingStatusSelector(
   changeUserPasswordActionTypes,
 );
-
-const ConnectedUserSettingsModal: React.ComponentType<BaseProps> = React.memo<BaseProps>(
-  function ConnectedUserSettingsModal(props) {
+const ConnectedUserSettingsModal: React.ComponentType<{}> = React.memo<{}>(
+  function ConnectedUserSettingsModal(): React.Node {
     const currentUserInfo = useSelector(state => state.currentUserInfo);
     const preRequestUserState = useSelector(preRequestUserStateSelector);
     const inputDisabled = useSelector(
@@ -427,9 +420,10 @@
     const dispatchActionPromise = useDispatchActionPromise();
     const boundLogOut = useServerCall(logOut);
 
+    const modalContext = useModalContext();
+
     return (
       <UserSettingsModal
-        {...props}
         currentUserInfo={currentUserInfo}
         preRequestUserState={preRequestUserState}
         inputDisabled={inputDisabled}
@@ -437,6 +431,7 @@
         changeUserPassword={callChangeUserPassword}
         dispatchActionPromise={dispatchActionPromise}
         logOut={boundLogOut}
+        clearModal={modalContext.clearModal}
       />
     );
   },
diff --git a/web/modals/chat/invalid-upload.react.js b/web/modals/chat/invalid-upload.react.js
--- a/web/modals/chat/invalid-upload.react.js
+++ b/web/modals/chat/invalid-upload.react.js
@@ -2,26 +2,29 @@
 
 import * as React from 'react';
 
+import { useModalContext } from '../../modals/modal-provider.react';
 import css from '../../style.css';
 import Modal from '../modal.react';
 
 type Props = {
-  +setModal: (modal: ?React.Node) => void,
+  +clearModal: () => void,
 };
 class InvalidUploadModal extends React.PureComponent<Props> {
   render(): React.Node {
     return (
-      <Modal name="Invalid upload" onClose={this.clearModal}>
+      <Modal name="Invalid upload" onClose={this.props.clearModal}>
         <div className={css['modal-body']}>
           <p>We don&apos;t support that file type yet :(</p>
         </div>
       </Modal>
     );
   }
+}
+
+function ConnectedInvalidUploadModal(): React.Node {
+  const modalContext = useModalContext();
 
-  clearModal: () => void = () => {
-    this.props.setModal(null);
-  };
+  return <InvalidUploadModal clearModal={modalContext.clearModal} />;
 }
 
-export default InvalidUploadModal;
+export default ConnectedInvalidUploadModal;
diff --git a/web/modals/chat/sidebar-list-modal.react.js b/web/modals/chat/sidebar-list-modal.react.js
--- a/web/modals/chat/sidebar-list-modal.react.js
+++ b/web/modals/chat/sidebar-list-modal.react.js
@@ -15,22 +15,20 @@
 import { useSelector } from '../../redux/redux-utils';
 import globalCSS from '../../style.css';
 import { MagnifyingGlass } from '../../vectors.react';
+import { useModalContext } from '../modal-provider.react';
 import Modal from '../modal.react';
 
 type Props = {
-  +setModal: (modal: ?React.Node) => void,
   +threadInfo: ThreadInfo,
 };
+
 function SidebarListModal(props: Props): React.Node {
-  const { setModal, threadInfo } = props;
+  const { threadInfo } = props;
   const [searchState, setSearchState] = React.useState({
     text: '',
     results: new Set<string>(),
   });
-
-  const clearModal = React.useCallback(() => {
-    setModal(null);
-  }, [setModal]);
+  const { clearModal } = useModalContext();
 
   const sidebarInfos = useSelector(
     state => sidebarInfoSelector(state)[threadInfo.id] ?? [],
diff --git a/web/modals/concurrent-modification-modal.react.js b/web/modals/concurrent-modification-modal.react.js
--- a/web/modals/concurrent-modification-modal.react.js
+++ b/web/modals/concurrent-modification-modal.react.js
@@ -3,16 +3,18 @@
 import * as React from 'react';
 
 import css from '../style.css';
+import { useModalContext } from './modal-provider.react';
 import Modal from './modal.react';
 
 type Props = {
   +onRefresh: () => void,
-  +onClose: () => void,
 };
 
 export default function ConcurrentModificationModal(props: Props): React.Node {
+  const modalContext = useModalContext();
+
   return (
-    <Modal name="Concurrent modification" onClose={props.onClose}>
+    <Modal name="Concurrent modification" onClose={modalContext.clearModal}>
       <div className={css['modal-body']}>
         <p>
           It looks like somebody is attempting to modify that field at the same
diff --git a/web/modals/history/history-modal.react.js b/web/modals/history/history-modal.react.js
--- a/web/modals/history/history-modal.react.js
+++ b/web/modals/history/history-modal.react.js
@@ -33,6 +33,7 @@
 import { dateFromString } from 'lib/utils/date-utils';
 
 import LoadingIndicator from '../../loading-indicator.react';
+import { useModalContext } from '../../modals/modal-provider.react';
 import { useSelector } from '../../redux/redux-utils';
 import { allDaysToEntries } from '../../selectors/entry-selectors';
 import Modal from '../modal.react';
@@ -43,7 +44,6 @@
 type BaseProps = {
   +mode: HistoryMode,
   +dayString: string,
-  +onClose: () => void,
   +currentEntryID?: ?string,
 };
 type Props = {
@@ -59,6 +59,7 @@
   +fetchRevisionsForEntry: (
     entryID: string,
   ) => Promise<$ReadOnlyArray<HistoryRevisionInfo>>,
+  +onClose: () => void,
 };
 type State = {
   +mode: HistoryMode,
@@ -262,6 +263,7 @@
     const callFetchEntries = useServerCall(fetchEntries);
     const callFetchRevisionsForEntry = useServerCall(fetchRevisionsForEntry);
     const dispatchActionPromise = useDispatchActionPromise();
+    const modalContext = useModalContext();
 
     return (
       <HistoryModal
@@ -273,6 +275,7 @@
         fetchEntries={callFetchEntries}
         fetchRevisionsForEntry={callFetchRevisionsForEntry}
         dispatchActionPromise={dispatchActionPromise}
+        onClose={modalContext.clearModal}
       />
     );
   },
diff --git a/web/modals/threads/thread-settings-modal.react.js b/web/modals/threads/thread-settings-modal.react.js
--- a/web/modals/threads/thread-settings-modal.react.js
+++ b/web/modals/threads/thread-settings-modal.react.js
@@ -36,6 +36,7 @@
 } from 'lib/utils/action-utils';
 import { firstLine } from 'lib/utils/string-utils';
 
+import { useModalContext } from '../../modals/modal-provider.react';
 import { useSelector } from '../../redux/redux-utils';
 import css from '../../style.css';
 import Modal from '../modal.react';
@@ -71,7 +72,6 @@
 
 type BaseProps = {
   +threadID: string,
-  +onClose: () => void,
 };
 type Props = {
   ...BaseProps,
@@ -87,6 +87,7 @@
   +changeThreadSettings: (
     update: UpdateThreadRequest,
   ) => Promise<ChangeThreadSettingsPayload>,
+  +onClose: () => void,
 };
 type State = {
   +queuedChanges: ThreadChanges,
@@ -564,9 +565,11 @@
     const threadInfo: ?ThreadInfo = useSelector(
       state => threadInfoSelector(state)[props.threadID],
     );
+    const modalContext = useModalContext();
+
     if (!threadInfo) {
       return (
-        <Modal onClose={props.onClose} name="Invalid thread">
+        <Modal onClose={modalContext.clearModal} name="Invalid thread">
           <div className={css['modal-body']}>
             <p>You no longer have permission to view this thread</p>
           </div>
@@ -584,6 +587,7 @@
         deleteThread={callDeleteThread}
         changeThreadSettings={callChangeThreadSettings}
         dispatchActionPromise={dispatchActionPromise}
+        onClose={modalContext.clearModal}
       />
     );
   },
diff --git a/web/sidebar/community-picker.react.js b/web/sidebar/community-picker.react.js
--- a/web/sidebar/community-picker.react.js
+++ b/web/sidebar/community-picker.react.js
@@ -4,16 +4,15 @@
 
 import Button from '../components/button.react';
 import UserSettingsModal from '../modals/account/user-settings-modal.react.js';
+import { useModalContext } from '../modals/modal-provider.react';
 import SWMansionIcon from '../SWMansionIcon.react';
 import css from './community-picker.css';
 
-type Props = { +setModal: (modal: ?React.Node) => void };
-
-function CommunityPicker(props: Props): React.Node {
-  const { setModal } = props;
+function CommunityPicker(): React.Node {
+  const { setModal } = useModalContext();
 
   const setModalToUserSettings = React.useCallback(() => {
-    setModal(<UserSettingsModal setModal={setModal} />);
+    setModal(<UserSettingsModal />);
   }, [setModal]);
 
   return (
diff --git a/web/sidebar/left-layout-aside.react.js b/web/sidebar/left-layout-aside.react.js
--- a/web/sidebar/left-layout-aside.react.js
+++ b/web/sidebar/left-layout-aside.react.js
@@ -5,15 +5,11 @@
 import AppSwitcher from './app-switcher.react';
 import CommunityPicker from './community-picker.react';
 import css from './left-layout-aside.css';
-type Props = {
-  +setModal: (modal: ?React.Node) => void,
-};
 
-function LeftLayoutAside(props: Props): React.Node {
-  const { setModal } = props;
+function LeftLayoutAside(): React.Node {
   return (
     <aside className={css.container}>
-      <CommunityPicker setModal={setModal} />
+      <CommunityPicker />
       <AppSwitcher />
     </aside>
   );
diff --git a/web/splash/splash.react.js b/web/splash/splash.react.js
--- a/web/splash/splash.react.js
+++ b/web/splash/splash.react.js
@@ -20,20 +20,18 @@
 
 import LoadingIndicator from '../loading-indicator.react';
 import LogInModal from '../modals/account/log-in-modal.react';
+import { useModalContext } from '../modals/modal-provider.react';
 import { useSelector } from '../redux/redux-utils';
 import css from './splash.css';
 
 const defaultRequestAccessScrollHeight = 390;
 
-type BaseProps = {
-  +setModal: (modal: ?React.Node) => void,
-  +currentModal: ?React.Node,
-};
 type Props = {
-  ...BaseProps,
   +loadingStatus: LoadingStatus,
   +dispatchActionPromise: DispatchActionPromise,
   +requestAccess: (accessRequest: AccessRequest) => Promise<void>,
+  +setModal: (modal: React.Node) => void,
+  +modal: ?React.Node,
 };
 type State = {
   +platform: DeviceType,
@@ -178,7 +176,7 @@
             </div>
           </div>
         </div>
-        {this.props.currentModal}
+        {this.props.modal}
       </React.Fragment>
     );
   }
@@ -201,7 +199,7 @@
 
   onClickLogIn = (event: SyntheticEvent<HTMLAnchorElement>) => {
     event.preventDefault();
-    this.props.setModal(<LogInModal setModal={this.props.setModal} />);
+    this.props.setModal(<LogInModal />);
   };
 
   onClickRequestAccess = (event: SyntheticEvent<HTMLAnchorElement>) => {
@@ -260,18 +258,22 @@
 const loadingStatusSelector = createLoadingStatusSelector(
   requestAccessActionTypes,
 );
-const ConnectedSplash: React.ComponentType<BaseProps> = React.memo<BaseProps>(
-  function ConnectedSplash(props) {
+
+const ConnectedSplash: React.ComponentType<{}> = React.memo<{}>(
+  function ConnectedSplash(): React.Node {
     const loadingStatus = useSelector(loadingStatusSelector);
     const callRequestAccess = useServerCall(requestAccess);
     const dispatchActionPromise = useDispatchActionPromise();
 
+    const modalContext = useModalContext();
+
     return (
       <Splash
-        {...props}
         loadingStatus={loadingStatus}
         requestAccess={callRequestAccess}
         dispatchActionPromise={dispatchActionPromise}
+        setModal={modalContext.setModal}
+        modal={modalContext.modal}
       />
     );
   },