From d6eacb79c6e94f8b9d962c040b3b709a551e4e52 Mon Sep 17 00:00:00 2001
From: Renaud Chaput <renchap@gmail.com>
Date: Thu, 19 Oct 2023 19:44:55 +0200
Subject: [PATCH] [Glitch] Upgrade to react-router v5

Port 1b70d7ed7c0fd3a9fcf028bf76b8c62ac8b3897f to glitch-soc

Co-authored-by: Claire <claire.github-309c@sitedethib.com>
Signed-off-by: Claire <claire.github-309c@sitedethib.com>
---
 .../glitch/components/column_back_button.jsx  | 21 +++----
 .../components/column_back_button_slim.jsx    | 20 ++++---
 .../glitch/components/column_header.jsx       | 23 +++----
 .../glitch/components/dropdown_menu.jsx       | 20 +++----
 .../flavours/glitch/components/modal_root.jsx | 12 ++--
 .../glitch/components/navigation_portal.jsx   | 36 -----------
 .../glitch/components/navigation_portal.tsx   | 25 ++++++++
 .../flavours/glitch/components/permalink.jsx  | 13 ++--
 .../flavours/glitch/components/router.tsx     | 28 ++++++---
 .../glitch/components/scrollable_list.jsx     | 37 +++++++++---
 .../flavours/glitch/components/status.jsx     | 27 ++++-----
 .../glitch/components/status_action_bar.jsx   | 25 ++++----
 .../glitch/components/status_content.jsx      |  7 ++-
 .../account/components/featured_tags.jsx      |  4 --
 .../features/account/components/header.jsx    | 13 ++--
 .../account_timeline/components/header.jsx    | 16 ++---
 .../components/moved_note.jsx                 | 16 ++---
 .../features/community_timeline/index.jsx     |  1 -
 .../compose/components/compose_form.jsx       | 14 ++---
 .../compose/components/reply_indicator.jsx    | 12 +---
 .../features/compose/components/search.jsx    | 40 ++++++-------
 .../features/compose/components/upload.jsx    |  4 --
 .../components/conversation.jsx               | 22 ++++---
 .../glitch/features/directory/index.jsx       |  4 --
 .../glitch/features/explore/index.jsx         |  1 -
 .../features/follow_recommendations/index.jsx | 14 ++---
 .../components/announcements.jsx              | 26 ++++----
 .../glitch/features/getting_started/index.jsx |  1 -
 .../features/getting_started_misc/index.jsx   |  1 -
 .../glitch/features/list_timeline/index.jsx   | 13 ++--
 .../notifications/components/admin_report.jsx | 18 +++---
 .../notifications/components/admin_signup.jsx | 18 +++---
 .../notifications/components/follow.jsx       | 18 +++---
 .../components/follow_request.jsx             | 13 ++--
 .../containers/notification_container.js      |  6 +-
 .../picture_in_picture/components/footer.jsx  | 17 +++---
 .../glitch/features/public_timeline/index.jsx |  1 -
 .../features/status/components/action_bar.jsx | 16 ++---
 .../status/components/detailed_status.jsx     | 25 ++++----
 .../flavours/glitch/features/status/index.jsx | 21 ++++---
 .../features/ui/components/audio_modal.jsx    |  4 --
 .../features/ui/components/boost_modal.jsx    | 12 ++--
 .../features/ui/components/columns_area.jsx   |  5 --
 .../ui/components/favourite_modal.jsx         | 11 ++--
 .../features/ui/components/list_panel.jsx     |  4 +-
 .../features/ui/components/media_modal.jsx    |  4 --
 .../ui/components/navigation_panel.jsx        |  3 +-
 .../flavours/glitch/features/ui/index.jsx     |  6 +-
 .../features/ui/util/react_router_helpers.jsx | 33 ++++------
 .../flavours/glitch/utils/dom_helpers.js      |  7 ---
 .../flavours/glitch/utils/react_router.jsx    | 60 +++++++++++++++++++
 51 files changed, 413 insertions(+), 385 deletions(-)
 delete mode 100644 app/javascript/flavours/glitch/components/navigation_portal.jsx
 create mode 100644 app/javascript/flavours/glitch/components/navigation_portal.tsx
 delete mode 100644 app/javascript/flavours/glitch/utils/dom_helpers.js
 create mode 100644 app/javascript/flavours/glitch/utils/react_router.jsx

diff --git a/app/javascript/flavours/glitch/components/column_back_button.jsx b/app/javascript/flavours/glitch/components/column_back_button.jsx
index 5a11a3d5ea..0562fae29b 100644
--- a/app/javascript/flavours/glitch/components/column_back_button.jsx
+++ b/app/javascript/flavours/glitch/components/column_back_button.jsx
@@ -4,26 +4,25 @@ import { createPortal } from 'react-dom';
 
 import { FormattedMessage } from 'react-intl';
 
-import { Icon } from 'flavours/glitch/components/icon';
+import { withRouter } from 'react-router-dom';
 
+import { Icon }  from 'flavours/glitch/components/icon';
+import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
 
