diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx
index 06a6ae24cac..31ffdf4ab24 100644
--- a/public/app/features/explore/Explore.tsx
+++ b/public/app/features/explore/Explore.tsx
@@ -18,15 +18,7 @@ import TableContainer from './TableContainer';
 import TimePicker, { parseTime } from './TimePicker';
 
 // Actions
-import {
-  changeSize,
-  changeTime,
-  initializeExplore,
-  modifyQueries,
-  scanStart,
-  scanStop,
-  setQueries,
-} from './state/actions';
+import { changeSize, changeTime, initializeExplore, modifyQueries, scanStart, setQueries } from './state/actions';
 
 // Types
 import { RawTimeRange, TimeRange, DataQuery } from '@grafana/ui';
@@ -35,6 +27,7 @@ import { StoreState } from 'app/types';
 import { LAST_USED_DATASOURCE_KEY, ensureQueries, DEFAULT_RANGE } from 'app/core/utils/explore';
 import { Emitter } from 'app/core/utils/emitter';
 import { ExploreToolbar } from './ExploreToolbar';
+import { scanStopAction } from './state/actionTypes';
 
 interface ExploreProps {
   StartPage?: any;
@@ -54,7 +47,7 @@ interface ExploreProps {
   scanning?: boolean;
   scanRange?: RawTimeRange;
   scanStart: typeof scanStart;
-  scanStop: typeof scanStop;
+  scanStopAction: typeof scanStopAction;
   setQueries: typeof setQueries;
   split: boolean;
   showingStartPage?: boolean;
@@ -171,7 +164,7 @@ export class Explore extends React.PureComponent<ExploreProps> {
   };
 
   onStopScanning = () => {
-    this.props.scanStop(this.props.exploreId);
+    this.props.scanStopAction({ exploreId: this.props.exploreId });
   };
 
   render() {
@@ -281,7 +274,7 @@ const mapDispatchToProps = {
   initializeExplore,
   modifyQueries,
   scanStart,
-  scanStop,
+  scanStopAction,
   setQueries,
 };
 
diff --git a/public/app/features/explore/QueryRow.tsx b/public/app/features/explore/QueryRow.tsx
index bbc0bf0d101..5e2e8442e54 100644
--- a/public/app/features/explore/QueryRow.tsx
+++ b/public/app/features/explore/QueryRow.tsx
@@ -9,20 +9,14 @@ import QueryEditor from './QueryEditor';
 import QueryTransactionStatus from './QueryTransactionStatus';
 
 // Actions
-import {
-  addQueryRow,
-  changeQuery,
-  highlightLogsExpression,
-  modifyQueries,
-  removeQueryRow,
-  runQueries,
-} from './state/actions';
+import { changeQuery, modifyQueries, runQueries, addQueryRow } from './state/actions';
 
 // Types
 import { StoreState } from 'app/types';
 import { RawTimeRange, DataQuery, ExploreDataSourceApi, QueryHint, QueryFixAction } from '@grafana/ui';
 import { QueryTransaction, HistoryItem, ExploreItemState, ExploreId } from 'app/types/explore';
 import { Emitter } from 'app/core/utils/emitter';
+import { highlightLogsExpressionAction, removeQueryRowAction } from './state/actionTypes';
 
 function getFirstHintFromTransactions(transactions: QueryTransaction[]): QueryHint {
   const transaction = transactions.find(qt => qt.hints && qt.hints.length > 0);
@@ -38,7 +32,7 @@ interface QueryRowProps {
   className?: string;
   exploreId: ExploreId;
   datasourceInstance: ExploreDataSourceApi;
-  highlightLogsExpression: typeof highlightLogsExpression;
+  highlightLogsExpressionAction: typeof highlightLogsExpressionAction;
   history: HistoryItem[];
   index: number;
   initialQuery: DataQuery;
@@ -46,7 +40,7 @@ interface QueryRowProps {
   queryTransactions: QueryTransaction[];
   exploreEvents: Emitter;
   range: RawTimeRange;
-  removeQueryRow: typeof removeQueryRow;
+  removeQueryRowAction: typeof removeQueryRowAction;
   runQueries: typeof runQueries;
 }
 
@@ -88,14 +82,15 @@ export class QueryRow extends PureComponent<QueryRowProps> {
 
   onClickRemoveButton = () => {
     const { exploreId, index } = this.props;
-    this.props.removeQueryRow(exploreId, index);
+    this.props.removeQueryRowAction({ exploreId, index });
   };
 
   updateLogsHighlights = _.debounce((value: DataQuery) => {
     const { datasourceInstance } = this.props;
     if (datasourceInstance.getHighlighterExpression) {
+      const { exploreId } = this.props;
       const expressions = [datasourceInstance.getHighlighterExpression(value)];
-      this.props.highlightLogsExpression(this.props.exploreId, expressions);
+      this.props.highlightLogsExpressionAction({ exploreId, expressions });
     }
   }, 500);
 
@@ -168,9 +163,9 @@ function mapStateToProps(state: StoreState, { exploreId, index }) {
 const mapDispatchToProps = {
   addQueryRow,
   changeQuery,
-  highlightLogsExpression,
+  highlightLogsExpressionAction,
   modifyQueries,
-  removeQueryRow,
+  removeQueryRowAction,
   runQueries,
 };
 
diff --git a/public/app/features/explore/Wrapper.tsx b/public/app/features/explore/Wrapper.tsx
index aca2e6d8cbd..f64b2704b71 100644
--- a/public/app/features/explore/Wrapper.tsx
+++ b/public/app/features/explore/Wrapper.tsx
@@ -7,16 +7,16 @@ import { StoreState } from 'app/types';
 import { ExploreId, ExploreUrlState } from 'app/types/explore';
 import { parseUrlState } from 'app/core/utils/explore';
 
-import { initializeExploreSplit, resetExplore } from './state/actions';
 import ErrorBoundary from './ErrorBoundary';
 import Explore from './Explore';
 import { CustomScrollbar } from '@grafana/ui';
+import { initializeExploreSplitAction, resetExploreAction } from './state/actionTypes';
 
 interface WrapperProps {
-  initializeExploreSplit: typeof initializeExploreSplit;
+  initializeExploreSplitAction: typeof initializeExploreSplitAction;
   split: boolean;
   updateLocation: typeof updateLocation;
-  resetExplore: typeof resetExplore;
+  resetExploreAction: typeof resetExploreAction;
   urlStates: { [key: string]: string };
 }
 
@@ -39,12 +39,12 @@ export class Wrapper extends Component<WrapperProps> {
 
   componentDidMount() {
     if (this.initialSplit) {
-      this.props.initializeExploreSplit();
+      this.props.initializeExploreSplitAction();
     }
   }
 
   componentWillUnmount() {
-    this.props.resetExplore();
+    this.props.resetExploreAction();
   }
 
   render() {
@@ -77,9 +77,9 @@ const mapStateToProps = (state: StoreState) => {
 };
 
 const mapDispatchToProps = {
-  initializeExploreSplit,
+  initializeExploreSplitAction,
   updateLocation,
-  resetExplore,
+  resetExploreAction,
 };
 
 export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(Wrapper));
diff --git a/public/app/features/explore/state/actionTypes.ts b/public/app/features/explore/state/actionTypes.ts
index 53954f4dc2b..05ef661a8e5 100644
--- a/public/app/features/explore/state/actionTypes.ts
+++ b/public/app/features/explore/state/actionTypes.ts
@@ -1,6 +1,13 @@
 // Types
 import { Emitter } from 'app/core/core';
-import { RawTimeRange, TimeRange, DataQuery, DataSourceSelectItem, DataSourceApi, QueryFixAction } from '@grafana/ui/src/types';
+import {
+  RawTimeRange,
+  TimeRange,
+  DataQuery,
+  DataSourceSelectItem,
+  DataSourceApi,
+  QueryFixAction,
+} from '@grafana/ui/src/types';
 import {
   ExploreId,
   ExploreItemState,
@@ -9,233 +16,26 @@ import {
   ResultType,
   QueryTransaction,
 } from 'app/types/explore';
+import { actionCreatorFactory, noPayloadActionCreatorFactory, ActionOf } from 'app/core/redux/actionCreatorFactory';
 
+/**  Higher order actions
+ *
+ */
 export enum ActionTypes {
-  AddQueryRow = 'explore/ADD_QUERY_ROW',
-  ChangeDatasource = 'explore/CHANGE_DATASOURCE',
-  ChangeQuery = 'explore/CHANGE_QUERY',
-  ChangeSize = 'explore/CHANGE_SIZE',
-  ChangeTime = 'explore/CHANGE_TIME',
-  ClearQueries = 'explore/CLEAR_QUERIES',
-  HighlightLogsExpression = 'explore/HIGHLIGHT_LOGS_EXPRESSION',
-  InitializeExplore = 'explore/INITIALIZE_EXPLORE',
   InitializeExploreSplit = 'explore/INITIALIZE_EXPLORE_SPLIT',
-  LoadDatasourceFailure = 'explore/LOAD_DATASOURCE_FAILURE',
-  LoadDatasourceMissing = 'explore/LOAD_DATASOURCE_MISSING',
-  LoadDatasourcePending = 'explore/LOAD_DATASOURCE_PENDING',
-  LoadDatasourceSuccess = 'explore/LOAD_DATASOURCE_SUCCESS',
-  ModifyQueries = 'explore/MODIFY_QUERIES',
-  QueryTransactionFailure = 'explore/QUERY_TRANSACTION_FAILURE',
-  QueryTransactionStart = 'explore/QUERY_TRANSACTION_START',
-  QueryTransactionSuccess = 'explore/QUERY_TRANSACTION_SUCCESS',
-  RemoveQueryRow = 'explore/REMOVE_QUERY_ROW',
-  RunQueries = 'explore/RUN_QUERIES',
-  RunQueriesEmpty = 'explore/RUN_QUERIES_EMPTY',
-  ScanRange = 'explore/SCAN_RANGE',
-  ScanStart = 'explore/SCAN_START',
-  ScanStop = 'explore/SCAN_STOP',
-  SetQueries = 'explore/SET_QUERIES',
   SplitClose = 'explore/SPLIT_CLOSE',
   SplitOpen = 'explore/SPLIT_OPEN',
-  StateSave = 'explore/STATE_SAVE',
-  ToggleGraph = 'explore/TOGGLE_GRAPH',
-  ToggleLogs = 'explore/TOGGLE_LOGS',
-  ToggleTable = 'explore/TOGGLE_TABLE',
-  UpdateDatasourceInstance = 'explore/UPDATE_DATASOURCE_INSTANCE',
   ResetExplore = 'explore/RESET_EXPLORE',
-  QueriesImported = 'explore/QueriesImported',
-}
-
-export interface AddQueryRowAction {
-  type: ActionTypes.AddQueryRow;
-  payload: {
-    exploreId: ExploreId;
-    index: number;
-    query: DataQuery;
-  };
-}
-
-export interface ChangeQueryAction {
-  type: ActionTypes.ChangeQuery;
-  payload: {
-    exploreId: ExploreId;
-    query: DataQuery;
-    index: number;
-    override: boolean;
-  };
-}
-
-export interface ChangeSizeAction {
-  type: ActionTypes.ChangeSize;
-  payload: {
-    exploreId: ExploreId;
-    width: number;
-    height: number;
-  };
-}
-
-export interface ChangeTimeAction {
-  type: ActionTypes.ChangeTime;
-  payload: {
-    exploreId: ExploreId;
-    range: TimeRange;
-  };
-}
-
-export interface ClearQueriesAction {
-  type: ActionTypes.ClearQueries;
-  payload: {
-    exploreId: ExploreId;
-  };
-}
-
-export interface HighlightLogsExpressionAction {
-  type: ActionTypes.HighlightLogsExpression;
-  payload: {
-    exploreId: ExploreId;
-    expressions: string[];
-  };
-}
-
-export interface InitializeExploreAction {
-  type: ActionTypes.InitializeExplore;
-  payload: {
-    exploreId: ExploreId;
-    containerWidth: number;
-    eventBridge: Emitter;
-    exploreDatasources: DataSourceSelectItem[];
-    queries: DataQuery[];
-    range: RawTimeRange;
-  };
 }
 
 export interface InitializeExploreSplitAction {
   type: ActionTypes.InitializeExploreSplit;
-}
-
-export interface LoadDatasourceFailureAction {
-  type: ActionTypes.LoadDatasourceFailure;
-  payload: {
-    exploreId: ExploreId;
-    error: string;
-  };
-}
-
-export interface LoadDatasourcePendingAction {
-  type: ActionTypes.LoadDatasourcePending;
-  payload: {
-    exploreId: ExploreId;
-    requestedDatasourceName: string;
-  };
-}
-
-export interface LoadDatasourceMissingAction {
-  type: ActionTypes.LoadDatasourceMissing;
-  payload: {
-    exploreId: ExploreId;
-  };
-}
-
-export interface LoadDatasourceSuccessAction {
-  type: ActionTypes.LoadDatasourceSuccess;
-  payload: {
-    exploreId: ExploreId;
-    StartPage?: any;
-    datasourceInstance: any;
-    history: HistoryItem[];
-    logsHighlighterExpressions?: any[];
-    showingStartPage: boolean;
-    supportsGraph: boolean;
-    supportsLogs: boolean;
-    supportsTable: boolean;
-  };
-}
-
-export interface ModifyQueriesAction {
-  type: ActionTypes.ModifyQueries;
-  payload: {
-    exploreId: ExploreId;
-    modification: QueryFixAction;
-    index: number;
-    modifier: (queries: DataQuery[], modification: QueryFixAction) => DataQuery[];
-  };
-}
-
-export interface QueryTransactionFailureAction {
-  type: ActionTypes.QueryTransactionFailure;
-  payload: {
-    exploreId: ExploreId;
-    queryTransactions: QueryTransaction[];
-  };
-}
-
-export interface QueryTransactionStartAction {
-  type: ActionTypes.QueryTransactionStart;
-  payload: {
-    exploreId: ExploreId;
-    resultType: ResultType;
-    rowIndex: number;
-    transaction: QueryTransaction;
-  };
-}
-
-export interface QueryTransactionSuccessAction {
-  type: ActionTypes.QueryTransactionSuccess;
-  payload: {
-    exploreId: ExploreId;
-    history: HistoryItem[];
-    queryTransactions: QueryTransaction[];
-  };
-}
-
-export interface RemoveQueryRowAction {
-  type: ActionTypes.RemoveQueryRow;
-  payload: {
-    exploreId: ExploreId;
-    index: number;
-  };
-}
-
-export interface RunQueriesEmptyAction {
-  type: ActionTypes.RunQueriesEmpty;
-  payload: {
-    exploreId: ExploreId;
-  };
-}
-
-export interface ScanStartAction {
-  type: ActionTypes.ScanStart;
-  payload: {
-    exploreId: ExploreId;
-    scanner: RangeScanner;
-  };
-}
-
-export interface ScanRangeAction {
-  type: ActionTypes.ScanRange;
-  payload: {
-    exploreId: ExploreId;
-    range: RawTimeRange;
-  };
-}
-
-export interface ScanStopAction {
-  type: ActionTypes.ScanStop;
-  payload: {
-    exploreId: ExploreId;
-  };
-}
-
-export interface SetQueriesAction {
-  type: ActionTypes.SetQueries;
-  payload: {
-    exploreId: ExploreId;
-    queries: DataQuery[];
-  };
+  payload: {};
 }
 
 export interface SplitCloseAction {
   type: ActionTypes.SplitClose;
+  payload: {};
 }
 
 export interface SplitOpenAction {
@@ -245,80 +45,384 @@ export interface SplitOpenAction {
   };
 }
 
-export interface StateSaveAction {
-  type: ActionTypes.StateSave;
-}
-
-export interface ToggleTableAction {
-  type: ActionTypes.ToggleTable;
-  payload: {
-    exploreId: ExploreId;
-  };
-}
-
-export interface ToggleGraphAction {
-  type: ActionTypes.ToggleGraph;
-  payload: {
-    exploreId: ExploreId;
-  };
-}
-
-export interface ToggleLogsAction {
-  type: ActionTypes.ToggleLogs;
-  payload: {
-    exploreId: ExploreId;
-  };
-}
-
-export interface UpdateDatasourceInstanceAction {
-  type: ActionTypes.UpdateDatasourceInstance;
-  payload: {
-    exploreId: ExploreId;
-    datasourceInstance: DataSourceApi;
-  };
-}
-
 export interface ResetExploreAction {
   type: ActionTypes.ResetExplore;
   payload: {};
 }
 
-export interface QueriesImported {
-  type: ActionTypes.QueriesImported;
-  payload: {
-    exploreId: ExploreId;
-    queries: DataQuery[];
-  };
+/**  Lower order actions
+ *
+ */
+export interface AddQueryRowPayload {
+  exploreId: ExploreId;
+  index: number;
+  query: DataQuery;
 }
 
-export type Action =
-  | AddQueryRowAction
-  | ChangeQueryAction
-  | ChangeSizeAction
-  | ChangeTimeAction
-  | ClearQueriesAction
-  | HighlightLogsExpressionAction
-  | InitializeExploreAction
+export interface ChangeQueryPayload {
+  exploreId: ExploreId;
+  query: DataQuery;
+  index: number;
+  override: boolean;
+}
+
+export interface ChangeSizePayload {
+  exploreId: ExploreId;
+  width: number;
+  height: number;
+}
+
+export interface ChangeTimePayload {
+  exploreId: ExploreId;
+  range: TimeRange;
+}
+
+export interface ClearQueriesPayload {
+  exploreId: ExploreId;
+}
+
+export interface HighlightLogsExpressionPayload {
+  exploreId: ExploreId;
+  expressions: string[];
+}
+
+export interface InitializeExplorePayload {
+  exploreId: ExploreId;
+  containerWidth: number;
+  eventBridge: Emitter;
+  exploreDatasources: DataSourceSelectItem[];
+  queries: DataQuery[];
+  range: RawTimeRange;
+}
+
+export interface LoadDatasourceFailurePayload {
+  exploreId: ExploreId;
+  error: string;
+}
+
+export interface LoadDatasourceMissingPayload {
+  exploreId: ExploreId;
+}
+
+export interface LoadDatasourcePendingPayload {
+  exploreId: ExploreId;
+  requestedDatasourceName: string;
+}
+
+export interface LoadDatasourceSuccessPayload {
+  exploreId: ExploreId;
+  StartPage?: any;
+  datasourceInstance: any;
+  history: HistoryItem[];
+  logsHighlighterExpressions?: any[];
+  showingStartPage: boolean;
+  supportsGraph: boolean;
+  supportsLogs: boolean;
+  supportsTable: boolean;
+}
+
+export interface ModifyQueriesPayload {
+  exploreId: ExploreId;
+  modification: QueryFixAction;
+  index: number;
+  modifier: (query: DataQuery, modification: QueryFixAction) => DataQuery;
+}
+
+export interface QueryTransactionFailurePayload {
+  exploreId: ExploreId;
+  queryTransactions: QueryTransaction[];
+}
+
+export interface QueryTransactionStartPayload {
+  exploreId: ExploreId;
+  resultType: ResultType;
+  rowIndex: number;
+  transaction: QueryTransaction;
+}
+
+export interface QueryTransactionSuccessPayload {
+  exploreId: ExploreId;
+  history: HistoryItem[];
+  queryTransactions: QueryTransaction[];
+}
+
+export interface RemoveQueryRowPayload {
+  exploreId: ExploreId;
+  index: number;
+}
+
+export interface RunQueriesEmptyPayload {
+  exploreId: ExploreId;
+}
+
+export interface ScanStartPayload {
+  exploreId: ExploreId;
+  scanner: RangeScanner;
+}
+
+export interface ScanRangePayload {
+  exploreId: ExploreId;
+  range: RawTimeRange;
+}
+
+export interface ScanStopPayload {
+  exploreId: ExploreId;
+}
+
+export interface SetQueriesPayload {
+  exploreId: ExploreId;
+  queries: DataQuery[];
+}
+
+export interface SplitOpenPayload {
+  itemState: ExploreItemState;
+}
+
+export interface ToggleTablePayload {
+  exploreId: ExploreId;
+}
+
+export interface ToggleGraphPayload {
+  exploreId: ExploreId;
+}
+
+export interface ToggleLogsPayload {
+  exploreId: ExploreId;
+}
+
+export interface UpdateDatasourceInstancePayload {
+  exploreId: ExploreId;
+  datasourceInstance: DataSourceApi;
+}
+
+export interface QueriesImportedPayload {
+  exploreId: ExploreId;
+  queries: DataQuery[];
+}
+
+/**
+ * Adds a query row after the row with the given index.
+ */
+export const addQueryRowAction = actionCreatorFactory<AddQueryRowPayload>('explore/ADD_QUERY_ROW').create();
+
+/**
+ * Loads a new datasource identified by the given name.
+ */
+export const changeDatasourceAction = noPayloadActionCreatorFactory('explore/CHANGE_DATASOURCE').create();
+
+/**
+ * Query change handler for the query row with the given index.
+ * If `override` is reset the query modifications and run the queries. Use this to set queries via a link.
+ */
+export const changeQueryAction = actionCreatorFactory<ChangeQueryPayload>('explore/CHANGE_QUERY').create();
+
+/**
+ * Keep track of the Explore container size, in particular the width.
+ * The width will be used to calculate graph intervals (number of datapoints).
+ */
+export const changeSizeAction = actionCreatorFactory<ChangeSizePayload>('explore/CHANGE_SIZE').create();
+
+/**
+ * Change the time range of Explore. Usually called from the Timepicker or a graph interaction.
+ */
+export const changeTimeAction = actionCreatorFactory<ChangeTimePayload>('explore/CHANGE_TIME').create();
+
+/**
+ * Clear all queries and results.
+ */
+export const clearQueriesAction = actionCreatorFactory<ClearQueriesPayload>('explore/CLEAR_QUERIES').create();
+
+/**
+ * Highlight expressions in the log results
+ */
+export const highlightLogsExpressionAction = actionCreatorFactory<HighlightLogsExpressionPayload>(
+  'explore/HIGHLIGHT_LOGS_EXPRESSION'
+).create();
+
+/**
+ * Initialize Explore state with state from the URL and the React component.
+ * Call this only on components for with the Explore state has not been initialized.
+ */
+export const initializeExploreAction = actionCreatorFactory<InitializeExplorePayload>(
+  'explore/INITIALIZE_EXPLORE'
+).create();
+
+/**
+ * Initialize the wrapper split state
+ */
+export const initializeExploreSplitAction = noPayloadActionCreatorFactory('explore/INITIALIZE_EXPLORE_SPLIT').create();
+
+/**
+ * Display an error that happened during the selection of a datasource
+ */
+export const loadDatasourceFailureAction = actionCreatorFactory<LoadDatasourceFailurePayload>(
+  'explore/LOAD_DATASOURCE_FAILURE'
+).create();
+
+/**
+ * Display an error when no datasources have been configured
+ */
+export const loadDatasourceMissingAction = actionCreatorFactory<LoadDatasourceMissingPayload>(
+  'explore/LOAD_DATASOURCE_MISSING'
+).create();
+
+/**
+ * Start the async process of loading a datasource to display a loading indicator
+ */
+export const loadDatasourcePendingAction = actionCreatorFactory<LoadDatasourcePendingPayload>(
+  'explore/LOAD_DATASOURCE_PENDING'
+).create();
+
+/**
+ * Datasource loading was successfully completed. The instance is stored in the state as well in case we need to
+ * run datasource-specific code. Existing queries are imported to the new datasource if an importer exists,
+ * e.g., Prometheus -> Loki queries.
+ */
+export const loadDatasourceSuccessAction = actionCreatorFactory<LoadDatasourceSuccessPayload>(
+  'explore/LOAD_DATASOURCE_SUCCESS'
+).create();
+
+/**
+ * Action to modify a query given a datasource-specific modifier action.
+ * @param exploreId Explore area
+ * @param modification Action object with a type, e.g., ADD_FILTER
+ * @param index Optional query row index. If omitted, the modification is applied to all query rows.
+ * @param modifier Function that executes the modification, typically `datasourceInstance.modifyQueries`.
+ */
+export const modifyQueriesAction = actionCreatorFactory<ModifyQueriesPayload>('explore/MODIFY_QUERIES').create();
+
+/**
+ * Mark a query transaction as failed with an error extracted from the query response.
+ * The transaction will be marked as `done`.
+ */
+export const queryTransactionFailureAction = actionCreatorFactory<QueryTransactionFailurePayload>(
+  'explore/QUERY_TRANSACTION_FAILURE'
+).create();
+
+/**
+ * Start a query transaction for the given result type.
+ * @param exploreId Explore area
+ * @param transaction Query options and `done` status.
+ * @param resultType Associate the transaction with a result viewer, e.g., Graph
+ * @param rowIndex Index is used to associate latency for this transaction with a query row
+ */
+export const queryTransactionStartAction = actionCreatorFactory<QueryTransactionStartPayload>(
+  'explore/QUERY_TRANSACTION_START'
+).create();
+
+/**
+ * Complete a query transaction, mark the transaction as `done` and store query state in URL.
+ * If the transaction was started by a scanner, it keeps on scanning for more results.
+ * Side-effect: the query is stored in localStorage.
+ * @param exploreId Explore area
+ * @param transactionId ID
+ * @param result Response from `datasourceInstance.query()`
+ * @param latency Duration between request and response
+ * @param queries Queries from all query rows
+ * @param datasourceId Origin datasource instance, used to discard results if current datasource is different
+ */
+export const queryTransactionSuccessAction = actionCreatorFactory<QueryTransactionSuccessPayload>(
+  'explore/QUERY_TRANSACTION_SUCCESS'
+).create();
+
+/**
+ * Remove query row of the given index, as well as associated query results.
+ */
+export const removeQueryRowAction = actionCreatorFactory<RemoveQueryRowPayload>('explore/REMOVE_QUERY_ROW').create();
+export const runQueriesAction = noPayloadActionCreatorFactory('explore/RUN_QUERIES').create();
+export const runQueriesEmptyAction = actionCreatorFactory<RunQueriesEmptyPayload>('explore/RUN_QUERIES_EMPTY').create();
+
+/**
+ * Start a scan for more results using the given scanner.
+ * @param exploreId Explore area
+ * @param scanner Function that a) returns a new time range and b) triggers a query run for the new range
+ */
+export const scanStartAction = actionCreatorFactory<ScanStartPayload>('explore/SCAN_START').create();
+export const scanRangeAction = actionCreatorFactory<ScanRangePayload>('explore/SCAN_RANGE').create();
+
+/**
+ * Stop any scanning for more results.
+ */
+export const scanStopAction = actionCreatorFactory<ScanStopPayload>('explore/SCAN_STOP').create();
+
+/**
+ * Reset queries to the given queries. Any modifications will be discarded.
+ * Use this action for clicks on query examples. Triggers a query run.
+ */
+export const setQueriesAction = actionCreatorFactory<SetQueriesPayload>('explore/SET_QUERIES').create();
+
+/**
+ * Close the split view and save URL state.
+ */
+export const splitCloseAction = noPayloadActionCreatorFactory('explore/SPLIT_CLOSE').create();
+
+/**
+ * Open the split view and copy the left state to be the right state.
+ * The right state is automatically initialized.
+ * The copy keeps all query modifications but wipes the query results.
+ */
+export const splitOpenAction = actionCreatorFactory<SplitOpenPayload>('explore/SPLIT_OPEN').create();
+export const stateSaveAction = noPayloadActionCreatorFactory('explore/STATE_SAVE').create();
+
+/**
+ * Expand/collapse the table result viewer. When collapsed, table queries won't be run.
+ */
+export const toggleTableAction = actionCreatorFactory<ToggleTablePayload>('explore/TOGGLE_TABLE').create();
+
+/**
+ * Expand/collapse the graph result viewer. When collapsed, graph queries won't be run.
+ */
+export const toggleGraphAction = actionCreatorFactory<ToggleGraphPayload>('explore/TOGGLE_GRAPH').create();
+
+/**
+ * Expand/collapse the logs result viewer. When collapsed, log queries won't be run.
+ */
+export const toggleLogsAction = actionCreatorFactory<ToggleLogsPayload>('explore/TOGGLE_LOGS').create();
+
+/**
+ * Updates datasource instance before datasouce loading has started
+ */
+export const updateDatasourceInstanceAction = actionCreatorFactory<UpdateDatasourceInstancePayload>(
+  'explore/UPDATE_DATASOURCE_INSTANCE'
+).create();
+
+/**
+ * Resets state for explore.
+ */
+export const resetExploreAction = noPayloadActionCreatorFactory('explore/RESET_EXPLORE').create();
+export const queriesImportedAction = actionCreatorFactory<QueriesImportedPayload>('explore/QueriesImported').create();
+
+export type HigherOrderAction =
   | InitializeExploreSplitAction
-  | LoadDatasourceFailureAction
-  | LoadDatasourceMissingAction
-  | LoadDatasourcePendingAction
-  | LoadDatasourceSuccessAction
-  | ModifyQueriesAction
-  | QueryTransactionFailureAction
-  | QueryTransactionStartAction
-  | QueryTransactionSuccessAction
-  | RemoveQueryRowAction
-  | RunQueriesEmptyAction
-  | ScanRangeAction
-  | ScanStartAction
-  | ScanStopAction
-  | SetQueriesAction
   | SplitCloseAction
   | SplitOpenAction
-  | ToggleGraphAction
-  | ToggleLogsAction
-  | ToggleTableAction
-  | UpdateDatasourceInstanceAction
   | ResetExploreAction
-  | QueriesImported;
+  | ActionOf<any>;
+
+export type Action =
+  | ActionOf<AddQueryRowPayload>
+  | ActionOf<ChangeQueryPayload>
+  | ActionOf<ChangeSizePayload>
+  | ActionOf<ChangeTimePayload>
+  | ActionOf<ClearQueriesPayload>
+  | ActionOf<HighlightLogsExpressionPayload>
+  | ActionOf<InitializeExplorePayload>
+  | ActionOf<LoadDatasourceFailurePayload>
+  | ActionOf<LoadDatasourceMissingPayload>
+  | ActionOf<LoadDatasourcePendingPayload>
+  | ActionOf<LoadDatasourceSuccessPayload>
+  | ActionOf<ModifyQueriesPayload>
+  | ActionOf<QueryTransactionFailurePayload>
+  | ActionOf<QueryTransactionStartPayload>
+  | ActionOf<QueryTransactionSuccessPayload>
+  | ActionOf<RemoveQueryRowPayload>
+  | ActionOf<RunQueriesEmptyPayload>
+  | ActionOf<ScanStartPayload>
+  | ActionOf<ScanRangePayload>
+  | ActionOf<SetQueriesPayload>
+  | ActionOf<SplitOpenPayload>
+  | ActionOf<ToggleTablePayload>
+  | ActionOf<ToggleGraphPayload>
+  | ActionOf<ToggleLogsPayload>
+  | ActionOf<UpdateDatasourceInstancePayload>
+  | ActionOf<QueriesImportedPayload>;
diff --git a/public/app/features/explore/state/actions.ts b/public/app/features/explore/state/actions.ts
index 63432e9c516..f32575edda5 100644
--- a/public/app/features/explore/state/actions.ts
+++ b/public/app/features/explore/state/actions.ts
@@ -32,40 +32,49 @@ import {
   QueryHint,
   QueryFixAction,
 } from '@grafana/ui/src/types';
+import { ExploreId, ExploreUrlState, RangeScanner, ResultType, QueryOptions } from 'app/types/explore';
 import {
-  ExploreId,
-  ExploreUrlState,
-  RangeScanner,
-  ResultType,
-  QueryOptions,
-  QueryTransaction,
-} from 'app/types/explore';
-
-import {
-  Action as ThunkableAction,
-  ActionTypes,
-  AddQueryRowAction,
-  ChangeSizeAction,
-  HighlightLogsExpressionAction,
-  LoadDatasourceFailureAction,
-  LoadDatasourceMissingAction,
-  LoadDatasourcePendingAction,
-  LoadDatasourceSuccessAction,
-  QueryTransactionStartAction,
-  ScanStopAction,
-  UpdateDatasourceInstanceAction,
-  QueriesImported,
-  ModifyQueriesAction,
+  Action,
+  updateDatasourceInstanceAction,
+  changeQueryAction,
+  changeSizeAction,
+  ChangeSizePayload,
+  changeTimeAction,
+  scanStopAction,
+  clearQueriesAction,
+  initializeExploreAction,
+  loadDatasourceMissingAction,
+  loadDatasourceFailureAction,
+  loadDatasourcePendingAction,
+  queriesImportedAction,
+  LoadDatasourceSuccessPayload,
+  loadDatasourceSuccessAction,
+  modifyQueriesAction,
+  queryTransactionFailureAction,
+  queryTransactionStartAction,
+  queryTransactionSuccessAction,
+  scanRangeAction,
+  runQueriesEmptyAction,
+  scanStartAction,
+  setQueriesAction,
+  splitCloseAction,
+  splitOpenAction,
+  toggleGraphAction,
+  toggleLogsAction,
+  toggleTableAction,
+  addQueryRowAction,
+  AddQueryRowPayload,
 } from './actionTypes';
+import { ActionOf } from 'app/core/redux/actionCreatorFactory';
 
-type ThunkResult<R> = ThunkAction<R, StoreState, undefined, ThunkableAction>;
+type ThunkResult<R> = ThunkAction<R, StoreState, undefined, Action>;
 
-/**
- * Adds a query row after the row with the given index.
- */
-export function addQueryRow(exploreId: ExploreId, index: number): AddQueryRowAction {
+// /**
+//  * Adds a query row after the row with the given index.
+//  */
+export function addQueryRow(exploreId: ExploreId, index: number): ActionOf<AddQueryRowPayload> {
   const query = generateEmptyQuery(index + 1);
-  return { type: ActionTypes.AddQueryRow, payload: { exploreId, index, query } };
+  return addQueryRowAction({ exploreId, index, query });
 }
 
 /**
@@ -79,7 +88,7 @@ export function changeDatasource(exploreId: ExploreId, datasource: string): Thun
 
     await dispatch(importQueries(exploreId, modifiedQueries, currentDataSourceInstance, newDataSourceInstance));
 
-    dispatch(updateDatasourceInstance(exploreId, newDataSourceInstance));
+    dispatch(updateDatasourceInstanceAction({ exploreId, datasourceInstance: newDataSourceInstance }));
     dispatch(loadDatasource(exploreId, newDataSourceInstance));
   };
 }
@@ -100,7 +109,7 @@ export function changeQuery(
       query = { ...generateEmptyQuery(index) };
     }
 
-    dispatch({ type: ActionTypes.ChangeQuery, payload: { exploreId, query, index, override } });
+    dispatch(changeQueryAction({ exploreId, query, index, override }));
     if (override) {
       dispatch(runQueries(exploreId));
     }
@@ -114,8 +123,8 @@ export function changeQuery(
 export function changeSize(
   exploreId: ExploreId,
   { height, width }: { height: number; width: number }
-): ChangeSizeAction {
-  return { type: ActionTypes.ChangeSize, payload: { exploreId, height, width } };
+): ActionOf<ChangeSizePayload> {
+  return changeSizeAction({ exploreId, height, width });
 }
 
 /**
@@ -123,7 +132,7 @@ export function changeSize(
  */
 export function changeTime(exploreId: ExploreId, range: TimeRange): ThunkResult<void> {
   return dispatch => {
-    dispatch({ type: ActionTypes.ChangeTime, payload: { exploreId, range } });
+    dispatch(changeTimeAction({ exploreId, range }));
     dispatch(runQueries(exploreId));
   };
 }
@@ -133,19 +142,12 @@ export function changeTime(exploreId: ExploreId, range: TimeRange): ThunkResult<
  */
 export function clearQueries(exploreId: ExploreId): ThunkResult<void> {
   return dispatch => {
-    dispatch(scanStop(exploreId));
-    dispatch({ type: ActionTypes.ClearQueries, payload: { exploreId } });
+    dispatch(scanStopAction({ exploreId }));
+    dispatch(clearQueriesAction({ exploreId }));
     dispatch(stateSave());
   };
 }
 
-/**
- * Highlight expressions in the log results
- */
-export function highlightLogsExpression(exploreId: ExploreId, expressions: string[]): HighlightLogsExpressionAction {
-  return { type: ActionTypes.HighlightLogsExpression, payload: { exploreId, expressions } };
-}
-
 /**
  * Initialize Explore state with state from the URL and the React component.
  * Call this only on components for with the Explore state has not been initialized.
@@ -167,18 +169,16 @@ export function initializeExplore(
         meta: ds.meta,
       }));
 
-    dispatch({
-      type: ActionTypes.InitializeExplore,
-      payload: {
+    dispatch(
+      initializeExploreAction({
         exploreId,
         containerWidth,
-        datasourceName,
         eventBridge,
         exploreDatasources,
         queries,
         range,
-      },
-    });
+      })
+    );
 
     if (exploreDatasources.length >= 1) {
       let instance;
@@ -195,75 +195,20 @@ export function initializeExplore(
         instance = await getDatasourceSrv().get();
       }
 
-      dispatch(updateDatasourceInstance(exploreId, instance));
+      dispatch(updateDatasourceInstanceAction({ exploreId, datasourceInstance: instance }));
       dispatch(loadDatasource(exploreId, instance));
     } else {
-      dispatch(loadDatasourceMissing(exploreId));
+      dispatch(loadDatasourceMissingAction({ exploreId }));
     }
   };
 }
 
-/**
- * Initialize the wrapper split state
- */
-export function initializeExploreSplit() {
-  return async dispatch => {
-    dispatch({ type: ActionTypes.InitializeExploreSplit });
-  };
-}
-
-/**
- * Display an error that happened during the selection of a datasource
- */
-export const loadDatasourceFailure = (exploreId: ExploreId, error: string): LoadDatasourceFailureAction => ({
-  type: ActionTypes.LoadDatasourceFailure,
-  payload: {
-    exploreId,
-    error,
-  },
-});
-
-/**
- * Display an error when no datasources have been configured
- */
-export const loadDatasourceMissing = (exploreId: ExploreId): LoadDatasourceMissingAction => ({
-  type: ActionTypes.LoadDatasourceMissing,
-  payload: { exploreId },
-});
-
-/**
- * Start the async process of loading a datasource to display a loading indicator
- */
-export const loadDatasourcePending = (
-  exploreId: ExploreId,
-  requestedDatasourceName: string
-): LoadDatasourcePendingAction => ({
-  type: ActionTypes.LoadDatasourcePending,
-  payload: {
-    exploreId,
-    requestedDatasourceName,
-  },
-});
-
-export const queriesImported = (exploreId: ExploreId, queries: DataQuery[]): QueriesImported => {
-  return {
-    type: ActionTypes.QueriesImported,
-    payload: {
-      exploreId,
-      queries,
-    },
-  };
-};
-
 /**
  * Datasource loading was successfully completed. The instance is stored in the state as well in case we need to
  * run datasource-specific code. Existing queries are imported to the new datasource if an importer exists,
  * e.g., Prometheus -> Loki queries.
  */
-export const loadDatasourceSuccess = (
-  exploreId: ExploreId,
-  instance: any,
-): LoadDatasourceSuccessAction => {
+export const loadDatasourceSuccess = (exploreId: ExploreId, instance: any): ActionOf<LoadDatasourceSuccessPayload> => {
   // Capabilities
   const supportsGraph = instance.meta.metrics;
   const supportsLogs = instance.meta.logs;
@@ -276,37 +221,18 @@ export const loadDatasourceSuccess = (
   // Save last-used datasource
   store.set(LAST_USED_DATASOURCE_KEY, instance.name);
 
-  return {
-    type: ActionTypes.LoadDatasourceSuccess,
-    payload: {
-      exploreId,
-      StartPage,
-      datasourceInstance: instance,
-      history,
-      showingStartPage: Boolean(StartPage),
-      supportsGraph,
-      supportsLogs,
-      supportsTable,
-    },
-  };
+  return loadDatasourceSuccessAction({
+    exploreId,
+    StartPage,
+    datasourceInstance: instance,
+    history,
+    showingStartPage: Boolean(StartPage),
+    supportsGraph,
+    supportsLogs,
+    supportsTable,
+  });
 };
 
-/**
- * Updates datasource instance before datasouce loading has started
- */
-export function updateDatasourceInstance(
-  exploreId: ExploreId,
-  instance: DataSourceApi
-): UpdateDatasourceInstanceAction {
-  return {
-    type: ActionTypes.UpdateDatasourceInstance,
-    payload: {
-      exploreId,
-      datasourceInstance: instance,
-    },
-  };
-}
-
 export function importQueries(
   exploreId: ExploreId,
   queries: DataQuery[],
@@ -332,7 +258,7 @@ export function importQueries(
       ...generateEmptyQuery(i),
     }));
 
-    dispatch(queriesImported(exploreId, nextQueries));
+    dispatch(queriesImportedAction({ exploreId, queries: nextQueries }));
   };
 }
 
@@ -344,7 +270,7 @@ export function loadDatasource(exploreId: ExploreId, instance: DataSourceApi): T
     const datasourceName = instance.name;
 
     // Keep ID to track selection
-    dispatch(loadDatasourcePending(exploreId, datasourceName));
+    dispatch(loadDatasourcePendingAction({ exploreId, requestedDatasourceName: datasourceName }));
 
     let datasourceError = null;
     try {
@@ -355,7 +281,7 @@ export function loadDatasource(exploreId: ExploreId, instance: DataSourceApi): T
     }
 
     if (datasourceError) {
-      dispatch(loadDatasourceFailure(exploreId, datasourceError));
+      dispatch(loadDatasourceFailureAction({ exploreId, error: datasourceError }));
       return;
     }
 
@@ -392,11 +318,7 @@ export function modifyQueries(
   modifier: any
 ): ThunkResult<void> {
   return dispatch => {
-    const modifyQueryAction: ModifyQueriesAction = {
-      type: ActionTypes.ModifyQueries,
-      payload: { exploreId, modification, index, modifier },
-    };
-    dispatch(modifyQueryAction);
+    dispatch(modifyQueriesAction({ exploreId, modification, index, modifier }));
     if (!modification.preventSubmit) {
       dispatch(runQueries(exploreId));
     }
@@ -461,29 +383,10 @@ export function queryTransactionFailure(
       return qt;
     });
 
-    dispatch({
-      type: ActionTypes.QueryTransactionFailure,
-      payload: { exploreId, queryTransactions: nextQueryTransactions },
-    });
+    dispatch(queryTransactionFailureAction({ exploreId, queryTransactions: nextQueryTransactions }));
   };
 }
 
-/**
- * Start a query transaction for the given result type.
- * @param exploreId Explore area
- * @param transaction Query options and `done` status.
- * @param resultType Associate the transaction with a result viewer, e.g., Graph
- * @param rowIndex Index is used to associate latency for this transaction with a query row
- */
-export function queryTransactionStart(
-  exploreId: ExploreId,
-  transaction: QueryTransaction,
-  resultType: ResultType,
-  rowIndex: number
-): QueryTransactionStartAction {
-  return { type: ActionTypes.QueryTransactionStart, payload: { exploreId, resultType, rowIndex, transaction } };
-}
-
 /**
  * Complete a query transaction, mark the transaction as `done` and store query state in URL.
  * If the transaction was started by a scanner, it keeps on scanning for more results.
@@ -540,14 +443,13 @@ export function queryTransactionSuccess(
     // Side-effect: Saving history in localstorage
     const nextHistory = updateHistory(history, datasourceId, queries);
 
-    dispatch({
-      type: ActionTypes.QueryTransactionSuccess,
-      payload: {
+    dispatch(
+      queryTransactionSuccessAction({
         exploreId,
         history: nextHistory,
         queryTransactions: nextQueryTransactions,
-      },
-    });
+      })
+    );
 
     // Keep scanning for results if this was the last scanning transaction
     if (scanning) {
@@ -555,26 +457,16 @@ export function queryTransactionSuccess(
         const other = nextQueryTransactions.find(qt => qt.scanning && !qt.done);
         if (!other) {
           const range = scanner();
-          dispatch({ type: ActionTypes.ScanRange, payload: { exploreId, range } });
+          dispatch(scanRangeAction({ exploreId, range }));
         }
       } else {
         // We can stop scanning if we have a result
-        dispatch(scanStop(exploreId));
+        dispatch(scanStopAction({ exploreId }));
       }
     }
   };
 }
 
-/**
- * Remove query row of the given index, as well as associated query results.
- */
-export function removeQueryRow(exploreId: ExploreId, index: number): ThunkResult<void> {
-  return dispatch => {
-    dispatch({ type: ActionTypes.RemoveQueryRow, payload: { exploreId, index } });
-    dispatch(runQueries(exploreId));
-  };
-}
-
 /**
  * Main action to run queries and dispatches sub-actions based on which result viewers are active
  */
@@ -592,7 +484,7 @@ export function runQueries(exploreId: ExploreId) {
     } = getState().explore[exploreId];
 
     if (!hasNonEmptyQuery(modifiedQueries)) {
-      dispatch({ type: ActionTypes.RunQueriesEmpty, payload: { exploreId } });
+      dispatch(runQueriesEmptyAction({ exploreId }));
       dispatch(stateSave()); // Remember to saves to state and update location
       return;
     }
@@ -673,7 +565,7 @@ function runQueriesForType(
         queryIntervals,
         scanning
       );
-      dispatch(queryTransactionStart(exploreId, transaction, resultType, rowIndex));
+      dispatch(queryTransactionStartAction({ exploreId, resultType, rowIndex, transaction }));
       try {
         const now = Date.now();
         const res = await datasourceInstance.query(transaction.options);
@@ -697,21 +589,14 @@ function runQueriesForType(
 export function scanStart(exploreId: ExploreId, scanner: RangeScanner): ThunkResult<void> {
   return dispatch => {
     // Register the scanner
-    dispatch({ type: ActionTypes.ScanStart, payload: { exploreId, scanner } });
+    dispatch(scanStartAction({ exploreId, scanner }));
     // Scanning must trigger query run, and return the new range
     const range = scanner();
     // Set the new range to be displayed
-    dispatch({ type: ActionTypes.ScanRange, payload: { exploreId, range } });
+    dispatch(scanRangeAction({ exploreId, range }));
   };
 }
 
-/**
- * Stop any scanning for more results.
- */
-export function scanStop(exploreId: ExploreId): ScanStopAction {
-  return { type: ActionTypes.ScanStop, payload: { exploreId } };
-}
-
 /**
  * Reset queries to the given queries. Any modifications will be discarded.
  * Use this action for clicks on query examples. Triggers a query run.
@@ -720,13 +605,7 @@ export function setQueries(exploreId: ExploreId, rawQueries: DataQuery[]): Thunk
   return dispatch => {
     // Inject react keys into query objects
     const queries = rawQueries.map(q => ({ ...q, ...generateEmptyQuery() }));
-    dispatch({
-      type: ActionTypes.SetQueries,
-      payload: {
-        exploreId,
-        queries,
-      },
-    });
+    dispatch(setQueriesAction({ exploreId, queries }));
     dispatch(runQueries(exploreId));
   };
 }
@@ -736,7 +615,7 @@ export function setQueries(exploreId: ExploreId, rawQueries: DataQuery[]): Thunk
  */
 export function splitClose(): ThunkResult<void> {
   return dispatch => {
-    dispatch({ type: ActionTypes.SplitClose });
+    dispatch(splitCloseAction());
     dispatch(stateSave());
   };
 }
@@ -755,7 +634,7 @@ export function splitOpen(): ThunkResult<void> {
       queryTransactions: [],
       initialQueries: leftState.modifiedQueries.slice(),
     };
-    dispatch({ type: ActionTypes.SplitOpen, payload: { itemState } });
+    dispatch(splitOpenAction({ itemState }));
     dispatch(stateSave());
   };
 }
@@ -791,7 +670,7 @@ export function stateSave() {
  */
 export function toggleGraph(exploreId: ExploreId): ThunkResult<void> {
   return (dispatch, getState) => {
-    dispatch({ type: ActionTypes.ToggleGraph, payload: { exploreId } });
+    dispatch(toggleGraphAction({ exploreId }));
     if (getState().explore[exploreId].showingGraph) {
       dispatch(runQueries(exploreId));
     }
@@ -803,7 +682,7 @@ export function toggleGraph(exploreId: ExploreId): ThunkResult<void> {
  */
 export function toggleLogs(exploreId: ExploreId): ThunkResult<void> {
   return (dispatch, getState) => {
-    dispatch({ type: ActionTypes.ToggleLogs, payload: { exploreId } });
+    dispatch(toggleLogsAction({ exploreId }));
     if (getState().explore[exploreId].showingLogs) {
       dispatch(runQueries(exploreId));
     }
@@ -815,18 +694,9 @@ export function toggleLogs(exploreId: ExploreId): ThunkResult<void> {
  */
 export function toggleTable(exploreId: ExploreId): ThunkResult<void> {
   return (dispatch, getState) => {
-    dispatch({ type: ActionTypes.ToggleTable, payload: { exploreId } });
+    dispatch(toggleTableAction({ exploreId }));
     if (getState().explore[exploreId].showingTable) {
       dispatch(runQueries(exploreId));
     }
   };
 }
-
-/**
- * Resets state for explore.
- */
-export function resetExplore(): ThunkResult<void> {
-  return dispatch => {
-    dispatch({ type: ActionTypes.ResetExplore, payload: {} });
-  };
-}
diff --git a/public/app/features/explore/state/reducers.test.ts b/public/app/features/explore/state/reducers.test.ts
index 8227a947c5b..44079313c04 100644
--- a/public/app/features/explore/state/reducers.test.ts
+++ b/public/app/features/explore/state/reducers.test.ts
@@ -1,42 +1,47 @@
-import { Action, ActionTypes } from './actionTypes';
 import { itemReducer, makeExploreItemState } from './reducers';
-import { ExploreId } from 'app/types/explore';
+import { ExploreId, ExploreItemState } from 'app/types/explore';
+import { reducerTester } from 'test/core/redux/reducerTester';
+import { scanStartAction, scanStopAction } from './actionTypes';
+import { Reducer } from 'redux';
+import { ActionOf } from 'app/core/redux/actionCreatorFactory';
 
 describe('Explore item reducer', () => {
   describe('scanning', () => {
     test('should start scanning', () => {
-      let state = makeExploreItemState();
-      const action: Action = {
-        type: ActionTypes.ScanStart,
-        payload: {
-          exploreId: ExploreId.left,
-          scanner: jest.fn(),
-        },
+      const scanner = jest.fn();
+      const initalState = {
+        ...makeExploreItemState(),
+        scanning: false,
+        scanner: undefined,
       };
-      state = itemReducer(state, action);
-      expect(state.scanning).toBeTruthy();
-      expect(state.scanner).toBe(action.payload.scanner);
+
+      reducerTester()
+        .givenReducer(itemReducer as Reducer<ExploreItemState, ActionOf<any>>, initalState)
+        .whenActionIsDispatched(scanStartAction({ exploreId: ExploreId.left, scanner }))
+        .thenStateShouldEqual({
+          ...makeExploreItemState(),
+          scanning: true,
+          scanner,
+        });
     });
     test('should stop scanning', () => {
-      let state = makeExploreItemState();
-      const start: Action = {
-        type: ActionTypes.ScanStart,
-        payload: {
-          exploreId: ExploreId.left,
-          scanner: jest.fn(),
-        },
+      const scanner = jest.fn();
+      const initalState = {
+        ...makeExploreItemState(),
+        scanning: true,
+        scanner,
+        scanRange: {},
       };
-      state = itemReducer(state, start);
-      expect(state.scanning).toBeTruthy();
-      const action: Action = {
-        type: ActionTypes.ScanStop,
-        payload: {
-          exploreId: ExploreId.left,
-        },
-      };
-      state = itemReducer(state, action);
-      expect(state.scanning).toBeFalsy();
-      expect(state.scanner).toBeUndefined();
+
+      reducerTester()
+        .givenReducer(itemReducer as Reducer<ExploreItemState, ActionOf<any>>, initalState)
+        .whenActionIsDispatched(scanStopAction({ exploreId: ExploreId.left }))
+        .thenStateShouldEqual({
+          ...makeExploreItemState(),
+          scanning: false,
+          scanner: undefined,
+          scanRange: undefined,
+        });
     });
   });
 });
diff --git a/public/app/features/explore/state/reducers.ts b/public/app/features/explore/state/reducers.ts
index 14c8d87bbd2..fc9be0c28b8 100644
--- a/public/app/features/explore/state/reducers.ts
+++ b/public/app/features/explore/state/reducers.ts
@@ -7,7 +7,36 @@ import {
 import { ExploreItemState, ExploreState, QueryTransaction } from 'app/types/explore';
 import { DataQuery } from '@grafana/ui/src/types';
 
-import { Action, ActionTypes } from './actionTypes';
+import { HigherOrderAction, ActionTypes } from './actionTypes';
+import { reducerFactory } from 'app/core/redux';
+import {
+  addQueryRowAction,
+  changeQueryAction,
+  changeSizeAction,
+  changeTimeAction,
+  clearQueriesAction,
+  highlightLogsExpressionAction,
+  initializeExploreAction,
+  updateDatasourceInstanceAction,
+  loadDatasourceFailureAction,
+  loadDatasourceMissingAction,
+  loadDatasourcePendingAction,
+  loadDatasourceSuccessAction,
+  modifyQueriesAction,
+  queryTransactionFailureAction,
+  queryTransactionStartAction,
+  queryTransactionSuccessAction,
+  removeQueryRowAction,
+  runQueriesEmptyAction,
+  scanRangeAction,
+  scanStartAction,
+  scanStopAction,
+  setQueriesAction,
+  toggleGraphAction,
+  toggleLogsAction,
+  toggleTableAction,
+  queriesImportedAction,
+} from './actionTypes';
 
 export const DEFAULT_RANGE = {
   from: 'now-6h',
@@ -58,9 +87,10 @@ export const initialExploreState: ExploreState = {
 /**
  * Reducer for an Explore area, to be used by the global Explore reducer.
  */
-export const itemReducer = (state, action: Action): ExploreItemState => {
-  switch (action.type) {
-    case ActionTypes.AddQueryRow: {
+export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemState)
+  .addMapper({
+    filter: addQueryRowAction,
+    mapper: (state, action): ExploreItemState => {
       const { initialQueries, modifiedQueries, queryTransactions } = state;
       const { index, query } = action.payload;
 
@@ -77,10 +107,7 @@ export const itemReducer = (state, action: Action): ExploreItemState => {
       // Ongoing transactions need to update their row indices
       const nextQueryTransactions = queryTransactions.map(qt => {
         if (qt.rowIndex > index) {
-          return {
-            ...qt,
-            rowIndex: qt.rowIndex + 1,
-          };
+          return { ...qt, rowIndex: qt.rowIndex + 1 };
         }
         return qt;
       });
@@ -92,9 +119,11 @@ export const itemReducer = (state, action: Action): ExploreItemState => {
         modifiedQueries: nextModifiedQueries,
         queryTransactions: nextQueryTransactions,
       };
-    }
-
-    case ActionTypes.ChangeQuery: {
+    },
+  })
+  .addMapper({
+    filter: changeQueryAction,
+    mapper: (state, action): ExploreItemState => {
       const { initialQueries, queryTransactions } = state;
       let { modifiedQueries } = state;
       const { query, index, override } = action.payload;
@@ -102,17 +131,11 @@ export const itemReducer = (state, action: Action): ExploreItemState => {
       // Fast path: only change modifiedQueries to not trigger an update
       modifiedQueries[index] = query;
       if (!override) {
-        return {
-          ...state,
-          modifiedQueries,
-        };
+        return { ...state, modifiedQueries };
       }
 
       // Override path: queries are completely reset
-      const nextQuery: DataQuery = {
-        ...query,
-        ...generateEmptyQuery(index),
-      };
+      const nextQuery: DataQuery = { ...query, ...generateEmptyQuery(index) };
       const nextQueries = [...initialQueries];
       nextQueries[index] = nextQuery;
       modifiedQueries = [...nextQueries];
@@ -126,9 +149,11 @@ export const itemReducer = (state, action: Action): ExploreItemState => {
         modifiedQueries: nextQueries.slice(),
         queryTransactions: nextQueryTransactions,
       };
-    }
-
-    case ActionTypes.ChangeSize: {
+    },
+  })
+  .addMapper({
+    filter: changeSizeAction,
+    mapper: (state, action): ExploreItemState => {
       const { range, datasourceInstance } = state;
       let interval = '1s';
       if (datasourceInstance && datasourceInstance.interval) {
@@ -137,16 +162,17 @@ export const itemReducer = (state, action: Action): ExploreItemState => {
       const containerWidth = action.payload.width;
       const queryIntervals = getIntervals(range, interval, containerWidth);
       return { ...state, containerWidth, queryIntervals };
-    }
-
-    case ActionTypes.ChangeTime: {
-      return {
-        ...state,
-        range: action.payload.range,
-      };
-    }
-
-    case ActionTypes.ClearQueries: {
+    },
+  })
+  .addMapper({
+    filter: changeTimeAction,
+    mapper: (state, action): ExploreItemState => {
+      return { ...state, range: action.payload.range };
+    },
+  })
+  .addMapper({
+    filter: clearQueriesAction,
+    mapper: (state): ExploreItemState => {
       const queries = ensureQueries();
       return {
         ...state,
@@ -155,14 +181,18 @@ export const itemReducer = (state, action: Action): ExploreItemState => {
         queryTransactions: [],
         showingStartPage: Boolean(state.StartPage),
       };
-    }
-
-    case ActionTypes.HighlightLogsExpression: {
+    },
+  })
+  .addMapper({
+    filter: highlightLogsExpressionAction,
+    mapper: (state, action): ExploreItemState => {
       const { expressions } = action.payload;
       return { ...state, logsHighlighterExpressions: expressions };
-    }
-
-    case ActionTypes.InitializeExplore: {
+    },
+  })
+  .addMapper({
+    filter: initializeExploreAction,
+    mapper: (state, action): ExploreItemState => {
       const { containerWidth, eventBridge, exploreDatasources, queries, range } = action.payload;
       return {
         ...state,
@@ -174,30 +204,37 @@ export const itemReducer = (state, action: Action): ExploreItemState => {
         initialized: true,
         modifiedQueries: queries.slice(),
       };
-    }
-
-    case ActionTypes.UpdateDatasourceInstance: {
+    },
+  })
+  .addMapper({
+    filter: updateDatasourceInstanceAction,
+    mapper: (state, action): ExploreItemState => {
       const { datasourceInstance } = action.payload;
-      return {
-        ...state,
-        datasourceInstance,
-        datasourceName: datasourceInstance.name,
-      };
-    }
-
-    case ActionTypes.LoadDatasourceFailure: {
+      return { ...state, datasourceInstance };
+      /*datasourceName: datasourceInstance.name removed after refactor, datasourceName does not exists on ExploreItemState */
+    },
+  })
+  .addMapper({
+    filter: loadDatasourceFailureAction,
+    mapper: (state, action): ExploreItemState => {
       return { ...state, datasourceError: action.payload.error, datasourceLoading: false };
-    }
-
-    case ActionTypes.LoadDatasourceMissing: {
+    },
+  })
+  .addMapper({
+    filter: loadDatasourceMissingAction,
+    mapper: (state): ExploreItemState => {
       return { ...state, datasourceMissing: true, datasourceLoading: false };
-    }
-
-    case ActionTypes.LoadDatasourcePending: {
+    },
+  })
+  .addMapper({
+    filter: loadDatasourcePendingAction,
+    mapper: (state, action): ExploreItemState => {
       return { ...state, datasourceLoading: true, requestedDatasourceName: action.payload.requestedDatasourceName };
-    }
-
-    case ActionTypes.LoadDatasourceSuccess: {
+    },
+  })
+  .addMapper({
+    filter: loadDatasourceSuccessAction,
+    mapper: (state, action): ExploreItemState => {
       const { containerWidth, range } = state;
       const {
         StartPage,
@@ -226,9 +263,11 @@ export const itemReducer = (state, action: Action): ExploreItemState => {
         logsHighlighterExpressions: undefined,
         queryTransactions: [],
       };
-    }
-
-    case ActionTypes.ModifyQueries: {
+    },
+  })
+  .addMapper({
+    filter: modifyQueriesAction,
+    mapper: (state, action): ExploreItemState => {
       const { initialQueries, modifiedQueries, queryTransactions } = state;
       const { modification, index, modifier } = action.payload;
       let nextQueries: DataQuery[];
@@ -246,12 +285,7 @@ export const itemReducer = (state, action: Action): ExploreItemState => {
         nextQueries = initialQueries.map((query, i) => {
           // Synchronize all queries with local query cache to ensure consistency
           // TODO still needed?
-          return i === index
-            ? {
-                ...modifier(modifiedQueries[i], modification),
-                ...generateEmptyQuery(i),
-              }
-            : query;
+          return i === index ? { ...modifier(modifiedQueries[i], modification), ...generateEmptyQuery(i) } : query;
         });
         nextQueryTransactions = queryTransactions
           // Consume the hint corresponding to the action
@@ -270,18 +304,18 @@ export const itemReducer = (state, action: Action): ExploreItemState => {
         modifiedQueries: nextQueries.slice(),
         queryTransactions: nextQueryTransactions,
       };
-    }
-
-    case ActionTypes.QueryTransactionFailure: {
+    },
+  })
+  .addMapper({
+    filter: queryTransactionFailureAction,
+    mapper: (state, action): ExploreItemState => {
       const { queryTransactions } = action.payload;
-      return {
-        ...state,
-        queryTransactions,
-        showingStartPage: false,
-      };
-    }
-
-    case ActionTypes.QueryTransactionStart: {
+      return { ...state, queryTransactions, showingStartPage: false };
+    },
+  })
+  .addMapper({
+    filter: queryTransactionStartAction,
+    mapper: (state, action): ExploreItemState => {
       const { queryTransactions } = state;
       const { resultType, rowIndex, transaction } = action.payload;
       // Discarding existing transactions of same type
@@ -292,14 +326,12 @@ export const itemReducer = (state, action: Action): ExploreItemState => {
       // Append new transaction
       const nextQueryTransactions: QueryTransaction[] = [...remainingTransactions, transaction];
 
-      return {
-        ...state,
-        queryTransactions: nextQueryTransactions,
-        showingStartPage: false,
-      };
-    }
-
-    case ActionTypes.QueryTransactionSuccess: {
+      return { ...state, queryTransactions: nextQueryTransactions, showingStartPage: false };
+    },
+  })
+  .addMapper({
+    filter: queryTransactionSuccessAction,
+    mapper: (state, action): ExploreItemState => {
       const { datasourceInstance, queryIntervals } = state;
       const { history, queryTransactions } = action.payload;
       const results = calculateResultsFromQueryTransactions(
@@ -308,16 +340,12 @@ export const itemReducer = (state, action: Action): ExploreItemState => {
         queryIntervals.intervalMs
       );
 
-      return {
-        ...state,
-        ...results,
-        history,
-        queryTransactions,
-        showingStartPage: false,
-      };
-    }
-
-    case ActionTypes.RemoveQueryRow: {
+      return { ...state, ...results, history, queryTransactions, showingStartPage: false };
+    },
+  })
+  .addMapper({
+    filter: removeQueryRowAction,
+    mapper: (state, action): ExploreItemState => {
       const { datasourceInstance, initialQueries, queryIntervals, queryTransactions } = state;
       let { modifiedQueries } = state;
       const { index } = action.payload;
@@ -346,21 +374,29 @@ export const itemReducer = (state, action: Action): ExploreItemState => {
         modifiedQueries: nextQueries.slice(),
         queryTransactions: nextQueryTransactions,
       };
-    }
-
-    case ActionTypes.RunQueriesEmpty: {
+    },
+  })
+  .addMapper({
+    filter: runQueriesEmptyAction,
+    mapper: (state): ExploreItemState => {
       return { ...state, queryTransactions: [] };
-    }
-
-    case ActionTypes.ScanRange: {
+    },
+  })
+  .addMapper({
+    filter: scanRangeAction,
+    mapper: (state, action): ExploreItemState => {
       return { ...state, scanRange: action.payload.range };
-    }
-
-    case ActionTypes.ScanStart: {
+    },
+  })
+  .addMapper({
+    filter: scanStartAction,
+    mapper: (state, action): ExploreItemState => {
       return { ...state, scanning: true, scanner: action.payload.scanner };
-    }
-
-    case ActionTypes.ScanStop: {
+    },
+  })
+  .addMapper({
+    filter: scanStopAction,
+    mapper: (state): ExploreItemState => {
       const { queryTransactions } = state;
       const nextQueryTransactions = queryTransactions.filter(qt => qt.scanning && !qt.done);
       return {
@@ -370,14 +406,18 @@ export const itemReducer = (state, action: Action): ExploreItemState => {
         scanRange: undefined,
         scanner: undefined,
       };
-    }
-
-    case ActionTypes.SetQueries: {
+    },
+  })
+  .addMapper({
+    filter: setQueriesAction,
+    mapper: (state, action): ExploreItemState => {
       const { queries } = action.payload;
       return { ...state, initialQueries: queries.slice(), modifiedQueries: queries.slice() };
-    }
-
-    case ActionTypes.ToggleGraph: {
+    },
+  })
+  .addMapper({
+    filter: toggleGraphAction,
+    mapper: (state): ExploreItemState => {
       const showingGraph = !state.showingGraph;
       let nextQueryTransactions = state.queryTransactions;
       if (!showingGraph) {
@@ -385,9 +425,11 @@ export const itemReducer = (state, action: Action): ExploreItemState => {
         nextQueryTransactions = state.queryTransactions.filter(qt => qt.resultType !== 'Graph');
       }
       return { ...state, queryTransactions: nextQueryTransactions, showingGraph };
-    }
-
-    case ActionTypes.ToggleLogs: {
+    },
+  })
+  .addMapper({
+    filter: toggleLogsAction,
+    mapper: (state): ExploreItemState => {
       const showingLogs = !state.showingLogs;
       let nextQueryTransactions = state.queryTransactions;
       if (!showingLogs) {
@@ -395,9 +437,11 @@ export const itemReducer = (state, action: Action): ExploreItemState => {
         nextQueryTransactions = state.queryTransactions.filter(qt => qt.resultType !== 'Logs');
       }
       return { ...state, queryTransactions: nextQueryTransactions, showingLogs };
-    }
-
-    case ActionTypes.ToggleTable: {
+    },
+  })
+  .addMapper({
+    filter: toggleTableAction,
+    mapper: (state): ExploreItemState => {
       const showingTable = !state.showingTable;
       if (showingTable) {
         return { ...state, showingTable, queryTransactions: state.queryTransactions };
@@ -412,25 +456,21 @@ export const itemReducer = (state, action: Action): ExploreItemState => {
       );
 
       return { ...state, ...results, queryTransactions: nextQueryTransactions, showingTable };
-    }
-
-    case ActionTypes.QueriesImported: {
-      return {
-        ...state,
-        initialQueries: action.payload.queries,
-        modifiedQueries: action.payload.queries.slice(),
-      };
-    }
-  }
-
-  return state;
-};
+    },
+  })
+  .addMapper({
+    filter: queriesImportedAction,
+    mapper: (state, action): ExploreItemState => {
+      return { ...state, initialQueries: action.payload.queries, modifiedQueries: action.payload.queries.slice() };
+    },
+  })
+  .create();
 
 /**
  * Global Explore reducer that handles multiple Explore areas (left and right).
  * Actions that have an `exploreId` get routed to the ExploreItemReducer.
  */
-export const exploreReducer = (state = initialExploreState, action: Action): ExploreState => {
+export const exploreReducer = (state = initialExploreState, action: HigherOrderAction): ExploreState => {
   switch (action.type) {
     case ActionTypes.SplitClose: {
       return { ...state, split: false };
@@ -453,10 +493,7 @@ export const exploreReducer = (state = initialExploreState, action: Action): Exp
     const { exploreId } = action.payload as any;
     if (exploreId !== undefined) {
       const exploreItemState = state[exploreId];
-      return {
-        ...state,
-        [exploreId]: itemReducer(exploreItemState, action),
-      };
+      return { ...state, [exploreId]: itemReducer(exploreItemState, action) };
     }
   }