diff --git a/frontend/src/metabase/entities/databases.js b/frontend/src/metabase/entities/databases.js
index 1f0fc2fdb8bc4a1be040eb03aee9c2473f153ed1..b387183a80d914d2103f720ce55e00c1a167c97e 100644
--- a/frontend/src/metabase/entities/databases.js
+++ b/frontend/src/metabase/entities/databases.js
@@ -11,7 +11,7 @@ import { MetabaseApi } from "metabase/services";
 import { DatabaseSchema } from "metabase/schema";
 import Fields from "metabase/entities/fields";
 
-import { getFields } from "metabase/selectors/metadata";
+import { getMetadata, getFields } from "metabase/selectors/metadata";
 import { createSelector } from "reselect";
 
 // OBJECT ACTIONS
@@ -57,6 +57,8 @@ const Databases = createEntity({
   },
 
   selectors: {
+    getObject: (state, { entityId }) => getMetadata(state).database(entityId),
+
     getHasSampleDataset: state =>
       _.any(Databases.selectors.getList(state), db => db.is_sample),
     getIdfields: createSelector(
diff --git a/frontend/src/metabase/entities/fields.js b/frontend/src/metabase/entities/fields.js
index 500c9db9dc0fdeffa99f78e1869c49371dba06c3..d7fcb83ee5bf8648eb02dc603c2d4e2b78fcd0e0 100644
--- a/frontend/src/metabase/entities/fields.js
+++ b/frontend/src/metabase/entities/fields.js
@@ -12,6 +12,8 @@ import { assocIn, updateIn } from "icepick";
 import { FieldSchema } from "metabase/schema";
 import { MetabaseApi } from "metabase/services";
 
+import { getMetadata } from "metabase/selectors/metadata";
+
 import {
   field_visibility_types,
   field_special_types,
@@ -40,6 +42,10 @@ export default createEntity({
   path: "/api/field",
   schema: FieldSchema,
 
+  selectors: {
+    getObject: (state, { entityId }) => getMetadata(state).field(entityId),
+  },
+
   // ACTION CREATORS
 
   objectActions: {
diff --git a/frontend/src/metabase/entities/metrics.js b/frontend/src/metabase/entities/metrics.js
index 0dfd89c2cb138e86213048a737ec814f3a608bb3..a23eb29ccd5345b83533a7aa0cef761fe385ec2e 100644
--- a/frontend/src/metabase/entities/metrics.js
+++ b/frontend/src/metabase/entities/metrics.js
@@ -4,6 +4,8 @@ import { MetricSchema } from "metabase/schema";
 import { color } from "metabase/lib/colors";
 import * as Urls from "metabase/lib/urls";
 
+import { getMetadata } from "metabase/selectors/metadata";
+
 const Metrics = createEntity({
   name: "metrics",
   nameOne: "metric",
@@ -30,6 +32,10 @@ const Metrics = createEntity({
     getIcon: metric => "sum",
   },
 
+  selectors: {
+    getObject: (state, { entityId }) => getMetadata(state).metric(entityId),
+  },
+
   form: {
     fields: [{ name: "name" }, { name: "description", type: "text" }],
   },
diff --git a/frontend/src/metabase/entities/segments.js b/frontend/src/metabase/entities/segments.js
index 21fb5615ec0051119458243cceaafcbfe9645892..fb1a6e30a6010f903e1036796c3f0c1342238fcd 100644
--- a/frontend/src/metabase/entities/segments.js
+++ b/frontend/src/metabase/entities/segments.js
@@ -6,6 +6,8 @@ import { SegmentSchema } from "metabase/schema";
 import { color } from "metabase/lib/colors";
 import * as Urls from "metabase/lib/urls";
 
+import { getMetadata } from "metabase/selectors/metadata";
+
 const Segments = createEntity({
   name: "segments",
   nameOne: "segment",
@@ -24,6 +26,10 @@ const Segments = createEntity({
     delete: null,
   },
 
+  selectors: {
+    getObject: (state, { entityId }) => getMetadata(state).segment(entityId),
+  },
+
   objectSelectors: {
     getName: segment => segment && segment.name,
     getUrl: segment =>
diff --git a/frontend/src/metabase/entities/tables.js b/frontend/src/metabase/entities/tables.js
index 6417822dc1333a4bea9f2382416d368696ca16ce..a06c7a8bb924f4994af8b151e838375314f54e30 100644
--- a/frontend/src/metabase/entities/tables.js
+++ b/frontend/src/metabase/entities/tables.js
@@ -176,6 +176,8 @@ const Tables = createEntity({
   },
 
   selectors: {
+    getObject: (state, { entityId }) => getMetadata(state).table(entityId),
+
     getTable: createSelector(
       // we wrap getMetadata to handle a circular dep issue
       [state => getMetadata(state), (state, props) => props.entityId],
diff --git a/frontend/src/metabase/lib/entities.js b/frontend/src/metabase/lib/entities.js
index 9f5bc72c6fa74469e021aa85a872f70497f4292d..7381846b6e3d11fec33bd07897a61a51c1089bed 100644
--- a/frontend/src/metabase/lib/entities.js
+++ b/frontend/src/metabase/lib/entities.js
@@ -9,6 +9,7 @@ import {
   withRequestState,
   withCachedDataAndRequestState,
 } from "metabase/lib/redux";
+import createCachedSelector from "re-reselect";
 
 import { addUndo } from "metabase/redux/undo";
 
@@ -458,10 +459,12 @@ export function createEntity(def: EntityDefinition): Entity {
   const getEntityId = (state, props) =>
     (props.params && props.params.entityId) || props.entityId;
 
-  const getObject = createSelector(
+  const getObject = createCachedSelector(
     [getEntities, getEntityId],
     (entities, entityId) => denormalize(entityId, entity.schema, entities),
-  );
+  )((state, { entityId }) =>
+    typeof entityId === "object" ? JSON.stringify(entityId) : entityId,
+  ); // must stringify objects
 
   // LIST SELECTORS
 
@@ -479,8 +482,13 @@ export function createEntity(def: EntityDefinition): Entity {
   );
 
   const getList = createSelector(
-    [getEntities, getEntityIds],
-    (entities, entityIds) => denormalize(entityIds, [entity.schema], entities),
+    [state => state, getEntityIds],
+    // delegate to getObject
+    (state, entityIds) =>
+      entityIds &&
+      entityIds.map(entityId =>
+        entity.selectors.getObject(state, { entityId }),
+      ),
   );
 
   // REQUEST STATE SELECTORS
diff --git a/frontend/src/metabase/store.js b/frontend/src/metabase/store.js
index da4b8fc0aa881b40429f576fdd4a522c15dc5024..b7e4ac6d0cce976a02eb4bea35673e83f4b533d4 100644
--- a/frontend/src/metabase/store.js
+++ b/frontend/src/metabase/store.js
@@ -13,7 +13,10 @@ import { DEBUG } from "metabase/lib/debug";
  * Provides the same functionality as redux-thunk and augments the dispatch method with
  * `dispatch.action(type, payload)` which creates an action that adheres to Flux Standard Action format.
  */
-const thunkWithDispatchAction = ({ dispatch, getState }) => next => action => {
+export const thunkWithDispatchAction = ({
+  dispatch,
+  getState,
+}) => next => action => {
   if (typeof action === "function") {
     const dispatchAugmented = Object.assign(dispatch, {
       action: (type, payload) => dispatch({ type, payload }),
diff --git a/frontend/test/metabase/entities/entities.unit.spec.js b/frontend/test/metabase/entities/entities.unit.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..14a6976cc8814258a3114dc3735d8c69a72c798b
--- /dev/null
+++ b/frontend/test/metabase/entities/entities.unit.spec.js
@@ -0,0 +1,70 @@
+import { createEntity, combineEntities } from "metabase/lib/entities";
+import requestsReducer from "metabase/redux/requests";
+import { combineReducers, applyMiddleware, createStore, compose } from "redux";
+
+import promise from "redux-promise";
+import { thunkWithDispatchAction } from "metabase/store";
+
+const widgets = createEntity({
+  name: "widgets",
+  api: {
+    get: ({ id }) => ({ id: id, name: "object" + id }),
+  },
+});
+
+const entities = combineEntities([widgets]);
+
+const reducer = combineReducers({
+  entities: entities.reducer,
+  requests: (state, action) =>
+    requestsReducer(entities.requestsReducer(state, action), action),
+});
+
+const initialState = {
+  entities: {
+    widgets: {
+      1: { name: "foo " },
+      2: { name: "bar" },
+    },
+  },
+};
+
+describe("entities", () => {
+  let store;
+  beforeEach(() => {
+    store = createStore(
+      reducer,
+      initialState,
+      compose(applyMiddleware(thunkWithDispatchAction, promise)),
+    );
+  });
+  describe("getObject", () => {
+    it("should return an object", () => {
+      expect(
+        widgets.selectors.getObject(initialState, { entityId: 1 }),
+      ).toEqual({ name: "foo " });
+    });
+    it("should cache the object", () => {
+      const a1 = widgets.selectors.getObject(initialState, { entityId: 1 });
+      const a2 = widgets.selectors.getObject(initialState, { entityId: 1 });
+      expect(a1).toBe(a2);
+    });
+    it("should cache multiple objects", () => {
+      const a1 = widgets.selectors.getObject(initialState, { entityId: 1 });
+      const b1 = widgets.selectors.getObject(initialState, { entityId: 2 });
+      const a2 = widgets.selectors.getObject(initialState, { entityId: 1 });
+      const b2 = widgets.selectors.getObject(initialState, { entityId: 2 });
+      expect(a1).toBe(a2);
+      expect(b1).toBe(b2);
+    });
+  });
+  describe("fetch", () => {
+    it("should fetch an entity", async () => {
+      await store.dispatch(widgets.actions.fetch({ id: 3 }));
+      const object = widgets.selectors.getObject(store.getState(), {
+        entityId: 3,
+      });
+      expect(object).toEqual({ id: 3, name: "object3" });
+    });
+  });
+});
diff --git a/package.json b/package.json
index b22dea9f8ce86ad1ebff03481762c3491c75a0f6..fc9786df4967a4673f41d88390bd53ee4d2bcdb5 100644
--- a/package.json
+++ b/package.json
@@ -43,6 +43,7 @@
     "number-to-locale-string": "^1.0.1",
     "password-generator": "^2.0.1",
     "prop-types": "^15.5.7",
+    "re-reselect": "^3.4.0",
     "react": "15",
     "react-addons-shallow-compare": "^15.2.1",
     "react-ansi-style": "^1.0.0",
diff --git a/yarn.lock b/yarn.lock
index 4b2d1aa3bba083a40aa1332448c06cba0ac81df4..88c0a5f235bb0cdd535cbdb83d552c5a1a9b33dc 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -11194,6 +11194,11 @@ rc@^1.1.7:
     minimist "^1.2.0"
     strip-json-comments "~2.0.1"
 
+re-reselect@^3.4.0:
+  version "3.4.0"
+  resolved "https://registry.yarnpkg.com/re-reselect/-/re-reselect-3.4.0.tgz#0f2303f3c84394f57f0cd31fea08a1ca4840a7cd"
+  integrity sha512-JsecfN+JlckncVXTWFWjn0Vk6uInl8GSf4eEd9tTk5qXHlgqkPdILpnYpgZcISXNYAzvfvsCZviaDk8AxyS5sg==
+
 react-addons-shallow-compare@^15.2.1:
   version "15.6.2"
   resolved "https://registry.yarnpkg.com/react-addons-shallow-compare/-/react-addons-shallow-compare-15.6.2.tgz#198a00b91fc37623db64a28fd17b596ba362702f"