-export default class ColumnBackButton extends PureComponent {
-
-  static contextTypes = {
-    router: PropTypes.object,
-  };
+class ColumnBackButton extends PureComponent {
 
   static propTypes = {
     multiColumn: PropTypes.bool,
+    ...WithRouterPropTypes,
   };
 
   handleClick = () => {
-    const { router } = this.context;
+    const { history } = this.props;
 
-    if (router.history.location?.state?.fromMastodon) {
-      router.history.goBack();
+    if (history.location?.state?.fromMastodon) {
+      history.goBack();
     } else {
-      router.history.push('/');
+      history.push('/');
     }
   };
 
@@ -57,3 +56,5 @@ export default class ColumnBackButton extends PureComponent {
   }
 
 }
+
+export default withRouter(ColumnBackButton);
diff --git a/app/javascript/flavours/glitch/components/column_back_button_slim.jsx b/app/javascript/flavours/glitch/components/column_back_button_slim.jsx
index 7b3bac45f9..4e4185387d 100644
--- a/app/javascript/flavours/glitch/components/column_back_button_slim.jsx
+++ b/app/javascript/flavours/glitch/components/column_back_button_slim.jsx
@@ -1,25 +1,27 @@
-import PropTypes from 'prop-types';
 import { PureComponent } from 'react';
 
 import { FormattedMessage } from 'react-intl';
 
+import { withRouter } from 'react-router-dom';
+
 import { Icon } from 'flavours/glitch/components/icon';
+import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
 
-export default class ColumnBackButtonSlim extends PureComponent {
+class ColumnBackButtonSlim extends PureComponent {
 
-  static contextTypes = {
-    router: PropTypes.object,
+  static propTypes = {
+    ...WithRouterPropTypes,
   };
 
   handleClick = () => {
-    const { router } = this.context;
+    const { location, history } = this.props;
 
     // Check if there is a previous page in the app to go back to per https://stackoverflow.com/a/70532858/9703201
     // When upgrading to V6, check `location.key !== 'default'` instead per https://github.com/remix-run/history/blob/main/docs/api-reference.md#location
-    if (router.route.location.key) {
-      router.history.goBack();
+    if (location.key) {
+      history.goBack();
     } else {
-      router.history.push('/');
+      history.push('/');
     }
   };
 
@@ -35,3 +37,5 @@ export default class ColumnBackButtonSlim extends PureComponent {
   }
 
 }
+
+export default withRouter(ColumnBackButtonSlim);
diff --git a/app/javascript/flavours/glitch/components/column_header.jsx b/app/javascript/flavours/glitch/components/column_header.jsx
index 8a68036e9c..fa13f181d0 100644
--- a/app/javascript/flavours/glitch/components/column_header.jsx
+++ b/app/javascript/flavours/glitch/components/column_header.jsx
@@ -5,8 +5,10 @@ import { createPortal } from 'react-dom';
 import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
 
 import classNames from 'classnames';
+import { withRouter } from 'react-router-dom';
 
-import { Icon } from 'flavours/glitch/components/icon';
+import { Icon }  from 'flavours/glitch/components/icon';
+import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
 
 const messages = defineMessages({
   show: { id: 'column_header.show_settings', defaultMessage: 'Show settings' },
@@ -18,7 +20,6 @@ const messages = defineMessages({
 class ColumnHeader extends PureComponent {
 
   static contextTypes = {
-    router: PropTypes.object,
     identity: PropTypes.object,
   };
 
@@ -38,6 +39,7 @@ class ColumnHeader extends PureComponent {
     onClick: PropTypes.func,
     appendContent: PropTypes.node,
     collapseIssues: PropTypes.bool,
+    ...WithRouterPropTypes,
   };
 
   state = {
@@ -63,12 +65,12 @@ class ColumnHeader extends PureComponent {
   };
 
   handleBackClick = () => {
-    const { router } = this.context;
+    const { history } = this.props;
 
-    if (router.history.location?.state?.fromMastodon) {
-      router.history.goBack();
+    if (history.location?.state?.fromMastodon) {
+      history.goBack();
     } else {
-      router.history.push('/');
+      history.push('/');
     }
   };
 
@@ -78,15 +80,14 @@ class ColumnHeader extends PureComponent {
 
   handlePin = () => {
     if (!this.props.pinned) {
-      this.context.router.history.replace('/');
+      this.props.history.replace('/');
     }
 
     this.props.onPin();
   };
 
   render () {
-    const { router } = this.context;
-    const { title, icon, active, children, pinned, multiColumn, extraButton, showBackButton, intl: { formatMessage }, placeholder, appendContent, collapseIssues } = this.props;
+    const { title, icon, active, children, pinned, multiColumn, extraButton, showBackButton, intl: { formatMessage }, placeholder, appendContent, collapseIssues, history } = this.props;
     const { collapsed, animating } = this.state;
 
     const wrapperClassName = classNames('column-header__wrapper', {
@@ -129,7 +130,7 @@ class ColumnHeader extends PureComponent {
       pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={this.handlePin}><Icon id='plus' /> <FormattedMessage id='column_header.pin' defaultMessage='Pin' /></button>;
     }
 
-    if (!pinned && ((multiColumn && router.history.location?.state?.fromMastodon) || showBackButton)) {
+    if (!pinned && ((multiColumn && history.location?.state?.fromMastodon) || showBackButton)) {
       backButton = (
         <button onClick={this.handleBackClick} className='column-header__back-button'>
           <Icon id='chevron-left' className='column-back-button__icon' fixedWidth />
@@ -215,4 +216,4 @@ class ColumnHeader extends PureComponent {
 
 }
 
-export default injectIntl(ColumnHeader);
+export default injectIntl(withRouter(ColumnHeader));
diff --git a/app/javascript/flavours/glitch/components/dropdown_menu.jsx b/app/javascript/flavours/glitch/components/dropdown_menu.jsx
index 0416df5d45..9d744cb495 100644
--- a/app/javascript/flavours/glitch/components/dropdown_menu.jsx
+++ b/app/javascript/flavours/glitch/components/dropdown_menu.jsx
@@ -2,13 +2,16 @@ import PropTypes from 'prop-types';
 import { PureComponent, cloneElement, Children } from 'react';
 
 import classNames from 'classnames';
+import { withRouter } from 'react-router-dom';
 
 import ImmutablePropTypes from 'react-immutable-proptypes';
 
 import { supportsPassiveEvents } from 'detect-passive-events';
 import Overlay from 'react-overlays/Overlay';
 
-import { CircularProgress } from "./circular_progress";
+import { CircularProgress } from 'flavours/glitch/components/circular_progress';
+import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
+
 import { IconButton } from './icon_button';
 
 const listenerOptions = supportsPassiveEvents ? { passive: true, capture: true } : true;
@@ -16,10 +19,6 @@ let id = 0;
 
 class DropdownMenu extends PureComponent {
 
-  static contextTypes = {
-    router: PropTypes.object,
-  };
-
   static propTypes = {
     items: PropTypes.oneOfType([PropTypes.array, ImmutablePropTypes.list]).isRequired,
     loading: PropTypes.bool,
@@ -159,11 +158,7 @@ class DropdownMenu extends PureComponent {
 
 }
 
-export default class Dropdown extends PureComponent {
-
-  static contextTypes = {
-    router: PropTypes.object,
-  };
+class Dropdown extends PureComponent {
 
   static propTypes = {
     children: PropTypes.node,
@@ -183,6 +178,7 @@ export default class Dropdown extends PureComponent {
     renderItem: PropTypes.func,
     renderHeader: PropTypes.func,
     onItemClick: PropTypes.func,
+    ...WithRouterPropTypes
   };
 
   static defaultProps = {
@@ -250,7 +246,7 @@ export default class Dropdown extends PureComponent {
       item.action();
     } else if (item && item.to) {
       e.preventDefault();
-      this.context.router.history.push(item.to);
+      this.props.history.push(item.to);
     }
   };
 
@@ -338,3 +334,5 @@ export default class Dropdown extends PureComponent {
   }
 
 }
+
+export default withRouter(Dropdown);
diff --git a/app/javascript/flavours/glitch/components/modal_root.jsx b/app/javascript/flavours/glitch/components/modal_root.jsx
index a99c51f924..cebc1040a7 100644
--- a/app/javascript/flavours/glitch/components/modal_root.jsx
+++ b/app/javascript/flavours/glitch/components/modal_root.jsx
@@ -2,14 +2,13 @@ import PropTypes from 'prop-types';
 import { PureComponent } from 'react';
 
 import 'wicg-inert';
+
 import { multiply } from 'color-blend';
 import { createBrowserHistory } from 'history';
 
-export default class ModalRoot extends PureComponent {
+import { WithOptionalRouterPropTypes, withOptionalRouter } from 'flavours/glitch/utils/react_router';
 
-  static contextTypes = {
-    router: PropTypes.object,
-  };
+class ModalRoot extends PureComponent {
 
   static propTypes = {
     children: PropTypes.node,
@@ -21,6 +20,7 @@ export default class ModalRoot extends PureComponent {
     }),
     noEsc: PropTypes.bool,
     ignoreFocus: PropTypes.bool,
+    ...WithOptionalRouterPropTypes,
   };
 
   activeElement = this.props.children ? document.activeElement : null;
@@ -56,7 +56,7 @@ export default class ModalRoot extends PureComponent {
   componentDidMount () {
     window.addEventListener('keyup', this.handleKeyUp, false);
     window.addEventListener('keydown', this.handleKeyDown, false);
-    this.history = this.context.router ? this.context.router.history : createBrowserHistory();
+    this.history = this.props.history || createBrowserHistory();
 
     if (this.props.children) {
       this._handleModalOpen();
@@ -160,3 +160,5 @@ export default class ModalRoot extends PureComponent {
   }
 
 }
+
+export default withOptionalRouter(ModalRoot);
diff --git a/app/javascript/flavours/glitch/components/navigation_portal.jsx b/app/javascript/flavours/glitch/components/navigation_portal.jsx
deleted file mode 100644
index e142a3ec60..0000000000
--- a/app/javascript/flavours/glitch/components/navigation_portal.jsx
+++ /dev/null
@@ -1,36 +0,0 @@
-import { PureComponent } from 'react';
-
-import { Switch, Route, withRouter } from 'react-router-dom';
-
-import AccountNavigation from 'flavours/glitch/features/account/navigation';
-import Trends from 'flavours/glitch/features/getting_started/containers/trends_container';
-import { showTrends } from 'flavours/glitch/initial_state';
-
-const DefaultNavigation = () => (
-  showTrends ? (
-    <>
-      <div className='flex-spacer' />
-      <Trends />
-    </>
-  ) : null
-);
-
-class NavigationPortal extends PureComponent {
-
-  render () {
-    return (
-      <Switch>
-        <Route path='/@:acct' exact component={AccountNavigation} />
-        <Route path='/@:acct/tagged/:tagged?' exact component={AccountNavigation} />
-        <Route path='/@:acct/with_replies' exact component={AccountNavigation} />
-        <Route path='/@:acct/followers' exact component={AccountNavigation} />
-        <Route path='/@:acct/following' exact component={AccountNavigation} />
-        <Route path='/@:acct/media' exact component={AccountNavigation} />
-        <Route component={DefaultNavigation} />
-      </Switch>
-    );
-  }
-
-}
-
-export default withRouter(NavigationPortal);
diff --git a/app/javascript/flavours/glitch/components/navigation_portal.tsx b/app/javascript/flavours/glitch/components/navigation_portal.tsx
new file mode 100644
index 0000000000..223cc24232
--- /dev/null
+++ b/app/javascript/flavours/glitch/components/navigation_portal.tsx
@@ -0,0 +1,25 @@
+import { Switch, Route } from 'react-router-dom';
+
+import AccountNavigation from 'flavours/glitch/features/account/navigation';
+import Trends from 'flavours/glitch/features/getting_started/containers/trends_container';
+import { showTrends } from 'flavours/glitch/initial_state';
+
+const DefaultNavigation: React.FC = () =>
+  showTrends ? (
+    <>
+      <div className='flex-spacer' />
+      <Trends />
+    </>
+  ) : null;
+
+export const NavigationPortal: React.FC = () => (
+  <Switch>
+    <Route path='/@:acct' exact component={AccountNavigation} />
+    <Route path='/@:acct/tagged/:tagged?' exact component={AccountNavigation} />
+    <Route path='/@:acct/with_replies' exact component={AccountNavigation} />
+    <Route path='/@:acct/followers' exact component={AccountNavigation} />
+    <Route path='/@:acct/following' exact component={AccountNavigation} />
+    <Route path='/@:acct/media' exact component={AccountNavigation} />
+    <Route component={DefaultNavigation} />
+  </Switch>
+);
diff --git a/app/javascript/flavours/glitch/components/permalink.jsx b/app/javascript/flavours/glitch/components/permalink.jsx
index fa33ce066a..5226895415 100644
--- a/app/javascript/flavours/glitch/components/permalink.jsx
+++ b/app/javascript/flavours/glitch/components/permalink.jsx
@@ -1,11 +1,9 @@
 import PropTypes from 'prop-types';
 import { PureComponent } from 'react';
 
-export default class Permalink extends PureComponent {
+import { withOptionalRouter, WithOptionalRouterPropTypes } from 'flavours/glitch/utils/react_router';
 
-  static contextTypes = {
-    router: PropTypes.object,
-  };
+class Permalink extends PureComponent {
 
   static propTypes = {
     className: PropTypes.string,
@@ -13,6 +11,7 @@ export default class Permalink extends PureComponent {
     to: PropTypes.string.isRequired,
     children: PropTypes.node,
     onInterceptClick: PropTypes.func,
+    ...WithOptionalRouterPropTypes,
   };
 
   handleClick = (e) => {
@@ -22,9 +21,9 @@ export default class Permalink extends PureComponent {
         return;
       }
 
-      if (this.context.router) {
+      if (this.props.history) {
         e.preventDefault();
-        this.context.router.history.push(this.props.to);
+        this.props.history.push(this.props.to);
       }
     }
   };
@@ -47,3 +46,5 @@ export default class Permalink extends PureComponent {
   }
 
 }
+
+export default withOptionalRouter(Permalink);
diff --git a/app/javascript/flavours/glitch/components/router.tsx b/app/javascript/flavours/glitch/components/router.tsx
index f093716517..93d4c0e936 100644
--- a/app/javascript/flavours/glitch/components/router.tsx
+++ b/app/javascript/flavours/glitch/components/router.tsx
@@ -1,15 +1,18 @@
 import type { PropsWithChildren } from 'react';
 import React from 'react';
 
-import { createBrowserHistory } from 'history';
 import { Router as OriginalRouter } from 'react-router';
 
+import type { LocationDescriptor, Path } from 'history';
+import { createBrowserHistory } from 'history';
+
 import { layoutFromWindow } from 'flavours/glitch/is_mobile';
 
 interface MastodonLocationState {
   fromMastodon?: boolean;
   mastodonModalKey?: string;
 }
+type HistoryPath = Path | LocationDescriptor<MastodonLocationState>;
 
 const browserHistory = createBrowserHistory<
   MastodonLocationState | undefined
@@ -17,25 +20,36 @@ const browserHistory = createBrowserHistory<
 const originalPush = browserHistory.push.bind(browserHistory);
 const originalReplace = browserHistory.replace.bind(browserHistory);
 
-browserHistory.push = (path: string, state?: MastodonLocationState) => {
+function extractRealPath(path: HistoryPath) {
+  if (typeof path === 'string') return path;
+  else return path.pathname;
+}
+
+browserHistory.push = (path: HistoryPath, state?: MastodonLocationState) => {
   state = state ?? {};
   state.fromMastodon = true;
 
-  if (layoutFromWindow() === 'multi-column' && !path.startsWith('/deck')) {
-    originalPush(`/deck${path}`, state);
+  const realPath = extractRealPath(path);
+  if (!realPath) return;
+
+  if (layoutFromWindow() === 'multi-column' && !realPath.startsWith('/deck')) {
+    originalPush(`/deck${realPath}`, state);
   } else {
     originalPush(path, state);
   }
 };
 
-browserHistory.replace = (path: string, state?: MastodonLocationState) => {
+browserHistory.replace = (path: HistoryPath, state?: MastodonLocationState) => {
   if (browserHistory.location.state?.fromMastodon) {
     state = state ?? {};
     state.fromMastodon = true;
   }
 
-  if (layoutFromWindow() === 'multi-column' && !path.startsWith('/deck')) {
-    originalReplace(`/deck${path}`, state);
+  const realPath = extractRealPath(path);
+  if (!realPath) return;
+
+  if (layoutFromWindow() === 'multi-column' && !realPath.startsWith('/deck')) {
+    originalReplace(`/deck${realPath}`, state);
   } else {
     originalReplace(path, state);
   }
diff --git a/app/javascript/flavours/glitch/components/scrollable_list.jsx b/app/javascript/flavours/glitch/components/scrollable_list.jsx
index 2a746c5bd1..a281e56154 100644
--- a/app/javascript/flavours/glitch/components/scrollable_list.jsx
+++ b/app/javascript/flavours/glitch/components/scrollable_list.jsx
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
 import { Children, cloneElement, PureComponent } from 'react';
 
 import classNames from 'classnames';
+import { useLocation } from 'react-router-dom';
 
 import { List as ImmutableList } from 'immutable';
 import { connect } from 'react-redux';
@@ -34,11 +35,32 @@ const mapStateToProps = (state, { scrollKey }) => {
   };
 };
 
-class ScrollableList extends PureComponent {
+// This component only exists to be able to call useLocation()
+const IOArticleContainerWrapper = ({id, index, listLength, intersectionObserverWrapper, trackScroll, scrollKey, children}) => {
+  const location = useLocation();
 
-  static contextTypes = {
-    router: PropTypes.object,
-  };
+  return (<IntersectionObserverArticleContainer
+    id={id}
+    index={index}
+    listLength={listLength}
+    intersectionObserverWrapper={intersectionObserverWrapper}
+    saveHeightKey={trackScroll ? `${location.key}:${scrollKey}` : null}
+  >
+    {children}
+  </IntersectionObserverArticleContainer>);
+};
+
+IOArticleContainerWrapper.propTypes =  {
+  id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+  index: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+  listLength: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+  scrollKey: PropTypes.string.isRequired,
+  intersectionObserverWrapper: PropTypes.object.isRequired,
+  trackScroll: PropTypes.bool.isRequired,
+  children: PropTypes.node,
+};
+
+class ScrollableList extends PureComponent {
 
   static propTypes = {
     scrollKey: PropTypes.string.isRequired,
@@ -331,13 +353,14 @@ class ScrollableList extends PureComponent {
             {loadPending}
 
             {Children.map(this.props.children, (child, index) => (
-              <IntersectionObserverArticleContainer
+              <IOArticleContainerWrapper
                 key={child.key}
                 id={child.key}
                 index={index}
                 listLength={childrenCount}
                 intersectionObserverWrapper={this.intersectionObserverWrapper}
-                saveHeightKey={trackScroll ? `${this.context.router.route.location.key}:${scrollKey}` : null}
+                trackScroll={trackScroll}
+                scrollKey={scrollKey}
               >
                 {cloneElement(child, {
                   getScrollPosition: this.getScrollPosition,
@@ -345,7 +368,7 @@ class ScrollableList extends PureComponent {
                   cachedMediaWidth: this.state.cachedMediaWidth,
                   cacheMediaWidth: this.cacheMediaWidth,
                 })}
-              </IntersectionObserverArticleContainer>
+              </IOArticleContainerWrapper>
             ))}
 
             {loadMore}
diff --git a/app/javascript/flavours/glitch/components/status.jsx b/app/javascript/flavours/glitch/components/status.jsx
index 36abc69930..96f6857961 100644
--- a/app/javascript/flavours/glitch/components/status.jsx
+++ b/app/javascript/flavours/glitch/components/status.jsx
@@ -14,6 +14,7 @@ import PollContainer from 'flavours/glitch/containers/poll_container';
 import NotificationOverlayContainer from 'flavours/glitch/features/notifications/containers/overlay_container';
 import { displayMedia } from 'flavours/glitch/initial_state';
 import { autoUnfoldCW } from 'flavours/glitch/utils/content_warning';
+import { withOptionalRouter, WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
 
 import Card from '../features/status/components/card';
 import Bundle from '../features/ui/components/bundle';
@@ -67,10 +68,6 @@ export const defaultMediaVisibility = (status, settings) => {
 
 class Status extends ImmutablePureComponent {
 
-  static contextTypes = {
-    router: PropTypes.object,
-  };
-
   static propTypes = {
     containerId: PropTypes.string,
     id: PropTypes.string,
@@ -117,6 +114,7 @@ class Status extends ImmutablePureComponent {
       inUse: PropTypes.bool,
       available: PropTypes.bool,
     }),
+    ...WithRouterPropTypes,
   };
 
   state = {
@@ -356,10 +354,9 @@ class Status extends ImmutablePureComponent {
   //  Otherwise, we open the url handed to us in `destination`, if
   //  applicable.
   parseClick = (e, destination) => {
-    const { router } = this.context;
-    const { status } = this.props;
+    const { status, history } = this.props;
     const { isCollapsed } = this.state;
-    if (!router) return;
+    if (!history) return;
 
     if (e.button === 0 && !(e.ctrlKey || e.altKey || e.metaKey)) {
       if (isCollapsed) this.setCollapsed(false);
@@ -377,7 +374,7 @@ class Status extends ImmutablePureComponent {
             status.getIn(['reblog', 'id'], status.get('id'))
           }`;
         }
-        router.history.push(destination);
+        history.push(destination);
       }
       e.preventDefault();
     }
@@ -431,7 +428,7 @@ class Status extends ImmutablePureComponent {
 
   handleHotkeyReply = e => {
     e.preventDefault();
-    this.props.onReply(this.props.status, this.context.router.history);
+    this.props.onReply(this.props.status, this.props.history);
   };
 
   handleHotkeyFavourite = (e) => {
@@ -448,16 +445,16 @@ class Status extends ImmutablePureComponent {
 
   handleHotkeyMention = e => {
     e.preventDefault();
-    this.props.onMention(this.props.status.get('account'), this.context.router.history);
+    this.props.onMention(this.props.status.get('account'), this.props.history);
   };
 
   handleHotkeyOpen = () => {
     const status = this.props.status;
-    this.context.router.history.push(`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`);
+    this.props.history.push(`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`);
   };
 
   handleHotkeyOpenProfile = () => {
-    this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`);
+    this.props.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`);
   };
 
   handleHotkeyMoveUp = e => {
@@ -514,7 +511,6 @@ class Status extends ImmutablePureComponent {
       parseClick,
       setCollapsed,
     } = this;
-    const { router } = this.context;
     const {
       intl,
       status,
@@ -533,6 +529,7 @@ class Status extends ImmutablePureComponent {
       previousId,
       nextInReplyToId,
       rootId,
+      history,
       ...other
     } = this.props;
     const { isCollapsed } = this.state;
@@ -828,7 +825,7 @@ class Status extends ImmutablePureComponent {
             onExpandedToggle={this.handleExpandedToggle}
             onTranslate={this.handleTranslate}
             parseClick={parseClick}
-            disabled={!router}
+            disabled={!history}
             tagLinks={settings.get('tag_misleading_links')}
             rewriteMentions={settings.get('rewrite_mentions')}
           />
@@ -854,4 +851,4 @@ class Status extends ImmutablePureComponent {
 
 }
 
-export default injectIntl(Status);
+export default withOptionalRouter(injectIntl(Status));
diff --git a/app/javascript/flavours/glitch/components/status_action_bar.jsx b/app/javascript/flavours/glitch/components/status_action_bar.jsx
index 8d32c6b5d7..b8dd63b270 100644
--- a/app/javascript/flavours/glitch/components/status_action_bar.jsx
+++ b/app/javascript/flavours/glitch/components/status_action_bar.jsx
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
 import { defineMessages, injectIntl } from 'react-intl';
 
 import classNames from 'classnames';
+import { withRouter } from 'react-router-dom';
 
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import ImmutablePureComponent from 'react-immutable-pure-component';
@@ -11,6 +12,7 @@ import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_cont
 import { me } from 'flavours/glitch/initial_state';
 import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'flavours/glitch/permissions';
 import { accountAdminLink, statusAdminLink } from 'flavours/glitch/utils/backend_links';
+import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
 
 import { IconButton } from './icon_button';
 import { RelativeTimestamp } from './relative_timestamp';
@@ -53,7 +55,6 @@ const messages = defineMessages({
 class StatusActionBar extends ImmutablePureComponent {
 
   static contextTypes = {
-    router: PropTypes.object,
     identity: PropTypes.object,
   };
 
@@ -80,6 +81,7 @@ class StatusActionBar extends ImmutablePureComponent {
     showReplyCount: PropTypes.bool,
     scrollKey: PropTypes.string,
     intl: PropTypes.object.isRequired,
+    ...WithRouterPropTypes,
   };
 
   // Avoid checking props that are functions (and whose equality will always
@@ -95,7 +97,7 @@ class StatusActionBar extends ImmutablePureComponent {
     const { signedIn } = this.context.identity;
 
     if (signedIn) {
-      this.props.onReply(this.props.status, this.context.router.history);
+      this.props.onReply(this.props.status, this.props.history);
     } else {
       this.props.onInteractionModal('reply', this.props.status);
     }
@@ -132,15 +134,15 @@ class StatusActionBar extends ImmutablePureComponent {
   };
 
   handleDeleteClick = () => {
-    this.props.onDelete(this.props.status, this.context.router.history);
+    this.props.onDelete(this.props.status, this.props.history);
   };
 
   handleRedraftClick = () => {
-    this.props.onDelete(this.props.status, this.context.router.history, true);
+    this.props.onDelete(this.props.status, this.props.history, true);
   };
 
   handleEditClick = () => {
-    this.props.onEdit(this.props.status, this.context.router.history);
+    this.props.onEdit(this.props.status, this.props.history);
   };
 
   handlePinClick = () => {
@@ -148,11 +150,11 @@ class StatusActionBar extends ImmutablePureComponent {
   };
 
   handleMentionClick = () => {
-    this.props.onMention(this.props.status.get('account'), this.context.router.history);
+    this.props.onMention(this.props.status.get('account'), this.props.history);
   };
 
   handleDirectClick = () => {
-    this.props.onDirect(this.props.status.get('account'), this.context.router.history);
+    this.props.onDirect(this.props.status.get('account'), this.props.history);
   };
 
   handleMuteClick = () => {
@@ -164,12 +166,7 @@ class StatusActionBar extends ImmutablePureComponent {
   };
 
   handleOpen = () => {
-    let state = { ...this.context.router.history.location.state };
-    if (state.mastodonModalKey) {
-      this.context.router.history.replace(`/@${this.props.status.getIn(['account', 'acct'])}/${this.props.status.get('id')}`);
-    } else {
-      this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}/${this.props.status.get('id')}`);
-    }
+    this.props.history.push(`/@${this.props.status.getIn(['account', 'acct'])}/${this.props.status.get('id')}`);
   };
 
   handleEmbed = () => {
@@ -340,4 +337,4 @@ class StatusActionBar extends ImmutablePureComponent {
 
 }
 
-export default injectIntl(StatusActionBar);
+export default withRouter(injectIntl(StatusActionBar));
diff --git a/app/javascript/flavours/glitch/components/status_content.jsx b/app/javascript/flavours/glitch/components/status_content.jsx
index 4719d7dcc7..03b2799168 100644
--- a/app/javascript/flavours/glitch/components/status_content.jsx
+++ b/app/javascript/flavours/glitch/components/status_content.jsx
@@ -4,6 +4,7 @@ import { PureComponent } from 'react';
 import { FormattedMessage, injectIntl } from 'react-intl';
 
 import classnames from 'classnames';
+import { withRouter } from 'react-router-dom';
 
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import { connect } from 'react-redux';
@@ -131,6 +132,10 @@ class StatusContent extends PureComponent {
     rewriteMentions: PropTypes.string,
     languages: ImmutablePropTypes.map,
     intl: PropTypes.object,
+    // from react-router
+    match: PropTypes.object.isRequired,
+    location: PropTypes.object.isRequired,
+    history: PropTypes.object.isRequired
   };
 
   static defaultProps = {
@@ -472,4 +477,4 @@ class StatusContent extends PureComponent {
 
 }
 
-export default connect(mapStateToProps)(injectIntl(StatusContent));
+export default withRouter(connect(mapStateToProps)(injectIntl(StatusContent)));
diff --git a/app/javascript/flavours/glitch/features/account/components/featured_tags.jsx b/app/javascript/flavours/glitch/features/account/components/featured_tags.jsx
index 87e88f2fa8..4ddf0dc524 100644
--- a/app/javascript/flavours/glitch/features/account/components/featured_tags.jsx
+++ b/app/javascript/flavours/glitch/features/account/components/featured_tags.jsx
@@ -14,10 +14,6 @@ const messages = defineMessages({
 
 class FeaturedTags extends ImmutablePureComponent {
 
-  static contextTypes = {
-    router: PropTypes.object,
-  };
-
   static propTypes = {
     account: ImmutablePropTypes.map,
     featuredTags: ImmutablePropTypes.list,
diff --git a/app/javascript/flavours/glitch/features/account/components/header.jsx b/app/javascript/flavours/glitch/features/account/components/header.jsx
index acebd9f322..9a20eaf6ea 100644
--- a/app/javascript/flavours/glitch/features/account/components/header.jsx
+++ b/app/javascript/flavours/glitch/features/account/components/header.jsx
@@ -4,6 +4,7 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 
 import classNames from 'classnames';
 import { Helmet } from 'react-helmet';
+import { withRouter } from 'react-router-dom';
 
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import ImmutablePureComponent from 'react-immutable-pure-component';
@@ -16,6 +17,7 @@ import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_cont
 import { autoPlayGif, me, domain } from 'flavours/glitch/initial_state';
 import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'flavours/glitch/permissions';
 import { preferencesLink, profileLink, accountAdminLink } from 'flavours/glitch/utils/backend_links';
+import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
 
 import AccountNoteContainer from '../containers/account_note_container';
 import FollowRequestNoteContainer from '../containers/follow_request_note_container';
@@ -81,10 +83,6 @@ const dateFormatOptions = {
 
 class Header extends ImmutablePureComponent {
 
-  static contextTypes = {
-    identity: PropTypes.object,
-  };
-
   static propTypes = {
     account: ImmutablePropTypes.map,
     identity_props: ImmutablePropTypes.list,
@@ -107,6 +105,11 @@ class Header extends ImmutablePureComponent {
     intl: PropTypes.object.isRequired,
     domain: PropTypes.string.isRequired,
     hidden: PropTypes.bool,
+    ...WithRouterPropTypes,
+  };
+
+  static contextTypes = {
+    identity: PropTypes.object,
   };
 
   openEditProfile = () => {
@@ -406,4 +409,4 @@ class Header extends ImmutablePureComponent {
 
 }
 
-export default injectIntl(Header);
+export default withRouter(injectIntl(Header));
diff --git a/app/javascript/flavours/glitch/features/account_timeline/components/header.jsx b/app/javascript/flavours/glitch/features/account_timeline/components/header.jsx
index 717114d5c6..1e91ffa112 100644
--- a/app/javascript/flavours/glitch/features/account_timeline/components/header.jsx
+++ b/app/javascript/flavours/glitch/features/account_timeline/components/header.jsx
@@ -2,18 +2,19 @@ import PropTypes from 'prop-types';
 
 import { FormattedMessage } from 'react-intl';
 
-import { NavLink } from 'react-router-dom';
+import { NavLink, withRouter } from 'react-router-dom';
 
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 
 import ActionBar from 'flavours/glitch/features/account/components/action_bar';
 import InnerHeader from 'flavours/glitch/features/account/components/header';
+import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
 
 import MemorialNote from './memorial_note';
 import MovedNote from './moved_note';
 
-export default class Header extends ImmutablePureComponent {
+class Header extends ImmutablePureComponent {
 
   static propTypes = {
     account: ImmutablePropTypes.map,
@@ -34,10 +35,7 @@ export default class Header extends ImmutablePureComponent {
     hideTabs: PropTypes.bool,
     domain: PropTypes.string.isRequired,
     hidden: PropTypes.bool,
-  };
-
-  static contextTypes = {
-    router: PropTypes.object,
+    ...WithRouterPropTypes,
   };
 
   handleFollow = () => {
@@ -49,11 +47,11 @@ export default class Header extends ImmutablePureComponent {
   };
 
   handleMention = () => {
-    this.props.onMention(this.props.account, this.context.router.history);
+    this.props.onMention(this.props.account, this.props.history);
   };
 
   handleDirect = () => {
-    this.props.onDirect(this.props.account, this.context.router.history);
+    this.props.onDirect(this.props.account, this.props.history);
   };
 
   handleReport = () => {
@@ -162,3 +160,5 @@ export default class Header extends ImmutablePureComponent {
   }
 
 }
+
+export default withRouter(Header);
diff --git a/app/javascript/flavours/glitch/features/account_timeline/components/moved_note.jsx b/app/javascript/flavours/glitch/features/account_timeline/components/moved_note.jsx
index 2e10ea94af..5d32f5a90f 100644
--- a/app/javascript/flavours/glitch/features/account_timeline/components/moved_note.jsx
+++ b/app/javascript/flavours/glitch/features/account_timeline/components/moved_note.jsx
@@ -1,30 +1,28 @@
-import PropTypes from 'prop-types';
-
 import { FormattedMessage } from 'react-intl';
 
+import { withRouter } from 'react-router-dom';
+
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 
 import { Icon } from 'flavours/glitch/components/icon';
+import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
 
 import AvatarOverlay from '../../../components/avatar_overlay';
 import { DisplayName } from '../../../components/display_name';
 
-export default class MovedNote extends ImmutablePureComponent {
-
-  static contextTypes = {
-    router: PropTypes.object,
-  };
+class MovedNote extends ImmutablePureComponent {
 
   static propTypes = {
     from: ImmutablePropTypes.map.isRequired,
     to: ImmutablePropTypes.map.isRequired,
+    ...WithRouterPropTypes,
   };
 
   handleAccountClick = e => {
     if (e.button === 0) {
       e.preventDefault();
-      this.context.router.history.push(`/@${this.props.to.get('acct')}`);
+      this.props.history.push(`/@${this.props.to.get('acct')}`);
     }
 
     e.stopPropagation();
@@ -50,3 +48,5 @@ export default class MovedNote extends ImmutablePureComponent {
   }
 
 }
+
+export default withRouter(MovedNote);
diff --git a/app/javascript/flavours/glitch/features/community_timeline/index.jsx b/app/javascript/flavours/glitch/features/community_timeline/index.jsx
index b65e41637c..f9436df0d0 100644
--- a/app/javascript/flavours/glitch/features/community_timeline/index.jsx
+++ b/app/javascript/flavours/glitch/features/community_timeline/index.jsx
@@ -44,7 +44,6 @@ class CommunityTimeline extends PureComponent {
   };
 
   static contextTypes = {
-    router: PropTypes.object,
     identity: PropTypes.object,
   };
 
diff --git a/app/javascript/flavours/glitch/features/compose/components/compose_form.jsx b/app/javascript/flavours/glitch/features/compose/components/compose_form.jsx
index 53e1bf79ae..e3ed207043 100644
--- a/app/javascript/flavours/glitch/features/compose/components/compose_form.jsx
+++ b/app/javascript/flavours/glitch/features/compose/components/compose_form.jsx
@@ -9,6 +9,7 @@ import { length } from 'stringz';
 
 import { maxChars } from 'flavours/glitch/initial_state';
 import { isMobile } from 'flavours/glitch/is_mobile';
+import { WithOptionalRouterPropTypes, withOptionalRouter } from 'flavours/glitch/utils/react_router';
 
 import AutosuggestInput from '../../../components/autosuggest_input';
 import AutosuggestTextarea from '../../../components/autosuggest_textarea';
@@ -38,11 +39,6 @@ const messages = defineMessages({
 });
 
 class ComposeForm extends ImmutablePureComponent {
-
-  static contextTypes = {
-    router: PropTypes.object,
-  };
-
   static propTypes = {
     intl: PropTypes.object.isRequired,
     text: PropTypes.string,
@@ -70,7 +66,6 @@ class ComposeForm extends ImmutablePureComponent {
     isInReply: PropTypes.bool,
     singleColumn: PropTypes.bool,
     lang: PropTypes.string,
-
     advancedOptions: ImmutablePropTypes.map,
     layout: PropTypes.string,
     media: ImmutablePropTypes.list,
@@ -82,6 +77,7 @@ class ComposeForm extends ImmutablePureComponent {
     onChangeSpoilerness: PropTypes.func,
     onChangeVisibility: PropTypes.func,
     onMediaDescriptionConfirm: PropTypes.func,
+    ...WithOptionalRouterPropTypes
   };
 
   static defaultProps = {
@@ -129,12 +125,12 @@ class ComposeForm extends ImmutablePureComponent {
     // Submit unless there are media with missing descriptions
     if (mediaDescriptionConfirmation && onMediaDescriptionConfirm && media && media.some(item => !item.get('description'))) {
       const firstWithoutDescription = media.find(item => !item.get('description'));
-      onMediaDescriptionConfirm(this.context.router ? this.context.router.history : null, firstWithoutDescription.get('id'), overriddenVisibility);
+      onMediaDescriptionConfirm(this.props.history || null, firstWithoutDescription.get('id'), overriddenVisibility);
     } else if (onSubmit) {
       if (onChangeVisibility && overriddenVisibility) {
         onChangeVisibility(overriddenVisibility);
       }
-      onSubmit(this.context.router ? this.context.router.history : null);
+      onSubmit(this.props.history || null);
     }
   };
 
@@ -390,4 +386,4 @@ class ComposeForm extends ImmutablePureComponent {
 
 }
 
-export default injectIntl(ComposeForm);
+export default withOptionalRouter(injectIntl(ComposeForm));
diff --git a/app/javascript/flavours/glitch/features/compose/components/reply_indicator.jsx b/app/javascript/flavours/glitch/features/compose/components/reply_indicator.jsx
index 8a1089bce7..51da8f297d 100644
--- a/app/javascript/flavours/glitch/features/compose/components/reply_indicator.jsx
+++ b/app/javascript/flavours/glitch/features/compose/components/reply_indicator.jsx
@@ -1,4 +1,3 @@
-//  Package imports.
 import PropTypes from 'prop-types';
 
 import { defineMessages, injectIntl } from 'react-intl';
@@ -6,18 +5,13 @@ import { defineMessages, injectIntl } from 'react-intl';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 
-//  Components.
 import AttachmentList from 'flavours/glitch/components/attachment_list';
 import { IconButton } from 'flavours/glitch/components/icon_button';
 import AccountContainer from 'flavours/glitch/containers/account_container';
-//  Messages.
-const messages = defineMessages({
-  cancel: {
-    defaultMessage: 'Cancel',
-    id: 'reply_indicator.cancel',
-  },
-});
 
+const messages = defineMessages({
+  cancel: { id: 'reply_indicator.cancel', defaultMessage: 'Cancel' },
+});
 
 class ReplyIndicator extends ImmutablePureComponent {
 
diff --git a/app/javascript/flavours/glitch/features/compose/components/search.jsx b/app/javascript/flavours/glitch/features/compose/components/search.jsx
index a06b83f6f2..09627bb4fa 100644
--- a/app/javascript/flavours/glitch/features/compose/components/search.jsx
+++ b/app/javascript/flavours/glitch/features/compose/components/search.jsx
@@ -4,14 +4,14 @@ import { PureComponent } from 'react';
 import { defineMessages, injectIntl, FormattedMessage, FormattedList } from 'react-intl';
 
 import classNames from 'classnames';
+import { withRouter } from 'react-router-dom';
 
 import ImmutablePropTypes from 'react-immutable-proptypes';
 
-
-import { Icon } from 'flavours/glitch/components/icon';
+import { Icon }  from 'flavours/glitch/components/icon';
 import { domain, searchEnabled } from 'flavours/glitch/initial_state';
-import { focusRoot } from 'flavours/glitch/utils/dom_helpers';
 import { HASHTAG_REGEX } from 'flavours/glitch/utils/hashtags';
+import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
 
 const messages = defineMessages({
   placeholder: { id: 'search.placeholder', defaultMessage: 'Search' },
@@ -32,7 +32,6 @@ const labelForRecentSearch = search => {
 class Search extends PureComponent {
 
   static contextTypes = {
-    router: PropTypes.object.isRequired,
     identity: PropTypes.object.isRequired,
   };
 
@@ -50,6 +49,7 @@ class Search extends PureComponent {
     openInRoute: PropTypes.bool,
     intl: PropTypes.object.isRequired,
     singleColumn: PropTypes.bool,
+    ...WithRouterPropTypes,
   };
 
   state = {
@@ -122,8 +122,7 @@ class Search extends PureComponent {
     switch(e.key) {
     case 'Escape':
       e.preventDefault();
-
-      focusRoot();
+      this._unfocus();
 
       break;
     case 'ArrowDown':
@@ -170,32 +169,29 @@ class Search extends PureComponent {
   };
 
   handleHashtagClick = () => {
-    const { router } = this.context;
-    const { value, onClickSearchResult } = this.props;
+    const { value, onClickSearchResult, history } = this.props;
 
     const query = value.trim().replace(/^#/, '');
 
-    router.history.push(`/tags/${query}`);
+    history.push(`/tags/${query}`);
     onClickSearchResult(query, 'hashtag');
     this._unfocus();
   };
 
   handleAccountClick = () => {
-    const { router } = this.context;
-    const { value, onClickSearchResult } = this.props;
+    const { value, onClickSearchResult, history } = this.props;
 
     const query = value.trim().replace(/^@/, '');
 
-    router.history.push(`/@${query}`);
+    history.push(`/@${query}`);
     onClickSearchResult(query, 'account');
     this._unfocus();
   };
 
   handleURLClick = () => {
-    const { router } = this.context;
-    const { onOpenURL } = this.props;
+    const { onOpenURL, history } = this.props;
 
-    onOpenURL(router.history);
+    onOpenURL(history);
     this._unfocus();
   };
 
@@ -208,13 +204,12 @@ class Search extends PureComponent {
   };
 
   handleRecentSearchClick = search => {
-    const { onChange } = this.props;
-    const { router } = this.context;
+    const { onChange, history } = this.props;
 
     if (search.get('type') === 'account') {
-      router.history.push(`/@${search.get('q')}`);
+      history.push(`/@${search.get('q')}`);
     } else if (search.get('type') === 'hashtag') {
-      router.history.push(`/tags/${search.get('q')}`);
+      history.push(`/tags/${search.get('q')}`);
     } else {
       onChange(search.get('q'));
       this._submit(search.get('type'));
@@ -246,8 +241,7 @@ class Search extends PureComponent {
   }
 
   _submit (type) {
-    const { onSubmit, openInRoute, value, onClickSearchResult } = this.props;
-    const { router } = this.context;
+    const { onSubmit, openInRoute, value, onClickSearchResult, history } = this.props;
 
     onSubmit(type);
 
@@ -256,7 +250,7 @@ class Search extends PureComponent {
     }
 
     if (openInRoute) {
-      router.history.push('/search');
+      history.push('/search');
     }
 
     this._unfocus();
@@ -403,4 +397,4 @@ class Search extends PureComponent {
 
 }
 
-export default injectIntl(Search);
+export default withRouter(injectIntl(Search));
diff --git a/app/javascript/flavours/glitch/features/compose/components/upload.jsx b/app/javascript/flavours/glitch/features/compose/components/upload.jsx
index 5685759f8d..91f649a131 100644
--- a/app/javascript/flavours/glitch/features/compose/components/upload.jsx
+++ b/app/javascript/flavours/glitch/features/compose/components/upload.jsx
@@ -13,10 +13,6 @@ import Motion from '../../ui/util/optional_motion';
 
 export default class Upload extends ImmutablePureComponent {
 
-  static contextTypes = {
-    router: PropTypes.object,
-  };
-
   static propTypes = {
     media: ImmutablePropTypes.map.isRequired,
     onUndo: PropTypes.func.isRequired,
diff --git a/app/javascript/flavours/glitch/features/direct_timeline/components/conversation.jsx b/app/javascript/flavours/glitch/features/direct_timeline/components/conversation.jsx
index 0a2262f281..0fdd175560 100644
--- a/app/javascript/flavours/glitch/features/direct_timeline/components/conversation.jsx
+++ b/app/javascript/flavours/glitch/features/direct_timeline/components/conversation.jsx
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 
 import classNames from 'classnames';
+import { withRouter } from 'react-router-dom';
 
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import ImmutablePureComponent from 'react-immutable-pure-component';
@@ -17,6 +18,7 @@ import { RelativeTimestamp } from 'flavours/glitch/components/relative_timestamp
 import StatusContent from 'flavours/glitch/components/status_content';
 import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container';
 import { autoPlayGif } from 'flavours/glitch/initial_state';
+import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
 
 const messages = defineMessages({
   more: { id: 'status.more', defaultMessage: 'More' },
@@ -30,10 +32,6 @@ const messages = defineMessages({
 
 class Conversation extends ImmutablePureComponent {
 
-  static contextTypes = {
-    router: PropTypes.object,
-  };
-
   static propTypes = {
     conversationId: PropTypes.string.isRequired,
     accounts: ImmutablePropTypes.list.isRequired,
@@ -45,6 +43,7 @@ class Conversation extends ImmutablePureComponent {
     markRead: PropTypes.func.isRequired,
     delete: PropTypes.func.isRequired,
     intl: PropTypes.object.isRequired,
+    ...WithRouterPropTypes,
   };
 
   state = {
@@ -52,9 +51,8 @@ class Conversation extends ImmutablePureComponent {
   };
 
   parseClick = (e, destination) => {
-    const { router } = this.context;
-    const { lastStatus, unread, markRead } = this.props;
-    if (!router) return;
+    const { history, lastStatus, unread, markRead } = this.props;
+    if (!history) return;
 
     if (e.button === 0 && !(e.ctrlKey || e.altKey || e.metaKey)) {
       if (destination === undefined) {
@@ -63,7 +61,7 @@ class Conversation extends ImmutablePureComponent {
         }
         destination = `/statuses/${lastStatus.get('id')}`;
       }
-      router.history.push(destination);
+      history.push(destination);
       e.preventDefault();
     }
   };
@@ -95,7 +93,7 @@ class Conversation extends ImmutablePureComponent {
   };
 
   handleClick = () => {
-    if (!this.context.router) {
+    if (!this.props.history) {
       return;
     }
 
@@ -105,7 +103,7 @@ class Conversation extends ImmutablePureComponent {
       markRead();
     }
 
-    this.context.router.history.push(`/@${lastStatus.getIn(['account', 'acct'])}/${lastStatus.get('id')}`);
+    this.props.history.push(`/@${lastStatus.getIn(['account', 'acct'])}/${lastStatus.get('id')}`);
   };
 
   handleMarkAsRead = () => {
@@ -113,7 +111,7 @@ class Conversation extends ImmutablePureComponent {
   };
 
   handleReply = () => {
-    this.props.reply(this.props.lastStatus, this.context.router.history);
+    this.props.reply(this.props.lastStatus, this.props.history);
   };
 
   handleDelete = () => {
@@ -232,4 +230,4 @@ class Conversation extends ImmutablePureComponent {
 
 }
 
-export default injectIntl(Conversation);
+export default withRouter(injectIntl(Conversation));
diff --git a/app/javascript/flavours/glitch/features/directory/index.jsx b/app/javascript/flavours/glitch/features/directory/index.jsx
index e1db3180fa..92e80bf956 100644
--- a/app/javascript/flavours/glitch/features/directory/index.jsx
+++ b/app/javascript/flavours/glitch/features/directory/index.jsx
@@ -36,10 +36,6 @@ const mapStateToProps = state => ({
 
 class Directory extends PureComponent {
 
-  static contextTypes = {
-    router: PropTypes.object,
-  };
-
   static propTypes = {
     isLoading: PropTypes.bool,
     accountIds: ImmutablePropTypes.list.isRequired,
diff --git a/app/javascript/flavours/glitch/features/explore/index.jsx b/app/javascript/flavours/glitch/features/explore/index.jsx
index 4a916c9958..6eeba46ce7 100644
--- a/app/javascript/flavours/glitch/features/explore/index.jsx
+++ b/app/javascript/flavours/glitch/features/explore/index.jsx
@@ -34,7 +34,6 @@ const mapStateToProps = state => ({
 class Explore extends PureComponent {
 
   static contextTypes = {
-    router: PropTypes.object,
     identity: PropTypes.object,
   };
 
diff --git a/app/javascript/flavours/glitch/features/follow_recommendations/index.jsx b/app/javascript/flavours/glitch/features/follow_recommendations/index.jsx
index 194784a7a5..037794e18b 100644
--- a/app/javascript/flavours/glitch/features/follow_recommendations/index.jsx
+++ b/app/javascript/flavours/glitch/features/follow_recommendations/index.jsx
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
 import { FormattedMessage } from 'react-intl';
 
 import { Helmet } from 'react-helmet';
+import { withRouter } from 'react-router-dom';
 
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import ImmutablePureComponent from 'react-immutable-pure-component';
@@ -14,6 +15,7 @@ import { fetchSuggestions } from 'flavours/glitch/actions/suggestions';
 import { markAsPartial } from 'flavours/glitch/actions/timelines';
 import Button from 'flavours/glitch/components/button';
 import Column from 'flavours/glitch/features/ui/components/column';
+import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
 import imageGreeting from 'mastodon/../images/elephant_ui_greeting.svg';
 
 import Account from './components/account';
@@ -25,14 +27,11 @@ const mapStateToProps = state => ({
 
 class FollowRecommendations extends ImmutablePureComponent {
 
-  static contextTypes = {
-    router: PropTypes.object.isRequired,
-  };
-
   static propTypes = {
     dispatch: PropTypes.func.isRequired,
     suggestions: ImmutablePropTypes.list,
     isLoading: PropTypes.bool,
+    ...WithRouterPropTypes,
   };
 
   componentDidMount () {
@@ -56,8 +55,7 @@ class FollowRecommendations extends ImmutablePureComponent {
   }
 
   handleDone = () => {
-    const { dispatch } = this.props;
-    const { router } = this.context;
+    const { history, dispatch } = this.props;
 
     dispatch(requestBrowserPermission((permission) => {
       if (permission === 'granted') {
@@ -71,7 +69,7 @@ class FollowRecommendations extends ImmutablePureComponent {
       }
     }));
 
-    router.history.push('/home');
+    history.push('/home');
   };
 
   render () {
@@ -118,4 +116,4 @@ class FollowRecommendations extends ImmutablePureComponent {
 
 }
 
-export default connect(mapStateToProps)(FollowRecommendations);
+export default withRouter(connect(mapStateToProps)(FollowRecommendations));
diff --git a/app/javascript/flavours/glitch/features/getting_started/components/announcements.jsx b/app/javascript/flavours/glitch/features/getting_started/components/announcements.jsx
index fd17f8c9d7..1037e8de1b 100644
--- a/app/javascript/flavours/glitch/features/getting_started/components/announcements.jsx
+++ b/app/javascript/flavours/glitch/features/getting_started/components/announcements.jsx
@@ -4,6 +4,7 @@ import { PureComponent } from 'react';
 import { defineMessages, injectIntl, FormattedMessage, FormattedDate } from 'react-intl';
 
 import classNames from 'classnames';
+import { withRouter } from 'react-router-dom';
 
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import ImmutablePureComponent from 'react-immutable-pure-component';
@@ -19,24 +20,19 @@ import EmojiPickerDropdown from 'flavours/glitch/features/compose/containers/emo
 import unicodeMapping from 'flavours/glitch/features/emoji/emoji_unicode_mapping_light';
 import { autoPlayGif, reduceMotion, disableSwiping, mascot } from 'flavours/glitch/initial_state';
 import { assetHost } from 'flavours/glitch/utils/config';
+import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
 import elephantUIPlane from 'mastodon/../images/elephant_ui_plane.svg';
 
-
-
 const messages = defineMessages({
   close: { id: 'lightbox.close', defaultMessage: 'Close' },
   previous: { id: 'lightbox.previous', defaultMessage: 'Previous' },
   next: { id: 'lightbox.next', defaultMessage: 'Next' },
 });
 
-class Content extends ImmutablePureComponent {
-
-  static contextTypes = {
-    router: PropTypes.object,
-  };
-
+class ContentWithRouter extends ImmutablePureComponent {
   static propTypes = {
     announcement: ImmutablePropTypes.map.isRequired,
+    ...WithRouterPropTypes,
   };
 
   setRef = c => {
@@ -91,25 +87,25 @@ class Content extends ImmutablePureComponent {
   }
 
   onMentionClick = (mention, e) => {
-    if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
+    if (this.props.history && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
       e.preventDefault();
-      this.context.router.history.push(`/@${mention.get('acct')}`);
+      this.props.history.push(`/@${mention.get('acct')}`);
     }
   };
 
   onHashtagClick = (hashtag, e) => {
     hashtag = hashtag.replace(/^#/, '');
 
-    if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
+    if (this.props.history&& e.button === 0 && !(e.ctrlKey || e.metaKey)) {
       e.preventDefault();
-      this.context.router.history.push(`/tags/${hashtag}`);
+      this.props.history.push(`/tags/${hashtag}`);
     }
   };
 
   onStatusClick = (status, e) => {
-    if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
+    if (this.props.history && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
       e.preventDefault();
-      this.context.router.history.push(`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`);
+      this.props.history.push(`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`);
     }
   };
 
@@ -155,6 +151,8 @@ class Content extends ImmutablePureComponent {
 
 }
 
+const Content = withRouter(ContentWithRouter);
+
 class Emoji extends PureComponent {
 
   static propTypes = {
diff --git a/app/javascript/flavours/glitch/features/getting_started/index.jsx b/app/javascript/flavours/glitch/features/getting_started/index.jsx
index 9a8ee3f091..0987ad9352 100644
--- a/app/javascript/flavours/glitch/features/getting_started/index.jsx
+++ b/app/javascript/flavours/glitch/features/getting_started/index.jsx
@@ -88,7 +88,6 @@ const badgeDisplay = (number, limit) => {
 class GettingStarted extends ImmutablePureComponent {
 
   static contextTypes = {
-    router: PropTypes.object.isRequired,
     identity: PropTypes.object,
   };
 
diff --git a/app/javascript/flavours/glitch/features/getting_started_misc/index.jsx b/app/javascript/flavours/glitch/features/getting_started_misc/index.jsx
index d7500fd822..2c2fbc05c0 100644
--- a/app/javascript/flavours/glitch/features/getting_started_misc/index.jsx
+++ b/app/javascript/flavours/glitch/features/getting_started_misc/index.jsx
@@ -28,7 +28,6 @@ const messages = defineMessages({
 class GettingStartedMisc extends ImmutablePureComponent {
 
   static contextTypes = {
-    router: PropTypes.object.isRequired,
     identity: PropTypes.object,
   };
 
diff --git a/app/javascript/flavours/glitch/features/list_timeline/index.jsx b/app/javascript/flavours/glitch/features/list_timeline/index.jsx
index d407e8a6ea..594cecaf74 100644
--- a/app/javascript/flavours/glitch/features/list_timeline/index.jsx
+++ b/app/javascript/flavours/glitch/features/list_timeline/index.jsx
@@ -4,6 +4,7 @@ import { PureComponent } from 'react';
 import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
 
 import { Helmet } from 'react-helmet';
+import { withRouter } from 'react-router-dom';
 
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import { connect } from 'react-redux';
@@ -22,6 +23,7 @@ import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator';
 import { RadioButton } from 'flavours/glitch/components/radio_button';
 import BundleColumnError from 'flavours/glitch/features/ui/components/bundle_column_error';
 import StatusListContainer from 'flavours/glitch/features/ui/containers/status_list_container';
+import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
 
 const messages = defineMessages({
   deleteMessage: { id: 'confirmations.delete_list.message', defaultMessage: 'Are you sure you want to permanently delete this list?' },
@@ -38,10 +40,6 @@ const mapStateToProps = (state, props) => ({
 
 class ListTimeline extends PureComponent {
 
-  static contextTypes = {
-    router: PropTypes.object,
-  };
-
   static propTypes = {
     params: PropTypes.object.isRequired,
     dispatch: PropTypes.func.isRequired,
@@ -50,6 +48,7 @@ class ListTimeline extends PureComponent {
     multiColumn: PropTypes.bool,
     list: PropTypes.oneOfType([ImmutablePropTypes.map, PropTypes.bool]),
     intl: PropTypes.object.isRequired,
+    ...WithRouterPropTypes,
   };
 
   handlePin = () => {
@@ -59,7 +58,7 @@ class ListTimeline extends PureComponent {
       dispatch(removeColumn(columnId));
     } else {
       dispatch(addColumn('LIST', { id: this.props.params.id }));
-      this.context.router.history.push('/');
+      this.props.history.push('/');
     }
   };
 
@@ -137,7 +136,7 @@ class ListTimeline extends PureComponent {
           if (columnId) {
             dispatch(removeColumn(columnId));
           } else {
-            this.context.router.history.push('/lists');
+            this.props.history.push('/lists');
           }
         },
       },
@@ -242,4 +241,4 @@ class ListTimeline extends PureComponent {
 
 }
 
-export default connect(mapStateToProps)(injectIntl(ListTimeline));
+export default withRouter(connect(mapStateToProps)(injectIntl(ListTimeline)));
diff --git a/app/javascript/flavours/glitch/features/notifications/components/admin_report.jsx b/app/javascript/flavours/glitch/features/notifications/components/admin_report.jsx
index 9f63b3ce79..213f319fc9 100644
--- a/app/javascript/flavours/glitch/features/notifications/components/admin_report.jsx
+++ b/app/javascript/flavours/glitch/features/notifications/components/admin_report.jsx
@@ -1,25 +1,24 @@
-//  Package imports.
 import PropTypes from 'prop-types';
 
 import { FormattedMessage } from 'react-intl';
 
 import classNames from 'classnames';
+import { withRouter } from 'react-router-dom';
 
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 
 import { HotKeys } from 'react-hotkeys';
 
-
-// Our imports.
 import { Icon } from 'flavours/glitch/components/icon';
 import Permalink from 'flavours/glitch/components/permalink';
+import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
 
 import NotificationOverlayContainer from '../containers/overlay_container';
 
 import Report from './report';
 
-export default class AdminReport extends ImmutablePureComponent {
+class AdminReport extends ImmutablePureComponent {
 
   static propTypes = {
     hidden: PropTypes.bool,
@@ -28,6 +27,7 @@ export default class AdminReport extends ImmutablePureComponent {
     notification: ImmutablePropTypes.map.isRequired,
     unread: PropTypes.bool,
     report: ImmutablePropTypes.map.isRequired,
+    ...WithRouterPropTypes,
   };
 
   handleMoveUp = () => {
@@ -45,15 +45,15 @@ export default class AdminReport extends ImmutablePureComponent {
   };
 
   handleOpenProfile = () => {
-    const { notification } = this.props;
-    this.context.router.history.push(`/@${notification.getIn(['account', 'acct'])}`);
+    const { history, notification } = this.props;
+    history.push(`/@${notification.getIn(['account', 'acct'])}`);
   };
 
   handleMention = e => {
     e.preventDefault();
 
-    const { notification, onMention } = this.props;
-    onMention(notification.get('account'), this.context.router.history);
+    const { history, notification, onMention } = this.props;
+    onMention(notification.get('account'), history);
   };
 
   getHandlers () {
@@ -111,3 +111,5 @@ export default class AdminReport extends ImmutablePureComponent {
   }
 
 }
+
+export default withRouter(AdminReport);
diff --git a/app/javascript/flavours/glitch/features/notifications/components/admin_signup.jsx b/app/javascript/flavours/glitch/features/notifications/components/admin_signup.jsx
index 8036f783a7..9d9a5f0c06 100644
--- a/app/javascript/flavours/glitch/features/notifications/components/admin_signup.jsx
+++ b/app/javascript/flavours/glitch/features/notifications/components/admin_signup.jsx
@@ -1,24 +1,23 @@
-//  Package imports.
 import PropTypes from 'prop-types';
 
 import { FormattedMessage } from 'react-intl';
 
 import classNames from 'classnames';
+import { withRouter } from 'react-router-dom';
 
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 
 import { HotKeys } from 'react-hotkeys';
 
-
-// Our imports.
 import { Icon } from 'flavours/glitch/components/icon';
 import Permalink from 'flavours/glitch/components/permalink';
 import AccountContainer from 'flavours/glitch/containers/account_container';
+import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
 
 import NotificationOverlayContainer from '../containers/overlay_container';
 
-export default class NotificationFollow extends ImmutablePureComponent {
+class NotificationAdminSignup extends ImmutablePureComponent {
 
   static propTypes = {
     hidden: PropTypes.bool,
@@ -26,6 +25,7 @@ export default class NotificationFollow extends ImmutablePureComponent {
     account: ImmutablePropTypes.map.isRequired,
     notification: ImmutablePropTypes.map.isRequired,
     unread: PropTypes.bool,
+    ...WithRouterPropTypes,
   };
 
   handleMoveUp = () => {
@@ -43,15 +43,15 @@ export default class NotificationFollow extends ImmutablePureComponent {
   };
 
   handleOpenProfile = () => {
-    const { notification } = this.props;
-    this.context.router.history.push(`/@${notification.getIn(['account', 'acct'])}`);
+    const { history, notification } = this.props;
+    history.push(`/@${notification.getIn(['account', 'acct'])}`);
   };
 
   handleMention = e => {
     e.preventDefault();
 
-    const { notification, onMention } = this.props;
-    onMention(notification.get('account'), this.context.router.history);
+    const { history, notification, onMention } = this.props;
+    onMention(notification.get('account'), history);
   };
 
   getHandlers () {
@@ -104,3 +104,5 @@ export default class NotificationFollow extends ImmutablePureComponent {
   }
 
 }
+
+export default withRouter(NotificationAdminSignup);
diff --git a/app/javascript/flavours/glitch/features/notifications/components/follow.jsx b/app/javascript/flavours/glitch/features/notifications/components/follow.jsx
index 7e2749d66d..06603d8e3e 100644
--- a/app/javascript/flavours/glitch/features/notifications/components/follow.jsx
+++ b/app/javascript/flavours/glitch/features/notifications/components/follow.jsx
@@ -1,24 +1,23 @@
-//  Package imports.
 import PropTypes from 'prop-types';
 
 import { FormattedMessage } from 'react-intl';
 
 import classNames from 'classnames';
+import { withRouter } from 'react-router-dom';
 
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 
 import { HotKeys } from 'react-hotkeys';
 
-
-// Our imports.
 import { Icon } from 'flavours/glitch/components/icon';
 import Permalink from 'flavours/glitch/components/permalink';
 import AccountContainer from 'flavours/glitch/containers/account_container';
+import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
 
 import NotificationOverlayContainer from '../containers/overlay_container';
 
-export default class NotificationFollow extends ImmutablePureComponent {
+class NotificationFollow extends ImmutablePureComponent {
 
   static propTypes = {
     hidden: PropTypes.bool,
@@ -26,6 +25,7 @@ export default class NotificationFollow extends ImmutablePureComponent {
     account: ImmutablePropTypes.map.isRequired,
     notification: ImmutablePropTypes.map.isRequired,
     unread: PropTypes.bool,
+    ...WithRouterPropTypes,
   };
 
   handleMoveUp = () => {
@@ -43,15 +43,15 @@ export default class NotificationFollow extends ImmutablePureComponent {
   };
 
   handleOpenProfile = () => {
-    const { notification } = this.props;
-    this.context.router.history.push(`/@${notification.getIn(['account', 'acct'])}`);
+    const { history, notification } = this.props;
+    history.push(`/@${notification.getIn(['account', 'acct'])}`);
   };
 
   handleMention = e => {
     e.preventDefault();
 
-    const { notification, onMention } = this.props;
-    onMention(notification.get('account'), this.context.router.history);
+    const { history, notification, onMention } = this.props;
+    onMention(notification.get('account'), history);
   };
 
   getHandlers () {
@@ -104,3 +104,5 @@ export default class NotificationFollow extends ImmutablePureComponent {
   }
 
 }
+
+export default withRouter(NotificationFollow);
diff --git a/app/javascript/flavours/glitch/features/notifications/components/follow_request.jsx b/app/javascript/flavours/glitch/features/notifications/components/follow_request.jsx
index d5ae9dd3f3..831095200a 100644
--- a/app/javascript/flavours/glitch/features/notifications/components/follow_request.jsx
+++ b/app/javascript/flavours/glitch/features/notifications/components/follow_request.jsx
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 
 import classNames from 'classnames';
+import { withRouter } from 'react-router-dom';
 
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import ImmutablePureComponent from 'react-immutable-pure-component';
@@ -14,6 +15,7 @@ import { DisplayName } from 'flavours/glitch/components/display_name';
 import { Icon } from 'flavours/glitch/components/icon';
 import { IconButton } from 'flavours/glitch/components/icon_button';
 import Permalink from 'flavours/glitch/components/permalink';
+import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
 
 import NotificationOverlayContainer from '../containers/overlay_container';
 
@@ -31,6 +33,7 @@ class FollowRequest extends ImmutablePureComponent {
     intl: PropTypes.object.isRequired,
     notification: ImmutablePropTypes.map.isRequired,
     unread: PropTypes.bool,
+    ...WithRouterPropTypes,
   };
 
   handleMoveUp = () => {
@@ -48,15 +51,15 @@ class FollowRequest extends ImmutablePureComponent {
   };
 
   handleOpenProfile = () => {
-    const { notification } = this.props;
-    this.context.router.history.push(`/@${notification.getIn(['account', 'acct'])}`);
+    const { history, notification } = this.props;
+    history.push(`/@${notification.getIn(['account', 'acct'])}`);
   };
 
   handleMention = e => {
     e.preventDefault();
 
-    const { notification, onMention } = this.props;
-    onMention(notification.get('account'), this.context.router.history);
+    const { history, notification, onMention } = this.props;
+    onMention(notification.get('account'), history);
   };
 
   getHandlers () {
@@ -135,4 +138,4 @@ class FollowRequest extends ImmutablePureComponent {
 
 }
 
-export default injectIntl(FollowRequest);
+export default withRouter(injectIntl(FollowRequest));
diff --git a/app/javascript/flavours/glitch/features/notifications/containers/notification_container.js b/app/javascript/flavours/glitch/features/notifications/containers/notification_container.js
index b39d1169f2..2fda4ac3a2 100644
--- a/app/javascript/flavours/glitch/features/notifications/containers/notification_container.js
+++ b/app/javascript/flavours/glitch/features/notifications/containers/notification_container.js
@@ -1,7 +1,5 @@
-//  Package imports.
 import { connect } from 'react-redux';
 
-//  Our imports.
 import { mentionCompose } from 'flavours/glitch/actions/compose';
 import { makeGetNotification } from 'flavours/glitch/selectors';
 
@@ -19,8 +17,8 @@ const makeMapStateToProps = () => {
 };
 
 const mapDispatchToProps = dispatch => ({
-  onMention: (account, router) => {
-    dispatch(mentionCompose(account, router));
+  onMention: (account, history) => {
+    dispatch(mentionCompose(account, history));
   },
 });
 
diff --git a/app/javascript/flavours/glitch/features/picture_in_picture/components/footer.jsx b/app/javascript/flavours/glitch/features/picture_in_picture/components/footer.jsx
index 77574dea09..51d7c10668 100644
--- a/app/javascript/flavours/glitch/features/picture_in_picture/components/footer.jsx
+++ b/app/javascript/flavours/glitch/features/picture_in_picture/components/footer.jsx
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
 import { defineMessages, injectIntl } from 'react-intl';
 
 import classNames from 'classnames';
+import { withRouter } from 'react-router-dom';
 
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import ImmutablePureComponent from 'react-immutable-pure-component';
@@ -15,6 +16,7 @@ import { openModal } from 'flavours/glitch/actions/modal';
 import { IconButton } from 'flavours/glitch/components/icon_button';
 import { me, boostModal } from 'flavours/glitch/initial_state';
 import { makeGetStatus } from 'flavours/glitch/selectors';
+import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
 
 const messages = defineMessages({
   reply: { id: 'status.reply', defaultMessage: 'Reply' },
@@ -44,7 +46,6 @@ const makeMapStateToProps = () => {
 class Footer extends ImmutablePureComponent {
 
   static contextTypes = {
-    router: PropTypes.object,
     identity: PropTypes.object,
   };
 
@@ -57,17 +58,17 @@ class Footer extends ImmutablePureComponent {
     showReplyCount: PropTypes.bool,
     withOpenButton: PropTypes.bool,
     onClose: PropTypes.func,
+    ...WithRouterPropTypes,
   };
 
   _performReply = () => {
-    const { dispatch, status, onClose } = this.props;
-    const { router } = this.context;
+    const { dispatch, status, onClose, history } = this.props;
 
     if (onClose) {
       onClose(true);
     }
 
-    dispatch(replyCompose(status, router.history));
+    dispatch(replyCompose(status, history));
   };
 
   handleReplyClick = () => {
@@ -151,9 +152,7 @@ class Footer extends ImmutablePureComponent {
   };
 
   handleOpenClick = e => {
-    const { router } = this.context;
-
-    if (e.button !== 0 || !router) {
+    if (e.button !== 0 || !history) {
       return;
     }
 
@@ -163,7 +162,7 @@ class Footer extends ImmutablePureComponent {
       onClose();
     }
 
-    router.history.push(`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`);
+    history.push(`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`);
   };
 
   render () {
@@ -229,4 +228,4 @@ class Footer extends ImmutablePureComponent {
 
 }
 
-export default connect(makeMapStateToProps)(injectIntl(Footer));
+export default  withRouter(connect(makeMapStateToProps)(injectIntl(Footer)));
diff --git a/app/javascript/flavours/glitch/features/public_timeline/index.jsx b/app/javascript/flavours/glitch/features/public_timeline/index.jsx
index 83ada8d3c0..dc5d53f9d4 100644
--- a/app/javascript/flavours/glitch/features/public_timeline/index.jsx
+++ b/app/javascript/flavours/glitch/features/public_timeline/index.jsx
@@ -48,7 +48,6 @@ class PublicTimeline extends PureComponent {
   };
 
   static contextTypes = {
-    router: PropTypes.object,
     identity: PropTypes.object,
   };
 
diff --git a/app/javascript/flavours/glitch/features/status/components/action_bar.jsx b/app/javascript/flavours/glitch/features/status/components/action_bar.jsx
index 5d48224fb7..9d8e0c945b 100644
--- a/app/javascript/flavours/glitch/features/status/components/action_bar.jsx
+++ b/app/javascript/flavours/glitch/features/status/components/action_bar.jsx
@@ -4,6 +4,7 @@ import { PureComponent } from 'react';
 import { defineMessages, injectIntl } from 'react-intl';
 
 import classNames from 'classnames';
+import { withRouter } from 'react-router-dom';
 
 import ImmutablePropTypes from 'react-immutable-proptypes';
 
@@ -12,6 +13,7 @@ import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_cont
 import { me } from 'flavours/glitch/initial_state';
 import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'flavours/glitch/permissions';
 import { accountAdminLink, statusAdminLink } from 'flavours/glitch/utils/backend_links';
+import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
 
 const messages = defineMessages({
   delete: { id: 'status.delete', defaultMessage: 'Delete' },
@@ -46,7 +48,6 @@ const messages = defineMessages({
 class ActionBar extends PureComponent {
 
   static contextTypes = {
-    router: PropTypes.object,
     identity: PropTypes.object,
   };
 
@@ -67,6 +68,7 @@ class ActionBar extends PureComponent {
     onPin: PropTypes.func,
     onEmbed: PropTypes.func,
     intl: PropTypes.object.isRequired,
+    ...WithRouterPropTypes,
   };
 
   handleReplyClick = () => {
@@ -86,23 +88,23 @@ class ActionBar extends PureComponent {
   };
 
   handleDeleteClick = () => {
-    this.props.onDelete(this.props.status, this.context.router.history);
+    this.props.onDelete(this.props.status, this.props.history);
   };
 
   handleRedraftClick = () => {
-    this.props.onDelete(this.props.status, this.context.router.history, true);
+    this.props.onDelete(this.props.status, this.props.history, true);
   };
 
   handleEditClick = () => {
-    this.props.onEdit(this.props.status, this.context.router.history);
+    this.props.onEdit(this.props.status, this.props.history);
   };
 
   handleDirectClick = () => {
-    this.props.onDirect(this.props.status.get('account'), this.context.router.history);
+    this.props.onDirect(this.props.status.get('account'), this.props.history);
   };
 
   handleMentionClick = () => {
-    this.props.onMention(this.props.status.get('account'), this.context.router.history);
+    this.props.onMention(this.props.status.get('account'), this.props.history);
   };
 
   handleMuteClick = () => {
@@ -234,4 +236,4 @@ class ActionBar extends PureComponent {
 
 }
 
-export default injectIntl(ActionBar);
+export default withRouter(injectIntl(ActionBar));
diff --git a/app/javascript/flavours/glitch/features/status/components/detailed_status.jsx b/app/javascript/flavours/glitch/features/status/components/detailed_status.jsx
index 1770436ae7..962a86da20 100644
--- a/app/javascript/flavours/glitch/features/status/components/detailed_status.jsx
+++ b/app/javascript/flavours/glitch/features/status/components/detailed_status.jsx
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
 import { injectIntl, FormattedDate } from 'react-intl';
 
 import classNames from 'classnames';
-import { Link } from 'react-router-dom';
+import { Link, withRouter } from 'react-router-dom';
 
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import ImmutablePureComponent from 'react-immutable-pure-component';
@@ -21,20 +21,14 @@ import VisibilityIcon from 'flavours/glitch/components/status_visibility_icon';
 import PollContainer from 'flavours/glitch/containers/poll_container';
 import Audio from 'flavours/glitch/features/audio';
 import Video from 'flavours/glitch/features/video';
+import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
 
 import scheduleIdleTask from '../../ui/util/schedule_idle_task';
 
 import Card from './card';
 
-
-
-
 class DetailedStatus extends ImmutablePureComponent {
 
-  static contextTypes = {
-    router: PropTypes.object,
-  };
-
   static propTypes = {
     status: ImmutablePropTypes.map,
     settings: ImmutablePropTypes.map.isRequired,
@@ -54,6 +48,7 @@ class DetailedStatus extends ImmutablePureComponent {
     }),
     onToggleMediaVisibility: PropTypes.func,
     intl: PropTypes.object.isRequired,
+    ...WithRouterPropTypes,
   };
 
   state = {
@@ -61,18 +56,18 @@ class DetailedStatus extends ImmutablePureComponent {
   };
 
   handleAccountClick = (e) => {
-    if (e.button === 0 && !(e.ctrlKey || e.altKey || e.metaKey) && this.context.router) {
+    if (e.button === 0 && !(e.ctrlKey || e.altKey || e.metaKey) && this.props.history) {
       e.preventDefault();
-      this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`);
+      this.props.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`);
     }
 
     e.stopPropagation();
   };
 
   parseClick = (e, destination) => {
-    if (e.button === 0 && !(e.ctrlKey || e.altKey || e.metaKey) && this.context.router) {
+    if (e.button === 0 && !(e.ctrlKey || e.altKey || e.metaKey) && this.props.history) {
       e.preventDefault();
-      this.context.router.history.push(destination);
+      this.props.history.push(destination);
     }
 
     e.stopPropagation();
@@ -253,7 +248,7 @@ class DetailedStatus extends ImmutablePureComponent {
 
     if (!['unlisted', 'public'].includes(status.get('visibility'))) {
       reblogLink = null;
-    } else if (this.context.router) {
+    } else if (this.props.history) {
       reblogLink = (
         <>
           {' ยท '}
@@ -279,7 +274,7 @@ class DetailedStatus extends ImmutablePureComponent {
       );
     }
 
-    if (this.context.router) {
+    if (this.props.history) {
       favouriteLink = (
         <Link to={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}/favourites`} className='detailed-status__link'>
           <Icon id='star' />
@@ -344,4 +339,4 @@ class DetailedStatus extends ImmutablePureComponent {
 
 }
 
-export default injectIntl(DetailedStatus);
+export default withRouter(injectIntl(DetailedStatus));
diff --git a/app/javascript/flavours/glitch/features/status/index.jsx b/app/javascript/flavours/glitch/features/status/index.jsx
index 25666aeea5..72a7e4009f 100644
--- a/app/javascript/flavours/glitch/features/status/index.jsx
+++ b/app/javascript/flavours/glitch/features/status/index.jsx
@@ -4,6 +4,7 @@ import { defineMessages, injectIntl } from 'react-intl';
 
 import classNames from 'classnames';
 import { Helmet } from 'react-helmet';
+import { withRouter } from 'react-router-dom';
 
 import Immutable from 'immutable';
 import ImmutablePropTypes from 'react-immutable-proptypes';
@@ -55,6 +56,7 @@ import Column from 'flavours/glitch/features/ui/components/column';
 import { boostModal, favouriteModal, deleteModal } from 'flavours/glitch/initial_state';
 import { makeGetStatus, makeGetPictureInPicture } from 'flavours/glitch/selectors';
 import { autoUnfoldCW } from 'flavours/glitch/utils/content_warning';
+import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
 
 import ColumnHeader from '../../components/column_header';
 import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../ui/util/fullscreen';
@@ -62,6 +64,7 @@ import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from
 import ActionBar from './components/action_bar';
 import DetailedStatus from './components/detailed_status';
 
+
 const messages = defineMessages({
   deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
   deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' },
@@ -182,7 +185,6 @@ const titleFromStatus = (intl, status) => {
 class Status extends ImmutablePureComponent {
 
   static contextTypes = {
-    router: PropTypes.object,
     identity: PropTypes.object,
   };
 
@@ -202,6 +204,7 @@ class Status extends ImmutablePureComponent {
       inUse: PropTypes.bool,
       available: PropTypes.bool,
     }),
+    ...WithRouterPropTypes
   };
 
   state = {
@@ -322,11 +325,11 @@ class Status extends ImmutablePureComponent {
             message: intl.formatMessage(messages.replyMessage),
             confirm: intl.formatMessage(messages.replyConfirm),
             onDoNotAsk: () => dispatch(changeLocalSetting(['confirm_before_clearing_draft'], false)),
-            onConfirm: () => dispatch(replyCompose(status, this.context.router.history)),
+            onConfirm: () => dispatch(replyCompose(status, this.props.history)),
           },
         }));
       } else {
-        dispatch(replyCompose(status, this.context.router.history));
+        dispatch(replyCompose(status, this.props.history));
       }
     } else {
       dispatch(openModal({
@@ -403,12 +406,12 @@ class Status extends ImmutablePureComponent {
     this.props.dispatch(editStatus(status.get('id'), history));
   };
 
-  handleDirectClick = (account, router) => {
-    this.props.dispatch(directCompose(account, router));
+  handleDirectClick = (account, history) => {
+    this.props.dispatch(directCompose(account, history));
   };
 
-  handleMentionClick = (account, router) => {
-    this.props.dispatch(mentionCompose(account, router));
+  handleMentionClick = (account, history) => {
+    this.props.dispatch(mentionCompose(account, history));
   };
 
   handleOpenMedia = (media, index, lang) => {
@@ -530,7 +533,7 @@ class Status extends ImmutablePureComponent {
   };
 
   handleHotkeyOpenProfile = () => {
-    this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`);
+    this.props.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`);
   };
 
   handleMoveUp = id => {
@@ -783,4 +786,4 @@ class Status extends ImmutablePureComponent {
 
 }
 
-export default injectIntl(connect(makeMapStateToProps)(Status));
+export default withRouter(injectIntl(connect(makeMapStateToProps)(Status)));
diff --git a/app/javascript/flavours/glitch/features/ui/components/audio_modal.jsx b/app/javascript/flavours/glitch/features/ui/components/audio_modal.jsx
index a6e15e5439..09897c1634 100644
--- a/app/javascript/flavours/glitch/features/ui/components/audio_modal.jsx
+++ b/app/javascript/flavours/glitch/features/ui/components/audio_modal.jsx
@@ -26,10 +26,6 @@ class AudioModal extends ImmutablePureComponent {
     onChangeBackgroundColor: PropTypes.func.isRequired,
   };
 
-  static contextTypes = {
-    router: PropTypes.object,
-  };
-
   render () {
     const { media, status, accountStaticAvatar, onClose } = this.props;
     const options = this.props.options || {};
diff --git a/app/javascript/flavours/glitch/features/ui/components/boost_modal.jsx b/app/javascript/flavours/glitch/features/ui/components/boost_modal.jsx
index b1c424ce34..d48553b4cf 100644
--- a/app/javascript/flavours/glitch/features/ui/components/boost_modal.jsx
+++ b/app/javascript/flavours/glitch/features/ui/components/boost_modal.jsx
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 
 import classNames from 'classnames';
+import { withRouter } from 'react-router-dom';
 
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import ImmutablePureComponent from 'react-immutable-pure-component';
@@ -18,6 +19,7 @@ import { RelativeTimestamp } from 'flavours/glitch/components/relative_timestamp
 import StatusContent from 'flavours/glitch/components/status_content';
 import VisibilityIcon from 'flavours/glitch/components/status_visibility_icon';
 import PrivacyDropdown from 'flavours/glitch/features/compose/components/privacy_dropdown';
+import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
 
 const messages = defineMessages({
   cancel_reblog: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
@@ -39,17 +41,13 @@ const mapDispatchToProps = dispatch => {
 };
 
 class BoostModal extends ImmutablePureComponent {
-
-  static contextTypes = {
-    router: PropTypes.object,
-  };
-
   static propTypes = {
     status: ImmutablePropTypes.map.isRequired,
     onReblog: PropTypes.func.isRequired,
     onClose: PropTypes.func.isRequired,
     missingMediaDescription: PropTypes.bool,
     intl: PropTypes.object.isRequired,
+    ...WithRouterPropTypes,
   };
 
   componentDidMount() {
@@ -65,7 +63,7 @@ class BoostModal extends ImmutablePureComponent {
     if (e.button === 0) {
       e.preventDefault();
       this.props.onClose();
-      this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`);
+      this.props.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`);
     }
   };
 
@@ -137,4 +135,4 @@ class BoostModal extends ImmutablePureComponent {
 
 }
 
-export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(BoostModal));
+export default withRouter(connect(mapStateToProps, mapDispatchToProps)(injectIntl(BoostModal)));
diff --git a/app/javascript/flavours/glitch/features/ui/components/columns_area.jsx b/app/javascript/flavours/glitch/features/ui/components/columns_area.jsx
index 6eb5ee0e52..98303ef2d5 100644
--- a/app/javascript/flavours/glitch/features/ui/components/columns_area.jsx
+++ b/app/javascript/flavours/glitch/features/ui/components/columns_area.jsx
@@ -45,11 +45,6 @@ const componentMap = {
 };
 
 export default class ColumnsArea extends ImmutablePureComponent {
-
-  static contextTypes = {
-    router: PropTypes.object.isRequired,
-  };
-
   static propTypes = {
     columns: ImmutablePropTypes.list.isRequired,
     singleColumn: PropTypes.bool,
diff --git a/app/javascript/flavours/glitch/features/ui/components/favourite_modal.jsx b/app/javascript/flavours/glitch/features/ui/components/favourite_modal.jsx
index fa955b0483..0c4683ecf1 100644
--- a/app/javascript/flavours/glitch/features/ui/components/favourite_modal.jsx
+++ b/app/javascript/flavours/glitch/features/ui/components/favourite_modal.jsx
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 
 import classNames from 'classnames';
+import { withRouter } from 'react-router-dom';
 
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import ImmutablePureComponent from 'react-immutable-pure-component';
@@ -15,6 +16,7 @@ import { Icon } from 'flavours/glitch/components/icon';
 import { RelativeTimestamp } from 'flavours/glitch/components/relative_timestamp';
 import StatusContent from 'flavours/glitch/components/status_content';
 import VisibilityIcon from 'flavours/glitch/components/status_visibility_icon';
+import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
 
 const messages = defineMessages({
   favourite: { id: 'status.favourite', defaultMessage: 'Favourite' },
@@ -22,15 +24,12 @@ const messages = defineMessages({
 
 class FavouriteModal extends ImmutablePureComponent {
 
-  static contextTypes = {
-    router: PropTypes.object,
-  };
-
   static propTypes = {
     status: ImmutablePropTypes.map.isRequired,
     onFavourite: PropTypes.func.isRequired,
     onClose: PropTypes.func.isRequired,
     intl: PropTypes.object.isRequired,
+    ...WithRouterPropTypes,
   };
 
   componentDidMount() {
@@ -46,7 +45,7 @@ class FavouriteModal extends ImmutablePureComponent {
     if (e.button === 0) {
       e.preventDefault();
       this.props.onClose();
-      this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`);
+      this.props.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`);
     }
   };
 
@@ -100,4 +99,4 @@ class FavouriteModal extends ImmutablePureComponent {
 
 }
 
-export default injectIntl(FavouriteModal);
+export default withRouter(injectIntl(FavouriteModal));
diff --git a/app/javascript/flavours/glitch/features/ui/components/list_panel.jsx b/app/javascript/flavours/glitch/features/ui/components/list_panel.jsx
index e6830e544a..9210bb4f39 100644
--- a/app/javascript/flavours/glitch/features/ui/components/list_panel.jsx
+++ b/app/javascript/flavours/glitch/features/ui/components/list_panel.jsx
@@ -1,7 +1,5 @@
 import PropTypes from 'prop-types';
 
-import { withRouter } from 'react-router-dom';
-
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 import { connect } from 'react-redux';
@@ -55,4 +53,4 @@ class ListPanel extends ImmutablePureComponent {
 
 }
 
-export default withRouter(connect(mapStateToProps)(ListPanel));
+export default connect(mapStateToProps)(ListPanel);
diff --git a/app/javascript/flavours/glitch/features/ui/components/media_modal.jsx b/app/javascript/flavours/glitch/features/ui/components/media_modal.jsx
index 344f97fc00..29821c388f 100644
--- a/app/javascript/flavours/glitch/features/ui/components/media_modal.jsx
+++ b/app/javascript/flavours/glitch/features/ui/components/media_modal.jsx
@@ -27,10 +27,6 @@ const messages = defineMessages({
 
 class MediaModal extends ImmutablePureComponent {
 
-  static contextTypes = {
-    router: PropTypes.object,
-  };
-
   static propTypes = {
     media: ImmutablePropTypes.list.isRequired,
     statusId: PropTypes.string,
diff --git a/app/javascript/flavours/glitch/features/ui/components/navigation_panel.jsx b/app/javascript/flavours/glitch/features/ui/components/navigation_panel.jsx
index 601732b8bb..f42fdf8f5e 100644
--- a/app/javascript/flavours/glitch/features/ui/components/navigation_panel.jsx
+++ b/app/javascript/flavours/glitch/features/ui/components/navigation_panel.jsx
@@ -3,7 +3,7 @@ import { Component } from 'react';
 
 import { defineMessages, injectIntl } from 'react-intl';
 
-import NavigationPortal from 'flavours/glitch/components/navigation_portal';
+import { NavigationPortal } from 'flavours/glitch/components/navigation_portal';
 import { timelinePreview, trendsEnabled } from 'flavours/glitch/initial_state';
 import { transientSingleColumn } from 'flavours/glitch/is_mobile';
 import { preferencesLink } from 'flavours/glitch/utils/backend_links';
@@ -36,7 +36,6 @@ const messages = defineMessages({
 class NavigationPanel extends Component {
 
   static contextTypes = {
-    router: PropTypes.object.isRequired,
     identity: PropTypes.object.isRequired,
   };
 
diff --git a/app/javascript/flavours/glitch/features/ui/index.jsx b/app/javascript/flavours/glitch/features/ui/index.jsx
index 44f394657a..35b2a6db4e 100644
--- a/app/javascript/flavours/glitch/features/ui/index.jsx
+++ b/app/javascript/flavours/glitch/features/ui/index.jsx
@@ -22,6 +22,7 @@ import { expandHomeTimeline } from 'flavours/glitch/actions/timelines';
 import PermaLink from 'flavours/glitch/components/permalink';
 import PictureInPicture from 'flavours/glitch/features/picture_in_picture';
 import { layoutFromWindow } from 'flavours/glitch/is_mobile';
+import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
 
 import initialState, { me, owner, singleUserMode, trendsEnabled, trendsAsLanding } from '../../initial_state';
 
@@ -269,8 +270,6 @@ class UI extends Component {
     hasMediaAttachments: PropTypes.bool,
     canUploadMore: PropTypes.bool,
     match: PropTypes.object.isRequired,
-    location: PropTypes.object.isRequired,
-    history: PropTypes.object.isRequired,
     intl: PropTypes.object.isRequired,
     dropdownMenuIsOpen: PropTypes.bool,
     unreadNotifications: PropTypes.number,
@@ -280,6 +279,7 @@ class UI extends Component {
     layout: PropTypes.string.isRequired,
     firstLaunch: PropTypes.bool,
     username: PropTypes.string,
+    ...WithRouterPropTypes,
   };
 
   state = {
@@ -416,7 +416,7 @@ class UI extends Component {
 
     // On first launch, redirect to the follow recommendations page
     if (signedIn && this.props.firstLaunch) {
-      this.context.router.history.replace('/start');
+      this.props.history.replace('/start');
       // TODO: this.props.dispatch(closeOnboarding());
     }
 
diff --git a/app/javascript/flavours/glitch/features/ui/util/react_router_helpers.jsx b/app/javascript/flavours/glitch/features/ui/util/react_router_helpers.jsx
index d0d034cbd4..0847aa4435 100644
--- a/app/javascript/flavours/glitch/features/ui/util/react_router_helpers.jsx
+++ b/app/javascript/flavours/glitch/features/ui/util/react_router_helpers.jsx
@@ -1,7 +1,7 @@
 import PropTypes from 'prop-types';
-import { Component, PureComponent, cloneElement, Children } from 'react';
+import { Component, cloneElement, Children } from 'react';
 
-import { Switch, Route } from 'react-router-dom';
+import { Switch, Route, useLocation } from 'react-router-dom';
 
 import StackTrace from 'stacktrace-js';
 
@@ -10,27 +10,20 @@ import ColumnLoading from 'flavours/glitch/features/ui/components/column_loading
 import BundleContainer from 'flavours/glitch/features/ui/containers/bundle_container';
 
 // Small wrapper to pass multiColumn to the route components
-export class WrappedSwitch extends PureComponent {
-  static contextTypes = {
-    router: PropTypes.object,
-  };
+export const WrappedSwitch = ({ multiColumn, children }) => {
+  const  location = useLocation();
 
-  render () {
-    const { multiColumn, children } = this.props;
-    const { location } = this.context.router.route;
+  const decklessLocation = multiColumn && location.pathname.startsWith('/deck')
+    ? {...location, pathname: location.pathname.slice(5)}
+    : location;
 
-    const decklessLocation = multiColumn && location.pathname.startsWith('/deck')
-      ? {...location, pathname: location.pathname.slice(5)}
-      : location;
+  return (
+    <Switch location={decklessLocation}>
+      {Children.map(children, child => child ? cloneElement(child, { multiColumn }) : null)}
+    </Switch>
+  );
+};
 
-    return (
-      <Switch location={decklessLocation}>
-        {Children.map(children, child => child ? cloneElement(child, { multiColumn }) : null)}
-      </Switch>
-    );
-  }
-
-}
 
 WrappedSwitch.propTypes = {
   multiColumn: PropTypes.bool,
diff --git a/app/javascript/flavours/glitch/utils/dom_helpers.js b/app/javascript/flavours/glitch/utils/dom_helpers.js
deleted file mode 100644
index bc5e200fcc..0000000000
--- a/app/javascript/flavours/glitch/utils/dom_helpers.js
+++ /dev/null
@@ -1,7 +0,0 @@
-//  Focuses the root element.
-export function focusRoot () {
-  let e;
-  if (document && (e = document.querySelector('.ui')) && (e = e.parentElement)) {
-    e.focus();
-  }
-}
diff --git a/app/javascript/flavours/glitch/utils/react_router.jsx b/app/javascript/flavours/glitch/utils/react_router.jsx
new file mode 100644
index 0000000000..a56883270b
--- /dev/null
+++ b/app/javascript/flavours/glitch/utils/react_router.jsx
@@ -0,0 +1,60 @@
+import PropTypes from "prop-types";
+
+import { __RouterContext } from "react-router";
+
+import hoistStatics from "hoist-non-react-statics";
+
+export const WithRouterPropTypes = {
+  match: PropTypes.object.isRequired,
+  location: PropTypes.object.isRequired,
+  history: PropTypes.object.isRequired,
+};
+
+export const WithOptionalRouterPropTypes = {
+  match: PropTypes.object,
+  location: PropTypes.object,
+  history: PropTypes.object,
+};
+
+// This is copied from https://github.com/remix-run/react-router/blob/v5.3.4/packages/react-router/modules/withRouter.js
+// but does not fail if called outside of a React Router context
+export function withOptionalRouter(Component) {
+  const displayName = `withRouter(${Component.displayName || Component.name})`;
+  const C = props => {
+    const { wrappedComponentRef, ...remainingProps } = props;
+
+    return (
+      <__RouterContext.Consumer>
+        {context => {
+          if(context)
+            return (
+              <Component
+                {...remainingProps}
+                {...context}
+                ref={wrappedComponentRef}
+              />
+            );
+          else
+            return (
+              <Component
+                {...remainingProps}
+                ref={wrappedComponentRef}
+              />
+            );
+        }}
+      </__RouterContext.Consumer>
+    );
+  };
+
+  C.displayName = displayName;
+  C.WrappedComponent = Component;
+  C.propTypes = {
+    wrappedComponentRef: PropTypes.oneOfType([
+      PropTypes.string,
+      PropTypes.func,
+      PropTypes.object
+    ])
+  };
+
+  return hoistStatics(C, Component);
+}