diff --git a/src/adapters/scene_direction.js b/src/adapters/scene_direction.js
index c39b8f463..3863ef2df 100644
--- a/src/adapters/scene_direction.js
+++ b/src/adapters/scene_direction.js
@@ -240,9 +240,9 @@ export default class SceneDirection {
     });
   }
 
-  refreshDirection(type, lngLat) {
+  refreshDirection(which, lngLat) {
     const newPoint = new LatLonPoi(lngLat);
-    fire('change_direction_point', type, '', newPoint);
+    fire('change_direction_point', which, newPoint);
   }
 
   reset() {
diff --git a/src/panel/PanelManager.jsx b/src/panel/PanelManager.jsx
index 0a96ebe53..28da6b006 100644
--- a/src/panel/PanelManager.jsx
+++ b/src/panel/PanelManager.jsx
@@ -4,7 +4,7 @@ import FavoritesPanel from './favorites/FavoritesPanel';
 import PoiPanel from './poi/PoiPanel';
 import ServicePanel from './service/ServicePanel';
 import CategoryPanel from 'src/panel/category/CategoryPanel';
-import DirectionPanel from 'src/panel/direction/DirectionPanel';
+import Directions from 'src/panel/direction/Directions';
 import Telemetry from 'src/libs/telemetry';
 import { parseQueryString, buildQueryString } from 'src/libs/url_utils';
 import { fire, listen, unListen } from 'src/libs/customEvents';
@@ -154,17 +154,13 @@ const PanelManager = ({ router }) => {
     });
 
     if (directionConf.enabled) {
-      const isPublicTransportActive =
-        (directionConf.publicTransport && directionConf.publicTransport.enabled) ||
-        parseQueryString(document.location.search)['pt'] === 'true';
-
       router.addRoute('Routes', '/routes(?:/?)(.*)', (routeParams, options) => {
         const params = parseQueryString(routeParams);
-        params.details = params.details === 'true';
+        params.activeDetails = params.details === 'true';
         params.activeRouteId = Number(params.selected) || 0;
         setPanelOptions({
-          ActivePanel: DirectionPanel,
-          options: { ...params, ...options, isPublicTransportActive },
+          ActivePanel: Directions,
+          options: { ...params, ...options },
           panelSize: 'default',
         });
       });
diff --git a/src/panel/direction/DesktopDirectionPanel.jsx b/src/panel/direction/DesktopDirectionPanel.jsx
new file mode 100644
index 000000000..e918fc568
--- /dev/null
+++ b/src/panel/direction/DesktopDirectionPanel.jsx
@@ -0,0 +1,35 @@
+import React, { useContext } from 'react';
+import { Panel, Divider, ShareMenu } from 'src/components/ui';
+import { Button, IconShare } from '@qwant/qwant-ponents';
+import { DirectionContext } from './directionStore';
+import { useI18n } from 'src/hooks';
+
+const DesktopDirectionPanel = ({ form, result, onClose, onShareClick }) => {
+  const { routes } = useContext(DirectionContext).state;
+  const { _ } = useI18n();
+
+  return (
+    <Panel className="direction-panel" onClose={onClose} renderHeader={form}>
+      <div className="direction-autocomplete_suggestions" />
+      {routes.length > 0 && (
+        <ShareMenu url={window.location.toString()}>
+          {openMenu => (
+            <Button
+              className="direction-panel-share-button u-ml-auto u-flex-shrink-0 u-mr-m"
+              variant="tertiary"
+              title={_('Share itinerary', 'direction')}
+              onClick={e => onShareClick(e, openMenu)}
+            >
+              <IconShare />
+              {_('Share itinerary', 'direction')}
+            </Button>
+          )}
+        </ShareMenu>
+      )}
+      <Divider paddingTop={8} paddingBottom={0} />
+      {result}
+    </Panel>
+  );
+};
+
+export default DesktopDirectionPanel;
diff --git a/src/panel/direction/DirectionForm.jsx b/src/panel/direction/DirectionForm.jsx
index cf170b49e..6497af03a 100644
--- a/src/panel/direction/DirectionForm.jsx
+++ b/src/panel/direction/DirectionForm.jsx
@@ -1,29 +1,27 @@
-import React, { useEffect, useRef } from 'react';
+import React, { useEffect, useRef, useState, useContext, useCallback } from 'react';
 import PropTypes from 'prop-types';
 import DirectionInput from './DirectionInput';
 import VehicleSelector from './VehicleSelector';
 import { Divider } from 'src/components/ui';
 import { IconArrowUpDown } from 'src/components/ui/icons';
 import { Button } from '@qwant/qwant-ponents';
+import * as address from 'src/libs/address';
+import { getInputValue } from 'src/libs/suggest';
+import { isNullOrEmpty } from 'src/libs/object';
 import { useI18n, useDevice } from 'src/hooks';
+import { DirectionContext } from './directionStore';
 
-const DirectionForm = ({
-  isLoading,
-  origin,
-  destination,
-  onChangeDirectionPoint,
-  onReversePoints,
-  vehicles,
-  onSelectVehicle,
-  activeVehicle,
-  isInitializing,
-  originInputText,
-  destinationInputText,
-}) => {
+const DirectionForm = ({ onReversePoints, onSelectVehicle, isInitializing }) => {
   const { _ } = useI18n();
   const { isMobile } = useDevice();
   const originRef = useRef(null);
   const destinationRef = useRef(null);
+  const {
+    state: { origin, destination, vehicles, vehicle, isLoading },
+    setPoint,
+  } = useContext(DirectionContext);
+  const [originInputText, setOriginInputText] = useState('');
+  const [destinationInputText, setDestinationInputText] = useState('');
 
   useEffect(() => {
     if (isMobile || isInitializing) {
@@ -45,6 +43,44 @@ const DirectionForm = ({
     }, 0);
   };
 
+  const onChangePoint = which => (value, point) => {
+    if (which === 'origin') {
+      setOriginInputText(value);
+    } else {
+      setDestinationInputText(value);
+    }
+    if (point || value === '') {
+      setPoint(which, point);
+    }
+  };
+
+  const setText = useCallback((which, point) => {
+    const setter = which === 'origin' ? setOriginInputText : setDestinationInputText;
+
+    async function fetchAddress(poi) {
+      poi.address = await address.fetch(poi);
+      setter(getInputValue(poi));
+    }
+
+    if (point) {
+      if (point.type === 'geoloc') {
+        setter(point.name);
+      } else if (isNullOrEmpty(point.address)) {
+        fetchAddress(point);
+      } else {
+        setter(getInputValue(point));
+      }
+    }
+  }, []);
+
+  useEffect(() => {
+    setText('origin', origin);
+  }, [origin, setText]);
+
+  useEffect(() => {
+    setText('destination', destination);
+  }, [destination, setText]);
+
   return (
     <div className="direction-form">
       <form className="direction-fields" noValidate>
@@ -55,7 +91,7 @@ const DirectionForm = ({
             point={origin}
             otherPoint={destination}
             pointType="origin"
-            onChangePoint={(input, point) => onChangeDirectionPoint('origin', input, point)}
+            onChangePoint={onChangePoint('origin')}
             ref={originRef}
             withGeoloc={destination ? destination.type !== 'geoloc' : true}
           />
@@ -66,7 +102,7 @@ const DirectionForm = ({
             point={destination}
             otherPoint={origin}
             pointType="destination"
-            onChangePoint={(input, point) => onChangeDirectionPoint('destination', input, point)}
+            onChangePoint={onChangePoint('destination')}
             ref={destinationRef}
             withGeoloc={origin ? origin.type !== 'geoloc' : true}
           />
@@ -85,7 +121,7 @@ const DirectionForm = ({
       </form>
       <VehicleSelector
         vehicles={vehicles}
-        activeVehicle={activeVehicle}
+        activeVehicle={vehicle}
         onSelectVehicle={onSelectVehicle}
       />
     </div>
@@ -93,17 +129,9 @@ const DirectionForm = ({
 };
 
 DirectionForm.propTypes = {
-  isLoading: PropTypes.bool,
-  origin: PropTypes.object,
-  destination: PropTypes.object,
-  onChangeDirectionPoint: PropTypes.func.isRequired,
   onReversePoints: PropTypes.func.isRequired,
-  vehicles: PropTypes.array.isRequired,
   onSelectVehicle: PropTypes.func.isRequired,
-  activeVehicle: PropTypes.string.isRequired,
   isInitializing: PropTypes.bool,
-  originInputText: PropTypes.string,
-  destinationInputText: PropTypes.string,
 };
 
 export default DirectionForm;
diff --git a/src/panel/direction/DirectionMap.jsx b/src/panel/direction/DirectionMap.jsx
new file mode 100644
index 000000000..854a7c603
--- /dev/null
+++ b/src/panel/direction/DirectionMap.jsx
@@ -0,0 +1,65 @@
+import { useContext, useEffect } from 'react';
+import { fire, listen, unListen } from 'src/libs/customEvents';
+import { DirectionContext } from './directionStore';
+
+const DirectionMap = () => {
+  const { state, setPoint } = useContext(DirectionContext);
+  const { origin, destination, vehicle, routes, activeRouteId } = state;
+
+  useEffect(() => {
+    if (origin) {
+      window.execOnMapLoaded(() => {
+        fire('set_origin', origin);
+        if (!destination) {
+          fire('fit_map', origin);
+        }
+      });
+    }
+
+    if (destination) {
+      window.execOnMapLoaded(() => {
+        fire('set_destination', destination);
+        if (!origin) {
+          fire('fit_map', destination);
+        }
+      });
+    }
+  }, [origin, destination]);
+
+  useEffect(() => {
+    const dragPointHandler = listen('change_direction_point', setPoint);
+    const setPointHandler = listen('set_direction_point', point => {
+      if (origin && destination) {
+        return;
+      }
+      // if one point is already filled, fill the other
+      setPoint({ type: origin ? 'destination' : 'origin', data: point });
+    });
+
+    return () => {
+      unListen(dragPointHandler);
+      unListen(setPointHandler);
+      fire('clean_routes');
+      fire('update_map_paddings');
+    };
+  }, [origin, destination, setPoint]);
+
+  useEffect(() => {
+    window.execOnMapLoaded(() => {
+      fire('set_routes', {
+        routes,
+        vehicle,
+        activeRouteId,
+      });
+    });
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [routes /* Omit activeRouteId and vehicle to prevent costly redraws */]);
+
+  useEffect(() => {
+    fire('set_main_route', { routeId: activeRouteId, fitView: true });
+  }, [activeRouteId]);
+
+  return null;
+};
+
+export default DirectionMap;
diff --git a/src/panel/direction/DirectionPanel.jsx b/src/panel/direction/DirectionPanel.jsx
index f09af0e1d..c242c7ef2 100644
--- a/src/panel/direction/DirectionPanel.jsx
+++ b/src/panel/direction/DirectionPanel.jsx
@@ -1,85 +1,64 @@
-/* globals _ */
-import React from 'react';
+import React, { useCallback, useContext, useEffect, useState } from 'react';
 import PropTypes from 'prop-types';
-import { Panel, Divider, ShareMenu } from 'src/components/ui';
-import { Button, IconShare } from '@qwant/qwant-ponents';
 import MobileDirectionPanel from './MobileDirectionPanel';
+import DesktopDirectionPanel from './DesktopDirectionPanel';
 import DirectionForm from './DirectionForm';
 import RouteResult from './RouteResult';
-import DirectionApi, { modes } from 'src/adapters/direction_api';
+import DirectionMap from './DirectionMap';
 import Telemetry from 'src/libs/telemetry';
 import { toUrl as poiToUrl, fromUrl as poiFromUrl } from 'src/libs/pois';
 import Error from 'src/adapters/error';
 import Poi from 'src/adapters/poi/poi.js';
-import { fire, listen, unListen } from 'src/libs/customEvents';
-import * as address from 'src/libs/address';
 import NavigatorGeolocalisationPoi from 'src/adapters/poi/specials/navigator_geolocalisation_poi';
-import { getInputValue } from 'src/libs/suggest';
 import { geolocationPermissions, getGeolocationPermission } from 'src/libs/geolocation';
 import { openPendingDirectionModal } from 'src/modals/GeolocationModal';
 import { updateQueryString } from 'src/libs/url_utils';
-import { isNullOrEmpty } from 'src/libs/object';
-import { usePageTitle, useDevice } from 'src/hooks';
+import { useDevice } from 'src/hooks';
+import { DirectionContext } from './directionStore';
 
-class DirectionPanel extends React.Component {
-  static propTypes = {
-    origin: PropTypes.string,
-    destination: PropTypes.string,
-    poi: PropTypes.object,
-    mode: PropTypes.string,
-    isPublicTransportActive: PropTypes.bool,
-    activeRouteId: PropTypes.number,
-    details: PropTypes.bool,
-    isMobile: PropTypes.bool,
-  };
-
-  static defaultProps = {
-    activeRouteId: 0,
-  };
-
-  constructor(props) {
-    super(props);
-
-    this.vehicles = [modes.DRIVING, modes.WALKING, modes.CYCLING];
-    if (this.props.isPublicTransportActive) {
-      this.vehicles.splice(1, 0, modes.PUBLIC_TRANSPORT);
-    }
-
-    const activeVehicle = this.vehicles.indexOf(props.mode) !== -1 ? props.mode : modes.DRIVING;
+const DirectionPanel = ({ poi, urlValues }) => {
+  const { state, dispatch } = useContext(DirectionContext);
+  const { origin, destination, vehicle, activeRouteId, activeDetails } = state;
+  const { isMobile } = useDevice();
+  const [isInitializing, setIsInitializing] = useState(true);
 
-    this.lastQueryId = 0;
+  useEffect(() => {
+    Telemetry.add(Telemetry.ITINERARY_OPEN);
+    initialize();
+    autoGeoloc();
 
-    this.state = {
-      vehicle: activeVehicle,
-      origin: null,
-      destination: (props.poi && Poi.deserialize(props.poi)) || null,
-      isLoading: false,
-      isDirty: true, // useful to track intermediary states, when API update call is not made yet
-      error: 0,
-      routes: [],
-      isInitializing: true,
-      originInputText: '',
-      destinationInputText: '',
+    return () => {
+      dispatch({ type: 'reset' });
     };
+  }, [dispatch, initialize, autoGeoloc]);
 
-    this.restorePoints(props);
-  }
-
-  async componentDidMount() {
-    Telemetry.add(Telemetry.ITINERARY_OPEN);
-    document.body.classList.add('directions-open');
-    this.dragPointHandler = listen('change_direction_point', this.changeDirectionPoint);
-    this.setPointHandler = listen('set_direction_point', this.setDirectionPoint);
+  // url side effect
+  useEffect(() => {
+    if (isInitializing) {
+      return;
+    }
+    const search = updateQueryString({
+      mode: vehicle,
+      origin: origin ? poiToUrl(origin) : null,
+      destination: destination ? poiToUrl(destination) : null,
+      selected: activeRouteId,
+      details: activeDetails,
+    });
+    const relativeUrl = 'routes/' + search;
+    // @TODO: not always replace=true
+    window.app.navigateTo(relativeUrl, window.history.state, { replace: true });
+  }, [isInitializing, origin, destination, vehicle, activeRouteId, activeDetails]);
 
+  const autoGeoloc = useCallback(async () => {
     // on mobile, when no origin is specified, try auto-geoloc
-    if (this.props.isMobile && !this.state.origin && !this.props.origin) {
+    if (isMobile && !urlValues.origin) {
       const geolocationPermission = await getGeolocationPermission();
       let modalAccepted = false;
 
       // on an empty form, if the user's position permission hasn't been asked yet, show modal
       if (
-        !this.state.destination &&
-        !this.props.destination &&
+        !urlValues.destination &&
+        !poi &&
         geolocationPermission === geolocationPermissions.PROMPT
       ) {
         modalAccepted = await openPendingDirectionModal();
@@ -87,335 +66,154 @@ class DirectionPanel extends React.Component {
 
       // If the user's position can be requested, put it in the origin field
       if (geolocationPermission === geolocationPermissions.GRANTED || modalAccepted) {
-        const origin = new NavigatorGeolocalisationPoi();
+        const position = new NavigatorGeolocalisationPoi();
         try {
-          await origin.geolocate({
+          await position.geolocate({
             displayErrorModal: false,
           });
-          this.setState({ origin, originInputText: origin.name }, this.update);
+          dispatch({ type: 'setOrigin', data: position });
         } catch (e) {
           // ignore possible error
         }
       }
     }
-  }
-
-  componentDidUpdate(prevProps, prevState) {
-    if (this.props.activeRouteId !== prevProps.activeRouteId && this.state.routes.length > 0) {
-      fire('set_main_route', { routeId: this.props.activeRouteId, fitView: true });
-      this.updateUrl({ params: { details: null }, replace: true });
-    }
+  }, [urlValues.origin, urlValues.destination, poi, isMobile, dispatch]);
 
-    if (this.state.routes.length !== 0 && prevState.routes.length === 0) {
-      fire('update_map_paddings');
-    }
-  }
-
-  componentWillUnmount() {
-    fire('clean_routes');
-    unListen(this.dragPointHandler);
-    unListen(this.setPointHandler);
-    document.body.classList.remove('directions-open');
-    fire('update_map_paddings');
-  }
-
-  async setTextInput(which, poi) {
-    if (isNullOrEmpty(poi.address)) {
-      // fetch missing address
-      poi.address = await address.fetch(poi);
+  const initialize = useCallback(() => {
+    if (!isInitializing) {
+      return;
     }
-    this.setState({ [which + 'InputText']: getInputValue(poi) });
-  }
 
-  async restorePoints({ origin: originUrlValue, destination: destinationUrlValue }) {
-    const poiRestorePromises = [
-      originUrlValue ? poiFromUrl(originUrlValue) : this.state.origin,
-      destinationUrlValue ? poiFromUrl(destinationUrlValue) : this.state.destination,
-    ];
-
-    try {
-      const [origin, destination] = await Promise.all(poiRestorePromises);
-      // Set markers
-      if (origin) {
-        window.execOnMapLoaded(() => {
-          fire('set_origin', origin);
-          if (!destination) {
-            fire('fit_map', origin);
-          }
-        });
-        this.setTextInput('origin', origin);
-      }
-
-      if (destination) {
-        window.execOnMapLoaded(() => {
-          fire('set_destination', destination);
-          if (!origin) {
-            fire('fit_map', destination);
-          }
+    // deserialize origin/destination points from url or direct passing (coming from a POI)
+    async function restorePoints() {
+      try {
+        const poiRestorePromises = [
+          urlValues.origin ? poiFromUrl(urlValues.origin) : null,
+          urlValues.destination
+            ? poiFromUrl(urlValues.destination)
+            : poi
+            ? Poi.deserialize(poi)
+            : null,
+        ];
+        const [initialOrigin, initialDestination] = await Promise.all(poiRestorePromises);
+        dispatch({
+          type: 'setParams',
+          data: {
+            origin: initialOrigin,
+            destination: initialDestination,
+            activeRouteId: urlValues.activeRouteId || 0,
+            activeDetails: urlValues.activeDetails,
+          },
         });
-        this.setTextInput('destination', destination);
-      }
-
-      this.setState(
-        {
-          origin,
-          destination,
-          isInitializing: false,
-        },
-        this.update
-      );
-    } catch (e) {
-      Error.sendOnce(
-        'direction_panel',
-        'restoreUrl',
-        `Error restoring Poi from Url ${originUrlValue} / ${destinationUrlValue}`,
-        e
-      );
-    }
-  }
-
-  computeRoutes = async () => {
-    const { origin, destination, vehicle } = this.state;
-    if (origin && destination) {
-      this.setState({
-        isDirty: false,
-        isLoading: true,
-        error: 0,
-        routes: [],
-      });
-      const currentQueryId = ++this.lastQueryId;
-      fire('set_origin', origin);
-      fire('set_destination', destination);
-      const directionResponse = await DirectionApi.search(origin, destination, vehicle);
-      // A more recent query was done in the meantime, ignore this result silently
-      if (currentQueryId !== this.lastQueryId) {
-        return;
+      } catch (e) {
+        Error.sendOnce(
+          'direction_panel',
+          'restoreUrl',
+          `Error restoring Poi from Url ${urlValues.origin} / ${urlValues.destination}`,
+          e
+        );
       }
-      if (directionResponse && directionResponse.error === 0) {
-        // Valid, non-empty response
-        const routes = directionResponse.data.routes
-          .sort((routeA, routeB) => routeA.duration - routeB.duration)
-          .map((route, i) => ({ ...route, id: i }));
 
-        this.setState({ isLoading: false, error: 0, routes }, () => {
-          const activeRouteId =
-            this.props.activeRouteId < this.state.routes.length ? this.props.activeRouteId : 0;
-          window.execOnMapLoaded(() => {
-            fire('set_routes', { routes, vehicle, activeRouteId });
-          });
-          this.updateUrl({ params: { selected: activeRouteId }, replace: true });
-        });
-      } else {
-        // Error or empty response
-        this.setState({ isLoading: false, error: directionResponse.error });
-        fire('clean_routes');
-      }
-    } else {
-      // When both fields are not filled yet or not filled anymore
-      this.setState({ isLoading: false, isDirty: false, error: 0, routes: [] });
-      fire('clean_routes');
-      if (origin) {
-        fire('set_origin', origin);
-      } else if (destination) {
-        fire('set_destination', destination);
-      }
+      setIsInitializing(false);
     }
-  };
 
-  updateUrl({ params = {}, replace = false } = {}) {
-    const search = updateQueryString({
-      mode: this.state.vehicle,
-      origin: this.state.origin ? poiToUrl(this.state.origin) : null,
-      destination: this.state.destination ? poiToUrl(this.state.destination) : null,
-      pt: this.props.isPublicTransportActive ? 'true' : null,
-      ...params,
-    });
-    const relativeUrl = 'routes/' + search;
-
-    window.app.navigateTo(relativeUrl, window.history.state, { replace });
-  }
-
-  update() {
-    this.updateUrl({ replace: true });
-    this.computeRoutes();
-  }
-
-  onSelectVehicle = vehicle => {
+    restorePoints();
+  }, [
+    urlValues.origin,
+    urlValues.destination,
+    urlValues.activeRouteId,
+    urlValues.activeDetails,
+    poi,
+    isInitializing,
+    dispatch,
+  ]);
+
+  const onSelectVehicle = vehicle => {
     Telemetry.add(Telemetry[`${('itinerary_mode_' + vehicle).toUpperCase()}`]);
-    this.setState({ vehicle, isDirty: true }, this.update);
+    dispatch({ type: 'setVehicle', data: vehicle });
   };
 
-  onClose = () => {
+  const onClose = () => {
     Telemetry.add(Telemetry.ITINERARY_CLOSE);
-    this.props.poi
+    poi
       ? window.history.back() // Go back to the poi panel
       : window.app.navigateTo('/');
   };
 
-  reversePoints = () => {
+  const reversePoints = () => {
     Telemetry.add(Telemetry.ITINERARY_INVERT);
-    this.setState(
-      previousState => ({
-        origin: previousState.destination,
-        destination: previousState.origin,
-        originInputText: previousState.destinationInputText,
-        destinationInputText: previousState.originInputText,
-        isDirty: true,
-      }),
-      this.update
-    );
+    dispatch({ type: 'reversePoints' });
   };
 
-  changeDirectionPoint = (which, value, point) => {
-    this.setState(
-      {
-        [which]: point,
-        isDirty: true,
-        [which + 'InputText']: value || '',
-      },
-      () => {
-        this.update();
-        // Retrieve addresses
-        if (point && point.type === 'latlon') {
-          this.setTextInput(which, this.state[which]);
-        }
-      }
-    );
-  };
-
-  setDirectionPoint = poi => {
-    if (this.state.origin && this.state.destination) {
-      return;
-    }
-    const which = this.state.origin ? 'destination' : 'origin';
-    this.setTextInput(which, poi);
-
-    // Update state
-    // (Call update() that will perform a search and redraw the UI if both fields are set)
-    this.setState(
-      {
-        [which]: poi,
-        isDirty: true,
-      },
-      this.update
-    );
-  };
-
-  handleShareClick = (e, handler) => {
+  const onShareClick = (e, handler) => {
     Telemetry.add(Telemetry.ITINERARY_SHARE);
     return handler(e);
   };
 
-  selectRoute = routeId => {
-    this.updateUrl({ params: { selected: routeId }, replace: true });
+  const selectRoute = routeId => {
+    dispatch({ type: 'setActiveRoute', data: routeId });
   };
 
-  toggleDetails = () => {
-    if (this.props.isMobile) {
-      if (this.props.details) {
+  const toggleDetails = () => {
+    if (isMobile) {
+      if (activeDetails) {
         window.app.navigateBack({
           relativeUrl: 'routes/' + updateQueryString({ details: false }),
         });
       } else {
-        this.updateUrl({ params: { details: true }, replace: false });
+        window.app.navigateTo(
+          'routes/' + updateQueryString({ details: true }),
+          window.history.state,
+          { replace: true }
+        );
       }
     } else {
-      this.updateUrl({ params: { details: !this.props.details }, replace: true });
+      dispatch({ type: 'setActiveDetails', data: !activeDetails });
     }
   };
 
-  render() {
-    const {
-      origin,
-      destination,
-      vehicle,
-      routes,
-      error,
-      isLoading,
-      isDirty,
-      isInitializing,
-      originInputText,
-      destinationInputText,
-    } = this.state;
-
-    const { activeRouteId, details: activeDetails, isMobile } = this.props;
-
-    const form = (
-      <DirectionForm
-        isLoading={isLoading}
-        origin={origin}
-        destination={destination}
-        originInputText={originInputText}
-        destinationInputText={destinationInputText}
-        onChangeDirectionPoint={this.changeDirectionPoint}
-        onReversePoints={this.reversePoints}
-        onEmptyOrigin={this.emptyOrigin}
-        onEmptyDestination={this.emptyDestination}
-        vehicles={this.vehicles}
-        onSelectVehicle={this.onSelectVehicle}
-        activeVehicle={vehicle}
-        isInitializing={isInitializing}
-      />
-    );
-
-    const result = (
-      <RouteResult
-        activeRouteId={activeRouteId}
-        activeDetails={activeDetails}
-        isLoading={isLoading || (routes.length > 0 && isDirty)}
-        vehicle={vehicle}
-        error={error}
-        routes={routes}
-        origin={origin}
-        destination={destination}
-        toggleDetails={this.toggleDetails}
-        selectRoute={this.selectRoute}
-      />
-    );
+  const form = (
+    <DirectionForm
+      onReversePoints={reversePoints}
+      onSelectVehicle={onSelectVehicle}
+      isInitializing={isInitializing}
+    />
+  );
+
+  const result = <RouteResult toggleDetails={toggleDetails} selectRoute={selectRoute} />;
+
+  return (
+    <>
+      <DirectionMap />
+      {isMobile ? (
+        <MobileDirectionPanel
+          form={form}
+          result={result}
+          toggleDetails={toggleDetails}
+          onClose={onClose}
+          onShareClick={onShareClick}
+        />
+      ) : (
+        <DesktopDirectionPanel
+          form={form}
+          result={result}
+          onClose={onClose}
+          onShareClick={onShareClick}
+        />
+      )}
+    </>
+  );
+};
 
-    return isMobile ? (
-      <MobileDirectionPanel
-        form={form}
-        result={result}
-        routes={routes}
-        origin={origin}
-        destination={destination}
-        vehicle={vehicle}
-        toggleDetails={this.toggleDetails}
-        activeDetails={activeDetails}
-        activeRouteId={activeRouteId}
-        onClose={this.onClose}
-        handleShareClick={this.handleShareClick}
-      />
-    ) : (
-      <Panel className="direction-panel" onClose={this.onClose} renderHeader={form}>
-        <div className="direction-autocomplete_suggestions" />
-        {routes.length > 0 && (
-          <ShareMenu url={window.location.toString()}>
-            {openMenu => (
-              <Button
-                className="direction-panel-share-button u-ml-auto u-flex-shrink-0 u-mr-m"
-                variant="tertiary"
-                title={_('Share itinerary', 'direction')}
-                onClick={e => this.handleShareClick(e, openMenu)}
-              >
-                <IconShare />
-                {_('Share itinerary', 'direction')}
-              </Button>
-            )}
-          </ShareMenu>
-        )}
-        <Divider paddingTop={8} paddingBottom={0} />
-        {result}
-      </Panel>
-    );
-  }
-}
+DirectionPanel.propTypes = {
+  poi: PropTypes.object,
+  urlValues: PropTypes.object,
+};
 
-const DirectionPanelFunc = props => {
-  usePageTitle(_('Directions'));
-  const { isMobile } = useDevice();
-  return <DirectionPanel isMobile={isMobile} {...props} />;
+const DirectionPanelWrapper = ({ origin, destination, activeDetails, activeRouteId, ...rest }) => {
+  return (
+    <DirectionPanel urlValues={{ origin, destination, activeDetails, activeRouteId }} {...rest} />
+  );
 };
 
-export default DirectionPanelFunc;
+export default DirectionPanelWrapper;
diff --git a/src/panel/direction/Directions.jsx b/src/panel/direction/Directions.jsx
new file mode 100644
index 000000000..06d0486df
--- /dev/null
+++ b/src/panel/direction/Directions.jsx
@@ -0,0 +1,25 @@
+import React, { useEffect } from 'react';
+import { DirectionProvider } from './directionStore';
+import DirectionPanel from './DirectionPanel';
+import { usePageTitle, useI18n } from 'src/hooks';
+
+const Directions = props => {
+  const { _ } = useI18n();
+
+  usePageTitle(_('Directions'));
+
+  useEffect(() => {
+    document.body.classList.add('directions-open');
+    return () => {
+      document.body.classList.remove('directions-open');
+    };
+  }, []);
+
+  return (
+    <DirectionProvider>
+      <DirectionPanel {...props} />
+    </DirectionProvider>
+  );
+};
+
+export default Directions;
diff --git a/src/panel/direction/MobileDirectionPanel.jsx b/src/panel/direction/MobileDirectionPanel.jsx
index 8bf219d41..6402a8a0b 100644
--- a/src/panel/direction/MobileDirectionPanel.jsx
+++ b/src/panel/direction/MobileDirectionPanel.jsx
@@ -1,4 +1,4 @@
-import React, { useState, useRef, useEffect } from 'react';
+import React, { useState, useRef, useEffect, useContext } from 'react';
 import { Panel, ShareMenu, FloatingButton, CloseButton } from 'src/components/ui';
 import { Flex, IconShare } from '@qwant/qwant-ponents';
 import MobileRouteDetails from './MobileRouteDetails';
@@ -7,26 +7,17 @@ import { getAllSteps } from 'src/libs/route_utils';
 import { fire } from 'src/libs/customEvents';
 import Telemetry from 'src/libs/telemetry';
 import { useI18n } from 'src/hooks';
+import { DirectionContext } from './directionStore';
 
 const MARGIN_TOP_OFFSET = 64; // reserve space to display map
 
-const MobileDirectionPanel = ({
-  form,
-  result,
-  routes,
-  origin,
-  destination,
-  vehicle,
-  toggleDetails,
-  activeDetails,
-  activeRouteId,
-  onClose,
-  handleShareClick,
-}) => {
+const MobileDirectionPanel = ({ form, result, toggleDetails, onClose, onShareClick }) => {
   const [marginTop, setMarginTop] = useState(0);
   const [activePreviewRoute, setActivePreviewRoute] = useState(null);
   const directionPanelRef = useRef(null);
   const { _ } = useI18n();
+  const { state } = useContext(DirectionContext);
+  const { origin, destination, vehicle, routes, activeRouteId, activeDetails } = state;
 
   useEffect(() => {
     setActivePreviewRoute(null);
@@ -78,7 +69,7 @@ const MobileDirectionPanel = ({
               {openMenu => (
                 <FloatingButton
                   title={_('Share itinerary', 'direction')}
-                  onClick={e => handleShareClick(e, openMenu)}
+                  onClick={e => onShareClick(e, openMenu)}
                   icon={<IconShare size={24} />}
                 />
               )}
diff --git a/src/panel/direction/RouteResult.jsx b/src/panel/direction/RouteResult.jsx
index a96486d83..60f9df8ba 100644
--- a/src/panel/direction/RouteResult.jsx
+++ b/src/panel/direction/RouteResult.jsx
@@ -1,27 +1,28 @@
-/* globals _ */
 import React, { useCallback, useEffect, useContext } from 'react';
 import PropTypes from 'prop-types';
 import { listen, unListen } from 'src/libs/customEvents';
 import Telemetry from 'src/libs/telemetry';
 import RoutesList from './RoutesList';
-import { SourceFooter, UserFeedbackYesNo } from 'src/components/ui';
-import { useDevice } from 'src/hooks';
+import { UserFeedbackYesNo } from 'src/components/ui';
+import { useDevice, useI18n } from 'src/hooks';
 import { PanelContext } from 'src/libs/panelContext';
+import { DirectionContext } from './directionStore';
 
-const RouteResult = ({
-  origin,
-  destination,
-  vehicle,
-  routes = [],
-  isLoading,
-  error,
-  activeRouteId,
-  activeDetails,
-  selectRoute,
-  toggleDetails,
-}) => {
+const RouteResult = ({ selectRoute, toggleDetails }) => {
   const { isMobile } = useDevice();
   const { size: panelSize } = useContext(PanelContext);
+  const { state } = useContext(DirectionContext);
+  const {
+    origin,
+    destination,
+    vehicle,
+    isLoading,
+    routes,
+    activeRouteId,
+    error,
+    activeDetails,
+  } = state;
+  const { _ } = useI18n();
 
   useEffect(() => {
     const routeSelectedOnMapHandler = listen('select_road_map', onSelectRoute);
@@ -81,23 +82,11 @@ const RouteResult = ({
           question={_('Satisfied with the results?')}
         />
       )}
-      {vehicle === 'publicTransport' && routes.length > 0 && (
-        <SourceFooter>
-          <a href="https://combigo.com/">{_('Results in partnership with Combigo')}</a>
-        </SourceFooter>
-      )}
     </>
   );
 };
 
 RouteResult.propTypes = {
-  routes: PropTypes.array,
-  origin: PropTypes.object,
-  destination: PropTypes.object,
-  vehicle: PropTypes.string,
-  isLoading: PropTypes.bool,
-  error: PropTypes.number,
-  activeRouteId: PropTypes.number,
   selectRoute: PropTypes.func.isRequired,
   toggleDetails: PropTypes.func.isRequired,
 };
diff --git a/src/panel/direction/directionStore.js b/src/panel/direction/directionStore.js
new file mode 100644
index 000000000..0e542a00d
--- /dev/null
+++ b/src/panel/direction/directionStore.js
@@ -0,0 +1,111 @@
+import React, { useReducer, useEffect, useCallback } from 'react';
+import DirectionApi, { modes } from 'src/adapters/direction_api';
+import { useConfig } from 'src/hooks';
+
+const initialState = {
+  origin: null,
+  destination: null,
+  vehicles: [],
+  vehicle: modes.DRIVING,
+  isLoading: false,
+  routes: [],
+  error: 0,
+  activeRouteId: 0,
+  activeDetails: false,
+};
+
+export const directionReducer = (state, action) => {
+  switch (action.type) {
+    case 'setOrigin':
+      return { ...state, origin: action.data };
+    case 'setDestination':
+      return { ...state, destination: action.data };
+    case 'setParams':
+      return { ...state, ...action.data };
+    case 'reversePoints':
+      return { ...state, destination: state.origin, origin: state.destination };
+    case 'setVehicle':
+      return {
+        ...state,
+        vehicle: state.vehicles.includes(action.data) ? action.data : modes.DRIVING,
+      };
+    case 'updating':
+      return { ...state, routes: [], isLoading: true, error: 0 };
+    case 'setRoutes':
+      return {
+        ...state,
+        routes: action.data,
+        isLoading: false,
+        error: 0,
+        activeRouteId: state.activeRouteId < action.data.length ? state.activeRouteId : 0,
+      };
+    case 'setError':
+      return { ...state, routes: [], isLoading: false, error: action.data };
+    case 'setActiveRoute':
+      return { ...state, activeRouteId: action.data, activeDetails: false };
+    case 'setActiveDetails':
+      return { ...state, activeDetails: action.data };
+    case 'clearRoutes':
+      return { ...state, routes: [], isLoading: false, error: 0 };
+    case 'reset':
+      return initialState;
+    default:
+      return state;
+  }
+};
+
+export const DirectionContext = React.createContext(initialState);
+
+let lastQueryId = 0;
+
+export const DirectionProvider = ({ children }) => {
+  const {
+    publicTransport: { enabled: ptEnabled },
+  } = useConfig('direction');
+  const vehicles = [modes.DRIVING, modes.WALKING, modes.CYCLING];
+  if (ptEnabled) {
+    vehicles.splice(1, 0, modes.PUBLIC_TRANSPORT);
+  }
+
+  const [state, dispatch] = useReducer(directionReducer, { ...initialState, vehicles });
+  const { origin, destination, vehicle } = state;
+
+  useEffect(() => {
+    const computeRoutes = async () => {
+      dispatch({ type: 'updating' });
+      const currentQueryId = ++lastQueryId;
+      const directionResponse = await DirectionApi.search(origin, destination, vehicle);
+      // A more recent query was done in the meantime, ignore this result silently
+      if (currentQueryId !== lastQueryId) {
+        return;
+      }
+      if (directionResponse?.error === 0) {
+        // Valid, non-empty response
+        const routes = directionResponse.data.routes
+          .sort((routeA, routeB) => routeA.duration - routeB.duration)
+          .map((route, i) => ({ ...route, id: i }));
+
+        dispatch({ type: 'setRoutes', data: routes });
+      } else {
+        dispatch({ type: 'setError', data: directionResponse.error });
+      }
+    };
+
+    if (origin && destination) {
+      computeRoutes();
+    } else {
+      dispatch({ type: 'clearRoutes' });
+    }
+  }, [origin, destination, vehicle]);
+
+  // helper actions to avoid using dispatch directly
+  const setPoint = useCallback((which, point) => {
+    dispatch({ type: which === 'origin' ? 'setOrigin' : 'setDestination', data: point });
+  }, []);
+
+  return (
+    <DirectionContext.Provider value={{ state, setPoint, dispatch }}>
+      {children}
+    </DirectionContext.Provider>
+  );
+};