Repository: apollographql/apollo-client

Problem statement:
Circular dependencies
```
store.js -> optimistic-data/store.js -> store.js
core/ObservableQuery.js -> core/QueryManager.js -> scheduler/scheduler.js -> core/ObservableQuery.js
core/QueryManager.js -> scheduler/scheduler.js -> core/QueryManager.js
optimistic-data/store.js -> store.js -> optimistic-data/store.js
scheduler/scheduler.js -> core/QueryManager.js -> scheduler/scheduler.js
```

@DxCx 
Write subscription results under `ROOT_SUBSCRIPTION` ID instead of `ROOT_QUERY`
This will help show in the Apollo dev tools whether data came from a subscription or query.


Hints:

Patch:
diff --git a/rollup.config.js b/rollup.config.js
new file mode 100644
--- /dev/null
+++ b/rollup.config.js
@@ -0,0 +1,19 @@
+export default {
+  entry: 'lib/src/index.js',
+  dest: 'lib/apollo.umd.js',
+  format: 'umd',
+  sourceMap: true,
+  moduleName: 'apollo',
+  onwarn
+};
+
+function onwarn(message) {
+  const suppressed = [
+    'UNRESOLVED_IMPORT',
+    'THIS_IS_UNDEFINED'
+  ];
+
+  if (!suppressed.find(code => message.code === code)) {
+    return console.warn(message.message);
+  }
+}
diff --git a/src/ApolloClient.ts b/src/ApolloClient.ts
--- a/src/ApolloClient.ts
+++ b/src/ApolloClient.ts
@@ -13,9 +13,6 @@ import {
 
 } from 'graphql';
 
-import isUndefined = require('lodash/isUndefined');
-import isString = require('lodash/isString');
-
 import {
   createApolloStore,
   ApolloStore,
@@ -30,10 +27,14 @@ import {
 
 import {
   QueryManager,
+} from './core/QueryManager';
+
+import {
   ApolloQueryResult,
   ResultComparator,
   ResultTransformer,
-} from './core/QueryManager';
+  IdGetter,
+} from './core/types';
 
 import {
   ObservableQuery,
@@ -44,15 +45,11 @@ import {
 } from './util/Observable';
 
 import {
-  DeprecatedWatchQueryOptions,
-  DeprecatedSubscriptionOptions,
+  WatchQueryOptions,
+  SubscriptionOptions,
   MutationOptions,
 } from './core/watchQueryOptions';
 
-import {
-  IdGetter,
-} from './data/extensions';
-
 import {
   MutationBehaviorReducerMap,
 } from './data/mutationResults';
@@ -61,12 +58,6 @@ import {
   storeKeyNameFromFieldNameAndArgs,
 } from './data/storeUtils';
 
-import { createFragment } from './fragments';
-
-import {
-  addFragmentsToDocument,
-} from './queries/getFromAST';
-
 import {
   version,
 } from './version';
@@ -85,12 +76,6 @@ function defaultReduxRootSelector(state: any) {
   return state[DEFAULT_REDUX_ROOT_KEY];
 }
 
-// deprecation warning flags
-let haveWarnedQuery = false;
-let haveWarnedWatchQuery = false;
-let haveWarnedMutation = false;
-let haveWarnedSubscription = false;
-
 /**
  * This is the primary Apollo Client class. It is used to send GraphQL documents (i.e. queries
  * and mutations) to a GraphQL spec-compliant server over a {@link NetworkInterface} instance,
@@ -192,7 +177,7 @@ export default class ApolloClient {
 
     if (!reduxRootSelector && reduxRootKey) {
       this.reduxRootSelector = (state: any) => state[reduxRootKey];
-    } else if (isString(reduxRootSelector)) {
+    } else if (typeof reduxRootSelector === 'string') {
       // for backwards compatibility, we set reduxRootKey if reduxRootSelector is a string
       this.reduxRootKey = reduxRootSelector as string;
       this.reduxRootSelector = (state: any) => state[reduxRootSelector as string];
@@ -207,6 +192,13 @@ export default class ApolloClient {
     this.networkInterface = networkInterface ? networkInterface :
       createNetworkInterface({ uri: '/graphql' });
     this.addTypename = addTypename;
+    if (resultTransformer) {
+      console.warn(
+        '"resultTransformer" is being considered for deprecation in an upcoming version. ' +
+        'If you are using it, please file an issue on apollostack/apollo-client ' +
+        'with a description of your use-case',
+      );
+    }
     this.resultTransformer = resultTransformer;
     this.resultComparator = resultComparator;
     this.shouldForceFetch = !(ssrMode || ssrForceFetchDelay > 0);
@@ -265,44 +257,17 @@ export default class ApolloClient {
    * a description of store reactivity.
    *
    */
-  public watchQuery(options: DeprecatedWatchQueryOptions): ObservableQuery {
+  public watchQuery<T>(options: WatchQueryOptions): ObservableQuery<T> {
     this.initStore();
 
     if (!this.shouldForceFetch && options.forceFetch) {
       options = {
         ...options,
         forceFetch: false,
-      } as DeprecatedWatchQueryOptions;
+      } as WatchQueryOptions;
     }
 
-    if (options.fragments && !haveWarnedWatchQuery && process.env.NODE_ENV !== 'production') {
-      console.warn(
-            '"fragments" option is deprecated and will be removed in the upcoming versions, ' +
-            'please refer to the documentation for how to define fragments: ' +
-            'http://dev.apollodata.com/react/fragments.html.',
-      );
-      /* istanbul ignore if */
-      if (process.env.NODE_ENV !== 'test') {
-        // When running tests, we want to print the warning every time
-        haveWarnedWatchQuery = true;
-      }
-    }
-
-    // Register each of the fragments present in the query document. The point
-    // is to prevent fragment name collisions with fragments that are in the query
-    // document itself.
-    createFragment(options.query, undefined, true);
-
-    // We add the fragments to the document to pass only the document around internally.
-    const fullDocument = addFragmentsToDocument(options.query, options.fragments);
-
-    const realOptions = {
-      ...options,
-      query: fullDocument,
-    };
-    delete realOptions.fragments;
-
-    return this.queryManager.watchQuery(realOptions);
+    return this.queryManager.watchQuery<T>(options);
   };
 
   /**
@@ -314,7 +279,7 @@ export default class ApolloClient {
    * how this query should be treated e.g. whether it is a polling query, whether it should hit the
    * server at all or just resolve from the cache, etc.
    */
-  public query(options: DeprecatedWatchQueryOptions): Promise<ApolloQueryResult> {
+  public query<T>(options: WatchQueryOptions): Promise<ApolloQueryResult<T>> {
     this.initStore();
 
     // XXX what if I pass pollInterval? Will it just keep running?
@@ -324,37 +289,10 @@ export default class ApolloClient {
       options = {
         ...options,
         forceFetch: false,
-      } as DeprecatedWatchQueryOptions;
+      } as WatchQueryOptions;
     }
 
-    if (options.fragments && !haveWarnedQuery && process.env.NODE_ENV !== 'production') {
-      console.warn(
-            '"fragments" option is deprecated and will be removed in the upcoming versions, ' +
-            'please refer to the documentation for how to define fragments: ' +
-            'http://dev.apollodata.com/react/fragments.html.',
-      );
-      /* istanbul ignore if */
-      if (process.env.NODE_ENV !== 'test') {
-        // When running tests, we want to print the warning every time
-        haveWarnedQuery = true;
-      }
-    }
-
-    // Register each of the fragments present in the query document. The point
-    // is to prevent fragment name collisions with fragments that are in the query
-    // document itself.
-    createFragment(options.query, undefined, true);
-
-    // We add the fragments to the document to pass only the document around internally.
-    const fullDocument = addFragmentsToDocument(options.query, options.fragments);
-
-    const realOptions = {
-      ...options,
-      query: fullDocument,
-    };
-    delete realOptions.fragments;
-
-    return this.queryManager.query(realOptions);
+    return this.queryManager.query<T>(options);
   };
 
   /**
@@ -370,9 +308,6 @@ export default class ApolloClient {
    * @param options.variables An object that maps from the name of a variable as used in the mutation
    * GraphQL document to that variable's value.
    *
-   * @param options.fragments A list of fragments as returned by {@link createFragment}. These fragments
-   * can be referenced from within the GraphQL mutation document.
-   *
    * @param options.optimisticResponse An object that represents the result of this mutation that will be
    * optimistically stored before the server has actually returned a result. This is most often
    * used for optimistic UI, where we want to be able to see the result of a mutation immediately,
@@ -388,58 +323,19 @@ export default class ApolloClient {
    * for this, you can simply refetch the queries that will be affected and achieve a consistent
    * store once these queries return.
    */
-  public mutate(options: MutationOptions): Promise<ApolloQueryResult> {
+  public mutate<T>(options: MutationOptions): Promise<ApolloQueryResult<T>> {
     this.initStore();
 
-    if (options.fragments && !haveWarnedMutation && process.env.NODE_ENV !== 'production') {
-      console.warn(
-            '"fragments" option is deprecated and will be removed in the upcoming versions, ' +
-            'please refer to the documentation for how to define fragments: ' +
-            'http://dev.apollodata.com/react/fragments.html.',
-      );
-      /* istanbul ignore if */
-      if (process.env.NODE_ENV !== 'test') {
-        // When running tests, we want to print the warning every time
-        haveWarnedMutation = true;
-      }
-    }
-
-    // We add the fragments to the document to pass only the document around internally.
-    const fullDocument = addFragmentsToDocument(options.mutation, options.fragments);
-
-    const realOptions = {
-      ...options,
-      mutation: fullDocument,
-    };
-    delete realOptions.fragments;
-
-    return this.queryManager.mutate(realOptions);
+    return this.queryManager.mutate<T>(options);
   };
 
-  public subscribe(options: DeprecatedSubscriptionOptions): Observable<any> {
+  public subscribe(options: SubscriptionOptions): Observable<any> {
     this.initStore();
 
-    if (options.fragments && !haveWarnedSubscription && process.env.NODE_ENV !== 'production') {
-      console.warn(
-            '"fragments" option is deprecated and will be removed in the upcoming versions, ' +
-            'please refer to the documentation for how to define fragments: ' +
-            'http://dev.apollodata.com/react/fragments.html.',
-      );
-      /* istanbul ignore if */
-      if (process.env.NODE_ENV !== 'test') {
-        // When running tests, we want to print the warning every time
-        haveWarnedSubscription = true;
-      }
-    }
-
-    // We add the fragments to the document to pass only the document around internally.
-    const fullDocument = addFragmentsToDocument(options.query, options.fragments);
-
     const realOptions = {
       ...options,
-      document: fullDocument,
+      document: options.query,
     };
-    delete realOptions.fragments;
     delete realOptions.query;
 
     return this.queryManager.startGraphQLSubscription(realOptions);
@@ -461,8 +357,13 @@ export default class ApolloClient {
       this.setStore(store);
 
       return (next: any) => (action: any) => {
+        const previousApolloState = this.queryManager.selectApolloState(store);
         const returnValue = next(action);
-        this.queryManager.broadcastNewStore(store.getState());
+        const newApolloState = this.queryManager.selectApolloState(store);
+
+        if (newApolloState !== previousApolloState) {
+          this.queryManager.broadcastNewStore(store.getState());
+        }
 
         if (this.devToolsHookCb) {
           this.devToolsHookCb({
@@ -538,7 +439,7 @@ export default class ApolloClient {
     }
 
     // ensure existing store has apolloReducer
-    if (isUndefined(reduxRootSelector(store.getState()))) {
+    if (typeof reduxRootSelector(store.getState()) === 'undefined') {
       throw new Error(
           'Existing store does not use apolloReducer. Please make sure the store ' +
           'is properly configured and "reduxRootSelector" is correctly specified.',
diff --git a/src/core/ObservableQuery.ts b/src/core/ObservableQuery.ts
--- a/src/core/ObservableQuery.ts
+++ b/src/core/ObservableQuery.ts
@@ -17,20 +17,21 @@ import {
 
 import {
   QueryManager,
+} from './QueryManager';
+
+import {
   ApolloQueryResult,
   FetchType,
-} from './QueryManager';
+} from './types';
 
 import { tryFunctionOrLogError } from '../util/errorHandling';
 
-import { NetworkStatus } from '../queries/store';
-
-import { addFragmentsToDocument } from '../queries/getFromAST';
+import { isEqual } from '../util/isEqual';
 
-import isEqual = require('lodash/isEqual');
+import { NetworkStatus } from '../queries/store';
 
-export type ApolloCurrentResult = {
-  data: any;
+export type ApolloCurrentResult<T> = {
+  data: T | {};
   loading: boolean;
   networkStatus: NetworkStatus;
   error?: ApolloError;
@@ -47,7 +48,7 @@ export interface UpdateQueryOptions {
   variables: Object;
 }
 
-export class ObservableQuery extends Observable<ApolloQueryResult> {
+export class ObservableQuery<T> extends Observable<ApolloQueryResult<T>> {
   public options: WatchQueryOptions;
   public queryId: string;
   /**
@@ -60,10 +61,10 @@ export class ObservableQuery extends Observable<ApolloQueryResult> {
   private shouldSubscribe: boolean;
   private scheduler: QueryScheduler;
   private queryManager: QueryManager;
-  private observers: Observer<ApolloQueryResult>[];
+  private observers: Observer<ApolloQueryResult<T>>[];
   private subscriptionHandles: Subscription[];
 
-  private lastResult: ApolloQueryResult;
+  private lastResult: ApolloQueryResult<T>;
   private lastError: ApolloError;
 
   constructor({
@@ -78,7 +79,7 @@ export class ObservableQuery extends Observable<ApolloQueryResult> {
     const queryManager = scheduler.queryManager;
     const queryId = queryManager.generateQueryId();
 
-    const subscriberFunction = (observer: Observer<ApolloQueryResult>) => {
+    const subscriberFunction = (observer: Observer<ApolloQueryResult<T>>) => {
       return this.onSubscribe(observer);
     };
 
@@ -95,7 +96,7 @@ export class ObservableQuery extends Observable<ApolloQueryResult> {
     this.subscriptionHandles = [];
   }
 
-  public result(): Promise<ApolloQueryResult> {
+  public result(): Promise<ApolloQueryResult<T>> {
     return new Promise((resolve, reject) => {
       const subscription = this.subscribe({
         next(result) {
@@ -111,7 +112,7 @@ export class ObservableQuery extends Observable<ApolloQueryResult> {
     });
   }
 
-  public currentResult(): ApolloCurrentResult {
+  public currentResult(): ApolloCurrentResult<T> {
     const { data, partial } = this.queryManager.getCurrentQueryResult(this, true);
     const queryStoreValue = this.queryManager.getApolloState().queries[this.queryId];
 
@@ -147,7 +148,13 @@ export class ObservableQuery extends Observable<ApolloQueryResult> {
     return { data, loading, networkStatus };
   }
 
-  public refetch(variables?: any): Promise<ApolloQueryResult> {
+  // Returns the last result that observer.next was called with. This is not the same as
+  // currentResult! If you're not sure which you need, then you probably need currentResult.
+  public getLastResult(): ApolloQueryResult<T> {
+    return this.lastResult;
+  }
+
+  public refetch(variables?: any): Promise<ApolloQueryResult<T>> {
     this.variables = {
       ...this.variables,
       ...variables,
@@ -175,7 +182,7 @@ export class ObservableQuery extends Observable<ApolloQueryResult> {
 
   public fetchMore(
     fetchMoreOptions: FetchMoreQueryOptions & FetchMoreOptions,
-  ): Promise<ApolloQueryResult> {
+  ): Promise<ApolloQueryResult<T>> {
     return Promise.resolve()
       .then(() => {
         const qid = this.queryManager.generateQueryId();
@@ -198,12 +205,9 @@ export class ObservableQuery extends Observable<ApolloQueryResult> {
           };
         }
 
-        // We add the fragments to the document to pass only the document around internally.
-        const fullQuery = addFragmentsToDocument(combinedOptions.query, combinedOptions.fragments);
-
         combinedOptions = {
           ...combinedOptions,
-          query: fullQuery,
+          query: combinedOptions.query,
           forceFetch: true,
         } as WatchQueryOptions;
         return this.queryManager.fetchQuery(qid, combinedOptions);
@@ -271,7 +275,7 @@ export class ObservableQuery extends Observable<ApolloQueryResult> {
     };
   }
 
-  public setOptions(opts: ModifiableWatchQueryOptions): Promise<ApolloQueryResult> {
+  public setOptions(opts: ModifiableWatchQueryOptions): Promise<ApolloQueryResult<T>> {
     const oldOptions = this.options;
     this.options = {
       ...this.options,
@@ -303,7 +307,7 @@ export class ObservableQuery extends Observable<ApolloQueryResult> {
    * @param variables: The new set of variables. If there are missing variables,
    * the previous values of those variables will be used.
    */
-  public setVariables(variables: any): Promise<ApolloQueryResult> {
+  public setVariables(variables: any): Promise<ApolloQueryResult<T>> {
     const newVariables = {
       ...this.variables,
       ...variables,
@@ -365,7 +369,7 @@ export class ObservableQuery extends Observable<ApolloQueryResult> {
     this.scheduler.startPollingQuery(this.options, this.queryId);
   }
 
-  private onSubscribe(observer: Observer<ApolloQueryResult>) {
+  private onSubscribe(observer: Observer<ApolloQueryResult<T>>) {
     this.observers.push(observer);
 
     // Deliver initial result
@@ -401,7 +405,7 @@ export class ObservableQuery extends Observable<ApolloQueryResult> {
 
   private setUpQuery() {
     if (this.shouldSubscribe) {
-      this.queryManager.addObservableQuery(this.queryId, this);
+      this.queryManager.addObservableQuery<T>(this.queryId, this);
     }
 
     if (!!this.options.pollInterval) {
@@ -410,21 +414,20 @@ export class ObservableQuery extends Observable<ApolloQueryResult> {
       }
 
       this.isCurrentlyPolling = true;
-      this.scheduler.startPollingQuery(
+      this.scheduler.startPollingQuery<T>(
         this.options,
         this.queryId,
       );
     }
 
-    const observer: Observer<ApolloQueryResult> = {
-      next: (result: ApolloQueryResult) => {
+    const observer: Observer<ApolloQueryResult<T>> = {
+      next: (result: ApolloQueryResult<T>) => {
+        this.lastResult = result;
         this.observers.forEach((obs) => {
           if (obs.next) {
             obs.next(result);
           }
         });
-
-        this.lastResult = result;
       },
       error: (error: ApolloError) => {
         this.observers.forEach((obs) => {
@@ -439,7 +442,7 @@ export class ObservableQuery extends Observable<ApolloQueryResult> {
       },
     };
 
-    this.queryManager.startQuery(
+    this.queryManager.startQuery<T>(
       this.queryId,
       this.options,
       this.queryManager.queryListenerForObserver(this.queryId, this.options, observer),
diff --git a/src/core/QueryManager.ts b/src/core/QueryManager.ts
--- a/src/core/QueryManager.ts
+++ b/src/core/QueryManager.ts
@@ -8,8 +8,21 @@ import {
   Deduplicator,
 } from '../transport/Deduplicator';
 
-import forOwn = require('lodash/forOwn');
-import isEqual = require('lodash/isEqual');
+import { isEqual } from '../util/isEqual';
+
+import {
+  ResultTransformer,
+  ResultComparator,
+  QueryListener,
+  ApolloQueryResult,
+  FetchType,
+  SubscriptionOptions,
+} from './types';
+
+import {
+  QueryStoreValue,
+  NetworkStatus,
+} from '../queries/store';
 
 import {
   ApolloStore,
@@ -18,10 +31,6 @@ import {
   ApolloReducerConfig,
 } from '../store';
 
-import {
-  QueryStoreValue,
-} from '../queries/store';
-
 import {
   checkDocument,
   getQueryDefinition,
@@ -40,6 +49,8 @@ import {
   createStoreReducer,
 } from '../data/resultReducers';
 
+import maybeDeepFreeze from '../util/maybeDeepFreeze';
+
 import {
   ExecutionResult,
   DocumentNode,
@@ -82,10 +93,6 @@ import {
   Observable,
 } from '../util/Observable';
 
-import {
-  NetworkStatus,
-} from '../queries/store';
-
 import { tryFunctionOrLogError } from '../util/errorHandling';
 
 import {
@@ -97,44 +104,6 @@ import { WatchQueryOptions } from './watchQueryOptions';
 
 import { ObservableQuery } from './ObservableQuery';
 
-export type QueryListener = (queryStoreValue: QueryStoreValue) => void;
-
-export interface SubscriptionOptions {
-  document: DocumentNode;
-  variables?: { [key: string]: any };
-};
-
-export type ApolloQueryResult = {
-  data: any;
-  loading: boolean;
-  networkStatus: NetworkStatus;
-
-  // This type is different from the ExecutionResult type because it doesn't include errors.
-  // Those are thrown via the standard promise/observer catch mechanism.
-};
-
-// A result transformer is given the data that is to be returned from the store from a query or
-// mutation, and can modify or observe it before the value is provided to your application.
-//
-// For watched queries, the transformer is only called when the data retrieved from the server is
-// different from previous.
-//
-// If the transformer wants to mutate results (say, by setting the prototype of result data), it
-// will likely need to be paired with a custom resultComparator.  By default, Apollo performs a
-// deep equality comparsion on results, and skips those that are considered equal - reducing
-// re-renders.
-export type ResultTransformer = (resultData: ApolloQueryResult) => ApolloQueryResult;
-
-// Controls how Apollo compares two query results and considers their equality.  Two equal results
-// will not trigger re-renders.
-export type ResultComparator = (result1: ApolloQueryResult, result2: ApolloQueryResult) => boolean;
-
-export enum FetchType {
-  normal = 1,
-  refetch = 2,
-  poll = 3,
-}
-
 export class QueryManager {
   public pollingTimers: {[queryId: string]: NodeJS.Timer | any}; //oddity in Typescript
   public scheduler: QueryScheduler;
@@ -161,8 +130,8 @@ export class QueryManager {
   // track of queries that are inflight and reject them in case some
   // destabalizing action occurs (e.g. reset of the Apollo store).
   private fetchQueryPromises: { [requestId: string]: {
-    promise: Promise<ApolloQueryResult>;
-    resolve: (result: ApolloQueryResult) => void;
+    promise: Promise<ApolloQueryResult<any>>;
+    resolve: (result: ApolloQueryResult<any>) => void;
     reject: (error: Error) => void;
   } };
 
@@ -170,7 +139,7 @@ export class QueryManager {
   // these to keep track of queries that are inflight and error on the observers associated
   // with them in case of some destabalizing action (e.g. reset of the Apollo store).
   private observableQueries: { [queryId: string]:  {
-    observableQuery: ObservableQuery;
+    observableQuery: ObservableQuery<any>;
   } };
 
   // A map going from the name of a query to an observer issued for it by watchQuery. This is
@@ -241,7 +210,7 @@ export class QueryManager {
     this.broadcastQueries();
   }
 
-  public mutate({
+  public mutate<T>({
     mutation,
     variables,
     resultBehaviors = [],
@@ -255,7 +224,7 @@ export class QueryManager {
     optimisticResponse?: Object,
     updateQueries?: MutationQueryReducersMap,
     refetchQueries?: string[],
-  }): Promise<ApolloQueryResult> {
+  }): Promise<ApolloQueryResult<T>> {
     const mutationId = this.generateQueryId();
 
     if (this.addTypename) {
@@ -317,7 +286,7 @@ export class QueryManager {
 
           refetchQueries.forEach((name) => { this.refetchQueryByName(name); });
           delete this.queryDocuments[mutationId];
-          resolve(this.transformResult(<ApolloQueryResult>result));
+          resolve(this.transformResult(<ApolloQueryResult<T>>result));
         })
         .catch((err) => {
           this.store.dispatch({
@@ -336,12 +305,12 @@ export class QueryManager {
 
   // Returns a query listener that will update the given observer based on the
   // results (or lack thereof) for a particular query.
-  public queryListenerForObserver(
+  public queryListenerForObserver<T>(
     queryId: string,
     options: WatchQueryOptions,
-    observer: Observer<ApolloQueryResult>,
+    observer: Observer<ApolloQueryResult<T>>,
   ): QueryListener {
-    let lastResult: ApolloQueryResult;
+    let lastResult: ApolloQueryResult<T>;
     return (queryStoreValue: QueryStoreValue) => {
       // The query store value can be undefined in the event of a store
       // reset.
@@ -388,22 +357,36 @@ export class QueryManager {
         } else {
           try {
             const resultFromStore = {
-              data: readQueryFromStore({
+              data: readQueryFromStore<T>({
                 store: this.getDataWithOptimisticResults(),
                 query: this.queryDocuments[queryId],
                 variables: queryStoreValue.previousVariables || queryStoreValue.variables,
                 returnPartialData: options.returnPartialData || noFetch,
                 config: this.reducerConfig,
+                previousResult: lastResult && lastResult.data,
               }),
               loading: queryStoreValue.loading,
               networkStatus: queryStoreValue.networkStatus,
             };
 
             if (observer.next) {
-              if (this.isDifferentResult(lastResult, resultFromStore)) {
+              const isDifferentResult =
+                this.resultComparator ? !this.resultComparator(lastResult, resultFromStore) : !(
+                  lastResult &&
+                  resultFromStore &&
+                  lastResult.loading === resultFromStore.loading &&
+                  lastResult.networkStatus === resultFromStore.networkStatus &&
+
+                  // We can do a strict equality check here because we include a `previousResult`
+                  // with `readQueryFromStore`. So if the results are the same they will be
+                  // referentially equal.
+                  lastResult.data === resultFromStore.data
+                );
+
+              if (isDifferentResult) {
                 lastResult = resultFromStore;
                 try {
-                  observer.next(this.transformResult(resultFromStore));
+                  observer.next(maybeDeepFreeze(this.transformResult(resultFromStore)));
                 } catch (e) {
                   console.error(`Error in observer.next \n${e.stack}`);
                 }
@@ -429,7 +412,7 @@ export class QueryManager {
   // supposed to be refetched in the event of a store reset. Once we unify error handling for
   // network errors and non-network errors, the shouldSubscribe option will go away.
 
-  public watchQuery(options: WatchQueryOptions, shouldSubscribe = true): ObservableQuery {
+  public watchQuery<T>(options: WatchQueryOptions, shouldSubscribe = true): ObservableQuery<T> {
     // Call just to get errors synchronously
     getQueryDefinition(options.query);
 
@@ -438,7 +421,7 @@ export class QueryManager {
       transformedOptions.query = addTypenameToDocument(transformedOptions.query);
     }
 
-    let observableQuery = new ObservableQuery({
+    let observableQuery = new ObservableQuery<T>({
       scheduler: this.scheduler,
       options: transformedOptions,
       shouldSubscribe: shouldSubscribe,
@@ -447,7 +430,7 @@ export class QueryManager {
     return observableQuery;
   }
 
-  public query(options: WatchQueryOptions): Promise<ApolloQueryResult> {
+  public query<T>(options: WatchQueryOptions): Promise<ApolloQueryResult<T>> {
     if (options.returnPartialData) {
       throw new Error('returnPartialData option only supported on watchQuery.');
     }
@@ -458,9 +441,9 @@ export class QueryManager {
 
     const requestId = this.idCounter;
     const resPromise = new Promise((resolve, reject) => {
-      this.addFetchQueryPromise(requestId, resPromise, resolve, reject);
+      this.addFetchQueryPromise<T>(requestId, resPromise, resolve, reject);
 
-      return this.watchQuery(options, false).result().then((result) => {
+      return this.watchQuery<T>(options, false).result().then((result) => {
         this.removeFetchQueryPromise(requestId);
         resolve(result);
       }).catch((error) => {
@@ -472,7 +455,7 @@ export class QueryManager {
     return resPromise;
   }
 
-  public fetchQuery(queryId: string, options: WatchQueryOptions, fetchType?: FetchType): Promise<ApolloQueryResult> {
+  public fetchQuery<T>(queryId: string, options: WatchQueryOptions, fetchType?: FetchType): Promise<ApolloQueryResult<T>> {
     const {
       variables = {},
       forceFetch = false,
@@ -574,6 +557,10 @@ export class QueryManager {
     return this.reduxRootSelector(this.store.getState());
   }
 
+  public selectApolloState(store: any) {
+    return this.reduxRootSelector(store.getState());
+  }
+
   public getInitialState(): { data: Object } {
     return { data: this.getApolloState().data };
   }
@@ -588,20 +575,19 @@ export class QueryManager {
   }
 
   // Adds a promise to this.fetchQueryPromises for a given request ID.
-  public addFetchQueryPromise(requestId: number, promise: Promise<ApolloQueryResult>,
-    resolve: (result: ApolloQueryResult) => void,
+  public addFetchQueryPromise<T>(requestId: number, promise: Promise<ApolloQueryResult<T>>,
+    resolve: (result: ApolloQueryResult<T>) => void,
     reject: (error: Error) => void) {
     this.fetchQueryPromises[requestId.toString()] = { promise, resolve, reject };
   }
 
-
   // Removes the promise in this.fetchQueryPromises for a particular request ID.
   public removeFetchQueryPromise(requestId: number) {
     delete this.fetchQueryPromises[requestId.toString()];
   }
 
   // Adds an ObservableQuery to this.observableQueries and to this.observableQueriesByName.
-  public addObservableQuery(queryId: string, observableQuery: ObservableQuery) {
+  public addObservableQuery<T>(queryId: string, observableQuery: ObservableQuery<T>) {
     this.observableQueries[queryId] = { observableQuery };
 
     // Insert the ObservableQuery into this.observableQueriesByName if the query has a name
@@ -659,10 +645,10 @@ export class QueryManager {
     });
   }
 
-  public startQuery(queryId: string, options: WatchQueryOptions, listener: QueryListener) {
+  public startQuery<T>(queryId: string, options: WatchQueryOptions, listener: QueryListener) {
     this.addQueryListener(queryId, listener);
 
-    this.fetchQuery(queryId, options)
+    this.fetchQuery<T>(queryId, options)
     // `fetchQuery` returns a Promise. In case of a failure it should be caucht or else the
     // console will show an `Uncaught (in promise)` message. Ignore the error for now.
     .catch((error: Error) => undefined);
@@ -749,11 +735,13 @@ export class QueryManager {
     this.stopQueryInStore(queryId);
   }
 
-  public getCurrentQueryResult(observableQuery: ObservableQuery, isOptimistic = false) {
+  public getCurrentQueryResult<T>(observableQuery: ObservableQuery<T>, isOptimistic = false) {
     const {
       variables,
       document } = this.getQueryParts(observableQuery);
 
+    const lastResult = observableQuery.getLastResult();
+
     const queryOptions = observableQuery.options;
     const readOptions: ReadQueryOptions = {
       // In case of an optimistic change, apply reducer on top of the
@@ -764,12 +752,13 @@ export class QueryManager {
       variables,
       returnPartialData: false,
       config: this.reducerConfig,
+      previousResult: lastResult ? lastResult.data : undefined,
     };
 
     try {
       // first try reading the full result from the store
       const data = readQueryFromStore(readOptions);
-      return { data, partial: false };
+      return maybeDeepFreeze({ data, partial: false });
     } catch (e) {
       // next, try reading partial results, if we want them
       if (queryOptions.returnPartialData || queryOptions.noFetch) {
@@ -782,12 +771,12 @@ export class QueryManager {
         }
       }
 
-      return { data: {}, partial: true };
+      return maybeDeepFreeze({ data: {}, partial: true });
     }
   }
 
-  public getQueryWithPreviousResult(queryIdOrObservable: string | ObservableQuery, isOptimistic = false) {
-    let observableQuery: ObservableQuery;
+  public getQueryWithPreviousResult<T>(queryIdOrObservable: string | ObservableQuery<T>, isOptimistic = false) {
+    let observableQuery: ObservableQuery<T>;
     if (typeof queryIdOrObservable === 'string') {
       if (!this.observableQueries[queryIdOrObservable]) {
         throw new Error(`ObservableQuery with this id doesn't exist: ${queryIdOrObservable}`);
@@ -812,7 +801,7 @@ export class QueryManager {
   }
 
   // Give the result transformer a chance to observe or modify result data before it is passed on.
-  public transformResult(result: ApolloQueryResult): ApolloQueryResult {
+  public transformResult<T>(result: ApolloQueryResult<T>): ApolloQueryResult<T> {
     if (!this.resultTransformer) {
       return result;
     } else {
@@ -822,7 +811,7 @@ export class QueryManager {
 
   // XXX: I think we just store this on the observable query at creation time
   // TODO LATER: rename this function. Its main role is to apply the transform, nothing else!
-  private getQueryParts(observableQuery: ObservableQuery) {
+  private getQueryParts<T>(observableQuery: ObservableQuery<T>) {
     const queryOptions = observableQuery.options;
 
     let transformedDoc = observableQuery.options.query;
@@ -919,7 +908,7 @@ export class QueryManager {
   // Takes a request id, query id, a query document and information associated with the query
   // and send it to the network interface. Returns
   // a promise for the result associated with that request.
-  private fetchRequest({
+  private fetchRequest<T>({
     requestId,
     queryId,
     document,
@@ -941,8 +930,8 @@ export class QueryManager {
       operationName: getOperationName(document),
     };
 
-    const retPromise = new Promise<ApolloQueryResult>((resolve, reject) => {
-      this.addFetchQueryPromise(requestId, retPromise, resolve, reject);
+    const retPromise = new Promise<ApolloQueryResult<T>>((resolve, reject) => {
+      this.addFetchQueryPromise<T>(requestId, retPromise, resolve, reject);
 
       this.deduplicator.query(request, this.queryDeduplication)
         .then((result: ExecutionResult) => {
@@ -1037,15 +1026,10 @@ export class QueryManager {
     }
   }
 
-  // check to see if two results are the same, given our resultComparator
-  private isDifferentResult(lastResult: ApolloQueryResult, newResult: ApolloQueryResult): boolean {
-    const comparator = this.resultComparator || isEqual;
-    return !comparator(lastResult, newResult);
-  }
-
   private broadcastQueries() {
     const queries = this.getApolloState().queries;
-    forOwn(this.queryListeners, (listeners: QueryListener[], queryId: string) => {
+    Object.keys(this.queryListeners).forEach((queryId: string) => {
+      const listeners = this.queryListeners[queryId];
       // XXX due to an unknown race condition listeners can sometimes be undefined here.
       // this prevents a crash but doesn't solve the root cause
       // see: https://github.com/apollostack/apollo-client/issues/833
diff --git a/src/core/types.ts b/src/core/types.ts
new file mode 100644
--- /dev/null
+++ b/src/core/types.ts
@@ -0,0 +1,45 @@
+import { DocumentNode } from 'graphql';
+import {
+  QueryStoreValue,
+  NetworkStatus,
+} from '../queries/store';
+
+export interface SubscriptionOptions {
+  document: DocumentNode;
+  variables?: { [key: string]: any };
+};
+
+export type QueryListener = (queryStoreValue: QueryStoreValue) => void;
+
+export type ApolloQueryResult<T> = {
+  data: T;
+  loading: boolean;
+  networkStatus: NetworkStatus;
+
+  // This type is different from the GraphQLResult type because it doesn't include errors.
+  // Those are thrown via the standard promise/observer catch mechanism.
+};
+
+// A result transformer is given the data that is to be returned from the store from a query or
+// mutation, and can modify or observe it before the value is provided to your application.
+//
+// For watched queries, the transformer is only called when the data retrieved from the server is
+// different from previous.
+//
+// If the transformer wants to mutate results (say, by setting the prototype of result data), it
+// will likely need to be paired with a custom resultComparator.  By default, Apollo performs a
+// deep equality comparsion on results, and skips those that are considered equal - reducing
+// re-renders.
+export type ResultTransformer = (resultData: ApolloQueryResult<any>) => ApolloQueryResult<any>;
+
+// Controls how Apollo compares two query results and considers their equality.  Two equal results
+// will not trigger re-renders.
+export type ResultComparator = (result1: ApolloQueryResult<any>, result2: ApolloQueryResult<any>) => boolean;
+
+export enum FetchType {
+  normal = 1,
+  refetch = 2,
+  poll = 3,
+}
+
+export type IdGetter = (value: Object) => string;
diff --git a/src/core/watchQueryOptions.ts b/src/core/watchQueryOptions.ts
--- a/src/core/watchQueryOptions.ts
+++ b/src/core/watchQueryOptions.ts
@@ -73,31 +73,9 @@ export interface WatchQueryOptions extends ModifiableWatchQueryOptions {
   metadata?: any;
 }
 
-// This interface is deprecated because we no longer pass around fragments separately in the core.
-export interface DeprecatedWatchQueryOptions extends ModifiableWatchQueryOptions {
-  /**
-   * A GraphQL document that consists of a single query to be sent down to the
-   * server.
-   */
-  query: DocumentNode;
-
-  /**
-   * A list of fragments that are returned by {@link createFragment} which can be
-   * referenced from the query document.
-   */
-  fragments?: FragmentDefinitionNode[];
-
-  /**
-   * Arbitrary metadata stored in Redux with this query.  Designed for debugging,
-   * developer tools, etc.
-   */
-  metadata?: any;
-}
-
 export interface FetchMoreQueryOptions {
   query?: DocumentNode;
   variables?: { [key: string]: any };
-  fragments?: FragmentDefinitionNode[];
 }
 
 export type SubscribeToMoreOptions = {
@@ -110,17 +88,15 @@ export type SubscribeToMoreOptions = {
   onError?: (error: Error) => void;
 };
 
-export interface DeprecatedSubscriptionOptions {
+export interface SubscriptionOptions {
   query: DocumentNode;
   variables?: { [key: string]: any };
-  fragments?: FragmentDefinitionNode[];
 };
 
 export interface MutationOptions {
   mutation: DocumentNode;
   variables?: Object;
   resultBehaviors?: MutationBehavior[];
-  fragments?: FragmentDefinitionNode[];
   optimisticResponse?: Object;
   updateQueries?: MutationQueryReducersMap;
   refetchQueries?: string[];
diff --git a/src/data/debug.ts b/src/data/debug.ts
--- a/src/data/debug.ts
+++ b/src/data/debug.ts
@@ -1,22 +1,23 @@
 // For development only!
-import isObject = require('lodash/isObject');
-import omit = require('lodash/omit');
-import mapValues = require('lodash/mapValues');
 
 export function stripLoc(obj: Object) {
   if (Array.isArray(obj)) {
     return obj.map(stripLoc);
   }
 
-  if (! isObject(obj)) {
+  if (obj === null || typeof obj !== 'object') {
     return obj;
   }
 
-  const omitted: Object = omit(obj, ['loc']);
+  const nextObj = {};
 
-  return mapValues(omitted, (value) => {
-    return stripLoc(value);
+  Object.keys(obj).forEach(key => {
+    if (key !== 'loc') {
+      nextObj[key] = stripLoc(obj[key]);
+    }
   });
+
+  return nextObj;
 }
 
 export function printAST(fragAst: Object) {
diff --git a/src/data/extensions.ts b/src/data/extensions.ts
deleted file mode 100644
--- a/src/data/extensions.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export type IdGetter = (value: Object) => string;
-
-export const getIdField = (data: { id: any }) => data.id;
diff --git a/src/data/mutationResults.ts b/src/data/mutationResults.ts
--- a/src/data/mutationResults.ts
+++ b/src/data/mutationResults.ts
@@ -8,8 +8,7 @@ import {
   ExecutionResult,
 } from 'graphql';
 
-import mapValues = require('lodash/mapValues');
-import cloneDeep = require('lodash/cloneDeep');
+import { cloneDeep } from '../util/cloneDeep';
 
 import { replaceQueryResults } from './replaceQueryResults';
 
@@ -183,8 +182,11 @@ function mutationResultDeleteReducer(state: NormalizedCache, {
   delete state[dataId];
 
   // Now we need to go through the whole store and remove all references
-  const newState = mapValues(state, (storeObj: StoreObject) => {
-    return removeRefsFromStoreObj(storeObj, dataId);
+  const newState: NormalizedCache = {};
+
+  Object.keys(state).forEach(key => {
+    const storeObj = state[key];
+    newState[key] = removeRefsFromStoreObj(storeObj, dataId);
   });
 
   return newState;
@@ -193,10 +195,15 @@ function mutationResultDeleteReducer(state: NormalizedCache, {
 function removeRefsFromStoreObj(storeObj: any, dataId: any) {
   let affected = false;
 
-  const cleanedObj = mapValues(storeObj, (value: any) => {
+  const cleanedObj: any = {};
+
+  Object.keys(storeObj).forEach(key => {
+    const value = storeObj[key];
+
     if (value && value.id === dataId) {
       affected = true;
-      return null;
+      cleanedObj[key] = null;
+      return;
     }
 
     if (Array.isArray(value)) {
@@ -204,12 +211,13 @@ function removeRefsFromStoreObj(storeObj: any, dataId: any) {
 
       if (filteredArray !== value) {
         affected = true;
-        return filteredArray;
+        cleanedObj[key] = filteredArray;
+        return;
       }
     }
 
-    // If not modified, return the original value
-    return value;
+    // If not modified, set the original value
+    cleanedObj[key] = value;
   });
 
   if (affected) {
diff --git a/src/data/readFromStore.ts b/src/data/readFromStore.ts
--- a/src/data/readFromStore.ts
+++ b/src/data/readFromStore.ts
@@ -5,6 +5,7 @@ import {
 import graphqlAnywhere, {
   Resolver,
   FragmentMatcher,
+  ExecInfo,
 } from 'graphql-anywhere';
 
 import {
@@ -26,6 +27,18 @@ import {
   ApolloReducerConfig,
 } from '../store';
 
+import { isEqual } from '../util/isEqual';
+
+/**
+ * The key which the cache id for a given value is stored in the result object. This key is private
+ * and should not be used by Apollo client users.
+ *
+ * Uses a symbol if available in the environment.
+ *
+ * @private
+ */
+export const ID_KEY = typeof Symbol !== 'undefined' ? Symbol('id') : '@@id';
+
 export type DiffResult = {
   result?: any;
   isMissing?: boolean;
@@ -36,6 +49,7 @@ export type ReadQueryOptions = {
   query: DocumentNode,
   variables?: Object,
   returnPartialData?: boolean,
+  previousResult?: any,
   config?: ApolloReducerConfig,
 };
 
@@ -47,37 +61,50 @@ export type CustomResolverMap = {
   },
 };
 
+/**
+ * This code needs an optional `previousResult` property on `IdValue` so that when the results
+ * returned from the store are the same, we can just return the `previousResult` and not a new
+ * value thus preserving referential equality.
+ *
+ * The `previousResult` property is added to our `IdValue`s in the `graphql-anywhere` resolver so
+ * that they can be in the right position for `resultMapper` to test equality and return whichever
+ * result is appropriate.
+ *
+ * `resultMapper` takes the `previousResult`s and performs a shallow referential equality check. If
+ * that passes then instead of returning the object created by `graphql-anywhere` the
+ * `resultMapper` function will instead return the `previousResult`. This process is bottom-up so
+ * we start at the leaf results and swap them for `previousResult`s all the way up until we get to
+ * the root object.
+ */
+interface IdValueWithPreviousResult extends IdValue {
+  previousResult?: any;
+}
+
 /**
  * Resolves the result of a query solely from the store (i.e. never hits the server).
  *
- * @param store The {@link NormalizedCache} used by Apollo for the `data` portion of the store.
+ * @param {Store} store The {@link NormalizedCache} used by Apollo for the `data` portion of the
+ * store.
  *
- * @param query The query document to resolve from the data available in the store.
+ * @param {DocumentNode} query The query document to resolve from the data available in the store.
  *
- * @param variables A map from the name of a variable to its value. These variables can be
- * referenced by the query document.
+ * @param {Object} [variables] A map from the name of a variable to its value. These variables can
+ * be referenced by the query document.
  *
- * @param returnPartialData If set to true, the query will be resolved even if all of the data
- * needed to resolve the query is not found in the store. The data keys that are not found will not
- * be present in the returned object. If set to false, an error will be thrown if there are fields
- * that cannot be resolved from the store.
+ * @param {boolean} [returnPartialData] If set to true, the query will be resolved even if all of
+ * the data needed to resolve the query is not found in the store. The data keys that are not found
+ * will not be present in the returned object. If set to false, an error will be thrown if there
+ * are fields that cannot be resolved from the store.
+ *
+ * @param {any} previousResult The previous result returned by this function for the same query.
+ * If nothing in the store changed since that previous result then values from the previous result
+ * will be returned to preserve referential equality.
  */
-export function readQueryFromStore({
-  store,
-  query,
-  variables,
-  returnPartialData = false,
-  config,
-}: ReadQueryOptions): Object {
-  const { result } = diffQueryAgainstStore({
-    query,
-    store,
+export function readQueryFromStore<QueryType>({ returnPartialData = false, ...options }: ReadQueryOptions): QueryType {
+  return diffQueryAgainstStore({
+    ...options,
     returnPartialData,
-    variables,
-    config,
-  });
-
-  return result;
+  }).result;
 }
 
 type ReadStoreContext = {
@@ -134,16 +161,17 @@ match fragments.`);
 
 const readStoreResolver: Resolver = (
   fieldName: string,
-  idValue: IdValue,
+  idValue: IdValueWithPreviousResult,
   args: any,
   context: ReadStoreContext,
+  { resultKey }: ExecInfo,
 ) => {
   assertIdValue(idValue);
 
   const objId = idValue.id;
   const obj = context.store[objId];
   const storeKeyName = storeKeyNameFromFieldNameAndArgs(fieldName, args);
-  const fieldValue = (obj || {})[storeKeyName];
+  let fieldValue = (obj || {})[storeKeyName];
 
   if (typeof fieldValue === 'undefined') {
     if (context.customResolvers && obj && (obj.__typename || objId === 'ROOT_QUERY')) {
@@ -170,11 +198,26 @@ Perhaps you want to use the \`returnPartialData\` option?`);
     return fieldValue;
   }
 
+  // if this is an object scalar, it must be a json blob and we have to unescape it
   if (isJsonValue(fieldValue)) {
-    // if this is an object scalar, it must be a json blob and we have to unescape it
+    // If the JSON blob is the same now as in the previous result, return the previous result to
+    // maintain referential equality.
+    //
+    // `isEqual` will first perform a referential equality check (with `===`) in case the JSON
+    // value has not changed in the store, and then a deep equality check if that fails in case a
+    // new JSON object was returned by the API but that object may still be the same.
+    if (idValue.previousResult && isEqual(idValue.previousResult[resultKey], fieldValue.json)) {
+      return idValue.previousResult[resultKey];
+    }
     return fieldValue.json;
   }
 
+  // If we had a previous result, try adding that previous result value for this field to our field
+  // value. This will create a new value without mutating the old one.
+  if (idValue.previousResult) {
+    fieldValue = addPreviousResultToIdValues(fieldValue, idValue.previousResult[resultKey]);
+  }
+
   return fieldValue;
 };
 
@@ -184,6 +227,7 @@ Perhaps you want to use the \`returnPartialData\` option?`);
  * @param  {DocumentNode} query A parsed GraphQL query document
  * @param  {Store} store The Apollo Client store object
  * @param  {boolean} [returnPartialData] Whether to throw an error if any fields are missing
+ * @param  {any} previousResult The previous result returned by this function for the same query
  * @return {result: Object, isMissing: [boolean]}
  */
 export function diffQueryAgainstStore({
@@ -191,6 +235,7 @@ export function diffQueryAgainstStore({
   query,
   variables,
   returnPartialData = true,
+  previousResult,
   config,
 }: ReadQueryOptions): DiffResult {
   // Throw the right validation error by trying to find a query in the document
@@ -209,10 +254,12 @@ export function diffQueryAgainstStore({
   const rootIdValue = {
     type: 'id',
     id: 'ROOT_QUERY',
+    previousResult,
   };
 
   const result = graphqlAnywhere(readStoreResolver, query, rootIdValue, context, variables, {
     fragmentMatcher,
+    resultMapper,
   });
 
   return {
@@ -228,3 +275,126 @@ an object reference. This should never happen during normal use unless you have
 that is directly manipulating the store; please file an issue.`);
   }
 }
+
+/**
+ * Adds a previous result value to id values in a nested array. For a single id value and a single
+ * previous result then the previous value is added directly.
+ *
+ * For arrays we put all of the ids from the previous result array in a map and add them to id
+ * values with the same id.
+ *
+ * This function does not mutate. Instead it returns new instances of modified values.
+ *
+ * @private
+ */
+function addPreviousResultToIdValues (value: any, previousResult: any): any {
+  // If the value is an `IdValue`, add the previous result to it whether or not that
+  // `previousResult` is undefined.
+  //
+  // If the value is an array, recurse over each item trying to add the `previousResult` for that
+  // item.
+  if (isIdValue(value)) {
+    return {
+      ...value,
+      previousResult,
+    };
+  } else if (Array.isArray(value)) {
+    const idToPreviousResult: { [id: string]: any } = {};
+
+    // If the previous result was an array, we want to build up our map of ids to previous results
+    // using the private `ID_KEY` property that is added in `resultMapper`.
+    if (Array.isArray(previousResult)) {
+      previousResult.forEach(item => {
+        if (item[ID_KEY]) {
+          idToPreviousResult[item[ID_KEY]] = item;
+        }
+      });
+    }
+
+    // For every value we want to add the previous result.
+    return value.map((item, i) => {
+      // By default the previous result for this item will be in the same array position as this
+      // item.
+      let itemPreviousResult = previousResult && previousResult[i];
+
+      // If the item is an id value, we should check to see if there is a previous result for this
+      // specific id. If there is, that will be the value for `itemPreviousResult`.
+      if (isIdValue(item)) {
+        itemPreviousResult = idToPreviousResult[item.id] || itemPreviousResult;
+      }
+
+      return addPreviousResultToIdValues(item, itemPreviousResult);
+    });
+  }
+  // Return the value, nothing changed.
+  return value;
+}
+
+/**
+ * Maps a result from `graphql-anywhere` to a final result value.
+ *
+ * If the result and the previous result from the `idValue` pass a shallow equality test, we just
+ * return the `previousResult` to maintain referential equality.
+ *
+ * We also add a private id property to the result that we can use later on.
+ *
+ * @private
+ */
+function resultMapper (resultFields: any, idValue: IdValueWithPreviousResult) {
+  // If we had a previous result, we may be able to return that and preserve referential equality
+  if (idValue.previousResult) {
+    const currentResultKeys = Object.keys(resultFields);
+
+    const sameAsPreviousResult =
+      // Confirm that we have the same keys in both the current result and the previous result.
+      Object.keys(idValue.previousResult)
+        .reduce((sameKeys, key) => sameKeys && currentResultKeys.indexOf(key) > -1, true) &&
+
+      // Perform a shallow comparison of the result fields with the previous result. If all of
+      // the shallow fields are referentially equal to the fields of the previous result we can
+      // just return the previous result.
+      //
+      // While we do a shallow comparison of objects, but we do a deep comparison of arrays.
+      currentResultKeys.reduce((same, key) => (
+        same && areNestedArrayItemsStrictlyEqual(resultFields[key], idValue.previousResult[key])
+      ), true);
+
+    if (sameAsPreviousResult) {
+      return idValue.previousResult;
+    }
+  }
+
+  // Add the id to the result fields. It should be non-enumerable so users can’t see it without
+  // trying very hard.
+  Object.defineProperty(resultFields, ID_KEY, {
+    enumerable: false,
+    configurable: false,
+    writable: false,
+    value: idValue.id,
+  });
+
+  return resultFields;
+}
+
+type NestedArray<T> = T | Array<T | Array<T | Array<T>>>;
+
+/**
+ * Compare all the items to see if they are all referentially equal in two arrays no matter how
+ * deeply nested the arrays are.
+ *
+ * @private
+ */
+function areNestedArrayItemsStrictlyEqual (a: NestedArray<any>, b: NestedArray<any>): boolean {
+  // If `a` and `b` are referentially equal, return true.
+  if (a === b) {
+    return true;
+  }
+  // If either `a` or `b` are not an array or not of the same length return false. `a` and `b` are
+  // known to not be equal here, we checked above.
+  if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) {
+    return false;
+  }
+  // Otherwise let us compare all of the array items (which are potentially nested arrays!) to see
+  // if they are equal.
+  return a.reduce((same, item, i) => same && areNestedArrayItemsStrictlyEqual(item, b[i]), true);
+}
diff --git a/src/data/scopeQuery.ts b/src/data/scopeQuery.ts
--- a/src/data/scopeQuery.ts
+++ b/src/data/scopeQuery.ts
@@ -13,8 +13,6 @@ import {
   resultKeyNameFromField,
 } from './storeUtils';
 
-import isNumber = require('lodash/isNumber');
-
 // The type of a path
 export type StorePath = (string|number)[];
 
@@ -51,7 +49,7 @@ export function scopeSelectionSetToResultPath({
 
   path
     // Arrays are not represented in GraphQL AST
-    .filter((pathSegment) => !isNumber(pathSegment))
+    .filter((pathSegment) => typeof pathSegment !== 'number')
     .forEach((pathSegment) => {
       currSelSet = followOnePathSegment(currSelSet, pathSegment as string, fragmentMap);
     });
diff --git a/src/data/store.ts b/src/data/store.ts
--- a/src/data/store.ts
+++ b/src/data/store.ts
@@ -101,7 +101,7 @@ export function data(
       // write to "the" store?
       let newState = writeResultToStore({
         result: action.result.data,
-        dataId: 'ROOT_QUERY', // TODO: is this correct? what am I doing here? What is dataId for??
+        dataId: 'ROOT_SUBSCRIPTION',
         document: action.document,
         variables: action.variables,
         store: clonedState,
diff --git a/src/data/storeUtils.ts b/src/data/storeUtils.ts
--- a/src/data/storeUtils.ts
+++ b/src/data/storeUtils.ts
@@ -15,9 +15,8 @@ import {
   NameNode,
 } from 'graphql';
 
-import isObject = require('lodash/isObject');
-
 function isStringValue(value: ValueNode): value is StringValueNode {
+
   return value.kind === 'StringValue';
 }
 
@@ -146,7 +145,11 @@ export interface JsonValue {
 export type StoreValue = number | string | string[] | IdValue | JsonValue | void;
 
 export function isIdValue(idObject: StoreValue): idObject is IdValue {
-  return (isObject(idObject) && (idObject as (IdValue | JsonValue)).type === 'id');
+  return (
+    idObject != null &&
+    typeof idObject === 'object' &&
+    (idObject as (IdValue | JsonValue)).type === 'id'
+  );
 }
 
 export function toIdValue(id: string, generated = false): IdValue {
@@ -158,5 +161,9 @@ export function toIdValue(id: string, generated = false): IdValue {
 }
 
 export function isJsonValue(jsonObject: StoreValue): jsonObject is JsonValue {
-  return (isObject(jsonObject) && (jsonObject as (IdValue | JsonValue)).type === 'json');
+  return (
+    jsonObject != null &&
+    typeof jsonObject === 'object' &&
+    (jsonObject as (IdValue | JsonValue)).type === 'json'
+  );
 }
diff --git a/src/data/writeToStore.ts b/src/data/writeToStore.ts
--- a/src/data/writeToStore.ts
+++ b/src/data/writeToStore.ts
@@ -1,6 +1,3 @@
-import isNull = require('lodash/isNull');
-import isUndefined = require('lodash/isUndefined');
-import isObject = require('lodash/isObject');
 
 import {
   getOperationDefinition,
@@ -35,7 +32,7 @@ import {
 
 import {
   IdGetter,
-} from './extensions';
+} from '../core/types';
 
 import {
   shouldInclude,
@@ -149,7 +146,7 @@ export function writeSelectionSetToStore({
       const resultFieldKey: string = resultKeyNameFromField(selection);
       const value: any = result[resultFieldKey];
 
-      if (!isUndefined(value)) {
+      if (value !== undefined) {
         writeFieldToStore({
           dataId,
           value,
@@ -241,16 +238,15 @@ function writeFieldToStore({
   // If we merge, this will be the generatedKey
   let generatedKey: string;
 
-  // If it's a scalar that's not a JSON blob, just store it in the store
-  if ((!field.selectionSet || isNull(value)) && !isObject(value)) {
-    storeValue = value;
-  } else if ((!field.selectionSet || isNull(value)) && isObject(value)) {
-    // If it is a scalar that's a JSON blob, we have to "escape" it so it can't
-    // pretend to be an id
-    storeValue = {
-      type: 'json',
-      json: value,
-    };
+  // If this is a scalar value...
+  if (!field.selectionSet || value === null) {
+    storeValue =
+      value != null && typeof value === 'object'
+        // If the scalar value is a JSON blob, we have to "escape" it so it can’t pretend to be
+        // an id.
+        ? { type: 'json', json: value }
+        // Otherwise, just store the scalar directly in the store.
+        : value;
   } else if (Array.isArray(value)) {
     const generatedId = `${dataId}.${storeFieldName}`;
 
@@ -340,7 +336,7 @@ function processArrayValue(
   context: WriteContext,
 ): any[] {
   return value.map((item: any, index: any) => {
-    if (isNull(item)) {
+    if (item === null) {
       return null;
     }
 
diff --git a/src/fragments.ts b/src/fragments.ts
deleted file mode 100644
--- a/src/fragments.ts
+++ /dev/null
@@ -1,89 +0,0 @@
-import {
-  DocumentNode,
-  FragmentDefinitionNode,
-} from 'graphql';
-
-import flatten = require('lodash/flatten');
-
-import {
-  getFragmentDefinitions,
-} from './queries/getFromAST';
-
-// A map going from the name of a fragment to that fragment's definition.
-// The point is to keep track of fragments that exist and print a warning if we encounter two
-// fragments that have the same name, i.e. the values *should* be of arrays of length 1.
-// Note: this variable is exported solely for unit testing purposes. It should not be touched
-// directly by application code.
-export let fragmentDefinitionsMap: { [fragmentName: string]: FragmentDefinitionNode[] } = {};
-
-// Specifies whether or not we should print warnings about conflicting fragment names.
-let printFragmentWarnings = true;
-
-// Takes a document, extracts the FragmentDefinitions from it and puts
-// them in this.fragmentDefinitions. The second argument specifies the fragments
-// that the fragment in the document depends on. The fragment definition array from the document
-// is concatenated with the fragment definition array passed as the second argument and this
-// concatenated array is returned.
-let haveWarned = false;
-
-export function createFragment(
-  doc: DocumentNode,
-  fragments: (FragmentDefinitionNode[] | FragmentDefinitionNode[][]) = [],
-  internalUse = false,
-): FragmentDefinitionNode[] {
-
-  if (!internalUse) {
-    if (! haveWarned) {
-      if (process.env.NODE_ENV !== 'production') {
-        console.warn(
-            '"createFragment" is deprecated and will be removed in version 0.6, ' +
-            'please refer to the documentation for how to define fragments: ' +
-            'http://dev.apollodata.com/react/fragments.html.',
-        );
-      }
-      /* istanbul ignore if */
-      if (process.env.NODE_ENV !== 'test') {
-        // When running tests, we want to print the warning every time
-        haveWarned = true;
-      }
-    }
-  }
-
-  fragments = flatten(fragments) as FragmentDefinitionNode[];
-  const fragmentDefinitions = getFragmentDefinitions(doc);
-  fragmentDefinitions.forEach((fragmentDefinition: FragmentDefinitionNode) => {
-    const fragmentName = fragmentDefinition.name.value;
-    if (fragmentDefinitionsMap.hasOwnProperty(fragmentName) &&
-        fragmentDefinitionsMap[fragmentName].indexOf(fragmentDefinition) === -1) {
-      // this is a problem because the app developer is trying to register another fragment with
-      // the same name as one previously registered. So, we tell them about it.
-      if (printFragmentWarnings) {
-        console.warn(`Warning: fragment with name ${fragmentDefinition.name.value} already exists.
-Apollo Client enforces all fragment names across your application to be unique; read more about
-this in the docs: http://docs.apollostack.com/`);
-      }
-
-      fragmentDefinitionsMap[fragmentName].push(fragmentDefinition);
-    } else if (!fragmentDefinitionsMap.hasOwnProperty(fragmentName)) {
-      fragmentDefinitionsMap[fragmentName] = [fragmentDefinition];
-    }
-  });
-
-  return fragments.concat(fragmentDefinitions);
-}
-
-// This function disables the warnings printed about fragment names. One place where this chould be
-// called is within writing unit tests that depend on Apollo Client and use named fragments that may
-// have the same name across different unit tests.
-export function disableFragmentWarnings() {
-  printFragmentWarnings = false;
-}
-
-export function enableFragmentWarnings() {
-  printFragmentWarnings = true;
-}
-
-// This function is used to be empty the namespace of fragment definitions. Used for unit tests.
-export function clearFragmentDefinitions() {
-  fragmentDefinitionsMap = {};
-}
diff --git a/src/index.ts b/src/index.ts
--- a/src/index.ts
+++ b/src/index.ts
@@ -30,7 +30,7 @@ import {
 import {
   WatchQueryOptions,
   MutationOptions,
-  DeprecatedSubscriptionOptions,
+  SubscriptionOptions,
 } from './core/watchQueryOptions';
 
 import {
@@ -62,16 +62,10 @@ import {
 } from './errors/ApolloError';
 
 import ApolloClient from './ApolloClient';
-import {
-  createFragment,
-  clearFragmentDefinitions,
-  disableFragmentWarnings,
-  enableFragmentWarnings,
-} from './fragments';
 
 import {
   ApolloQueryResult,
-} from './core/QueryManager';
+} from './core/types';
 
 import {
   toIdValue,
@@ -91,12 +85,6 @@ export {
   NetworkStatus,
   ApolloError,
 
-  // fragment stuff
-  createFragment,
-  clearFragmentDefinitions,
-  disableFragmentWarnings,
-  enableFragmentWarnings,
-
   getQueryDefinition,
   getFragmentDefinitions,
   FragmentMap,
@@ -116,7 +104,7 @@ export {
   MutationBehavior,
   MutationQueryReducersMap,
   Subscription,
-  DeprecatedSubscriptionOptions as SubscriptionOptions,
+  SubscriptionOptions,
   ApolloStore,
   ApolloClient
 };
diff --git a/src/optimistic-data/store.ts b/src/optimistic-data/store.ts
--- a/src/optimistic-data/store.ts
+++ b/src/optimistic-data/store.ts
@@ -14,11 +14,10 @@ import {
 } from '../data/storeUtils';
 
 import {
-  getDataWithOptimisticResults,
   Store,
 } from '../store';
 
-import pick = require('lodash/pick');
+  import { assign } from '../util/assign';
 
 // a stack of patches of new or changed documents
 export type OptimisticStore = {
@@ -28,6 +27,14 @@ export type OptimisticStore = {
 
 const optimisticDefaultState: any[] = [];
 
+export function getDataWithOptimisticResults(store: Store): NormalizedCache {
+  if (store.optimistic.length === 0) {
+    return store.data;
+  }
+  const patches = store.optimistic.map(opt => opt.data);
+  return assign({}, store.data, ...patches) as NormalizedCache;
+}
+
 export function optimistic(
   previousState = optimisticDefaultState,
   action: any,
@@ -49,7 +56,7 @@ export function optimistic(
     const fakeStore = {
       ...store,
       optimistic: previousState,
-    } as Store;
+    };
     const optimisticData = getDataWithOptimisticResults(fakeStore);
     const fakeDataResultState = data(
       optimisticData,
@@ -61,9 +68,13 @@ export function optimistic(
 
     // TODO: apply extra reducers and resultBehaviors to optimistic store?
 
-    const changedKeys = Object.keys(fakeDataResultState).filter(
-      key => optimisticData[key] !== fakeDataResultState[key]);
-    const patch = pick(fakeDataResultState, changedKeys);
+    const patch: any = {};
+
+    Object.keys(fakeDataResultState).forEach(key => {
+      if (optimisticData[key] !== fakeDataResultState[key]) {
+        patch[key] = fakeDataResultState[key];
+      }
+    });
 
     const optimisticState = {
       data: patch,
diff --git a/src/queries/getFromAST.ts b/src/queries/getFromAST.ts
--- a/src/queries/getFromAST.ts
+++ b/src/queries/getFromAST.ts
@@ -4,9 +4,6 @@ import {
   FragmentDefinitionNode,
 } from 'graphql';
 
-import countBy = require('lodash/countBy');
-import identity = require('lodash/identity');
-import uniq = require('lodash/uniq');
 
 export function getMutationDefinition(doc: DocumentNode): OperationDefinitionNode {
   checkDocument(doc);
@@ -33,19 +30,26 @@ export function checkDocument(doc: DocumentNode) {
 string in a "gql" tag? http://docs.apollostack.com/apollo-client/core.html#gql`);
   }
 
-  const definitionTypes = doc.definitions.map((definition) => {
-    if (definition.kind !== 'OperationDefinition' && definition.kind !== 'FragmentDefinition') {
-      throw new Error(`Schema type definitions not allowed in queries. Found: "${definition.kind}"`);
-    }
+  let foundOperation = false;
 
-    return definition.kind;
+  doc.definitions.forEach((definition) => {
+    switch (definition.kind) {
+      // If this is a fragment that’s fine.
+      case 'FragmentDefinition':
+        break;
+      // We can only find one operation, so the first time nothing happens. The second time we
+      // encounter an operation definition we throw an error.
+      case 'OperationDefinition':
+        if (foundOperation) {
+          throw new Error('Queries must have exactly one operation definition.');
+        }
+        foundOperation = true;
+        break;
+      // If this is any other operation kind, throw an error.
+      default:
+        throw new Error(`Schema type definitions not allowed in queries. Found: "${definition.kind}"`);
+    }
   });
-  const typeCounts = countBy(definitionTypes, identity);
-
-  // can't have more than one operation definition per query
-  if (typeCounts['OperationDefinition'] > 1) {
-    throw new Error('Queries must have exactly one operation definition.');
-  }
 }
 
 export function getOperationName(doc: DocumentNode): string {
@@ -144,17 +148,3 @@ export function createFragmentMap(fragments: FragmentDefinitionNode[] = []): Fra
 
   return symTable;
 }
-
-// Utility function that takes a list of fragment definitions and adds them to a particular
-// document.
-export function addFragmentsToDocument(queryDoc: DocumentNode,
-  fragments: FragmentDefinitionNode[]): DocumentNode {
-  if (!fragments) {
-    return queryDoc;
-  }
-  checkDocument(queryDoc);
-  return ({
-    ...queryDoc,
-    definitions: uniq(queryDoc.definitions.concat(fragments)),
-  }) as DocumentNode;
-}
diff --git a/src/queries/queryTransform.ts b/src/queries/queryTransform.ts
--- a/src/queries/queryTransform.ts
+++ b/src/queries/queryTransform.ts
@@ -11,7 +11,7 @@ import {
   checkDocument,
 } from './getFromAST';
 
-import cloneDeep = require('lodash/cloneDeep');
+import { cloneDeep } from '../util/cloneDeep';
 
 const TYPENAME_FIELD: FieldNode = {
   kind: 'Field',
diff --git a/src/queries/store.ts b/src/queries/store.ts
--- a/src/queries/store.ts
+++ b/src/queries/store.ts
@@ -18,7 +18,7 @@ import {
   GraphQLError,
 } from 'graphql';
 
-import isEqual = require('lodash/isEqual');
+import { isEqual } from '../util/isEqual';
 
 export interface QueryStore {
   [queryId: string]: QueryStoreValue;
diff --git a/src/scheduler/scheduler.ts b/src/scheduler/scheduler.ts
--- a/src/scheduler/scheduler.ts
+++ b/src/scheduler/scheduler.ts
@@ -10,10 +10,13 @@
 
 import {
   QueryManager,
-  QueryListener,
-  FetchType,
 } from '../core/QueryManager';
 
+import {
+  FetchType,
+  QueryListener,
+} from '../core/types';
+
 import { ObservableQuery } from '../core/ObservableQuery';
 
 import { WatchQueryOptions } from '../core/watchQueryOptions';
@@ -58,9 +61,9 @@ export class QueryScheduler {
     return queries[queryId] && queries[queryId].networkStatus !== NetworkStatus.ready;
   }
 
-  public fetchQuery(queryId: string, options: WatchQueryOptions, fetchType: FetchType) {
+  public fetchQuery<T>(queryId: string, options: WatchQueryOptions, fetchType: FetchType) {
     return new Promise((resolve, reject) => {
-      this.queryManager.fetchQuery(queryId, options, fetchType).then((result) => {
+      this.queryManager.fetchQuery<T>(queryId, options, fetchType).then((result) => {
         resolve(result);
       }).catch((error) => {
         reject(error);
@@ -68,7 +71,7 @@ export class QueryScheduler {
     });
   }
 
-  public startPollingQuery(
+  public startPollingQuery<T>(
     options: WatchQueryOptions,
     queryId?: string,
     listener?: QueryListener,
@@ -82,7 +85,7 @@ export class QueryScheduler {
     if (listener) {
       this.queryManager.addQueryListener(queryId, listener);
     }
-    this.addQueryOnInterval(queryId, options);
+    this.addQueryOnInterval<T>(queryId, options);
 
     return queryId;
   }
@@ -94,7 +97,7 @@ export class QueryScheduler {
   }
 
   // Fires the all of the queries on a particular interval. Called on a setInterval.
-  public fetchQueriesOnInterval(interval: number) {
+  public fetchQueriesOnInterval<T>(interval: number) {
     // XXX this "filter" here is nasty, because it does two things at the same time.
     // 1. remove queries that have stopped polling
     // 2. call fetchQueries for queries that are polling and not in flight.
@@ -116,7 +119,7 @@ export class QueryScheduler {
       const queryOptions = this.registeredQueries[queryId];
       const pollingOptions = { ...queryOptions } as WatchQueryOptions;
       pollingOptions.forceFetch = true;
-      this.fetchQuery(queryId, pollingOptions, FetchType.poll);
+      this.fetchQuery<T>(queryId, pollingOptions, FetchType.poll);
       return true;
     });
 
@@ -129,7 +132,7 @@ export class QueryScheduler {
   // Adds a query on a particular interval to this.intervalQueries and then fires
   // that query with all the other queries executing on that interval. Note that the query id
   // and query options must have been added to this.registeredQueries before this function is called.
-  public addQueryOnInterval(queryId: string, queryOptions: WatchQueryOptions) {
+  public addQueryOnInterval<T>(queryId: string, queryOptions: WatchQueryOptions) {
     const interval = queryOptions.pollInterval;
 
     // If there are other queries on this interval, this query will just fire with those
@@ -140,17 +143,17 @@ export class QueryScheduler {
       this.intervalQueries[interval] = [queryId];
       // set up the timer for the function that will handle this interval
       this.pollingTimers[interval] = setInterval(() => {
-        this.fetchQueriesOnInterval(interval);
+        this.fetchQueriesOnInterval<T>(interval);
       }, interval);
     }
   }
 
   // Used only for unit testing.
-  public registerPollingQuery(queryOptions: WatchQueryOptions): ObservableQuery {
+  public registerPollingQuery<T>(queryOptions: WatchQueryOptions): ObservableQuery<T> {
     if (!queryOptions.pollInterval) {
       throw new Error('Attempted to register a non-polling query with the scheduler.');
     }
-    return new ObservableQuery({
+    return new ObservableQuery<T>({
       scheduler: this,
       options: queryOptions,
     });
diff --git a/src/store.ts b/src/store.ts
--- a/src/store.ts
+++ b/src/store.ts
@@ -27,7 +27,9 @@ import {
 import {
   optimistic,
   OptimisticStore,
+  getDataWithOptimisticResults,
 } from './optimistic-data/store';
+export { getDataWithOptimisticResults };
 
 import {
   ApolloAction,
@@ -35,7 +37,7 @@ import {
 
 import {
   IdGetter,
-} from './data/extensions';
+} from './core/types';
 
 import {
   MutationBehaviorReducerMap,
@@ -45,7 +47,7 @@ import {
   CustomResolverMap,
 } from './data/readFromStore';
 
-import assign = require('lodash/assign');
+import { assign } from './util/assign';
 
 export interface Store {
   data: NormalizedCache;
@@ -109,6 +111,14 @@ export function createApolloReducer(config: ApolloReducerConfig): Function {
         config,
       );
 
+      if (state.data === newState.data &&
+      state.mutations === newState.mutations &&
+      state.queries === newState.queries &&
+      state.optimistic === newState.optimistic &&
+      state.reducerError === newState.reducerError) {
+        return state;
+      }
+
       return newState;
     } catch (reducerError) {
       return {
@@ -176,17 +186,8 @@ export function createApolloStore({
   );
 }
 
-
 export type ApolloReducerConfig = {
   dataIdFromObject?: IdGetter;
   mutationBehaviorReducers?: MutationBehaviorReducerMap;
   customResolvers?: CustomResolverMap;
 };
-
-export function getDataWithOptimisticResults(store: Store): NormalizedCache {
-  if (store.optimistic.length === 0) {
-    return store.data;
-  }
-  const patches = store.optimistic.map(opt => opt.data);
-  return assign({}, store.data, ...patches) as NormalizedCache;
-}
diff --git a/src/transport/batchedNetworkInterface.ts b/src/transport/batchedNetworkInterface.ts
--- a/src/transport/batchedNetworkInterface.ts
+++ b/src/transport/batchedNetworkInterface.ts
@@ -2,11 +2,6 @@ import {
   ExecutionResult,
 } from 'graphql';
 
-import 'whatwg-fetch';
-
-import assign = require('lodash/assign');
-import isNumber = require('lodash/isNumber');
-
 import {
   HTTPFetchNetworkInterface,
   HTTPNetworkInterface,
@@ -19,6 +14,8 @@ import {
   QueryBatcher,
 } from './batching';
 
+import { assign } from '../util/assign';
+
 // An implementation of the network interface that operates over HTTP and batches
 // together requests over the HTTP transport. Note that this implementation will only work correctly
 // for GraphQL server implementations that support batching. If such a server is not available, you
@@ -31,7 +28,7 @@ export class HTTPBatchedNetworkInterface extends HTTPFetchNetworkInterface {
   constructor(uri: string, pollInterval: number, fetchOpts: RequestInit) {
     super(uri, fetchOpts);
 
-    if (!isNumber(pollInterval)) {
+    if (typeof pollInterval !== 'number') {
       throw new Error(`pollInterval must be a number, got ${pollInterval}`);
     }
 
@@ -50,7 +47,7 @@ export class HTTPBatchedNetworkInterface extends HTTPFetchNetworkInterface {
 
   // made public for testing only
   public batchQuery(requests: Request[]): Promise<ExecutionResult[]> {
-    const options = assign({}, this._opts);
+    const options = { ...this._opts };
 
     // Apply the middlewares to each of the requests
     const middlewarePromises: Promise<RequestAndOptions>[] = [];
@@ -118,15 +115,17 @@ export class HTTPBatchedNetworkInterface extends HTTPFetchNetworkInterface {
       return printRequest(request);
     });
 
-    return fetch(this._uri, assign({}, this._opts, {
+    return fetch(this._uri, {
+      ...this._opts,
       body: JSON.stringify(printedRequests),
       method: 'POST',
-    }, options, {
-      headers: assign({}, {
+      ...options,
+      headers: {
         Accept: '*/*',
         'Content-Type': 'application/json',
-      }, options.headers),
-    }));
+        ...(options.headers as { [headerName: string]: string }),
+      },
+    });
   };
 }
 
diff --git a/src/transport/networkInterface.ts b/src/transport/networkInterface.ts
--- a/src/transport/networkInterface.ts
+++ b/src/transport/networkInterface.ts
@@ -1,8 +1,3 @@
-import isString = require('lodash/isString');
-import assign = require('lodash/assign');
-import mapValues = require('lodash/mapValues');
-import 'whatwg-fetch';
-
 import {
   ExecutionResult,
   DocumentNode,
@@ -77,9 +72,10 @@ export interface ResponseAndOptions {
 }
 
 export function printRequest(request: Request): PrintedRequest {
-  return mapValues(request, (val: any, key: any) => {
-    return key === 'query' ? print(val) : val;
-  }) as any as PrintedRequest;
+  return {
+    ...request,
+    query: print(request.query),
+  };
 }
 
 // TODO: refactor
@@ -95,12 +91,12 @@ export class HTTPFetchNetworkInterface implements NetworkInterface {
       throw new Error('A remote enpdoint is required for a network layer');
     }
 
-    if (!isString(uri)) {
+    if (typeof uri !== 'string') {
       throw new Error('Remote endpoint must be a string');
     }
 
     this._uri = uri;
-    this._opts = assign({}, opts);
+    this._opts = { ...opts };
     this._middlewares = [];
     this._afterwares = [];
   }
@@ -159,19 +155,21 @@ export class HTTPFetchNetworkInterface implements NetworkInterface {
     request,
     options,
   }: RequestAndOptions): Promise<IResponse> {
-    return fetch(this._uri, assign({}, this._opts, {
+    return fetch(this._uri, {
+      ...this._opts,
       body: JSON.stringify(printRequest(request)),
       method: 'POST',
-    }, options, {
-      headers: assign({}, {
+      ...options,
+      headers: {
         Accept: '*/*',
         'Content-Type': 'application/json',
-      }, options.headers),
-    }));
+        ...(options.headers as { [headerName: string]: string }),
+      },
+    });
   };
 
   public query(request: Request): Promise<ExecutionResult> {
-    const options = assign({}, this._opts);
+    const options = { ...this._opts };
 
     return this.applyMiddlewares({
       request,
@@ -234,7 +232,7 @@ export function createNetworkInterface(
 
   // We want to change the API in the future so that you just pass all of the options as one
   // argument, so even though the internals work with two arguments we're warning here.
-  if (isString(uriOrInterfaceOpts)) {
+  if (typeof uriOrInterfaceOpts === 'string') {
     console.warn(`Passing the URI as the first argument to createNetworkInterface is deprecated \
 as of Apollo Client 0.5. Please pass it as the "uri" property of the network interface options.`);
     opts = secondArgOpts;
@@ -244,5 +242,15 @@ as of Apollo Client 0.5. Please pass it as the "uri" property of the network int
     uri = (uriOrInterfaceOpts as NetworkInterfaceOptions).uri;
   }
 
+  // Warn if there is no global `fetch` implementation.
+  if (typeof fetch === 'undefined') {
+    console.warn([
+      '[apollo-client]: An implementation for the fetch browser API could not be found. Apollo',
+      'client requires fetch to execute GraphQL queries against your API server. Please include a',
+      'global fetch implementation such as [whatwg-fetch](http://npmjs.com/whatwg-fetch) so that',
+      'Apollo client can run in this environment.',
+    ].join(' '));
+  }
+
   return new HTTPFetchNetworkInterface(uri, opts);
 }
diff --git a/src/util/assign.ts b/src/util/assign.ts
new file mode 100644
--- /dev/null
+++ b/src/util/assign.ts
@@ -0,0 +1,20 @@
+/**
+ * Adds the properties of one or more source objects to a target object. Works exactly like
+ * `Object.assign`, but as a utility to maintain support for IE 11.
+ *
+ * @see https://github.com/apollostack/apollo-client/pull/1009
+ */
+export function assign <A, B>(a: A, b: B): A & B;
+export function assign <A, B, C>(a: A, b: B, c: C): A & B & C;
+export function assign <A, B, C, D>(a: A, b: B, c: C, d: D): A & B & C & D;
+export function assign <A, B, C, D, E>(a: A, b: B, c: C, d: D, e: E): A & B & C & D & E;
+export function assign (target: any, ...sources: Array<any>): any;
+export function assign (
+  target: { [key: string]: any },
+  ...sources: Array<{ [key: string]: any }>,
+): { [key: string]: any } {
+  sources.forEach(source => Object.keys(source).forEach(key => {
+    target[key] = source[key];
+  }));
+  return target;
+}
diff --git a/src/util/cloneDeep.ts b/src/util/cloneDeep.ts
new file mode 100644
--- /dev/null
+++ b/src/util/cloneDeep.ts
@@ -0,0 +1,23 @@
+/**
+ * Deeply clones a value to create a new instance.
+ */
+export function cloneDeep <T>(value: T): T {
+  // If the value is an array, create a new array where every item has been cloned.
+  if (Array.isArray(value)) {
+    return value.map(item => cloneDeep(item)) as any;
+  }
+  // If the value is an object, go through all of the object’s properties and add them to a new
+  // object.
+  if (value !== null && typeof value === 'object') {
+    const nextValue: any = {};
+    for (const key in value) {
+      if (value.hasOwnProperty(key)) {
+        nextValue[key] = cloneDeep(value[key]);
+      }
+    }
+    return nextValue;
+  }
+  // Otherwise this is some primitive value and it is therefore immutable so we can just return it
+  // directly.
+  return value;
+}
diff --git a/src/util/isEqual.ts b/src/util/isEqual.ts
new file mode 100644
--- /dev/null
+++ b/src/util/isEqual.ts
@@ -0,0 +1,35 @@
+/**
+ * Performs a deep equality check on two JavaScript values.
+ */
+export function isEqual (a: any, b: any): boolean {
+  // If the two values are strictly equal, we are good.
+  if (a === b) {
+    return true;
+  }
+  // If a and b are both objects, we will compare their properties. This will compare arrays as
+  // well.
+  if (a != null && typeof a === 'object' && b != null && typeof b === 'object') {
+    // Compare all of the keys in `a`. If one of the keys has a different value, or that key does
+    // not exist in `b` return false immediately.
+    for (const key in a) {
+      if (a.hasOwnProperty(key)) {
+        if (!b.hasOwnProperty(key)) {
+          return false;
+        }
+        if (!isEqual(a[key], b[key])) {
+          return false;
+        }
+      }
+    }
+    // Look through all the keys in `b`. If `b` has a key that `a` does not, return false.
+    for (const key in b) {
+      if (!a.hasOwnProperty(key)) {
+        return false;
+      }
+    }
+    // If we made it this far the objects are equal!
+    return true;
+  }
+  // Otherwise the values are not equal.
+  return false;
+}
diff --git a/src/util/maybeDeepFreeze.ts b/src/util/maybeDeepFreeze.ts
new file mode 100644
--- /dev/null
+++ b/src/util/maybeDeepFreeze.ts
@@ -0,0 +1,23 @@
+
+// taken straight from https://github.com/substack/deep-freeze to avoid import hassles with rollup
+function deepFreeze (o: any) {
+  Object.freeze(o);
+
+  Object.getOwnPropertyNames(o).forEach(function (prop) {
+    if (o.hasOwnProperty(prop)
+    && o[prop] !== null
+    && (typeof o[prop] === 'object' || typeof o[prop] === 'function')
+    && !Object.isFrozen(o[prop])) {
+      deepFreeze(o[prop]);
+    }
+  });
+
+  return o;
+};
+
+export default function maybeDeepFreeze(obj: any) {
+  if (process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test') {
+    return deepFreeze(obj);
+  }
+  return obj;
+}


Test patch:
diff --git a/test/ObservableQuery.ts b/test/ObservableQuery.ts
--- a/test/ObservableQuery.ts
+++ b/test/ObservableQuery.ts
@@ -215,7 +215,7 @@ describe('ObservableQuery', () => {
       const variables2 = { first: 1 };
 
 
-      const observable: ObservableQuery = mockWatchQuery({
+      const observable: ObservableQuery<any> = mockWatchQuery({
         request: { query: queryWithVars, variables: variables1 },
         result: { data },
       }, {
@@ -236,7 +236,7 @@ describe('ObservableQuery', () => {
     });
 
     it('does a network request if forceFetch becomes true', (done) => {
-      const observable: ObservableQuery = mockWatchQuery({
+      const observable: ObservableQuery<any> = mockWatchQuery({
         request: { query, variables },
         result: { data: dataOne },
       }, {
@@ -257,7 +257,7 @@ describe('ObservableQuery', () => {
 
     it('does a network request if noFetch becomes true then store is reset then noFetch becomes false', (done) => {
       let queryManager: QueryManager = null;
-      let observable: ObservableQuery = null;
+      let observable: ObservableQuery<any> = null;
       const testQuery = gql`
         query {
           author {
@@ -310,7 +310,7 @@ describe('ObservableQuery', () => {
 
     it('does a network request if noFetch becomes false', (done) => {
       let queryManager: QueryManager = null;
-      let observable: ObservableQuery = null;
+      let observable: ObservableQuery<any> = null;
       const testQuery = gql`
         query {
           author {
@@ -355,7 +355,7 @@ describe('ObservableQuery', () => {
 
   describe('setVariables', () => {
     it('reruns query if the variables change', (done) => {
-      const observable: ObservableQuery = mockWatchQuery({
+      const observable: ObservableQuery<any> = mockWatchQuery({
         request: { query, variables },
         result: { data: dataOne },
       }, {
@@ -449,7 +449,7 @@ describe('ObservableQuery', () => {
     });
 
     it('reruns observer callback if the variables change but data does not', (done) => {
-      const observable: ObservableQuery = mockWatchQuery({
+      const observable: ObservableQuery<any> = mockWatchQuery({
         request: { query, variables },
         result: { data: dataOne },
       }, {
@@ -482,7 +482,7 @@ describe('ObservableQuery', () => {
 
       manager.query({ query, variables: differentVariables })
         .then(() => {
-          const observable: ObservableQuery = manager.watchQuery({ query, variables });
+          const observable: ObservableQuery<any> = manager.watchQuery({ query, variables });
 
           let errored = false;
           subscribeAndCount(done, observable, (handleCount, result) => {
@@ -501,7 +501,7 @@ describe('ObservableQuery', () => {
     });
 
     it('does not rerun query if variables do not change', (done) => {
-      const observable: ObservableQuery = mockWatchQuery({
+      const observable: ObservableQuery<any> = mockWatchQuery({
         request: { query, variables },
         result: { data: dataOne },
       }, {
@@ -528,7 +528,7 @@ describe('ObservableQuery', () => {
       // The expected behavior is that the original variables are forgotten
       // and the query stays in loading state until the result for the new variables
       // has returned.
-      const observable: ObservableQuery = mockWatchQuery({
+      const observable: ObservableQuery<any> = mockWatchQuery({
         request: { query, variables },
         result: { data: dataOne },
         delay: 20,
@@ -553,7 +553,7 @@ describe('ObservableQuery', () => {
 
   describe('currentResult', () => {
     it('returns the current query status immediately', (done) => {
-      const observable: ObservableQuery = mockWatchQuery({
+      const observable: ObservableQuery<any> = mockWatchQuery({
         request: { query, variables },
         result: { data: dataOne },
         delay: 100,
diff --git a/test/QueryManager.ts b/test/QueryManager.ts
--- a/test/QueryManager.ts
+++ b/test/QueryManager.ts
@@ -15,10 +15,6 @@ import {
   ApolloStore,
 } from '../src/store';
 
-import {
-  getIdField,
-} from '../src/data/extensions';
-
 import gql from 'graphql-tag';
 
 import {
@@ -36,13 +32,13 @@ import ApolloClient, {
 
 import {
   ApolloQueryResult,
-} from '../src/core/QueryManager';
+} from '../src/core/types';
 
 import { createStore, combineReducers, applyMiddleware } from 'redux';
 
 import * as Rx from 'rxjs';
 
-import assign = require('lodash/assign');
+import { assign } from 'lodash';
 
 import mockNetworkInterface, {
   ParsedRequest,
@@ -62,7 +58,7 @@ import {
 
 import { NetworkStatus } from '../src/queries/store';
 
-import wrap from './util/wrap';
+import wrap, { withWarning } from './util/wrap';
 
 import observableToPromise, {
   observableToPromiseAndSubscription,
@@ -122,7 +118,7 @@ describe('QueryManager', () => {
     error?: Error,
     result?: ExecutionResult,
     delay?: number,
-    observer: Observer<ApolloQueryResult>,
+    observer: Observer<ApolloQueryResult<any>>,
   }) => {
     const queryManager = mockQueryManager({
       request: { query, variables },
@@ -131,7 +127,7 @@ describe('QueryManager', () => {
       delay,
     });
     const finalOptions = assign({ query, variables }, queryOptions) as WatchQueryOptions;
-    return queryManager.watchQuery(finalOptions).subscribe({
+    return queryManager.watchQuery<any>(finalOptions).subscribe({
       next: wrap(done, observer.next),
       error: observer.error,
     });
@@ -208,12 +204,14 @@ describe('QueryManager', () => {
     request,
     firstResult,
     secondResult,
+    thirdResult,
   }: {
     request: ParsedRequest,
     firstResult: ExecutionResult,
     secondResult: ExecutionResult,
+    thirdResult?: ExecutionResult,
   }) => {
-    return mockQueryManager(
+    const args = [
       {
         request,
         result: firstResult,
@@ -222,7 +220,13 @@ describe('QueryManager', () => {
         request,
         result: secondResult,
       },
-    );
+    ];
+
+    if (thirdResult) {
+      args.push({ request, result: thirdResult });
+    }
+
+    return mockQueryManager(...args);
   };
 
   it('properly roundtrips through a Redux store', (done) => {
@@ -582,9 +586,9 @@ describe('QueryManager', () => {
     let subOneCount = 0;
 
     // pre populate data to avoid contention
-    queryManager.query(request)
+    queryManager.query<any>(request)
       .then(() => {
-        const handle = queryManager.watchQuery(request);
+        const handle = queryManager.watchQuery<any>(request);
 
         const subOne = handle.subscribe({
           next(result) {
@@ -658,7 +662,7 @@ describe('QueryManager', () => {
       secondResult: { data: data2 },
     });
 
-    const observable = queryManager.watchQuery(request);
+    const observable = queryManager.watchQuery<any>(request);
     return observableToPromise({ observable },
       (result) => {
         assert.deepEqual(result.data, data1);
@@ -668,6 +672,123 @@ describe('QueryManager', () => {
     );
   });
 
+  it('will return referentially equivalent data if nothing changed in a refetch', done => {
+    const request = {
+      query: gql`
+        {
+          a
+          b { c }
+          d { e f { g } }
+        }
+      `,
+    };
+
+    const data1 = {
+      a: 1,
+      b: { c: 2 },
+      d: { e: 3, f: { g: 4 } },
+    };
+
+    const data2 = {
+      a: 1,
+      b: { c: 2 },
+      d: { e: 30, f: { g: 4 } },
+    };
+
+    const data3 = {
+      a: 1,
+      b: { c: 2 },
+      d: { e: 3, f: { g: 4 } },
+    };
+
+    const queryManager = mockRefetch({
+      request,
+      firstResult: { data: data1 },
+      secondResult: { data: data2 },
+      thirdResult: { data: data3 },
+    });
+
+    const observable = queryManager.watchQuery<any>(request);
+
+    let count = 0;
+    let firstResultData: any;
+
+    observable.subscribe({
+      next: result => {
+        try {
+          switch (count++) {
+            case 0:
+              assert.deepEqual(result.data, data1);
+              firstResultData = result.data;
+              observable.refetch();
+              break;
+            case 1:
+              assert.deepEqual(result.data, data2);
+              assert.notStrictEqual(result.data, firstResultData);
+              assert.strictEqual(result.data.b, firstResultData.b);
+              assert.notStrictEqual(result.data.d, firstResultData.d);
+              assert.strictEqual(result.data.d.f, firstResultData.d.f);
+              observable.refetch();
+              break;
+            case 2:
+              assert.deepEqual(result.data, data3);
+              assert.notStrictEqual(result.data, firstResultData);
+              assert.strictEqual(result.data.b, firstResultData.b);
+              assert.notStrictEqual(result.data.d, firstResultData.d);
+              assert.strictEqual(result.data.d.f, firstResultData.d.f);
+              done();
+              break;
+            default:
+              throw new Error('Next run too many times.');
+          }
+        } catch (error) {
+          done(error);
+        }
+      },
+      error: error =>
+        done(error),
+    });
+  });
+
+  it('will return referentially equivalent data in getCurrentResult if nothing changed', done => {
+    const request = {
+      query: gql`
+        {
+          a
+          b { c }
+          d { e f { g } }
+        }
+      `,
+    };
+
+    const data1 = {
+      a: 1,
+      b: { c: 2 },
+      d: { e: 3, f: { g: 4 } },
+    };
+
+    const queryManager = mockQueryManager({
+      request,
+      result: { data: data1 },
+    });
+
+    const observable = queryManager.watchQuery<any>(request);
+
+    observable.subscribe({
+      next: result => {
+        try {
+          assert.deepEqual(result.data, data1);
+          assert.strictEqual(result.data, observable.currentResult().data);
+          done();
+        } catch (error) {
+          done(error);
+        }
+      },
+      error: error =>
+        done(error),
+    });
+  });
+
   it('sets networkStatus to `refetch` when refetching', () => {
     const request = {
       query: gql`
@@ -699,7 +820,7 @@ describe('QueryManager', () => {
       secondResult: { data: data2 },
     });
 
-    const observable = queryManager.watchQuery(request);
+    const observable = queryManager.watchQuery<any>(request);
     return observableToPromise({ observable },
       (result) => {
         assert.deepEqual(result.data, data1);
@@ -740,7 +861,7 @@ describe('QueryManager', () => {
       secondResult: { data: data2 },
     });
 
-    const handle = queryManager.watchQuery(request);
+    const handle = queryManager.watchQuery<any>(request);
     handle.subscribe({});
 
     return handle.refetch().then(
@@ -808,7 +929,7 @@ describe('QueryManager', () => {
       },
     );
 
-    const observable = queryManager.watchQuery({ query });
+    const observable = queryManager.watchQuery<any>({ query });
     return observableToPromise({ observable },
       (result) => {
         assert.deepEqual(result.data, data1);
@@ -868,7 +989,7 @@ describe('QueryManager', () => {
       },
     );
 
-    const observable = queryManager.watchQuery({ query });
+    const observable = queryManager.watchQuery<any>({ query });
     const originalOptions = assign({}, observable.options);
     return observableToPromise({ observable },
       (result) => {
@@ -927,7 +1048,7 @@ describe('QueryManager', () => {
       },
     );
 
-    const observable = queryManager.watchQuery({
+    const observable = queryManager.watchQuery<any>({
       query,
       pollInterval: 200,
     });
@@ -988,7 +1109,7 @@ describe('QueryManager', () => {
       },
     );
 
-    const observable = queryManager.watchQuery({
+    const observable = queryManager.watchQuery<any>({
       query,
       pollInterval: 30,
       notifyOnNetworkStatusChange: true,
@@ -1063,10 +1184,10 @@ describe('QueryManager', () => {
     );
 
     // First, prime the store so that query diffing removes the query
-    return queryManager.query({
+    return queryManager.query<any>({
       query: primeQuery,
     }).then(() => {
-      const handle = queryManager.watchQuery({
+      const handle = queryManager.watchQuery<any>({
         query: complexQuery,
         returnPartialData: true,
       });
@@ -1078,6 +1199,21 @@ describe('QueryManager', () => {
     });
   });
 
+  it('deepFreezes results in development mode', () => {
+    const query = gql`{ stuff }`;
+    const data = { stuff: 'wonderful' };
+    const queryManager = mockQueryManager({
+      request: { query },
+      result: { data },
+    });
+
+    return queryManager.query({ query })
+    .then(result => {
+      assert.deepEqual(result.data, data);
+      assert.throws( () => (result.data as any).stuff = 'awful' );
+    });
+  });
+
   it('should error if we pass noFetch on a polling query', (done) => {
     assert.throw(() => {
       assertWithObserver({
@@ -1134,10 +1270,10 @@ describe('QueryManager', () => {
     );
 
     // First, prime the cache
-    return queryManager.query({
+    return queryManager.query<any>({
       query: primeQuery,
     }).then(() => {
-      const handle = queryManager.watchQuery({
+      const handle = queryManager.watchQuery<any>({
         query: complexQuery,
         noFetch: true,
       });
@@ -1170,6 +1306,8 @@ describe('QueryManager', () => {
     });
   });
 
+  const getIdField = ({id}: {id: string}) => id;
+
   it('runs a mutation with object parameters and puts the result in the store', () => {
     const data = {
       makeListPrivate: {
@@ -1342,6 +1480,83 @@ describe('QueryManager', () => {
     );
   });
 
+  it('does not call broadcastNewStore when Apollo state is not affected by an action', () => {
+    const query = gql`
+      query fetchLuke($id: String) {
+        people_one(id: $id) {
+          name
+        }
+      }
+    `;
+
+    const variables = {
+      id: '1',
+    };
+
+    const data1 = {
+      people_one: {
+        name: 'Luke Skywalker',
+      },
+    };
+
+    const data2 = {
+      people_one: {
+        name: 'Luke Skywalker has a new name',
+      },
+    };
+
+    function testReducer (state = false, action: any): boolean {
+      if (action.type === 'TOGGLE') {
+        return true;
+      }
+      return state;
+    }
+    const client = new ApolloClient();
+    const store = createStore(
+      combineReducers({
+        test: testReducer,
+        apollo: client.reducer() as any, // XXX see why this type fails
+      }),
+      applyMiddleware(client.middleware()),
+    );
+    const qm = createQueryManager({
+      networkInterface: mockNetworkInterface(
+        {
+          request: { query, variables },
+          result: { data: data1 },
+        },
+        {
+          request: { query, variables },
+          result: { data: data2 },
+        },
+      ),
+      store: store,
+    });
+
+    const observable = qm.watchQuery({ query, variables });
+
+    return observableToPromise({ observable },
+      (result) => {
+        assert.deepEqual(result.data, data1);
+        observable.refetch();
+      },
+      (result) => {
+        assert.deepEqual(result.data, data2);
+
+        // here's the actual test. Everything else is just setup.
+        let called = false;
+        client.queryManager.broadcastNewStore = (s: any) => {
+          called = true;
+        };
+        store.dispatch({
+          type: 'TOGGLE',
+        });
+        assert.equal((store.getState() as any).test, true, 'test state should have been updated');
+        assert.equal(called, false, 'broadcastNewStore should not have been called');
+      },
+    );
+  });
+
   it(`doesn't return data while query is loading`, () => {
     const query1 = gql`
       {
@@ -1383,8 +1598,8 @@ describe('QueryManager', () => {
       },
     );
 
-    const observable1 = queryManager.watchQuery({ query: query1 });
-    const observable2 = queryManager.watchQuery({ query: query2 });
+    const observable1 = queryManager.watchQuery<any>({ query: query1 });
+    const observable2 = queryManager.watchQuery<any>({ query: query2 });
 
     return Promise.all([
       observableToPromise({ observable: observable1 },
@@ -1441,11 +1656,11 @@ describe('QueryManager', () => {
       },
     );
 
-    const observable = queryManager.watchQuery({ query: query1 });
+    const observable = queryManager.watchQuery<any>({ query: query1 });
     return observableToPromise({ observable },
       (result) => {
         assert.deepEqual(result.data, data1);
-        queryManager.query({ query: query2 });
+        queryManager.query<any>({ query: query2 });
       },
       // 3 because the query init action for the second query causes a callback
       (result) => assert.deepEqual(result.data, {
@@ -1492,7 +1707,7 @@ describe('QueryManager', () => {
           result: { data: data2 },
         },
       );
-      const observable = queryManager.watchQuery({
+      const observable = queryManager.watchQuery<any>({
         query,
         variables,
         pollInterval: 50,
@@ -1502,6 +1717,7 @@ describe('QueryManager', () => {
         (result) => assert.deepEqual(result.data, data1),
         (result) => assert.deepEqual(result.data, data2),
       );
+
     });
 
     it('should let you handle multiple polled queries and unsubscribe from one of them', (done) => {
@@ -1711,7 +1927,7 @@ describe('QueryManager', () => {
         },
       );
 
-      const observable = queryManager.watchQuery({
+      const observable = queryManager.watchQuery<any>({
         query,
         variables,
         pollInterval: 50,
@@ -1769,7 +1985,7 @@ describe('QueryManager', () => {
         },
       );
 
-      const observable = queryManager.watchQuery({ query, variables });
+      const observable = queryManager.watchQuery<any>({ query, variables });
       observable.startPolling(50);
 
       return observableToPromise({ observable },
@@ -1813,7 +2029,7 @@ describe('QueryManager', () => {
           result: { data: data2 },
         },
       );
-      const observable = queryManager.watchQuery({
+      const observable = queryManager.watchQuery<any>({
         query,
         variables,
         pollInterval: 50,
@@ -1890,7 +2106,7 @@ describe('QueryManager', () => {
   it('warns if you forget the template literal tag', () => {
     const queryManager = mockQueryManager();
     assert.throws(() => {
-      queryManager.query({
+      queryManager.query<any>({
         // Bamboozle TypeScript into letting us do this
         query: 'string' as any as DocumentNode,
       });
@@ -1904,7 +2120,7 @@ describe('QueryManager', () => {
     }, /wrap the query string in a "gql" tag/);
 
     assert.throws(() => {
-      queryManager.watchQuery({
+      queryManager.watchQuery<any>({
         // Bamboozle TypeScript into letting us do this
         query: 'string' as any as DocumentNode,
       });
@@ -2054,7 +2270,7 @@ describe('QueryManager', () => {
         },
       };
       queryManager = createQueryManager({ networkInterface });
-      const observable = queryManager.watchQuery({ query });
+      const observable = queryManager.watchQuery<any>({ query });
 
       // wait just to make sure the observable doesn't fire again
       return observableToPromise({ observable, wait: 0 },
@@ -2066,7 +2282,7 @@ describe('QueryManager', () => {
 
     it('should not refetch toredown queries', (done) => {
       let queryManager: QueryManager = null;
-      let observable: ObservableQuery = null;
+      let observable: ObservableQuery<any> = null;
       const query = gql`
         query {
           author {
@@ -2138,7 +2354,7 @@ describe('QueryManager', () => {
         },
       };
       queryManager = createQueryManager({ networkInterface });
-      const observable = queryManager.watchQuery({ query });
+      const observable = queryManager.watchQuery<any>({ query });
 
       // wait to make sure store reset happened
       return observableToPromise({ observable, wait: 20 },
@@ -2186,7 +2402,8 @@ describe('QueryManager', () => {
           }
         }`;
       const queryManager = mockQueryManager();
-      const mockObservableQuery: ObservableQuery = {
+
+      const mockObservableQuery: ObservableQuery<any> = {
         refetch(variables: any): Promise<ExecutionResult> {
           done();
           return null;
@@ -2195,10 +2412,10 @@ describe('QueryManager', () => {
           query: query,
         },
         scheduler: queryManager.scheduler,
-      } as any as ObservableQuery;
+      } as any as ObservableQuery<any>;
 
       const queryId = 'super-fake-id';
-      queryManager.addObservableQuery(queryId, mockObservableQuery);
+      queryManager.addObservableQuery<any>(queryId, mockObservableQuery);
       queryManager.resetStore();
     });
 
@@ -2215,7 +2432,7 @@ describe('QueryManager', () => {
       options.noFetch = true;
       options.query = query;
       let refetchCount = 0;
-      const mockObservableQuery: ObservableQuery = {
+      const mockObservableQuery: ObservableQuery<any> = {
         refetch(variables: any): Promise<ExecutionResult> {
           refetchCount ++;
           done();
@@ -2223,10 +2440,10 @@ describe('QueryManager', () => {
         },
         options,
         queryManager: queryManager,
-      } as any as ObservableQuery;
+      } as any as ObservableQuery<any>;
 
       const queryId = 'super-fake-id';
-      queryManager.addObservableQuery(queryId, mockObservableQuery);
+      queryManager.addObservableQuery<any>(queryId, mockObservableQuery);
       queryManager.resetStore();
       setTimeout(() => {
         assert.equal(refetchCount, 0);
@@ -2260,7 +2477,7 @@ describe('QueryManager', () => {
       };
 
       queryManager = createQueryManager({ networkInterface });
-      queryManager.query({ query }).then((result) => {
+      queryManager.query<any>({ query }).then((result) => {
         done(new Error('query() gave results on a store reset'));
       }).catch((error) => {
         done();
@@ -2374,10 +2591,10 @@ describe('QueryManager', () => {
         error: new Error('Network error ocurred'),
       },
     );
-    queryManager.query({ query }).then((result) => {
+    queryManager.query<any>({ query }).then((result) => {
       assert.deepEqual(result.data, data);
 
-      queryManager.query({ query, forceFetch: true }).then(() => {
+      queryManager.query<any>({ query, forceFetch: true }).then(() => {
         done(new Error('Returned a result when it was not supposed to.'));
       }).catch((error) => {
         // make that the error thrown doesn't empty the state
@@ -2445,7 +2662,7 @@ describe('QueryManager', () => {
         error: new Error('Network error occurred.'),
       },
     );
-    const observable = queryManager.watchQuery({ query, pollInterval: 20 });
+    const observable = queryManager.watchQuery<any>({ query, pollInterval: 20 });
 
     return observableToPromise({
         observable,
@@ -2495,7 +2712,7 @@ describe('QueryManager', () => {
       },
     );
 
-    const observable = queryManager.watchQuery({ query });
+    const observable = queryManager.watchQuery<any>({ query });
     return Promise.all<any[] | void>([
       // we wait for a little bit to ensure the result of the second query
       // don't trigger another subscription event
@@ -2504,7 +2721,7 @@ describe('QueryManager', () => {
           assert.deepEqual(result.data, data);
         },
       ),
-      queryManager.query({ query }).then((result) => {
+      queryManager.query<any>({ query }).then((result) => {
         assert.deepEqual(result.data, data);
       }),
     ]);
@@ -2607,8 +2824,8 @@ describe('QueryManager', () => {
       store,
     });
 
-    const observable1 = queryManager.watchQuery({ query: query1 });
-    const observable2 = queryManager.watchQuery({ query: query2 });
+    const observable1 = queryManager.watchQuery<any>({ query: query1 });
+    const observable2 = queryManager.watchQuery<any>({ query: query2 });
 
     // I'm not sure the waiting 60 here really is required, but the test used to do it
     return Promise.all([
@@ -2678,8 +2895,8 @@ describe('QueryManager', () => {
       store,
     });
 
-    const observableWithId = queryManager.watchQuery({ query: queryWithId });
-    const observableWithoutId = queryManager.watchQuery({ query: queryWithoutId });
+    const observableWithId = queryManager.watchQuery<any>({ query: queryWithId });
+    const observableWithoutId = queryManager.watchQuery<any>({ query: queryWithoutId });
 
     // I'm not sure the waiting 60 here really is required, but the test used to do it
     return Promise.all([
@@ -2766,8 +2983,8 @@ describe('QueryManager', () => {
       store,
     });
 
-    const observableWithId = queryManager.watchQuery({ query: queryWithId });
-    const observableWithoutId = queryManager.watchQuery({ query: queryWithoutId });
+    const observableWithId = queryManager.watchQuery<any>({ query: queryWithId });
+    const observableWithoutId = queryManager.watchQuery<any>({ query: queryWithoutId });
 
     // I'm not sure the waiting 60 here really is required, but the test used to do it
     return Promise.all([
@@ -2829,8 +3046,8 @@ describe('QueryManager', () => {
         },
       );
 
-      return queryManager.query({ query: primeQuery }).then((primeResult) => {
-        const observable = queryManager.watchQuery({ query, returnPartialData: true });
+      return queryManager.query<any>({ query: primeQuery }).then((primeResult) => {
+        const observable = queryManager.watchQuery<any>({ query, returnPartialData: true });
 
         return observableToPromise({ observable },
           (result) => {
@@ -2937,7 +3154,7 @@ describe('QueryManager', () => {
           result: { data: mutationData },
         },
       );
-      const observable = queryManager.watchQuery({ query });
+      const observable = queryManager.watchQuery<any>({ query });
       return observableToPromise({ observable },
         (result) => {
           assert.deepEqual(result.data, data);
@@ -2994,7 +3211,7 @@ describe('QueryManager', () => {
           result: { data: mutationData },
         },
       );
-      const observable = queryManager.watchQuery({ query });
+      const observable = queryManager.watchQuery<any>({ query });
       return observableToPromise({ observable },
         (result) => {
           assert.deepEqual(result.data, data);
@@ -3056,7 +3273,7 @@ describe('QueryManager', () => {
         },
       );
 
-      const observable = queryManager.watchQuery({ query });
+      const observable = queryManager.watchQuery<any>({ query });
       return observableToPromise({ observable },
         (result) => {
           assert.deepEqual(result.data, data);
@@ -3082,31 +3299,33 @@ describe('QueryManager', () => {
     let transformCount: number;
 
     beforeEach(() => {
-      transformCount = 0;
+      withWarning( () => {
+       transformCount = 0;
 
-      const networkInterface: NetworkInterface = {
-        query(request: Request): Promise<ExecutionResult> {
-          return Promise.resolve(response);
-        },
-      };
-
-      client = new ApolloClient({
-        networkInterface,
-        resultTransformer(result: ExecutionResult) {
-          transformCount++;
-          return {
-            data: assign({}, result.data, {transformCount}),
-            loading: false,
-            networkStatus: NetworkStatus.ready,
-          };
-        },
-      });
+        const networkInterface: NetworkInterface = {
+          query(request: Request): Promise<ExecutionResult> {
+            return Promise.resolve(response);
+          },
+        };
+
+        client = new ApolloClient({
+          networkInterface,
+          resultTransformer(result: ExecutionResult) {
+            transformCount++;
+            return {
+              data: assign({}, result.data, {transformCount}),
+              loading: false,
+              networkStatus: NetworkStatus.ready,
+            };
+          },
+        });
+      }, /resultTransformer/);
     });
 
     it('transforms query() results', () => {
       response = {data: {foo: 123}};
       return client.query({query: gql`{ foo }`})
-        .then((result: ApolloQueryResult) => {
+        .then((result: ApolloQueryResult<any>) => {
           assert.deepEqual(result.data, {foo: 123, transformCount: 1});
         });
     });
@@ -3147,7 +3366,7 @@ describe('QueryManager', () => {
     it('transforms mutate() results', () => {
       response = {data: {foo: 123}};
       return client.mutate({mutation: gql`mutation makeChanges { foo }`})
-        .then((result: ApolloQueryResult) => {
+        .then((result: ApolloQueryResult<any>) => {
           assert.deepEqual(result.data, {foo: 123, transformCount: 1});
         });
     });
@@ -3162,25 +3381,27 @@ describe('QueryManager', () => {
     let response: any;
 
     beforeEach(() => {
-      const networkInterface: NetworkInterface = {
-        query(request: Request): Promise<ExecutionResult> {
-          return Promise.resolve(response);
-        },
-      };
+      withWarning( () => {
+        const networkInterface: NetworkInterface = {
+          query(request: Request): Promise<ExecutionResult> {
+            return Promise.resolve(response);
+          },
+        };
 
-      client = new ApolloClient({
-        networkInterface,
-        resultTransformer(result: ApolloQueryResult) {
-          result.data.__proto__ = Model.prototype;
-          return result;
-        },
-        resultComparator(result1: ApolloQueryResult, result2: ApolloQueryResult) {
-          // A real example would, say, deep compare the two while ignoring prototypes.
-          const foo1 = result1 && result1.data && result1.data.foo;
-          const foo2 = result2 && result2.data && result2.data.foo;
-          return foo1 === foo2;
-        },
-      });
+        client = new ApolloClient({
+          networkInterface,
+          resultTransformer(result: ApolloQueryResult<any>) {
+            result.data.__proto__ = Model.prototype;
+            return result;
+          },
+          resultComparator(result1: ApolloQueryResult<any>, result2: ApolloQueryResult<any>) {
+            // A real example would, say, deep compare the two while ignoring prototypes.
+            const foo1 = result1 && result1.data && result1.data.foo;
+            const foo2 = result2 && result2.data && result2.data.foo;
+            return foo1 === foo2;
+          },
+        });
+      }, /resultTransformer/);
     });
 
     it('does not transform identical watchQuery() results, according to the comparator', () => {
@@ -3233,7 +3454,7 @@ describe('QueryManager', () => {
 
     const queryManager = mockRefetch({ request, firstResult, secondResult });
 
-    const handle = queryManager.watchQuery(request);
+    const handle = queryManager.watchQuery<any>(request);
 
     handle.subscribe({
       error: () => { /* nothing */ },
diff --git a/test/assign.ts b/test/assign.ts
new file mode 100644
--- /dev/null
+++ b/test/assign.ts
@@ -0,0 +1,30 @@
+import { assign } from '../src/util/assign';
+import { assert } from 'chai';
+
+describe('assign', () => {
+  it('will merge many objects together', () => {
+    assert.deepEqual(assign({ a: 1 }, { b: 2 }), { a: 1, b: 2 });
+    assert.deepEqual(assign({ a: 1 }, { b: 2 }, { c: 3 }), { a: 1, b: 2, c: 3 });
+    assert.deepEqual(assign({ a: 1 }, { b: 2 }, { c: 3 }, { d: 4 }), { a: 1, b: 2, c: 3, d: 4 });
+  });
+
+  it('will merge many objects together shallowly', () => {
+    assert.deepEqual(assign({ x: { a: 1 } }, { x: { b: 2 } }), { x: { b: 2 } });
+    assert.deepEqual(assign({ x: { a: 1 } }, { x: { b: 2 } }, { x: { c: 3 } }), { x: { c: 3 } });
+    assert.deepEqual(assign({ x: { a: 1 } }, { x: { b: 2 } }, { x: { c: 3 } }, { x: { d: 4 } }), { x: { d: 4 } });
+  });
+
+  it('will mutate and return the source objects', () => {
+    const source1 = { a: 1 };
+    const source2 = { a: 1 };
+    const source3 = { a: 1 };
+
+    assert.strictEqual(assign(source1, { b: 2 }), source1);
+    assert.strictEqual(assign(source2, { b: 2 }, { c: 3 }), source2);
+    assert.strictEqual(assign(source3, { b: 2 }, { c: 3 }, { d: 4 }), source3);
+
+    assert.deepEqual(source1, { a: 1, b: 2 });
+    assert.deepEqual(source2, { a: 1, b: 2, c: 3 });
+    assert.deepEqual(source3, { a: 1, b: 2, c: 3, d: 4 });
+  });
+});
diff --git a/test/batchedNetworkInterface.ts b/test/batchedNetworkInterface.ts
--- a/test/batchedNetworkInterface.ts
+++ b/test/batchedNetworkInterface.ts
@@ -1,6 +1,6 @@
 import { assert } from 'chai';
 
-import merge = require('lodash/merge');
+import { merge } from 'lodash';
 
 import { HTTPBatchedNetworkInterface } from '../src/transport/batchedNetworkInterface';
 
@@ -19,8 +19,6 @@ import { AfterwareInterface } from '../src/transport/afterware';
 
 import { ExecutionResult } from 'graphql';
 
-import 'whatwg-fetch';
-
 import gql from 'graphql-tag';
 
 describe('HTTPBatchedNetworkInterface', () => {
diff --git a/test/client.ts b/test/client.ts
--- a/test/client.ts
+++ b/test/client.ts
@@ -3,19 +3,13 @@ const { assert } = chai;
 import * as sinon from 'sinon';
 
 import ApolloClient, {
-  createFragment,
-  clearFragmentDefinitions,
-  disableFragmentWarnings,
   printAST,
-  enableFragmentWarnings,
 } from '../src';
 
 import {
   disableFragmentWarnings as graphqlTagDisableFragmentWarnings,
 } from 'graphql-tag';
 
-import { fragmentDefinitionsMap } from '../src/fragments';
-
 import {
   GraphQLError,
   ExecutionResult,
@@ -80,15 +74,12 @@ import { withWarning } from './util/wrap';
 
 import observableToPromise from './util/observableToPromise';
 
-import cloneDeep = require('lodash/cloneDeep');
-
-import assign = require('lodash/assign');
+import { cloneDeep, assign } from 'lodash';
 
 // make it easy to assert with promises
 chai.use(chaiAsPromised);
 
 // Turn off warnings for repeated fragment names
-disableFragmentWarnings();
 graphqlTagDisableFragmentWarnings();
 
 describe('client', () => {
@@ -1310,461 +1301,6 @@ it('should not let errors in observer.next reach the store', (done) => {
     assert.equal(printAST(query), print(query));
   });
 
-  describe('fragment referencing', () => {
-    afterEach(() => {
-      // after each test, we have to empty out fragmentDefinitionsMap since that is
-      // global state that will be held across all client instances.
-      clearFragmentDefinitions();
-    });
-
-    it('should return a fragment def with a unique name', () => {
-      const fragment = gql`
-        fragment authorDetails on Author {
-          author {
-            firstName
-            lastName
-          }
-        }
-      `;
-      const fragmentDefs = createFragment(fragment);
-      assert.equal(fragmentDefs.length, 1);
-      assert.equal(print(fragmentDefs[0]), print(getFragmentDefinitions(fragment)[0]));
-    });
-
-    it('should correctly return multiple fragments from a single document', () => {
-      const fragmentDoc = gql`
-        fragment authorDetails on Author {
-          firstName
-          lastName
-        }
-        fragment personDetails on Person {
-          name
-        }
-        `;
-      const fragmentDefs = createFragment(fragmentDoc);
-      assert.equal(fragmentDefs.length, 2);
-      const expFragmentDefs = getFragmentDefinitions(fragmentDoc);
-      assert.equal(print(fragmentDefs[0]), print(expFragmentDefs[0]));
-      assert.equal(print(fragmentDefs[1]), print(expFragmentDefs[1]));
-    });
-
-    it('should correctly return fragment defs with one fragment depending on another', () => {
-      const fragmentDoc = gql`
-        fragment authorDetails on Author {
-          firstName
-          lastName
-          ...otherAuthorDetails
-        }`;
-      const otherFragmentDoc = gql`
-        fragment otherFragmentDoc on Author {
-          address
-        }`;
-      const fragmentDefs = createFragment(fragmentDoc, getFragmentDefinitions(otherFragmentDoc));
-      assert.equal(fragmentDefs.length, 2);
-      const expFragmentDefs = getFragmentDefinitions(otherFragmentDoc)
-        .concat(getFragmentDefinitions(fragmentDoc));
-      assert.deepEqual(fragmentDefs.map(print), expFragmentDefs.map(print));
-    });
-
-    it('should return fragment defs with a multiple fragments depending on other fragments', () => {
-      const fragmentDoc = gql`
-        fragment authorDetails on Author {
-          firstName
-          lastName
-          ...otherAuthorDetails
-        }
-
-        fragment onlineAuthorDetails on Author {
-          email
-          ...otherAuthorDetails
-        }`;
-      const otherFragmentDoc = gql`
-        fragment otherAuthorDetails on Author {
-          address
-        }`;
-      const fragmentDefs = createFragment(fragmentDoc, getFragmentDefinitions(otherFragmentDoc));
-      assert.equal(fragmentDefs.length, 3);
-
-      const expFragmentDefs = getFragmentDefinitions(otherFragmentDoc)
-        .concat(getFragmentDefinitions(fragmentDoc));
-      assert.deepEqual(fragmentDefs.map(print), expFragmentDefs.map(print));
-    });
-
-    it('should always return a flat array of fragment defs', () => {
-      const fragmentDoc1 = gql`
-        fragment authorDetails on Author {
-          firstName
-          lastName
-          ...otherAuthorDetails
-        }`;
-      const fragmentDoc2 = gql`
-        fragment otherAuthorDetails on Author {
-          address
-        }`;
-      const fragmentDoc3 = gql`
-        fragment personDetails on Person {
-          personDetails
-        }`;
-      const fragments1 = createFragment(fragmentDoc1);
-      const fragments2 = createFragment(fragmentDoc2);
-      const fragments3 = createFragment(fragmentDoc3, [fragments1, fragments2]);
-      assert.equal(fragments1.length, 1);
-      assert.equal(fragments2.length, 1);
-      assert.equal(fragments3.length, 3);
-    });
-
-    it('should add a fragment to the fragmentDefinitionsMap', () => {
-      const fragmentDoc = gql`
-        fragment authorDetails on Author {
-          firstName
-          lastName
-        }`;
-      assert.equal(Object.keys(fragmentDefinitionsMap).length, 0);
-      createFragment(fragmentDoc);
-      assert.equal(Object.keys(fragmentDefinitionsMap).length, 1);
-      assert(fragmentDefinitionsMap.hasOwnProperty('authorDetails'));
-      assert.equal(fragmentDefinitionsMap['authorDetails'].length, 1);
-      assert.equal(print(fragmentDefinitionsMap['authorDetails']), print(getFragmentDefinitions(fragmentDoc)[0]));
-    });
-
-    it('should add fragments with the same name to fragmentDefinitionsMap + print warning', () => {
-      const fragmentDoc = gql`
-        fragment authorDetails on Author {
-          firstName
-          lastName
-        }
-        fragment authorDetails on Author {
-          address
-        }`;
-
-      // hacky solution that allows us to test whether the warning is printed
-      const oldWarn = console.warn;
-      console.warn = (str: string) => {
-        if (!str.match(/deprecated/)) {
-          assert.include(str, 'Warning: fragment with name');
-        }
-      };
-
-      createFragment(fragmentDoc);
-      assert.equal(Object.keys(fragmentDefinitionsMap).length, 1);
-      assert.equal(fragmentDefinitionsMap['authorDetails'].length, 2);
-      console.warn = oldWarn;
-    });
-
-    it('should issue a warning if we try query with a conflicting fragment name', () => {
-      enableFragmentWarnings();
-
-      const client = new ApolloClient({
-        networkInterface: mockNetworkInterface(),
-        addTypename: false,
-      });
-      const fragmentDoc = gql`
-        fragment authorDetails on Author {
-          firstName
-          lastName
-        }`;
-      const queryDoc = gql`
-        query {
-          author {
-            firstName
-            lastName
-          }
-        }
-        fragment authorDetails on Author {
-          firstName
-          lastName
-        }`;
-      createFragment(fragmentDoc);
-
-      withWarning(() => {
-        client.query({ query: queryDoc });
-      }, /Warning: fragment with name/);
-
-      disableFragmentWarnings();
-    });
-
-    it('should issue a warning if we try to watchQuery with a conflicting fragment name', () => {
-      enableFragmentWarnings();
-
-      const client = new ApolloClient({
-        networkInterface: mockNetworkInterface(),
-        addTypename: false,
-      });
-      const fragmentDoc = gql`
-        fragment authorDetails on Author {
-          firstName
-          lastName
-        }`;
-      const queryDoc = gql`
-        query {
-          author {
-            firstName
-            lastName
-          }
-        }
-        fragment authorDetails on Author {
-          firstName
-          lastName
-        }`;
-      createFragment(fragmentDoc);
-
-      withWarning(() => {
-        client.watchQuery({ query: queryDoc });
-      }, /Warning: fragment with name/);
-
-      disableFragmentWarnings();
-    });
-
-    it('should allow passing fragments to query', () => {
-      const queryDoc = gql`
-        query {
-          author {
-            __typename
-            ...authorDetails
-          }
-        }`;
-      const composedQuery = gql`
-        query {
-          author {
-            __typename
-            ...authorDetails
-          }
-        }
-        fragment authorDetails on Author {
-          firstName
-          lastName
-        }`;
-      const data = {
-        author: {
-          __typename: 'Author',
-          firstName: 'John',
-          lastName: 'Smith',
-        },
-      };
-      const networkInterface = mockNetworkInterface({
-        request: { query: composedQuery },
-        result: { data },
-      });
-      const client = new ApolloClient({
-        networkInterface,
-        addTypename: false,
-      });
-      const fragmentDefs = createFragment(gql`
-        fragment authorDetails on Author {
-          firstName
-          lastName
-        }`);
-
-      return client.query({ query: queryDoc, fragments: fragmentDefs }).then((result) => {
-        assert.deepEqual(result.data, data);
-      });
-    });
-
-    it('show allow passing fragments to mutate', () => {
-      const mutationDoc = gql`
-        mutation createAuthor {
-          createAuthor {
-            __typename
-            ...authorDetails
-          }
-        }`;
-      const composedMutation = gql`
-        mutation createAuthor {
-          createAuthor {
-            __typename
-            ...authorDetails
-          }
-        }
-        fragment authorDetails on Author {
-          firstName
-          lastName
-        }`;
-      const data = {
-        createAuthor: {
-          __typename: 'Author',
-          firstName: 'John',
-          lastName: 'Smith',
-        },
-      };
-      const networkInterface = mockNetworkInterface({
-        request: { query: composedMutation },
-        result: { data },
-      });
-      const client = new ApolloClient({
-        networkInterface,
-        addTypename: false,
-      });
-      const fragmentDefs = createFragment(gql`
-        fragment authorDetails on Author {
-          firstName
-          lastName
-        }`);
-
-      return client.mutate({ mutation: mutationDoc, fragments: fragmentDefs }).then((result) => {
-        assert.deepEqual(result, { data });
-      });
-    });
-
-    it('should allow passing fragments to watchQuery', () => {
-      const queryDoc = gql`
-        query {
-          author {
-            __typename
-            ...authorDetails
-          }
-        }`;
-      const composedQuery = gql`
-        query {
-          author {
-            __typename
-            ...authorDetails
-          }
-        }
-        fragment authorDetails on Author {
-          firstName
-          lastName
-        }`;
-      const data = {
-        author: {
-          __typename: 'Author',
-          firstName: 'John',
-          lastName: 'Smith',
-        },
-      };
-      const networkInterface = mockNetworkInterface({
-        request: { query: composedQuery },
-        result: { data },
-      });
-      const client = new ApolloClient({
-        networkInterface,
-        addTypename: false,
-      });
-      const fragmentDefs = createFragment(gql`
-        fragment authorDetails on Author {
-          firstName
-          lastName
-        }`);
-
-      const observable = client.watchQuery({ query: queryDoc, fragments: fragmentDefs });
-
-      return observableToPromise({ observable }, (result) => {
-        assert.deepEqual(result.data, data);
-      });
-    });
-
-    it('should allow passing fragments in polling queries', () => {
-      const queryDoc = gql`
-        query {
-          author {
-            __typename
-            ...authorDetails
-          }
-        }`;
-      const composedQuery = gql`
-        query {
-          author {
-            __typename
-            ...authorDetails
-          }
-        }
-        fragment authorDetails on Author {
-          firstName
-          lastName
-        }`;
-      const data = {
-        author: {
-          __typename: 'Author',
-          firstName: 'John',
-          lastName: 'Smith',
-        },
-      };
-      const networkInterface = mockNetworkInterface({
-        request: { query: composedQuery },
-        result: { data },
-      });
-      const client = new ApolloClient({
-        networkInterface,
-        addTypename: false,
-      });
-      const fragmentDefs = createFragment(gql`
-        fragment authorDetails on Author {
-          firstName
-          lastName
-        }`);
-
-      const observable = client.watchQuery({
-        query: queryDoc,
-        pollInterval: 30,
-        fragments: fragmentDefs,
-      });
-
-      return observableToPromise({ observable }, (result) => {
-        assert.deepEqual(result.data, data);
-      });
-    });
-
-    it('should not print a warning if we call disableFragmentWarnings', (done) => {
-      const oldWarn = console.warn;
-      console.warn = (str: string) => {
-        if (!str.match(/deprecated/)) {
-          done(new Error('Returned a warning despite calling disableFragmentWarnings'));
-        }
-      };
-      disableFragmentWarnings();
-      createFragment(gql`
-        fragment authorDetails on Author {
-          firstName
-        }
-      `);
-      createFragment(gql`
-        fragment authorDetails on Author {
-          lastName
-        }`);
-
-      // create fragment operates synchronously so if it returns and doesn't call
-      // console.warn, we are done.
-      setTimeout(() => {
-        console.warn = oldWarn;
-        done();
-      }, 100);
-    });
-
-    it('should not add multiple instances of the same fragment to fragmentDefinitionsMap', () => {
-      createFragment(gql`
-        fragment authorDetails on Author {
-          author {
-            firstName
-            lastName
-          }
-        }`);
-      createFragment(gql`
-        fragment authorDetails on Author {
-          author {
-            firstName
-            lastName
-          }
-        }`);
-      assert(fragmentDefinitionsMap.hasOwnProperty('authorDetails'));
-      assert.equal(fragmentDefinitionsMap['authorDetails'].length, 1);
-    });
-
-    it('should not mutate the input document when querying', () => {
-      const client = new ApolloClient();
-
-      const fragments = createFragment(gql`
-        fragment authorDetails on Author {
-          author {
-            firstName
-            lastName
-          }
-        }`);
-      const query = gql`{ author { ...authorDetails } }`;
-      const initialDefinitions = query.definitions;
-      client.query({query, fragments});
-      assert.equal(query.definitions, initialDefinitions);
-    });
-  });
-
   it('should pass a network error correctly on a mutation', (done) => {
     const mutation = gql`
       mutation {
@@ -2126,13 +1662,33 @@ it('should not let errors in observer.next reach the store', (done) => {
         });
     });
   });
+
+  it('should propagate errors from network interface to observers', (done) => {
+
+    const networkInterface = {
+      query: () => Promise.reject(new Error('Uh oh!')),
+    };
+
+    const client = new ApolloClient({
+      networkInterface,
+      addTypename: false,
+    });
+
+    const handle = client.watchQuery({ query: gql`query { a b c }` });
+
+    handle.subscribe({
+      error(error) {
+        assert.equal(error.message, 'Network error: Uh oh!');
+        done();
+      },
+    });
+  });
 });
 
 function clientRoundrip(
   query: DocumentNode,
   data: ExecutionResult,
   variables?: any,
-  fragments?: FragmentDefinitionNode[],
 ) {
   const networkInterface = mockNetworkInterface({
     request: { query: cloneDeep(query) },
@@ -2143,7 +1699,7 @@ function clientRoundrip(
     networkInterface,
   });
 
-  return client.query({ query, variables, fragments })
+  return client.query({ query, variables })
     .then((result) => {
       assert.deepEqual(result.data, data);
     });
diff --git a/test/cloneDeep.ts b/test/cloneDeep.ts
new file mode 100644
--- /dev/null
+++ b/test/cloneDeep.ts
@@ -0,0 +1,56 @@
+import { cloneDeep } from '../src/util/cloneDeep';
+import { assert } from 'chai';
+
+describe('cloneDeep', () => {
+  it('will clone primitive values', () => {
+    assert.equal(cloneDeep(undefined), undefined);
+    assert.equal(cloneDeep(null), null);
+    assert.equal(cloneDeep(true), true);
+    assert.equal(cloneDeep(false), false);
+    assert.equal(cloneDeep(-1), -1);
+    assert.equal(cloneDeep(+1), +1);
+    assert.equal(cloneDeep(0.5), 0.5);
+    assert.equal(cloneDeep('hello'), 'hello');
+    assert.equal(cloneDeep('world'), 'world');
+  });
+
+  it('will clone objects', () => {
+    const value1 = {};
+    const value2 = { a: 1, b: 2, c: 3 };
+    const value3 = { x: { a: 1, b: 2, c: 3 }, y: { a: 1, b: 2, c: 3 } };
+
+    const clonedValue1 = cloneDeep(value1);
+    const clonedValue2 = cloneDeep(value2);
+    const clonedValue3 = cloneDeep(value3);
+
+    assert.deepEqual(clonedValue1, value1);
+    assert.deepEqual(clonedValue2, value2);
+    assert.deepEqual(clonedValue3, value3);
+
+    assert.notStrictEqual(clonedValue1, value1);
+    assert.notStrictEqual(clonedValue2, value2);
+    assert.notStrictEqual(clonedValue3, value3);
+    assert.notStrictEqual(clonedValue3.x, value3.x);
+    assert.notStrictEqual(clonedValue3.y, value3.y);
+  });
+
+  it('will clone arrays', () => {
+    const value1: Array<number> = [];
+    const value2 = [1, 2, 3];
+    const value3 = [[1, 2, 3], [1, 2, 3]];
+
+    const clonedValue1 = cloneDeep(value1);
+    const clonedValue2 = cloneDeep(value2);
+    const clonedValue3 = cloneDeep(value3);
+
+    assert.deepEqual(clonedValue1, value1);
+    assert.deepEqual(clonedValue2, value2);
+    assert.deepEqual(clonedValue3, value3);
+
+    assert.notStrictEqual(clonedValue1, value1);
+    assert.notStrictEqual(clonedValue2, value2);
+    assert.notStrictEqual(clonedValue3, value3);
+    assert.notStrictEqual(clonedValue3[0], value3[0]);
+    assert.notStrictEqual(clonedValue3[1], value3[1]);
+  });
+});
diff --git a/test/diffAgainstStore.ts b/test/diffAgainstStore.ts
--- a/test/diffAgainstStore.ts
+++ b/test/diffAgainstStore.ts
@@ -2,14 +2,11 @@ import { assert } from 'chai';
 
 import {
   diffQueryAgainstStore,
+  ID_KEY,
 } from '../src/data/readFromStore';
 
 import { writeQueryToStore } from '../src/data/writeToStore';
 
-import {
-  getIdField,
-} from '../src/data/extensions';
-
 import gql from 'graphql-tag';
 
 describe('diffing queries against the store', () => {
@@ -58,6 +55,8 @@ describe('diffing queries against the store', () => {
       },
     };
 
+    const getIdField = ({id}: {id: string}) => id;
+
     const store = writeQueryToStore({
       result,
       query: firstQuery,
@@ -348,4 +347,378 @@ describe('diffing queries against the store', () => {
       });
     });
   });
+
+  it('will add a private id property', () => {
+    const query = gql`
+      query {
+        a { id b }
+        c { d e { id f } g { h } }
+      }
+    `;
+
+    const queryResult = {
+      a: [
+        { id: 'a:1', b: 1.1 },
+        { id: 'a:2', b: 1.2 },
+        { id: 'a:3', b: 1.3 },
+      ],
+      c: {
+        d: 2,
+        e: [
+          { id: 'e:1', f: 3.1 },
+          { id: 'e:2', f: 3.2 },
+          { id: 'e:3', f: 3.3 },
+          { id: 'e:4', f: 3.4 },
+          { id: 'e:5', f: 3.5 },
+        ],
+        g: { h: 4 },
+      },
+    };
+
+    const store = writeQueryToStore({
+      query,
+      result: queryResult,
+      dataIdFromObject: ({ id }: { id: string }) => id,
+    });
+
+    const { result } = diffQueryAgainstStore({
+      store,
+      query,
+    });
+
+    assert.deepEqual(result, queryResult);
+    assert.equal(result[ID_KEY], 'ROOT_QUERY');
+    assert.equal(result.a[0][ID_KEY], 'a:1');
+    assert.equal(result.a[1][ID_KEY], 'a:2');
+    assert.equal(result.a[2][ID_KEY], 'a:3');
+    assert.equal(result.c[ID_KEY], '$ROOT_QUERY.c');
+    assert.equal(result.c.e[0][ID_KEY], 'e:1');
+    assert.equal(result.c.e[1][ID_KEY], 'e:2');
+    assert.equal(result.c.e[2][ID_KEY], 'e:3');
+    assert.equal(result.c.e[3][ID_KEY], 'e:4');
+    assert.equal(result.c.e[4][ID_KEY], 'e:5');
+    assert.equal(result.c.g[ID_KEY], '$ROOT_QUERY.c.g');
+  });
+
+  describe('referential equality preservation', () => {
+    it('will return the previous result if there are no changes', () => {
+      const query = gql`
+        query {
+          a { b }
+          c { d e { f } }
+        }
+      `;
+
+      const queryResult = {
+        a: { b: 1 },
+        c: { d: 2, e: { f: 3 } },
+      };
+
+      const store = writeQueryToStore({
+        query,
+        result: queryResult,
+      });
+
+      const previousResult = {
+        a: { b: 1 },
+        c: { d: 2, e: { f: 3 } },
+      };
+
+      const { result } = diffQueryAgainstStore({
+        store,
+        query,
+        previousResult,
+      });
+
+      assert.deepEqual(result, queryResult);
+      assert.strictEqual(result, previousResult);
+    });
+
+    it('will return parts of the previous result that changed', () => {
+      const query = gql`
+        query {
+          a { b }
+          c { d e { f } }
+        }
+      `;
+
+      const queryResult = {
+        a: { b: 1 },
+        c: { d: 2, e: { f: 3 } },
+      };
+
+      const store = writeQueryToStore({
+        query,
+        result: queryResult,
+      });
+
+      const previousResult = {
+        a: { b: 1 },
+        c: { d: 20, e: { f: 3 } },
+      };
+
+      const { result } = diffQueryAgainstStore({
+        store,
+        query,
+        previousResult,
+      });
+
+      assert.deepEqual(result, queryResult);
+      assert.notStrictEqual(result, previousResult);
+      assert.strictEqual(result.a, previousResult.a);
+      assert.notStrictEqual(result.c, previousResult.c);
+      assert.strictEqual(result.c.e, previousResult.c.e);
+    });
+
+    it('will return the previous result if there are no changes in child arrays', () => {
+      const query = gql`
+        query {
+          a { b }
+          c { d e { f } }
+        }
+      `;
+
+      const queryResult = {
+        a: [{ b: 1.1 }, { b: 1.2 }, { b: 1.3 }],
+        c: { d: 2, e: [{ f: 3.1 }, { f: 3.2 }, { f: 3.3 }, { f: 3.4 }, { f: 3.5 }] },
+      };
+
+      const store = writeQueryToStore({
+        query,
+        result: queryResult,
+      });
+
+      const previousResult = {
+        a: [{ b: 1.1 }, { b: 1.2 }, { b: 1.3 }],
+        c: { d: 2, e: [{ f: 3.1 }, { f: 3.2 }, { f: 3.3 }, { f: 3.4 }, { f: 3.5 }] },
+      };
+
+      const { result } = diffQueryAgainstStore({
+        store,
+        query,
+        previousResult,
+      });
+
+      assert.deepEqual(result, queryResult);
+      assert.strictEqual(result, previousResult);
+    });
+
+    it('will not add zombie items when previousResult starts with the same items', () => {
+      const query = gql`
+        query {
+          a { b }
+        }
+      `;
+
+      const queryResult = {
+        a: [{ b: 1.1 }, { b: 1.2 }],
+      };
+
+      const store = writeQueryToStore({
+        query,
+        result: queryResult,
+      });
+
+      const previousResult = {
+        a: [{ b: 1.1 }, { b: 1.2 }, { b: 1.3 }],
+      };
+
+      const { result } = diffQueryAgainstStore({
+        store,
+        query,
+        previousResult,
+      });
+
+      assert.deepEqual(result, queryResult);
+      assert.strictEqual(result.a[0], previousResult.a[0]);
+      assert.strictEqual(result.a[1], previousResult.a[1]);
+    });
+
+    it('will return the previous result if there are no changes in nested child arrays', () => {
+      const query = gql`
+        query {
+          a { b }
+          c { d e { f } }
+        }
+      `;
+
+      const queryResult = {
+        a: [[[[[{ b: 1.1 }, { b: 1.2 }, { b: 1.3 }]]]]],
+        c: { d: 2, e: [[{ f: 3.1 }, { f: 3.2 }, { f: 3.3 }], [{ f: 3.4 }, { f: 3.5 }]] },
+      };
+
+      const store = writeQueryToStore({
+        query,
+        result: queryResult,
+      });
+
+      const previousResult = {
+        a: [[[[[{ b: 1.1 }, { b: 1.2 }, { b: 1.3 }]]]]],
+        c: { d: 2, e: [[{ f: 3.1 }, { f: 3.2 }, { f: 3.3 }], [{ f: 3.4 }, { f: 3.5 }]] },
+      };
+
+      const { result } = diffQueryAgainstStore({
+        store,
+        query,
+        previousResult,
+      });
+
+      assert.deepEqual(result, queryResult);
+      assert.strictEqual(result, previousResult);
+    });
+
+    it('will return parts of the previous result if there are changes in child arrays', () => {
+      const query = gql`
+        query {
+          a { b }
+          c { d e { f } }
+        }
+      `;
+
+      const queryResult = {
+        a: [{ b: 1.1 }, { b: 1.2 }, { b: 1.3 }],
+        c: { d: 2, e: [{ f: 3.1 }, { f: 3.2 }, { f: 3.3 }, { f: 3.4 }, { f: 3.5 }] },
+      };
+
+      const store = writeQueryToStore({
+        query,
+        result: queryResult,
+      });
+
+      const previousResult = {
+        a: [{ b: 1.1 }, { b: -1.2 }, { b: 1.3 }],
+        c: { d: 20, e: [{ f: 3.1 }, { f: 3.2 }, { f: 3.3 }, { f: 3.4 }, { f: 3.5 }] },
+      };
+
+      const { result } = diffQueryAgainstStore({
+        store,
+        query,
+        previousResult,
+      });
+
+      assert.deepEqual(result, queryResult);
+      assert.notStrictEqual(result, previousResult);
+      assert.notStrictEqual(result.a, previousResult.a);
+      assert.strictEqual(result.a[0], previousResult.a[0]);
+      assert.notStrictEqual(result.a[1], previousResult.a[1]);
+      assert.strictEqual(result.a[2], previousResult.a[2]);
+      assert.notStrictEqual(result.c, previousResult.c);
+      assert.notStrictEqual(result.c.e, previousResult.c.e);
+      assert.strictEqual(result.c.e[0], previousResult.c.e[0]);
+      assert.strictEqual(result.c.e[1], previousResult.c.e[1]);
+      assert.strictEqual(result.c.e[2], previousResult.c.e[2]);
+      assert.strictEqual(result.c.e[3], previousResult.c.e[3]);
+      assert.strictEqual(result.c.e[4], previousResult.c.e[4]);
+    });
+
+    it('will return the same items in a different order with `dataIdFromObject`', () => {
+      const query = gql`
+        query {
+          a { id b }
+          c { d e { id f } g { h } }
+        }
+      `;
+
+      const queryResult = {
+        a: [
+          { id: 'a:1', b: 1.1 },
+          { id: 'a:2', b: 1.2 },
+          { id: 'a:3', b: 1.3 },
+        ],
+        c: {
+          d: 2,
+          e: [
+            { id: 'e:1', f: 3.1 },
+            { id: 'e:2', f: 3.2 },
+            { id: 'e:3', f: 3.3 },
+            { id: 'e:4', f: 3.4 },
+            { id: 'e:5', f: 3.5 },
+          ],
+          g: { h: 4 },
+        },
+      };
+
+      const store = writeQueryToStore({
+        query,
+        result: queryResult,
+        dataIdFromObject: ({ id }: { id: string }) => id,
+      });
+
+      const previousResult = {
+        a: [
+          { id: 'a:3', b: 1.3, [ID_KEY]: 'a:3' },
+          { id: 'a:2', b: 1.2, [ID_KEY]: 'a:2' },
+          { id: 'a:1', b: 1.1, [ID_KEY]: 'a:1' },
+        ],
+        c: {
+          d: 2,
+          e: [
+            { id: 'e:4', f: 3.4, [ID_KEY]: 'e:4' },
+            { id: 'e:2', f: 3.2, [ID_KEY]: 'e:2' },
+            { id: 'e:5', f: 3.5, [ID_KEY]: 'e:5' },
+            { id: 'e:3', f: 3.3, [ID_KEY]: 'e:3' },
+            { id: 'e:1', f: 3.1, [ID_KEY]: 'e:1' },
+          ],
+          g: { h: 4 },
+        },
+      };
+
+      const { result } = diffQueryAgainstStore({
+        store,
+        query,
+        previousResult,
+      });
+
+      assert.deepEqual(result, queryResult);
+      assert.notStrictEqual(result, previousResult);
+      assert.notStrictEqual(result.a, previousResult.a);
+      assert.strictEqual(result.a[0], previousResult.a[2]);
+      assert.strictEqual(result.a[1], previousResult.a[1]);
+      assert.strictEqual(result.a[2], previousResult.a[0]);
+      assert.notStrictEqual(result.c, previousResult.c);
+      assert.notStrictEqual(result.c.e, previousResult.c.e);
+      assert.strictEqual(result.c.e[0], previousResult.c.e[4]);
+      assert.strictEqual(result.c.e[1], previousResult.c.e[1]);
+      assert.strictEqual(result.c.e[2], previousResult.c.e[3]);
+      assert.strictEqual(result.c.e[3], previousResult.c.e[0]);
+      assert.strictEqual(result.c.e[4], previousResult.c.e[2]);
+      assert.strictEqual(result.c.g, previousResult.c.g);
+    });
+
+    it('will return the same JSON scalar field object', () => {
+      const query = gql`
+        {
+          a { b c }
+          d { e f }
+        }
+      `;
+
+      const queryResult = {
+        a: { b: 1, c: { x: 2, y: 3, z: 4 } },
+        d: { e: 5, f: { x: 6, y: 7, z: 8 } },
+      };
+
+      const store = writeQueryToStore({
+        query,
+        result: queryResult,
+      });
+
+      const previousResult = {
+        a: { b: 1, c: { x: 2, y: 3, z: 4 } },
+        d: { e: 50, f: { x: 6, y: 7, z: 8 } },
+      };
+
+      const { result } = diffQueryAgainstStore({
+        store,
+        query,
+        previousResult,
+      });
+
+      assert.deepEqual(result, queryResult);
+      assert.notStrictEqual(result, previousResult);
+      assert.strictEqual(result.a, previousResult.a);
+      assert.notStrictEqual(result.d, previousResult.d);
+      assert.strictEqual(result.d.f, previousResult.d.f);
+    });
+  });
 });
diff --git a/test/directives.ts b/test/directives.ts
--- a/test/directives.ts
+++ b/test/directives.ts
@@ -11,7 +11,7 @@ import {
 
 import gql from 'graphql-tag';
 
-import cloneDeep = require('lodash/cloneDeep');
+import { cloneDeep } from 'lodash';
 
 describe('query directives', () => {
   it('should should not include a skipped field', () => {
diff --git a/test/fetchMore.ts b/test/fetchMore.ts
--- a/test/fetchMore.ts
+++ b/test/fetchMore.ts
@@ -5,17 +5,10 @@ import mockNetworkInterface from './mocks/mockNetworkInterface';
 import ApolloClient from '../src';
 import { ObservableQuery } from '../src/core/ObservableQuery';
 
-import assign = require('lodash/assign');
-import clonedeep = require('lodash/cloneDeep');
+import { assign, cloneDeep } from 'lodash';
 
 import gql from 'graphql-tag';
 
-import { addFragmentsToDocument } from '../src/queries/getFromAST';
-
-import {
-  createFragment,
-} from '../src/index';
-
 describe('updateQuery on a simple query', () => {
   const query = gql`
     query thing {
@@ -60,10 +53,10 @@ describe('updateQuery on a simple query', () => {
 
     return new Promise((resolve) => setTimeout(resolve))
       .then(() => obsHandle)
-      .then((watchedQuery: ObservableQuery) => {
+      .then((watchedQuery: ObservableQuery<any>) => {
         assert.equal(latestResult.data.entry.value, 1);
         watchedQuery.updateQuery((prevResult: any) => {
-          const res = clonedeep(prevResult);
+          const res = cloneDeep(prevResult);
           res.entry.value = 2;
           return res;
         });
@@ -115,7 +108,7 @@ describe('fetchMore on an observable query', () => {
       },
     },
   };
-  const resultMore = clonedeep(result);
+  const resultMore = cloneDeep(result);
   const result2: any = {
     data: {
       __typename: 'Query',
@@ -150,7 +143,7 @@ describe('fetchMore on an observable query', () => {
       addTypename: true,
     });
 
-    const obsHandle = client.watchQuery({
+    const obsHandle = client.watchQuery<any>({
       query,
       variables,
     });
@@ -181,7 +174,7 @@ describe('fetchMore on an observable query', () => {
       return watchedQuery.fetchMore({
         variables: { start: 10 }, // rely on the fact that the original variables had limit: 10
         updateQuery: (prev, options) => {
-          const state = clonedeep(prev) as any;
+          const state = cloneDeep(prev) as any;
           state.entry.comments = [...state.entry.comments, ...(options.fetchMoreResult as any).data.entry.comments];
           return state;
         },
@@ -211,54 +204,7 @@ describe('fetchMore on an observable query', () => {
         query: query2,
         variables: variables2,
         updateQuery: (prev, options) => {
-          const state = clonedeep(prev) as any;
-          state.entry.comments = [...state.entry.comments, ...(options.fetchMoreResult as any).data.comments];
-          return state;
-        },
-      });
-    }).then(() => {
-      const comments = latestResult.data.entry.comments;
-      assert.lengthOf(comments, 20);
-      for (let i = 1; i <= 10; i++) {
-        assert.equal(comments[i - 1].text, `comment ${i}`);
-      }
-      for (let i = 11; i <= 20; i++) {
-        assert.equal(comments[i - 1].text, `new comment ${i}`);
-      }
-      unsetup();
-    });
-  });
-
-  it('fetching more with fragments', () => {
-    latestResult = null;
-      // identical to query2, but with a fragment
-      const query3 = gql`
-      query NewComments($start: Int!, $limit: Int!) {
-        comments(start: $start, limit: $limit) {
-          ...textFragment
-          __typename
-        }
-      }
-    `;
-    const fragment = createFragment(gql`
-      fragment textFragment on Comment {
-        text
-        __typename
-      }
-    `);
-    return setup({
-      request: {
-        query: addFragmentsToDocument(query3, fragment),
-        variables: variables2,
-      },
-      result: result2,
-    }).then((watchedQuery) => {
-      return watchedQuery.fetchMore({
-        query: query3,
-        variables: variables2,
-        fragments: fragment,
-        updateQuery: (prev, options) => {
-          const state = clonedeep(prev) as any;
+          const state = cloneDeep(prev) as any;
           state.entry.comments = [...state.entry.comments, ...(options.fetchMoreResult as any).data.comments];
           return state;
         },
diff --git a/test/fixtures/redux-todomvc/reducers.ts b/test/fixtures/redux-todomvc/reducers.ts
--- a/test/fixtures/redux-todomvc/reducers.ts
+++ b/test/fixtures/redux-todomvc/reducers.ts
@@ -1,5 +1,5 @@
 import { combineReducers } from 'redux';
-import assign = require('lodash/assign');
+import { assign } from 'lodash';
 
 import {
   ADD_TODO,
diff --git a/test/getFromAST.ts b/test/getFromAST.ts
--- a/test/getFromAST.ts
+++ b/test/getFromAST.ts
@@ -6,7 +6,6 @@ import {
   createFragmentMap,
   FragmentMap,
   getOperationName,
-  addFragmentsToDocument,
 } from '../src/queries/getFromAST';
 
 import {
@@ -14,10 +13,6 @@ import {
   OperationDefinitionNode,
 } from 'graphql';
 
-import {
-  createFragment,
-} from '../src';
-
 import { print } from 'graphql-tag/printer';
 import gql from 'graphql-tag';
 import { assert } from 'chai';
@@ -234,136 +229,4 @@ describe('AST utility functions', () => {
       getQueryDefinition(queryWithTypeDefination);
     }, 'Schema type definitions not allowed in queries. Found: "InputObjectTypeDefinition"');
   });
-
-  it('should attach fragments properly', () => {
-    const subjectInfo = createFragment(gql`
-      fragment subjectInfo on Subject {
-        id
-        name
-      }`,
-    );
-
-    const businessAreaInfo = createFragment(gql`
-      fragment businessAreaInfo on BusinessArea {
-        id
-        name
-        subjects {
-          ...subjectInfo
-        }
-      }`,
-      [subjectInfo],
-    );
-
-    const query = gql`
-      query {
-        businessAreas {
-          ...businessAreaInfo
-        }
-      }
-    `;
-
-    const fullDoc = addFragmentsToDocument(query, businessAreaInfo);
-
-    assert.equal(print(fullDoc), `{
-  businessAreas {
-    ...businessAreaInfo
-  }
-}
-
-fragment subjectInfo on Subject {
-  id
-  name
-}
-
-fragment businessAreaInfo on BusinessArea {
-  id
-  name
-  subjects {
-    ...subjectInfo
-  }
-}
-`);
-  });
-
-  it('should only attach distinct fragments', () => {
-    const subjectInfo = createFragment(gql`
-      fragment subjectInfo on Subject {
-        id
-        name
-      }`,
-    );
-
-    const businessAreaInfo = createFragment(gql`
-      fragment businessAreaInfo on BusinessArea {
-        id
-        name
-        subjects {
-          ...subjectInfo
-        }
-      }`,
-      [subjectInfo, subjectInfo],
-    );
-
-    // to test that depending on the same fragment twice in different fragments won't add it twice.
-    const whateverAreaInfo = createFragment(gql`
-      fragment whateverAreaInfo on WhateverArea {
-        subject {
-          ...subjectInfo
-        }
-      }`,
-      [subjectInfo],
-    );
-
-    // XXX we don't attach a fragment here, because if we do, it won't be === to
-    // the same fragment created with createFragment.
-    const query = gql`
-      query {
-        businessAreas {
-          ...businessAreaInfo
-        }
-        whateverAreas {
-          ...whateverAreaInfo
-        }
-      }
-
-      #fragment subjectInfo on Subject {
-      #  id
-      #  name
-      #}
-    `;
-
-    let fullDoc = addFragmentsToDocument(query, businessAreaInfo);
-    fullDoc = addFragmentsToDocument(fullDoc, whateverAreaInfo);
-    // tests to make sure we can't add subjectInfo twice, even in separate call
-    fullDoc = addFragmentsToDocument(fullDoc, subjectInfo);
-
-    assert.equal(print(fullDoc), `{
-  businessAreas {
-    ...businessAreaInfo
-  }
-  whateverAreas {
-    ...whateverAreaInfo
-  }
-}
-
-fragment subjectInfo on Subject {
-  id
-  name
-}
-
-fragment businessAreaInfo on BusinessArea {
-  id
-  name
-  subjects {
-    ...subjectInfo
-  }
-}
-
-fragment whateverAreaInfo on WhateverArea {
-  subject {
-    ...subjectInfo
-  }
-}
-`);
-  });
 });
diff --git a/test/graphqlSubscriptions.ts b/test/graphqlSubscriptions.ts
--- a/test/graphqlSubscriptions.ts
+++ b/test/graphqlSubscriptions.ts
@@ -6,7 +6,7 @@ import {
   assert,
 } from 'chai';
 
-import clonedeep = require('lodash/cloneDeep');
+import { cloneDeep } from 'lodash';
 
 import { isSubscriptionResultAction } from '../src/actions';
 
@@ -282,7 +282,7 @@ describe('GraphQL Subscriptions', () => {
       reducer: (previousResult, action) => {
         counter++;
         if (isSubscriptionResultAction(action)) {
-          const newResult = clonedeep(previousResult) as any;
+          const newResult = cloneDeep(previousResult) as any;
           newResult.number++;
           return newResult;
         }
@@ -312,52 +312,4 @@ describe('GraphQL Subscriptions', () => {
       network.fireResult(id);
     }
   });
-
-  // it('should work with an observable query', (done) => {
-  //   const network = mockSubscriptionNetworkInterface([sub2], {
-  //     request: {
-  //       query: commentsQuery,
-  //       variables: commentsVariables,
-  //     },
-  //     result: commentsResult, // list of 10 comments
-  //   });
-  //   const client = new ApolloClient({
-  //     networkInterface: network,
-  //   });
-  //   client.query({
-  //     query: commentsQuery,
-  //     variables: commentsVariables,
-  //   }).then(() => {
-  //     const graphQLSubscriptionOptions = {
-  //       subscription: commentsSub,
-  //       variables: commentsVariables,
-  //       updateQuery: (prev, updateOptions) => {
-  //         const state = clonedeep(prev) as any;
-  //         // prev is that data field of the query result
-  //         // updateOptions.subscriptionResult is the result entry from the subscription result
-  //         state.entry.comments = [...state.entry.comments, ...(updateOptions.subscriptionResult as any).entry.comments];
-  //         return state;
-  //       },
-  //     };
-  //     const obsHandle = client.watchQuery(commentsWatchQueryOptions);
-
-  //     obsHandle.subscribe({
-  //       next(result) {
-  //         let expectedComments = [];
-  //         for (let i = 1; i <= 11; i++) {
-  //           expectedComments.push({ text: `comment ${i}` });
-  //         }
-  //         assert.equal(result.data.entry.comments.length, 11);
-  //         assert.deepEqual(result.data.entry.comments, expectedComments);
-  //         done();
-  //       },
-  //     });
-
-  //     const id = obsHandle.startGraphQLSubscription(graphQLSubscriptionOptions);
-  //     network.fireResult(id);
-  //   });
-  // });
-
-  // TODO: test that we can make two subscriptions one one watchquery.
-
 });
diff --git a/test/isEqual.ts b/test/isEqual.ts
new file mode 100644
--- /dev/null
+++ b/test/isEqual.ts
@@ -0,0 +1,58 @@
+import { isEqual } from '../src/util/isEqual';
+import { assert } from 'chai';
+
+describe('isEqual', () => {
+  it('should return true for equal primitive values', () => {
+    assert(isEqual(undefined, undefined));
+    assert(isEqual(null, null));
+    assert(isEqual(true, true));
+    assert(isEqual(false, false));
+    assert(isEqual(-1, -1));
+    assert(isEqual(+1, +1));
+    assert(isEqual(42, 42));
+    assert(isEqual(0, 0));
+    assert(isEqual(0.5, 0.5));
+    assert(isEqual('hello', 'hello'));
+    assert(isEqual('world', 'world'));
+  });
+
+  it('should return false for not equal primitive values', () => {
+    assert(!isEqual(undefined, null));
+    assert(!isEqual(null, undefined));
+    assert(!isEqual(true, false));
+    assert(!isEqual(false, true));
+    assert(!isEqual(-1, +1));
+    assert(!isEqual(+1, -1));
+    assert(!isEqual(42, 42.00000000000001));
+    assert(!isEqual(0, 0.5));
+    assert(!isEqual('hello', 'world'));
+    assert(!isEqual('world', 'hello'));
+  });
+
+  it('should return false when comparing primitives with objects', () => {
+    assert(!isEqual({}, null));
+    assert(!isEqual(null, {}));
+    assert(!isEqual({}, true));
+    assert(!isEqual(true, {}));
+    assert(!isEqual({}, 42));
+    assert(!isEqual(42, {}));
+    assert(!isEqual({}, 'hello'));
+    assert(!isEqual('hello', {}));
+  });
+
+  it('should correctly compare shallow objects', () => {
+    assert(isEqual({}, {}));
+    assert(isEqual({ a: 1, b: 2, c: 3 }, { a: 1, b: 2, c: 3 }));
+    assert(!isEqual({ a: 1, b: 2, c: 3 }, { a: 3, b: 2, c: 1 }));
+    assert(!isEqual({ a: 1, b: 2, c: 3 }, { a: 1, b: 2 }));
+    assert(!isEqual({ a: 1, b: 2 }, { a: 1, b: 2, c: 3 }));
+  });
+
+  it('should correctly compare deep objects', () => {
+    assert(isEqual({ x: {} }, { x: {} }));
+    assert(isEqual({ x: { a: 1, b: 2, c: 3 } }, { x: { a: 1, b: 2, c: 3 } }));
+    assert(!isEqual({ x: { a: 1, b: 2, c: 3 } }, { x: { a: 3, b: 2, c: 1 } }));
+    assert(!isEqual({ x: { a: 1, b: 2, c: 3 } }, { x: { a: 1, b: 2 } }));
+    assert(!isEqual({ x: { a: 1, b: 2 } }, { x: { a: 1, b: 2, c: 3 } }));
+  });
+});
diff --git a/test/mockNetworkInterface.ts b/test/mockNetworkInterface.ts
--- a/test/mockNetworkInterface.ts
+++ b/test/mockNetworkInterface.ts
@@ -7,7 +7,7 @@ import {
   MockedSubscription,
 } from './mocks/mockNetworkInterface';
 
-import omit = require('lodash/omit');
+import { omit } from 'lodash';
 
 import gql from 'graphql-tag';
 
diff --git a/test/mocks/mockFetch.ts b/test/mocks/mockFetch.ts
--- a/test/mocks/mockFetch.ts
+++ b/test/mocks/mockFetch.ts
@@ -1,5 +1,3 @@
-import 'whatwg-fetch';
-
 // This is an implementation of a mocked window.fetch implementation similar in
 // structure to the MockedNetworkInterface.
 
diff --git a/test/mutationResults.ts b/test/mutationResults.ts
--- a/test/mutationResults.ts
+++ b/test/mutationResults.ts
@@ -7,8 +7,7 @@ import { isMutationResultAction, isQueryResultAction } from '../src/actions';
 
 import { Subscription } from '../src/util/Observable';
 
-import assign = require('lodash/assign');
-import clonedeep = require('lodash/cloneDeep');
+import { assign, cloneDeep } from 'lodash';
 
 import { ObservableQuery } from '../src/core/ObservableQuery';
 
@@ -743,7 +742,7 @@ describe('mutation results', () => {
           reducer: (previousResult, action) => {
             counter++;
             if (isMutationResultAction(action)) {
-              const newResult = clonedeep(previousResult) as any;
+              const newResult = cloneDeep(previousResult) as any;
               newResult.todoList.todos.unshift(action.result.data['createTodo']);
               return newResult;
             }
@@ -776,7 +775,7 @@ describe('mutation results', () => {
 
     it('passes variables', () => {
       let counter = 0;
-      let observableQuery: ObservableQuery;
+      let observableQuery: ObservableQuery<any>;
       let subscription: any;
 
       return setup({
@@ -799,8 +798,8 @@ describe('mutation results', () => {
           reducer: (previousResult, action, variables: any) => {
             counter++;
             if (isMutationResultAction(action) && variables['id'] === 5) {
-              const newResult = clonedeep(previousResult) as any;
-              newResult.todoList.todos.unshift((action.result.data as any).createTodo);
+              const newResult = cloneDeep(previousResult) as any;
+              newResult.todoList.todos.unshift(action.result.data['createTodo']);
               return newResult;
             }
             return previousResult;
@@ -854,7 +853,7 @@ describe('mutation results', () => {
           reducer: (previousResult, action) => {
             if (isMutationResultAction(action) && action.operationName === 'createTodo') {
               counter++;
-              const newResult = clonedeep(previousResult) as any;
+              const newResult = cloneDeep(previousResult) as any;
               newResult.todoList.todos.unshift(action.result.data['createTodo']);
               return newResult;
             }
@@ -871,7 +870,7 @@ describe('mutation results', () => {
           reducer: (previousResult, action) => {
             if (isMutationResultAction(action) && action.operationName === 'wrongName') {
               counter++; // shouldn't be called, so counter shouldn't increase.
-              const newResult = clonedeep(previousResult) as any;
+              const newResult = cloneDeep(previousResult) as any;
               newResult.todoList.todos.unshift(action.result.data['createTodo']);
               return newResult;
             }
@@ -919,7 +918,7 @@ describe('mutation results', () => {
           reducer: (previousResult, action) => {
             counter++;
             if (isQueryResultAction(action)) {
-              const newResult = clonedeep(previousResult) as any;
+              const newResult = cloneDeep(previousResult) as any;
               newResult.todoList.todos.unshift(action.result.data['newTodos'][0]);
               return newResult;
             }
@@ -1003,7 +1002,7 @@ describe('mutation results', () => {
           reducer: (previousResult, action) => {
             counter++;
             if (isMutationResultAction(action)) {
-              const newResult = clonedeep(previousResult) as any;
+              const newResult = cloneDeep(previousResult) as any;
               newResult.todoList.todos.unshift(action.result.data['createTodo']);
               return newResult;
             }
@@ -1018,7 +1017,7 @@ describe('mutation results', () => {
           reducer: (previousResult, action) => {
             counter2++;
             if (isMutationResultAction(action) && action.result.data['createTodo'].completed) {
-              const newResult = clonedeep(previousResult) as any;
+              const newResult = cloneDeep(previousResult) as any;
               newResult.todoList.filteredTodos.unshift(action.result.data['createTodo']);
               return newResult;
             }
@@ -1117,6 +1116,31 @@ describe('mutation results', () => {
       });
     });
 
+    it('does not swallow errors', done => {
+      client = new ApolloClient({
+        networkInterface: mockNetworkInterface({
+          request: { query },
+          result,
+        }),
+      });
+
+      const observable = client.watchQuery({
+        query,
+        reducer: () => {
+          throw new Error('Don’t swallow me right up!');
+        },
+      });
+
+      observable.subscribe({
+        next: () => {
+          done(new Error('`next` should not be called.'));
+        },
+        error: error => {
+          assert(/swallow/.test(error.message));
+          done();
+        },
+      });
+    });
   });
 
 
@@ -1170,7 +1194,7 @@ describe('mutation results', () => {
               assert.equal(mResult.data.createTodo.id, '99');
               assert.equal(mResult.data.createTodo.text, 'This one was created with a mutation.');
 
-              const state = clonedeep(prev) as any;
+              const state = cloneDeep(prev) as any;
               state.todoList.todos.unshift(mResult.data.createTodo);
               return state;
             },
@@ -1209,7 +1233,7 @@ describe('mutation results', () => {
             assert.equal(mResult.data.createTodo.id, '99');
             assert.equal(mResult.data.createTodo.text, 'This one was created with a mutation.');
 
-            const state = clonedeep(prev) as any;
+            const state = cloneDeep(prev) as any;
             state.todoList.todos.unshift(mResult.data.createTodo);
             return state;
           },
@@ -1233,7 +1257,7 @@ describe('mutation results', () => {
             updateQueries: {
               todoList: (prev, options) => {
                 const mResult = options.mutationResult as any;
-                const state = clonedeep(prev) as any;
+                const state = cloneDeep(prev) as any;
                 // It's unfortunate that this function is called at all, but we are removing
                 // the updateQueries API soon so it won't matter.
                 state.todoList.todos.unshift(mResult.data && mResult.data.createTodo);
@@ -1248,7 +1272,7 @@ describe('mutation results', () => {
               updateQueries: {
                 todoList: (prev, options) => {
                   const mResult = options.mutationResult as any;
-                  const state = clonedeep(prev) as any;
+                  const state = cloneDeep(prev) as any;
                   state.todoList.todos.unshift(mResult.data.createTodo);
                   return state;
                 },
diff --git a/test/networkInterface.ts b/test/networkInterface.ts
--- a/test/networkInterface.ts
+++ b/test/networkInterface.ts
@@ -1,8 +1,7 @@
 import * as chai from 'chai';
 import * as chaiAsPromised from 'chai-as-promised';
 
-import assign = require('lodash/assign');
-import isequal = require('lodash/isEqual');
+import { assign, isEqual } from 'lodash';
 import * as fetchMock from 'fetch-mock';
 
 // make it easy to assert with promises
@@ -116,12 +115,12 @@ describe('network interface', () => {
       }
 
       if (query === print(simpleQueryWithVar)
-          && isequal(variables, { personNum: 1 })) {
+          && isEqual(variables, { personNum: 1 })) {
         return simpleResult;
       }
 
       if (query === print(complexQueryWithTwoVars)
-          && isequal(variables, { personNum: 1, filmNum: 1 })) {
+          && isEqual(variables, { personNum: 1, filmNum: 1 })) {
         return complexResult;
       }
 
@@ -155,6 +154,29 @@ describe('network interface', () => {
       }, /Passing the URI as the first argument to createNetworkInterface is deprecated/);
     });
 
+    it('will warn if there is no global fetch implementation', () => {
+      const origWarn = console.warn;
+      const origFetch = (global as any).fetch;
+
+      const warnCalls: Array<Array<any>> = [];
+
+      console.warn = (...args: Array<any>) => warnCalls.push(args);
+
+      delete (global as any).fetch;
+
+      assert.equal(warnCalls.length, 0);
+
+      createNetworkInterface({ uri: '/graphql' });
+
+      assert.equal(warnCalls.length, 1);
+      assert.equal(warnCalls[0].length, 1);
+      assert(/the fetch browser API could not be found/.test(warnCalls[0][0]));
+
+      // Put everything back the way it was.
+      console.warn = origWarn;
+      (global as any).fetch = origFetch;
+    });
+
     it('should create an instance with a given uri', () => {
       const networkInterface = createNetworkInterface({ uri: '/graphql' });
       assert.equal(networkInterface._uri, '/graphql');
diff --git a/test/optimistic.ts b/test/optimistic.ts
--- a/test/optimistic.ts
+++ b/test/optimistic.ts
@@ -2,13 +2,11 @@ import * as chai from 'chai';
 const { assert } = chai;
 
 import mockNetworkInterface from './mocks/mockNetworkInterface';
-import ApolloClient, { createFragment } from '../src';
+import ApolloClient from '../src';
 import { MutationBehaviorReducerArgs, MutationBehavior, MutationQueryReducersMap } from '../src/data/mutationResults';
 import { NormalizedCache, StoreObject } from '../src/data/storeUtils';
-import { addFragmentsToDocument } from '../src/queries/getFromAST';
 
-import assign = require('lodash/assign');
-import clonedeep = require('lodash/cloneDeep');
+import { assign, cloneDeep} from 'lodash';
 
 import { Subscription } from '../src/util/Observable';
 
@@ -722,7 +720,7 @@ describe('optimistic mutation results', () => {
               const mResult = options.mutationResult as any;
               assert.equal(mResult.data.createTodo.id, '99');
 
-              const state = clonedeep(prev) as any;
+              const state = cloneDeep(prev) as any;
               state.todoList.todos.unshift(mResult.data.createTodo);
               return state;
             },
@@ -772,7 +770,7 @@ describe('optimistic mutation results', () => {
           todoList: (prev, options) => {
             const mResult = options.mutationResult as any;
 
-            const state = clonedeep(prev) as any;
+            const state = cloneDeep(prev) as any;
             state.todoList.todos.unshift(mResult.data.createTodo);
             return state;
           },
@@ -846,7 +844,7 @@ describe('optimistic mutation results', () => {
           todoList: (prev, options) => {
             const mResult = options.mutationResult as any;
 
-            const state = clonedeep(prev) as any;
+            const state = cloneDeep(prev) as any;
             state.todoList.todos.unshift(mResult.data.createTodo);
             return state;
           },
@@ -951,7 +949,7 @@ describe('optimistic mutation results', () => {
           reducer: (previousResult, action) => {
             counter++;
             if (isMutationResultAction(action)) {
-              const newResult = clonedeep(previousResult) as any;
+              const newResult = cloneDeep(previousResult) as any;
               newResult.todoList.todos.unshift(action.result.data.createTodo);
               return newResult;
             }
@@ -979,7 +977,7 @@ describe('optimistic mutation results', () => {
           reducer: (previousResult, action) => {
             counter++;
             if (isMutationResultAction(action)) {
-              const newResult = clonedeep(previousResult) as any;
+              const newResult = cloneDeep(previousResult) as any;
               newResult.todoList.todos.unshift(action.result.data['createTodo']);
               return newResult;
             }
@@ -1028,24 +1026,6 @@ describe('optimistic mutation - githunt comments', () => {
       }
     }
   `;
-  const fragment = createFragment(gql`
-    fragment authorFields on User {
-      postedBy {
-        login
-        html_url
-      }
-    }
-  `);
-  const fragmentWithTypenames = createFragment(gql`
-    fragment authorFields on User {
-      postedBy {
-        login
-        html_url
-        __typename
-      }
-      __typename
-    }
-  `);
   const queryWithFragment = gql`
     query Comment($repoName: String!) {
       entry(repoFullName: $repoName) {
@@ -1054,6 +1034,13 @@ describe('optimistic mutation - githunt comments', () => {
         }
       }
     }
+
+    fragment authorFields on User {
+      postedBy {
+        login
+        html_url
+      }
+    }
   `;
   const variables = {
     repoName: 'org/repo',
@@ -1091,7 +1078,7 @@ describe('optimistic mutation - githunt comments', () => {
       result,
     }, {
       request: {
-        query: addFragmentsToDocument(addTypenameToDocument(queryWithFragment), fragment),
+        query: addTypenameToDocument(queryWithFragment),
         variables,
       },
       result,
@@ -1146,7 +1133,7 @@ describe('optimistic mutation - githunt comments', () => {
   const updateQueries = {
     Comment: (prev, { mutationResult: mutationResultArg }) => {
       const newComment = (mutationResultArg as any).data.submitComment;
-      const state = clonedeep(prev);
+      const state = cloneDeep(prev);
       (state as any).entry.comments.unshift(newComment);
       return state;
     },
@@ -1196,49 +1183,6 @@ describe('optimistic mutation - githunt comments', () => {
       assert.equal(newResult.data.entry.comments.length, 2);
     });
   });
-
-  it('can post a new comment (with fragments)', () => {
-    const mutationVariables = {
-      repoFullName: 'org/repo',
-      commentContent: 'New Comment',
-    };
-
-    let subscriptionHandle: Subscription;
-    return setup({
-      request: {
-        query: addFragmentsToDocument(addTypenameToDocument(mutationWithFragment), fragmentWithTypenames),
-        variables: mutationVariables,
-      },
-      result: mutationResult,
-    })
-    .then(() => {
-        // we have to actually subscribe to the query to be able to update it
-        return new Promise( (resolve, reject) => {
-          const handle = client.watchQuery({
-            query: queryWithFragment,
-            variables,
-            fragments: fragment,
-          });
-          subscriptionHandle = handle.subscribe({
-            next(res) { resolve(res); },
-          });
-        });
-      })
-    .then(() => {
-      return client.mutate({
-        mutation: mutationWithFragment,
-        optimisticResponse,
-        variables: mutationVariables,
-        updateQueries,
-        fragments: fragment,
-      });
-    }).then(() => {
-      return client.query({ query: queryWithFragment, variables, fragments: fragment });
-    }).then((newResult: any) => {
-      subscriptionHandle.unsubscribe();
-      assert.equal(newResult.data.entry.comments.length, 2);
-    });
-  });
 });
 
 function realIdValue(id: string) {
diff --git a/test/readFromStore.ts b/test/readFromStore.ts
--- a/test/readFromStore.ts
+++ b/test/readFromStore.ts
@@ -1,6 +1,5 @@
 import { assert } from 'chai';
-import assign = require('lodash/assign');
-import omit = require('lodash/omit');
+import { assign, omit } from 'lodash';
 
 import {
   readQueryFromStore,
diff --git a/test/scheduler.ts b/test/scheduler.ts
--- a/test/scheduler.ts
+++ b/test/scheduler.ts
@@ -324,7 +324,7 @@ describe('QueryScheduler', () => {
       queryManager,
     });
     const queryId = 'fake-id';
-    scheduler.addQueryOnInterval(queryId, queryOptions);
+    scheduler.addQueryOnInterval<any>(queryId, queryOptions);
     assert.equal(Object.keys(scheduler.intervalQueries).length, 1);
     assert.equal(Object.keys(scheduler.intervalQueries)[0], queryOptions.pollInterval.toString());
     const queries = (<any>scheduler.intervalQueries)[queryOptions.pollInterval.toString()];
diff --git a/test/subscribeToMore.ts b/test/subscribeToMore.ts
--- a/test/subscribeToMore.ts
+++ b/test/subscribeToMore.ts
@@ -4,10 +4,8 @@ const { assert } = chai;
 import {
   mockSubscriptionNetworkInterface,
 } from './mocks/mockNetworkInterface';
-import ApolloClient from '../src';
 
-// import assign = require('lodash/assign');
-// import clonedeep = require('lodash/cloneDeep');
+import ApolloClient from '../src';
 
 import gql from 'graphql-tag';
 
diff --git a/test/tests.ts b/test/tests.ts
--- a/test/tests.ts
+++ b/test/tests.ts
@@ -18,7 +18,7 @@ require('source-map-support').install();
 console.warn = console.error = (...messages: string[]) => {
   console.log(`==> Error in test: Tried to log warning or error with message:
 `, ...messages);
-  if (!process.env.CI && !messages[0].match(/deprecated/)) {
+  if (!process.env.CI) {
     process.exit(1);
   }
 };
@@ -50,3 +50,6 @@ import './batchedNetworkInterface';
 import './ObservableQuery';
 import './subscribeToMore';
 import './customResolvers';
+import './isEqual';
+import './cloneDeep';
+import './assign';
diff --git a/test/util/observableToPromise.ts b/test/util/observableToPromise.ts
--- a/test/util/observableToPromise.ts
+++ b/test/util/observableToPromise.ts
@@ -1,5 +1,5 @@
 import { ObservableQuery } from '../../src/core/ObservableQuery';
-import { ApolloQueryResult } from '../../src/core/QueryManager';
+import { ApolloQueryResult } from '../../src/core/types';
 import { Subscription } from '../../src/util/Observable';
 
 /**
@@ -12,13 +12,13 @@ import { Subscription } from '../../src/util/Observable';
  * @param errorCallbacks an expected set of errors
  */
 export type Options = {
-  observable: ObservableQuery,
+  observable: ObservableQuery<any>,
   shouldResolve?: boolean,
   wait?: number,
   errorCallbacks?: ((error: Error) => any)[],
 };
 
-export type ResultCallback = ((result: ApolloQueryResult) => any);
+export type ResultCallback = ((result: ApolloQueryResult<any>) => any);
 
 // Take an observable and N callbacks, and observe the observable,
 // ensuring it is called exactly N times, resolving once it has done so.
diff --git a/test/util/subscribeAndCount.ts b/test/util/subscribeAndCount.ts
--- a/test/util/subscribeAndCount.ts
+++ b/test/util/subscribeAndCount.ts
@@ -1,11 +1,11 @@
 import { ObservableQuery } from '../../src/core/ObservableQuery';
-import { ApolloQueryResult } from '../../src/core/QueryManager';
+import { ApolloQueryResult } from '../../src/core/types';
 import { Subscription } from '../../src/util/Observable';
 
 import wrap from './wrap';
 
-export default function(done: MochaDone, observable: ObservableQuery,
-    cb: (handleCount: number, result: ApolloQueryResult) => any): Subscription {
+export default function(done: MochaDone, observable: ObservableQuery<any>,
+    cb: (handleCount: number, result: ApolloQueryResult<any>) => any): Subscription {
   let handleCount = 0;
   return observable.subscribe({
     next: wrap(done, result => {
diff --git a/test/writeToStore.ts b/test/writeToStore.ts
--- a/test/writeToStore.ts
+++ b/test/writeToStore.ts
@@ -1,7 +1,5 @@
 import { assert } from 'chai';
-import cloneDeep = require('lodash/cloneDeep');
-import assign = require('lodash/assign');
-import omit = require('lodash/omit');
+import { cloneDeep, assign, omit } from 'lodash';
 
 import {
   writeQueryToStore,
@@ -12,10 +10,6 @@ import {
   storeKeyNameFromField,
 } from '../src/data/storeUtils';
 
-import {
-  getIdField,
-} from '../src/data/extensions';
-
 import {
   NormalizedCache,
 } from '../src/data/storeUtils';
@@ -30,6 +24,8 @@ import {
 
 import gql from 'graphql-tag';
 
+const getIdField = ({id}: {id: string}) => id;
+
 describe('writing to the store', () => {
   it('properly normalizes a trivial item', () => {
     const query = gql`
diff --git a/tsconfig.test.json b/tsconfig.test.json
new file mode 100644
--- /dev/null
+++ b/tsconfig.test.json
@@ -0,0 +1,6 @@
+{
+  "extends": "./tsconfig",
+  "compilerOptions": {
+    "module": "commonjs"
+  }
+}