Repository: elastic/search-ui

Issue:
Add a "useSearch" hook as alternative to "withSearch"
This is just a more modern API

zustand has a good example of what our hook might look like:

https://github.com/react-spring/zustand

Discussion:
@m-sureshraj: @JasonStoltz I can fix this issue.@JasonStoltz: @m-sureshraj Yes! I'm going to publish a PR very soon for performance issues that will relate to this. So just wait for that PR to merge to avoid conflicts.@JasonStoltz: @m-sureshraj https://github.com/elastic/search-ui/pull/249@m-sureshraj: @JasonStoltz Is there any blockers for this ticket?@JasonStoltz: @m-sureshraj No blockers, feel free to work on this. I suggest posting the proposed API for this here for discussion before you get too far along on this task, it could help save you some time on the PR if we have the discussion early. @m-sureshraj: Sure @JasonStoltz, I have not yet started the task. I will post the new API changes here for the discussion soon.@botelastic: This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs.
Is this issue still important to you? If so, please leave a comment and let us know. As always, thank you for your contributions.
@calvinl: :( 

Would love to see this come back up into active development. @botelastic: This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs.
Is this issue still important to you? If so, please leave a comment and let us know. As always, thank you for your contributions.
@botelastic: This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs.
Is this issue still important to you? If so, please leave a comment and let us know. As always, thank you for your contributions.
@botelastic: This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs.
Is this issue still important to you? If so, please leave a comment and let us know. As always, thank you for your contributions.
@rlueder: I wrote a custom `useSearch` hook and it works well, I can access any of the `SearchState` values from any other component in my app, examples below:

I created a `Providers.tsx` to wrap my `App` component entry point:

```
import { SearchContext } from "@/hooks/useSearch";

export const Providers = ({ children }: ProvidersProps) => {
  return (
    <SearchProvider config={searchProviderconfig}>
      <WithSearch
        mapContextToProps={(context: SearchContextState) => context}
      >
        {(contextProps: SearchContextState) => (
          <SearchContext.Provider value={contextProps}>
            {children}
          </SearchContext.Provider>
        )}
      </WithSearch>
    </SearchProvider>
  );
};
```
Then `useSearch` hook looks like this:

```
import { createContext, useContext } from "react";

import type { SearchContextState } from "@elastic/search-ui";

export const SearchContext = createContext<SearchContextState | undefined>(
  undefined,
);

export const useSearch = () => {
  const context = useContext(SearchContext);

  if (context === undefined) {
    throw new Error("useSearch must be used within a SearchContextProvider");
  }

  return context;
};

export default useSearch;

```

And this is how it's used:

```
const SomeComponent = () => {
    const { reset, results, resultSearchTerm, totalResults } = useSearch(); // or any other values from SearchContext
    return (
        <div>
            <p>You have {totalResults} results!</p>
        </div>
    );
};

export default SomeComponent;
```


Solution patch:
diff --git a/ADVANCED.md b/ADVANCED.md
index ba9df78..870c76f 100644
--- a/ADVANCED.md
+++ b/ADVANCED.md
@@ -4,9 +4,10 @@
 2. [Component Reference](#component-reference)
 3. [Customization](#customization)
 4. [Advanced Configuration](#advanced-configuration)
-5. [Connectors and Handlers](#connectors-and-handlers)
-6. [Build Your Own Component](#build-your-own-component)
-7. [Search UI Contributor's Guide](#search-ui-contributors-guide)
+5. [Build Your Own Component](#build-your-own-component)
+6. [Connectors and Handlers](#connectors-and-handlers)
+7. [Performance](#performance)
+8. [Search UI Contributor's Guide](#search-ui-contributors-guide)
 
 # Headless Core
 
@@ -75,76 +76,73 @@ It looks like this:
 If you wish to work with Search UI outside of a particular Component, you'll work
 directly with the core.
 
-There's 3 ways you can do this:
+There are two methods for accessing the headless core directly, `withSearch` and
+`WithSearch`. They use the [HOC](https://reactjs.org/docs/higher-order-components.html) and
+[Render Props](https://reactjs.org/docs/render-props.html) patterns, respectively. The two methods
+are similar, and choosing between the two is mostly personal preference.
 
-### 1. The Render Prop on SearchProvider
+Both methods expose a `mapContextToProps` function which allows you to pick which state and actions
+from context you need to work with.
 
-When you configure `SearchProvider`, you need to provide a function as the child of the provider.
+### mapContextToProps
 
-That function is actually a [Render Prop](https://reactjs.org/docs/render-props.html) that exposes the Context for you to work with.
+`mapContextToProps` allows you to pick which state and actions
+from Context you need to work with. `withSearch` and `WithSearch` both use [React.PureComponent](https://reactjs.org/docs/react-api.html#reactpurecomponent),
+and will only re-render when the picked state has changed.
 
-One use case for that would be to render a "loading" indicator any time the application is fetching data.
+| name    | type   | description         |
+| ------- | ------ | ------------------- |
+| context | Object | The current Context |
+| props   | Object | The current props   |
 
-For example:
+ex.:
 
 ```jsx
-<SearchProvider config={config}>
-  {{isLoading} => (
-    <div className="App">
-      {isLoading && <div>I'm loading now</div>}
-      {!isLoading && <Layout
-        header={<SearchBox />}
-        bodyContent={<Results titleField="title" urlField="nps_link" />}
-      />}
-    </div>
-  )}
-</SearchProvider>
+// Selects `searchTerm` and `setSearchTerm` for use in Component
+withSearch(({ searchTerm, setSearchTerm }) => ({
+  searchTerm,
+  setSearchTerm
+}))(Component);
+
+// Uses current `props` to conditionally modify context
+withSearch(({ searchTerm }, { someProp }) => ({
+  searchTerm: someProp ? "" : searchTerm
+}))(Component);
 ```
 
-You could also use it to add a clear filters button:
+### withSearch
 
-```jsx
-<SearchProvider config={config}>
-  {({ isLoading, clearFilters }) => (
-    <div className="App">
-      {isLoading && <div>I'm loading now</div>}
-      {!isLoading && (
-        <Layout
-          header={
-            <div>
-              <SearchBox />
-              <button onClick={clearFilters}>Clear</button>
-            </div>
-          }
-          bodyContent={<Results titleField="title" urlField="nps_link" />}
-        />
-      )}
-    </div>
-  )}
-</SearchProvider>
-```
-
-### 2. `withSearch`
+This is the [HOC](https://reactjs.org/docs/higher-order-components.html) approach to working with the
+core.
 
 This is typically used for creating your own Components.
 
 See [Build Your Own Component](#build-your-own-component).
 
-### 3. With `SearchConsumer`
+### WithSearch
 
-If you prefer a [Render Props](https://reactjs.org/docs/render-props.html) approach rather than
-the [Higher Order Components](https://reactjs.org/docs/higher-order-components.html) approach of `withSearch`, you can use `SearchConsumer`.
+This is the [Render Props](https://reactjs.org/docs/render-props.html) approach to working with the core.
 
-```jsx
-import { SearchConsumer } from "@elastic/react-search-ui";
-```
+One use case for that would be to render a "loading" indicator any time the application is fetching data.
+
+For example:
 
 ```jsx
-<SearchConsumer>
-  {{ isLoading } => (
-    <div>isLoading</div>
-  )}
-</SearchConsumer>
+<SearchProvider config={config}>
+  <WithSearch mapContextToProps={({ isLoading }) => ({ isLoading })}>
+    {({ isLoading }) => (
+      <div className="App">
+        {isLoading && <div>I'm loading now</div>}
+        {!isLoading && (
+          <Layout
+            header={<SearchBox />}
+            bodyContent={<Results titleField="title" urlField="nps_link" />}
+          />
+        )}
+      </div>
+    )}
+  </WithSearch>
+</SearchProvider>
 ```
 
 ## Headless Core Reference
@@ -157,10 +155,10 @@ It exposes the [State](#state) and [Actions](#actions) of the core in a [Context
 
 Params:
 
-| name     | type     | description                                                                                                         |
-| -------- | -------- | ------------------------------------------------------------------------------------------------------------------- |
-| config   | Object   | See the [Configuration Options](#config) section.                                                                   |
-| children | function | A render prop function that accepts the [Context](#context) as a parameter. <br/><br/>`(context) => return <div />` |
+| name     | type       | description                                       |
+| -------- | ---------- | ------------------------------------------------- |
+| config   | Object     | See the [Configuration Options](#config) section. |
+| children | React Node |                                                   |
 
 ### Context
 
@@ -262,6 +260,9 @@ Application state is the general application state.
 
 # Component Reference
 
+Note that all components in this library are Pure Components. Read more
+about that [here](#performance).
+
 The following Components are available:
 
 - [SearchBox](#searchbox)
@@ -671,15 +672,15 @@ import { MultiCheckboxFacet } from "@elastic/react-search-ui-views";
 
 ### Properties
 
-| Name       | type            | Required? | Default                                                                        | Options                                                                                                                                                       | Description                                                                                                                                                                                                                                                |
-| ---------- | --------------- | --------- | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| className  | String          | no        |                                                                                |                                                                                                                                                               |                                                                                                                                                                                                                                                            |
-| field      | String          | yes       |                                                                                |                                                                                                                                                               | Field name corresponding to this filter. This requires that the corresponding field has been configured in `facets` on the top level Provider.                                                                                                             |
-| filterType | String          | no        | "all"                                                                          | "all", "any", "none"                                                                                                                                          | The type of filter to apply with the selected values. I.e., should "all" of the values match, or just "any" of the values, or "none" of the values. Note: See the example above which describes using "disjunctive" facets in conjunction with filterType. |
-| label      | String          | yes       |                                                                                |                                                                                                                                                               | A static label to show in the facet filter.                                                                                                                                                                                                                |
-| show       | Number          | no        | 5                                                                             |                                                                                                                                                               | The number of facet filter options to show before concatenating with a "more" link.                                                                                                                                                                        |
-| view       | Render Function | no        | [MultiCheckboxFacet](packages/react-search-ui-views/src/MultiCheckboxFacet.js) | [SingleLinksFacet](packages/react-search-ui-views/src/SingleLinksFacet.js) <br/> [SingleSelectFacet](packages/react-search-ui-views/src/SingleSelectFacet.js) | Used to override the default view for this Component. See [Customization: Component views and HTML](#component-views-and-html) for more information.                                                                                                       |
-| isFilterable | Boolean | no | false | | Whether or not to show Facet quick filter. |
+| Name         | type            | Required? | Default                                                                        | Options                                                                                                                                                       | Description                                                                                                                                                                                                                                                |
+| ------------ | --------------- | --------- | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| className    | String          | no        |                                                                                |                                                                                                                                                               |                                                                                                                                                                                                                                                            |
+| field        | String          | yes       |                                                                                |                                                                                                                                                               | Field name corresponding to this filter. This requires that the corresponding field has been configured in `facets` on the top level Provider.                                                                                                             |
+| filterType   | String          | no        | "all"                                                                          | "all", "any", "none"                                                                                                                                          | The type of filter to apply with the selected values. I.e., should "all" of the values match, or just "any" of the values, or "none" of the values. Note: See the example above which describes using "disjunctive" facets in conjunction with filterType. |
+| label        | String          | yes       |                                                                                |                                                                                                                                                               | A static label to show in the facet filter.                                                                                                                                                                                                                |
+| show         | Number          | no        | 5                                                                              |                                                                                                                                                               | The number of facet filter options to show before concatenating with a "more" link.                                                                                                                                                                        |
+| view         | Render Function | no        | [MultiCheckboxFacet](packages/react-search-ui-views/src/MultiCheckboxFacet.js) | [SingleLinksFacet](packages/react-search-ui-views/src/SingleLinksFacet.js) <br/> [SingleSelectFacet](packages/react-search-ui-views/src/SingleSelectFacet.js) | Used to override the default view for this Component. See [Customization: Component views and HTML](#component-views-and-html) for more information.                                                                                                       |
+| isFilterable | Boolean         | no        | false                                                                          |                                                                                                                                                               | Whether or not to show Facet quick filter.                                                                                                                                                                                                                 |
 
 ---
 
@@ -878,10 +879,11 @@ return <PagingInfo view={PagingInfoView} />;
 
 We have two primary recommendations for customizing Component behavior:
 
-1. Override state and action props before they are passed to your Component, using `mapContextToProps`.
+1. Override state and action props before they are passed to your Component, using the `mapContextToProps` param. This
+   will override the default [mapContextToProps](#mapContextToProps) for the component.
 2. Override props before they are passed to your Component's view.
 
-### mapContextToProps
+### Override mapContextToProps
 
 Every Component supports a `mapContextToProps` prop, which allows you to modify state and actions
 before they are received by the Component.
@@ -1098,7 +1100,7 @@ const connector = new AppSearchAPIConnector({
 });
 ```
 
-## Build Your Own Component
+# Build Your Own Component
 
 **Learn about the [Headless Core](#headless-core) concepts first!**
 
@@ -1111,7 +1113,7 @@ There might be cases where we do not have the Component you need.
 In this case, we provide a [Higher Order Component](https://reactjs.org/docs/higher-order-components.html)
 called [withSearch](./src/withSearch.js).
 
-It gives you access to work directly with Search UI's [Context](#headless-core#headless-core-concepts).
+It gives you access to work directly with Search UI's [Headless Core](#headless-core).
 
 This lets you create your own Components for Search UI.
 
@@ -1131,10 +1133,19 @@ function ClearFilters({ filters, clearFilters }) {
   );
 }
 
-export default withSearch(ClearFilters);
+export default withSearch(({ filters, clearFilters }) => ({
+  filters,
+  clearFilters
+}))(ClearFilters);
 ```
 
-## Connectors and Handlers
+Note that `withSearch` accepts a `mapContextToProps` function as the first parameter. Read more about that
+in the [mapContextToProps](#mapContextToProps) section.
+
+Also note that all components created with `withSearch` will be Pure Components. Read more
+about that [here](#performance).
+
+# Connectors and Handlers
 
 **Learn about the [Headless Core](#headless-core) concepts first!**
 
@@ -1261,6 +1272,50 @@ need to have in common is an `additionalOptions` parameter.
 For error handling, a method must throw any error with a "message" field populated for any unrecoverable error. This
 includes things like 404s, 500s, etc.
 
+# Performance
+
+This library is optimized to avoid full sub-tree re-rendering, and so that components only re-render when state changes
+that are relevant to those particular components occur.
+
+In order to accomplish this, all components within
+this library are "Pure Components". You can read more about the concept and potential pitfalls
+of Pure Components in the React
+[Optimizing Performance](https://reactjs.org/docs/optimizing-performance.html#avoid-reconciliation) guide.
+
+The thing to be most cautious of is not to
+[mutate state](https://reactjs.org/docs/optimizing-performance.html#the-power-of-not-mutating-data) that you will
+pass as props to any of these components.
+
+Example of what not to do:
+
+```jsx
+class SomeComponent extends React.Component {
+  changeSorting = () => {
+    const { options } = this.state;
+    // Mutating an existing array in state rather than creating a new one is bad. Since Sorting component is "Pure"
+    // it won't update after calling `setState` here.
+    options.push("newOption");
+    this.setState({ options });
+  };
+
+  render() {
+    const { options } = this.state;
+    return <Sorting options={options} />;
+  }
+}
+```
+
+Instead, do:
+
+```jsx
+// Create a new options array and copy the old values into that new array.
+this.setState(prevState => ({ options: [...prevState.options, "newOption"] }));
+```
+
+If you ever need to debug performance related issues, see the instructions in the Optimizing Performance guide for
+enabling the "Highlight Updates" feature in the
+[React Developer tools for Chrome](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi).
+
 # Search UI Contributor's Guide
 
 How to build and contribute to Search UI.
diff --git a/README.md b/README.md
index 578ffae..1b9c3b4 100644
--- a/README.md
+++ b/README.md
@@ -81,14 +81,12 @@ export default function App() {
         apiConnector: connector
       }}
     >
-      {() => (
-        <div className="App">
-          <Layout
-            header={<SearchBox />}
-            bodyContent={<Results titleField="title" urlField="nps_link" />}
-          />
-        </div>
-      )}
+      <div className="App">
+        <Layout
+          header={<SearchBox />}
+          bodyContent={<Results titleField="title" urlField="nps_link" />}
+        />
+      </div>
     </SearchProvider>
   );
 }
@@ -98,19 +96,27 @@ Or go "headless", and take complete control over the look and feel of your searc
 
 ```jsx
 <SearchProvider config={config}>
-  {({ searchTerm, setSearchTerm, results }) => {
-    return (
-      <div>
-        <input
-          value={searchTerm}
-          onChange={e => setSearchTerm(e.target.value)}
-        />
-        {results.map(r => (
-          <div key={r.id.raw}>{r.title.raw}</div>
-        ))}
-      </div>
-    );
-  }}
+  <WithSearch
+    mapContextToProps={({ searchTerm, setSearchTerm, results }) => ({
+      searchTerm,
+      setSearchTerm,
+      results
+    })}
+  >
+    {({ searchTerm, setSearchTerm, results }) => {
+      return (
+        <div>
+          <input
+            value={searchTerm}
+            onChange={e => setSearchTerm(e.target.value)}
+          />
+          {results.map(r => (
+            <div key={r.id.raw}>{r.title.raw}</div>
+          ))}
+        </div>
+      );
+    }}
+  </WithSearch>
 </SearchProvider>
 ```
 
@@ -170,13 +176,13 @@ It is where you configure your search experience, and it ties all of your compon
     apiConnector: connector
   }}
 >
-  {() => <div className="App">{/* Place Components here! */}</div>}
+  <div className="App">{/* Place Components here! */}</div>
 </SearchProvider>
 ```
 
 For more on components, continue to the next section!
 
-While components can be handy, your search experience sometimes has requirements that don't quite fit what components provide "out of the box". In this case, it can be convenient to work directly with the "actions" and "state" provided by something we call the "SearchDriver". `SearchProvider` exposes those in a [Render Prop](https://reactjs.org/docs/render-props.html), which gives you maximum flexibility over your experience.
+While components can be handy, your search experience sometimes has requirements that don't quite fit what components provide "out of the box". In this case, it can be convenient to work directly with the "actions" and "state" provided by something we call the "SearchDriver". You can use `WithSearch` to access those in a [Render Prop](https://reactjs.org/docs/render-props.html), which gives you maximum flexibility over your experience.
 
 ```jsx
 <SearchProvider
@@ -184,9 +190,16 @@ While components can be handy, your search experience sometimes has requirements
     apiConnector: connector
   }}
 >
-  {({ searchTerm, setSearchTerm }) => (
-    <div className="App">{/* Work directly with state and actions! */}</div>
-  )}
+  <WithSearch
+    mapContextToProps={({ searchTerm, setSearchTerm }) => ({
+      searchTerm,
+      setSearchTerm
+    })}
+  >
+    {({ searchTerm, setSearchTerm }) => (
+      <div className="App">{/* Work directly with state and actions! */}</div>
+    )}
+  </WithSearch>
 </SearchProvider>
 ```
 
@@ -208,16 +221,14 @@ Each Component - like `SearchBox` and `Results` - need only be a child of the `S
     apiConnector: connector
   }}
 >
-  {() => (
-    <div className="App">
-      <div className="Header">
-        <SearchBox />
-      </div>
-      <div className="Body">
-        <Results titleField="title" urlField="nps_link" />
-      </div>
+  <div className="App">
+    <div className="Header">
+      <SearchBox />
+    </div>
+    <div className="Body">
+      <Results titleField="title" urlField="nps_link" />
     </div>
-  )}
+  </div>
 </SearchProvider>
 ```
 
diff --git a/examples/elasticsearch/src/App.js b/examples/elasticsearch/src/App.js
index 76c8d07..7e5fe7d 100644
--- a/examples/elasticsearch/src/App.js
+++ b/examples/elasticsearch/src/App.js
@@ -4,6 +4,7 @@ import {
   ErrorBoundary,
   Facet,
   SearchProvider,
+  WithSearch,
   SearchBox,
   Results,
   PagingInfo,
@@ -21,114 +22,127 @@ import "@elastic/react-search-ui-views/lib/styles/styles.css";
 import buildRequest from "./buildRequest";
 import buildState from "./buildState";
 
-export default function App() {
-  return (
-    <SearchProvider
-      config={{
-        debug: true,
-        onResultClick: () => {
-          /* Not implemented */
-        },
-        onAutocompleteResultClick: () => {
-          /* Not implemented */
-        },
-        onAutocomplete: async ({ searchTerm }) => {
-          const body = buildRequest({ searchTerm });
+const config = {
+  debug: true,
+  onResultClick: () => {
+    /* Not implemented */
+  },
+  onAutocompleteResultClick: () => {
+    /* Not implemented */
+  },
+  onAutocomplete: async ({ searchTerm }) => {
+    const body = buildRequest({ searchTerm });
 
-          const response = await fetch(".netlify/functions/search", {
-            method: "POST",
-            headers: { "content-type": "application/json" },
-            body: JSON.stringify(body)
-          });
-          const json = await response.json();
-          const state = buildState(json);
-          return {
-            autocompletedResults: state.results
-          };
-        },
-        onSearch: async state => {
-          const { resultsPerPage } = state;
-          const body = buildRequest(state);
+    const response = await fetch(".netlify/functions/search", {
+      method: "POST",
+      headers: { "content-type": "application/json" },
+      body: JSON.stringify(body)
+    });
+    const json = await response.json();
+    const state = buildState(json);
+    return {
+      autocompletedResults: state.results
+    };
+  },
+  onSearch: async state => {
+    const { resultsPerPage } = state;
+    const body = buildRequest(state);
 
-          const response = await fetch(".netlify/functions/search", {
-            method: "POST",
-            headers: { "content-type": "application/json" },
-            body: JSON.stringify(body)
-          });
-          const json = await response.json();
+    const response = await fetch(".netlify/functions/search", {
+      method: "POST",
+      headers: { "content-type": "application/json" },
+      body: JSON.stringify(body)
+    });
+    const json = await response.json();
 
-          return buildState(json, resultsPerPage);
-        }
-      }}
-    >
-      {({ wasSearched }) => (
-        <div className="App">
-          <ErrorBoundary>
-            <Layout
-              header={
-                <SearchBox
-                  autocompleteMinimumCharacters={3}
-                  autocompleteResults={{
-                    linkTarget: "_blank",
-                    sectionTitle: "Results",
-                    titleField: "title",
-                    urlField: "nps_link",
-                    shouldTrackClickThrough: true,
-                    clickThroughTags: ["test"]
-                  }}
-                  autocompleteSuggestions={true}
-                />
-              }
-              sideContent={
-                <div>
-                  {wasSearched && (
-                    <Sorting
-                      label={"Sort by"}
-                      sortOptions={[
-                        {
-                          name: "Relevance",
-                          value: "",
-                          direction: ""
-                        },
-                        {
-                          name: "Title",
-                          value: "title",
-                          direction: "asc"
-                        }
-                      ]}
-                    />
-                  )}
-                  <Facet field="states" label="States" filterType="any" isFilterable={true} />
-                  <Facet
-                    field="world_heritage_site"
-                    label="World Heritage Site?"
+    return buildState(json, resultsPerPage);
+  }
+};
+
+export default function App() {
+  return (
+    <SearchProvider config={config}>
+      <WithSearch
+        mapContextToProps={({ wasSearched }) => ({ wasSearched })}
+      >
+        {({ wasSearched }) => (
+          <div className="App">
+            <ErrorBoundary>
+              <Layout
+                header={
+                  <SearchBox
+                    autocompleteMinimumCharacters={3}
+                    autocompleteResults={{
+                      linkTarget: "_blank",
+                      sectionTitle: "Results",
+                      titleField: "title",
+                      urlField: "nps_link",
+                      shouldTrackClickThrough: true,
+                      clickThroughTags: ["test"]
+                    }}
+                    autocompleteSuggestions={true}
                   />
-                  <Facet
-                    field="visitors"
-                    label="Visitors"
-                    view={SingleLinksFacet}
+                }
+                sideContent={
+                  <div>
+                    {wasSearched && (
+                      <Sorting
+                        label={"Sort by"}
+                        sortOptions={[
+                          {
+                            name: "Relevance",
+                            value: "",
+                            direction: ""
+                          },
+                          {
+                            name: "Title",
+                            value: "title",
+                            direction: "asc"
+                          }
+                        ]}
+                      />
+                    )}
+                    <Facet
+                      field="states"
+                      label="States"
+                      filterType="any"
+                      isFilterable={true}
+                    />
+                    <Facet
+                      field="world_heritage_site"
+                      label="World Heritage Site?"
+                    />
+                    <Facet
+                      field="visitors"
+                      label="Visitors"
+                      view={SingleLinksFacet}
+                    />
+                    <Facet
+                      field="acres"
+                      label="Acres"
+                      view={SingleSelectFacet}
+                    />
+                  </div>
+                }
+                bodyContent={
+                  <Results
+                    titleField="title"
+                    urlField="nps_link"
+                    shouldTrackClickThrough={true}
                   />
-                  <Facet field="acres" label="Acres" view={SingleSelectFacet} />
-                </div>
-              }
-              bodyContent={
-                <Results
-                  titleField="title"
-                  urlField="nps_link"
-                  shouldTrackClickThrough={true}
-                />
-              }
-              bodyHeader={
-                <React.Fragment>
-                  {wasSearched && <PagingInfo />}
-                  {wasSearched && <ResultsPerPage />}
-                </React.Fragment>
-              }
-              bodyFooter={<Paging />}
-            />
-          </ErrorBoundary>
-        </div>
-      )}
+                }
+                bodyHeader={
+                  <React.Fragment>
+                    {wasSearched && <PagingInfo />}
+                    {wasSearched && <ResultsPerPage />}
+                  </React.Fragment>
+                }
+                bodyFooter={<Paging />}
+              />
+            </ErrorBoundary>
+          </div>
+        )}
+      </WithSearch>
     </SearchProvider>
   );
 }
diff --git a/examples/sandbox/src/App.js b/examples/sandbox/src/App.js
index 83d622a..1948fe3 100755
--- a/examples/sandbox/src/App.js
+++ b/examples/sandbox/src/App.js
@@ -10,7 +10,8 @@ import {
   PagingInfo,
   ResultsPerPage,
   Paging,
-  Sorting
+  Sorting,
+  WithSearch
 } from "@elastic/react-search-ui";
 import {
   Layout,
@@ -19,6 +20,19 @@ import {
 } from "@elastic/react-search-ui-views";
 import "@elastic/react-search-ui-views/lib/styles/styles.css";
 
+const SORT_OPTIONS = [
+  {
+    name: "Relevance",
+    value: "",
+    direction: ""
+  },
+  {
+    name: "Title",
+    value: "title",
+    direction: "asc"
+  }
+];
+
 let connector;
 if (process.env.REACT_APP_SOURCE === "SITE_SEARCH") {
   connector = new SiteSearchAPIConnector({
@@ -38,154 +52,153 @@ if (process.env.REACT_APP_SOURCE === "SITE_SEARCH") {
   });
 }
 
-export default function App() {
-  return (
-    <SearchProvider
-      config={{
-        debug: true,
-        searchQuery: {
-          result_fields: {
-            title: {
-              snippet: {
-                size: 100,
-                fallback: true
-              }
-            },
-            nps_link: {
-              raw: {}
-            },
-            description: {
-              snippet: {
-                size: 100,
-                fallback: true
-              }
-            }
-          },
-          disjunctiveFacets: ["acres", "states"],
-          facets: {
-            world_heritage_site: { type: "value" },
-            states: { type: "value", size: 30 },
-            acres: {
-              type: "range",
-              ranges: [
-                { from: -1, name: "Any" },
-                { from: 0, to: 1000, name: "Small" },
-                { from: 1001, to: 100000, name: "Medium" },
-                { from: 100001, name: "Large" }
-              ]
-            },
-            visitors: {
-              type: "range",
-              ranges: [
-                { from: 0, to: 10000, name: "0 - 10000" },
-                { from: 10001, to: 100000, name: "10001 - 100000" },
-                { from: 100001, to: 500000, name: "100001 - 500000" },
-                { from: 500001, to: 1000000, name: "500001 - 1000000" },
-                { from: 1000001, to: 5000000, name: "1000001 - 5000000" },
-                { from: 5000001, to: 10000000, name: "5000001 - 10000000" },
-                { from: 10000001, name: "10000001+" }
-              ]
-            }
+const config = {
+  debug: true,
+  searchQuery: {
+    result_fields: {
+      title: {
+        snippet: {
+          size: 100,
+          fallback: true
+        }
+      },
+      nps_link: {
+        raw: {}
+      },
+      description: {
+        snippet: {
+          size: 100,
+          fallback: true
+        }
+      }
+    },
+    disjunctiveFacets: ["acres", "states"],
+    facets: {
+      world_heritage_site: { type: "value" },
+      states: { type: "value", size: 30 },
+      acres: {
+        type: "range",
+        ranges: [
+          { from: -1, name: "Any" },
+          { from: 0, to: 1000, name: "Small" },
+          { from: 1001, to: 100000, name: "Medium" },
+          { from: 100001, name: "Large" }
+        ]
+      },
+      visitors: {
+        type: "range",
+        ranges: [
+          { from: 0, to: 10000, name: "0 - 10000" },
+          { from: 10001, to: 100000, name: "10001 - 100000" },
+          { from: 100001, to: 500000, name: "100001 - 500000" },
+          { from: 500001, to: 1000000, name: "500001 - 1000000" },
+          { from: 1000001, to: 5000000, name: "1000001 - 5000000" },
+          { from: 5000001, to: 10000000, name: "5000001 - 10000000" },
+          { from: 10000001, name: "10000001+" }
+        ]
+      }
+    }
+  },
+  autocompleteQuery: {
+    results: {
+      resultsPerPage: 5,
+      result_fields: {
+        title: {
+          snippet: {
+            size: 100,
+            fallback: true
           }
         },
-        autocompleteQuery: {
-          results: {
-            resultsPerPage: 5,
-            result_fields: {
-              title: {
-                snippet: {
-                  size: 100,
-                  fallback: true
-                }
-              },
-              nps_link: {
-                raw: {}
-              }
-            }
-          },
-          suggestions: {
-            types: {
-              documents: {
-                fields: ["title"]
-              }
-            },
-            size: 4
-          }
-        },
-        apiConnector: connector
-      }}
-    >
-      {({ wasSearched }) => (
-        <div className="App">
-          <ErrorBoundary>
-            <Layout
-              header={
-                <SearchBox
-                  autocompleteMinimumCharacters={3}
-                  autocompleteResults={{
-                    linkTarget: "_blank",
-                    sectionTitle: "Results",
-                    titleField: "title",
-                    urlField: "nps_link",
-                    shouldTrackClickThrough: true,
-                    clickThroughTags: ["test"]
-                  }}
-                  autocompleteSuggestions={true}
-                  debounceLength={0}
-                />
-              }
-              sideContent={
-                <div>
-                  {wasSearched && (
-                    <Sorting
-                      label={"Sort by"}
-                      sortOptions={[
-                        {
-                          name: "Relevance",
-                          value: "",
-                          direction: ""
-                        },
-                        {
-                          name: "Title",
-                          value: "title",
-                          direction: "asc"
-                        }
-                      ]}
-                    />
-                  )}
-
-                  <Facet field="states" label="States" filterType="any" isFilterable={true} />
+        nps_link: {
+          raw: {}
+        }
+      }
+    },
+    suggestions: {
+      types: {
+        documents: {
+          fields: ["title"]
+        }
+      },
+      size: 4
+    }
+  },
+  apiConnector: connector
+};
 
-                  <Facet
-                    field="world_heritage_site"
-                    label="World Heritage Site?"
-                  />
-                  <Facet
-                    field="visitors"
-                    label="Visitors"
-                    view={SingleLinksFacet}
-                  />
-                  <Facet field="acres" label="Acres" view={SingleSelectFacet} />
-                </div>
-              }
-              bodyContent={
-                <Results
-                  titleField="title"
-                  urlField="nps_link"
-                  shouldTrackClickThrough={true}
+export default function App() {
+  return (
+    <SearchProvider config={config}>
+      <WithSearch
+        mapContextToProps={({ wasSearched }) => ({ wasSearched })}
+      >
+        {({ wasSearched }) => {
+          return (
+            <div className="App">
+              <ErrorBoundary>
+                <Layout
+                  header={
+                    <SearchBox
+                      autocompleteMinimumCharacters={3}
+                      autocompleteResults={{
+                        linkTarget: "_blank",
+                        sectionTitle: "Results",
+                        titleField: "title",
+                        urlField: "nps_link",
+                        shouldTrackClickThrough: true,
+                        clickThroughTags: ["test"]
+                      }}
+                      autocompleteSuggestions={true}
+                      debounceLength={0}
+                    />
+                  }
+                  sideContent={
+                    <div>
+                      {wasSearched && (
+                        <Sorting label={"Sort by"} sortOptions={SORT_OPTIONS} />
+                      )}
+                      <Facet
+                        field="states"
+                        label="States"
+                        filterType="any"
+                        isFilterable={true}
+                      />
+                      <Facet
+                        field="world_heritage_site"
+                        label="World Heritage Site?"
+                      />
+                      <Facet
+                        field="visitors"
+                        label="Visitors"
+                        view={SingleLinksFacet}
+                      />
+                      <Facet
+                        field="acres"
+                        label="Acres"
+                        view={SingleSelectFacet}
+                      />
+                    </div>
+                  }
+                  bodyContent={
+                    <Results
+                      titleField="title"
+                      urlField="nps_link"
+                      shouldTrackClickThrough={true}
+                    />
+                  }
+                  bodyHeader={
+                    <React.Fragment>
+                      {wasSearched && <PagingInfo />}
+                      {wasSearched && <ResultsPerPage />}
+                    </React.Fragment>
+                  }
+                  bodyFooter={<Paging />}
                 />
-              }
-              bodyHeader={
-                <React.Fragment>
-                  {wasSearched && <PagingInfo />}
-                  {wasSearched && <ResultsPerPage />}
-                </React.Fragment>
-              }
-              bodyFooter={<Paging />}
-            />
-          </ErrorBoundary>
-        </div>
-      )}
+              </ErrorBoundary>
+            </div>
+          );
+        }}
+      </WithSearch>
     </SearchProvider>
   );
 }
diff --git a/packages/react-search-ui-views/package-lock.json b/packages/react-search-ui-views/package-lock.json
index d32e57a..3440c1e 100644
--- a/packages/react-search-ui-views/package-lock.json
+++ b/packages/react-search-ui-views/package-lock.json
@@ -6037,7 +6037,8 @@
         "ansi-regex": {
           "version": "2.1.1",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "aproba": {
           "version": "1.2.0",
@@ -6058,12 +6059,14 @@
         "balanced-match": {
           "version": "1.0.0",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "brace-expansion": {
           "version": "1.1.11",
           "bundled": true,
           "dev": true,
+          "optional": true,
           "requires": {
             "balanced-match": "^1.0.0",
             "concat-map": "0.0.1"
@@ -6078,17 +6081,20 @@
         "code-point-at": {
           "version": "1.1.0",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "concat-map": {
           "version": "0.0.1",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "console-control-strings": {
           "version": "1.1.0",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "core-util-is": {
           "version": "1.0.2",
@@ -6205,7 +6211,8 @@
         "inherits": {
           "version": "2.0.3",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "ini": {
           "version": "1.3.5",
@@ -6217,6 +6224,7 @@
           "version": "1.0.0",
           "bundled": true,
           "dev": true,
+          "optional": true,
           "requires": {
             "number-is-nan": "^1.0.0"
           }
@@ -6231,6 +6239,7 @@
           "version": "3.0.4",
           "bundled": true,
           "dev": true,
+          "optional": true,
           "requires": {
             "brace-expansion": "^1.1.7"
           }
@@ -6238,12 +6247,14 @@
         "minimist": {
           "version": "0.0.8",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "minipass": {
           "version": "2.2.4",
           "bundled": true,
           "dev": true,
+          "optional": true,
           "requires": {
             "safe-buffer": "^5.1.1",
             "yallist": "^3.0.0"
@@ -6262,6 +6273,7 @@
           "version": "0.5.1",
           "bundled": true,
           "dev": true,
+          "optional": true,
           "requires": {
             "minimist": "0.0.8"
           }
@@ -6342,7 +6354,8 @@
         "number-is-nan": {
           "version": "1.0.1",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "object-assign": {
           "version": "4.1.1",
@@ -6354,6 +6367,7 @@
           "version": "1.4.0",
           "bundled": true,
           "dev": true,
+          "optional": true,
           "requires": {
             "wrappy": "1"
           }
@@ -6439,7 +6453,8 @@
         "safe-buffer": {
           "version": "5.1.1",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "safer-buffer": {
           "version": "2.1.2",
@@ -6475,6 +6490,7 @@
           "version": "1.0.2",
           "bundled": true,
           "dev": true,
+          "optional": true,
           "requires": {
             "code-point-at": "^1.0.0",
             "is-fullwidth-code-point": "^1.0.0",
@@ -6494,6 +6510,7 @@
           "version": "3.0.1",
           "bundled": true,
           "dev": true,
+          "optional": true,
           "requires": {
             "ansi-regex": "^2.0.0"
           }
@@ -6537,12 +6554,14 @@
         "wrappy": {
           "version": "1.0.2",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "yallist": {
           "version": "3.0.2",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         }
       }
     },
@@ -11373,15 +11392,27 @@
       }
     },
     "react": {
-      "version": "16.6.0",
-      "resolved": "https://registry.npmjs.org/react/-/react-16.6.0.tgz",
-      "integrity": "sha512-zJPnx/jKtuOEXCbQ9BKaxDMxR0001/hzxXwYxG8septeyYGfsgAei6NgfbVgOhbY1WOP2o3VPs/E9HaN+9hV3Q==",
+      "version": "16.8.6",
+      "resolved": "https://registry.npmjs.org/react/-/react-16.8.6.tgz",
+      "integrity": "sha512-pC0uMkhLaHm11ZSJULfOBqV4tIZkx87ZLvbbQYunNixAAvjnC+snJCg0XQXn9VIsttVsbZP/H/ewzgsd5fxKXw==",
       "dev": true,
       "requires": {
         "loose-envify": "^1.1.0",
         "object-assign": "^4.1.1",
         "prop-types": "^15.6.2",
-        "scheduler": "^0.10.0"
+        "scheduler": "^0.13.6"
+      },
+      "dependencies": {
+        "scheduler": {
+          "version": "0.13.6",
+          "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.13.6.tgz",
+          "integrity": "sha512-IWnObHt413ucAYKsD9J1QShUKkbKLQQHdxRyw73sw4FN26iWr3DY/H34xGPe4nmL1DwXyWmSWmMrA9TfQbE/XQ==",
+          "dev": true,
+          "requires": {
+            "loose-envify": "^1.1.0",
+            "object-assign": "^4.1.1"
+          }
+        }
       }
     },
     "react-dev-utils": {
@@ -11532,15 +11563,27 @@
       }
     },
     "react-dom": {
-      "version": "16.6.0",
-      "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.6.0.tgz",
-      "integrity": "sha512-Stm2D9dXEUUAQdvpvhvFj/DEXwC2PAL/RwEMhoN4dvvD2ikTlJegEXf97xryg88VIAU22ZAP7n842l+9BTz6+w==",
+      "version": "16.8.6",
+      "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.8.6.tgz",
+      "integrity": "sha512-1nL7PIq9LTL3fthPqwkvr2zY7phIPjYrT0jp4HjyEQrEROnw4dG41VVwi/wfoCneoleqrNX7iAD+pXebJZwrwA==",
       "dev": true,
       "requires": {
         "loose-envify": "^1.1.0",
         "object-assign": "^4.1.1",
         "prop-types": "^15.6.2",
-        "scheduler": "^0.10.0"
+        "scheduler": "^0.13.6"
+      },
+      "dependencies": {
+        "scheduler": {
+          "version": "0.13.6",
+          "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.13.6.tgz",
+          "integrity": "sha512-IWnObHt413ucAYKsD9J1QShUKkbKLQQHdxRyw73sw4FN26iWr3DY/H34xGPe4nmL1DwXyWmSWmMrA9TfQbE/XQ==",
+          "dev": true,
+          "requires": {
+            "loose-envify": "^1.1.0",
+            "object-assign": "^4.1.1"
+          }
+        }
       }
     },
     "react-error-overlay": {
diff --git a/packages/react-search-ui-views/package.json b/packages/react-search-ui-views/package.json
index 53c756f..c0ef57c 100644
--- a/packages/react-search-ui-views/package.json
+++ b/packages/react-search-ui-views/package.json
@@ -36,7 +36,7 @@
     "url": "https://github.com/elastic/search-ui/issues"
   },
   "peerDependencies": {
-    "react": "^16.6.0"
+    "react": "^16.8.0"
   },
   "jest": {
     "setupTestFrameworkScriptFile": "<rootDir>/src/setupTests.js",
@@ -64,8 +64,8 @@
     "npm-run-all": "^4.1.5",
     "postcss-cli": "^6.1.1",
     "postcss-import": "^12.0.1",
-    "react": "^16.6.0",
-    "react-dom": "^16.6.0",
+    "react": "^16.8.0",
+    "react-dom": "^16.8.0",
     "rimraf": "^2.6.2"
   },
   "dependencies": {
diff --git a/packages/react-search-ui/package-lock.json b/packages/react-search-ui/package-lock.json
index 4231cd4..e981179 100644
--- a/packages/react-search-ui/package-lock.json
+++ b/packages/react-search-ui/package-lock.json
@@ -1897,7 +1897,8 @@
         "ansi-regex": {
           "version": "2.1.1",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "aproba": {
           "version": "1.2.0",
@@ -1918,12 +1919,14 @@
         "balanced-match": {
           "version": "1.0.0",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "brace-expansion": {
           "version": "1.1.11",
           "bundled": true,
           "dev": true,
+          "optional": true,
           "requires": {
             "balanced-match": "^1.0.0",
             "concat-map": "0.0.1"
@@ -1938,17 +1941,20 @@
         "code-point-at": {
           "version": "1.1.0",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "concat-map": {
           "version": "0.0.1",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "console-control-strings": {
           "version": "1.1.0",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "core-util-is": {
           "version": "1.0.2",
@@ -2065,7 +2071,8 @@
         "inherits": {
           "version": "2.0.3",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "ini": {
           "version": "1.3.5",
@@ -2077,6 +2084,7 @@
           "version": "1.0.0",
           "bundled": true,
           "dev": true,
+          "optional": true,
           "requires": {
             "number-is-nan": "^1.0.0"
           }
@@ -2091,6 +2099,7 @@
           "version": "3.0.4",
           "bundled": true,
           "dev": true,
+          "optional": true,
           "requires": {
             "brace-expansion": "^1.1.7"
           }
@@ -2098,12 +2107,14 @@
         "minimist": {
           "version": "0.0.8",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "minipass": {
           "version": "2.2.4",
           "bundled": true,
           "dev": true,
+          "optional": true,
           "requires": {
             "safe-buffer": "^5.1.1",
             "yallist": "^3.0.0"
@@ -2122,6 +2133,7 @@
           "version": "0.5.1",
           "bundled": true,
           "dev": true,
+          "optional": true,
           "requires": {
             "minimist": "0.0.8"
           }
@@ -2202,7 +2214,8 @@
         "number-is-nan": {
           "version": "1.0.1",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "object-assign": {
           "version": "4.1.1",
@@ -2214,6 +2227,7 @@
           "version": "1.4.0",
           "bundled": true,
           "dev": true,
+          "optional": true,
           "requires": {
             "wrappy": "1"
           }
@@ -2299,7 +2313,8 @@
         "safe-buffer": {
           "version": "5.1.1",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "safer-buffer": {
           "version": "2.1.2",
@@ -2335,6 +2350,7 @@
           "version": "1.0.2",
           "bundled": true,
           "dev": true,
+          "optional": true,
           "requires": {
             "code-point-at": "^1.0.0",
             "is-fullwidth-code-point": "^1.0.0",
@@ -2354,6 +2370,7 @@
           "version": "3.0.1",
           "bundled": true,
           "dev": true,
+          "optional": true,
           "requires": {
             "ansi-regex": "^2.0.0"
           }
@@ -2397,12 +2414,14 @@
         "wrappy": {
           "version": "1.0.2",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "yallist": {
           "version": "3.0.2",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         }
       }
     },
@@ -2900,7 +2919,8 @@
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
       "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
-      "dev": true
+      "dev": true,
+      "optional": true
     },
     "is-finite": {
       "version": "1.0.2",
@@ -5249,27 +5269,51 @@
       }
     },
     "react": {
-      "version": "16.6.0",
-      "resolved": "https://registry.npmjs.org/react/-/react-16.6.0.tgz",
-      "integrity": "sha512-zJPnx/jKtuOEXCbQ9BKaxDMxR0001/hzxXwYxG8septeyYGfsgAei6NgfbVgOhbY1WOP2o3VPs/E9HaN+9hV3Q==",
+      "version": "16.8.6",
+      "resolved": "https://registry.npmjs.org/react/-/react-16.8.6.tgz",
+      "integrity": "sha512-pC0uMkhLaHm11ZSJULfOBqV4tIZkx87ZLvbbQYunNixAAvjnC+snJCg0XQXn9VIsttVsbZP/H/ewzgsd5fxKXw==",
       "dev": true,
       "requires": {
         "loose-envify": "^1.1.0",
         "object-assign": "^4.1.1",
         "prop-types": "^15.6.2",
-        "scheduler": "^0.10.0"
+        "scheduler": "^0.13.6"
+      },
+      "dependencies": {
+        "scheduler": {
+          "version": "0.13.6",
+          "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.13.6.tgz",
+          "integrity": "sha512-IWnObHt413ucAYKsD9J1QShUKkbKLQQHdxRyw73sw4FN26iWr3DY/H34xGPe4nmL1DwXyWmSWmMrA9TfQbE/XQ==",
+          "dev": true,
+          "requires": {
+            "loose-envify": "^1.1.0",
+            "object-assign": "^4.1.1"
+          }
+        }
       }
     },
     "react-dom": {
-      "version": "16.6.0",
-      "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.6.0.tgz",
-      "integrity": "sha512-Stm2D9dXEUUAQdvpvhvFj/DEXwC2PAL/RwEMhoN4dvvD2ikTlJegEXf97xryg88VIAU22ZAP7n842l+9BTz6+w==",
+      "version": "16.8.6",
+      "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.8.6.tgz",
+      "integrity": "sha512-1nL7PIq9LTL3fthPqwkvr2zY7phIPjYrT0jp4HjyEQrEROnw4dG41VVwi/wfoCneoleqrNX7iAD+pXebJZwrwA==",
       "dev": true,
       "requires": {
         "loose-envify": "^1.1.0",
         "object-assign": "^4.1.1",
         "prop-types": "^15.6.2",
-        "scheduler": "^0.10.0"
+        "scheduler": "^0.13.6"
+      },
+      "dependencies": {
+        "scheduler": {
+          "version": "0.13.6",
+          "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.13.6.tgz",
+          "integrity": "sha512-IWnObHt413ucAYKsD9J1QShUKkbKLQQHdxRyw73sw4FN26iWr3DY/H34xGPe4nmL1DwXyWmSWmMrA9TfQbE/XQ==",
+          "dev": true,
+          "requires": {
+            "loose-envify": "^1.1.0",
+            "object-assign": "^4.1.1"
+          }
+        }
       }
     },
     "react-is": {
diff --git a/packages/react-search-ui/package.json b/packages/react-search-ui/package.json
index 8436fef..1373c63 100644
--- a/packages/react-search-ui/package.json
+++ b/packages/react-search-ui/package.json
@@ -38,7 +38,7 @@
     "debounce-fn": "^1.0.0"
   },
   "peerDependencies": {
-    "react": "^16.6.0"
+    "react": "^16.8.0"
   },
   "jest": {
     "setupTestFrameworkScriptFile": "<rootDir>/src/setupTests.js",
@@ -57,8 +57,8 @@
     "enzyme-to-json": "^3.3.4",
     "jest": "^23.6.0",
     "npm-run-all": "^4.1.5",
-    "react": "^16.6.0",
-    "react-dom": "^16.6.0",
+    "react": "^16.8.0",
+    "react-dom": "^16.8.0",
     "rimraf": "^2.6.2"
   }
 }
diff --git a/packages/react-search-ui/src/SearchConsumer.js b/packages/react-search-ui/src/SearchConsumer.js
deleted file mode 100644
index 9fac3b7..0000000
--- a/packages/react-search-ui/src/SearchConsumer.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import SearchContext from "./SearchContext";
-
-export default SearchContext.Consumer;
diff --git a/packages/react-search-ui/src/SearchProvider.js b/packages/react-search-ui/src/SearchProvider.js
index b3397a7..ff98c3f 100644
--- a/packages/react-search-ui/src/SearchProvider.js
+++ b/packages/react-search-ui/src/SearchProvider.js
@@ -1,57 +1,61 @@
 import PropTypes from "prop-types";
 import React, { Component } from "react";
-import { SearchDriver } from "@elastic/search-ui";
 
+import { SearchDriver } from "@elastic/search-ui";
 import SearchContext from "./SearchContext";
 
 /**
- * The SearchProvider is the glue that connects the SearchDriver to
- * our React App.
- *
- * It "subscribes" to the SearchDriver in order to be notified of state
- * changes. It then syncs that state with its own state and passes that state
- * down to child components in a React Context. It will also pass down "Actions"
- * from the SearchDriver, which allow child components to update state.
- *
- * Any "Container" component placed within this context will have access
- * to the Driver's state and Actions.
- *
+ * The SearchProvider primarily holds a reference to the SearchDriver and
+ * exposes it to the rest of the application in a Context.
  */
 class SearchProvider extends Component {
   static propTypes = {
-    children: PropTypes.func.isRequired,
+    children: PropTypes.node.isRequired,
     // Not providing a shape here because the shape matches the shape of
     // SearchDriver. SearchDriver can do it's own parameter validation.
     config: PropTypes.object
   };
 
-  subscription = state => {
-    this.setState(state);
-  };
+  constructor() {
+    super();
+    this.state = {
+      driver: null
+    };
+  }
 
   componentDidMount() {
     const { config } = this.props;
-    this.driver = new SearchDriver(config);
-    this.setState(this.driver.getState());
-    this.driver.subscribeToStateChanges(this.subscription);
+    // This initialization is done inside of componentDidMount, because initializing the SearchDriver server side
+    // will error out, since the driver depends on window. Placing the initialization inside of componentDidMount
+    // assures that it won't attempt to initialize server side.
+    const driver = new SearchDriver(config);
+    this.setState({
+      driver
+    });
   }
 
   componentWillUnmount() {
-    this.driver.tearDown();
+    this.state.driver.tearDown();
   }
 
   render() {
     const { children } = this.props;
-    if (!this.driver) return null;
 
-    const providerValue = {
-      ...this.state,
-      ...this.driver.getActions()
-    };
+    // Since driver is initialized in componentDidMount above, we are waiting
+    // to render until the driver is available.
+    if (!this.state.driver) return null;
 
+    // Passing the entire "this.state" to the Context is significant. Because
+    // Context determines when to re-render based on referential identity
+    // something like this could cause unnecessary renders:
+    //
+    // <SearchContext.Provider value={{driver: this.state.driver}}>
+    //
+    // By passing the entire state, we ensure that re-renders only occur when
+    // state is actually updated.
     return (
-      <SearchContext.Provider value={providerValue}>
-        {children(providerValue)}
+      <SearchContext.Provider value={this.state}>
+        {children}
       </SearchContext.Provider>
     );
   }
diff --git a/packages/react-search-ui/src/WithSearchRenderProps.js b/packages/react-search-ui/src/WithSearchRenderProps.js
new file mode 100644
index 0000000..61b6027
--- /dev/null
+++ b/packages/react-search-ui/src/WithSearchRenderProps.js
@@ -0,0 +1,18 @@
+import React from "react";
+import withSearch from "./withSearch";
+import PropTypes from "prop-types";
+
+function WithSearch({ mapContextToProps, children }) {
+  const Search = withSearch(mapContextToProps)(props => {
+    return children(props);
+  });
+
+  return <Search />;
+}
+
+WithSearch.propTypes = {
+  mapContextToProps: PropTypes.func,
+  children: PropTypes.func.isRequired
+};
+
+export default WithSearch;
diff --git a/packages/react-search-ui/src/containers/ErrorBoundary.js b/packages/react-search-ui/src/containers/ErrorBoundary.js
index 1749262..4be85a5 100644
--- a/packages/react-search-ui/src/containers/ErrorBoundary.js
+++ b/packages/react-search-ui/src/containers/ErrorBoundary.js
@@ -26,4 +26,4 @@ export class ErrorBoundaryContainer extends Component {
   }
 }
 
-export default withSearch(ErrorBoundaryContainer);
+export default withSearch(({ error }) => ({ error }))(ErrorBoundaryContainer);
diff --git a/packages/react-search-ui/src/containers/Facet.js b/packages/react-search-ui/src/containers/Facet.js
index fa1bfd2..1f9a4c5 100644
--- a/packages/react-search-ui/src/containers/Facet.js
+++ b/packages/react-search-ui/src/containers/Facet.js
@@ -115,4 +115,12 @@ export class FacetContainer extends Component {
   }
 }
 
-export default withSearch(FacetContainer);
+export default withSearch(
+  ({ filters, facets, addFilter, removeFilter, setFilter }) => ({
+    filters,
+    facets,
+    addFilter,
+    removeFilter,
+    setFilter
+  })
+)(FacetContainer);
diff --git a/packages/react-search-ui/src/containers/Paging.js b/packages/react-search-ui/src/containers/Paging.js
index 685b848..54042f3 100644
--- a/packages/react-search-ui/src/containers/Paging.js
+++ b/packages/react-search-ui/src/containers/Paging.js
@@ -36,4 +36,11 @@ PagingContainer.propTypes = {
   setCurrent: PropTypes.func.isRequired
 };
 
-export default withSearch(PagingContainer);
+export default withSearch(
+  ({ current, resultsPerPage, totalPages, setCurrent }) => ({
+    current,
+    resultsPerPage,
+    totalPages,
+    setCurrent
+  })
+)(PagingContainer);
diff --git a/packages/react-search-ui/src/containers/PagingInfo.js b/packages/react-search-ui/src/containers/PagingInfo.js
index 2980ed7..ad94e37 100644
--- a/packages/react-search-ui/src/containers/PagingInfo.js
+++ b/packages/react-search-ui/src/containers/PagingInfo.js
@@ -43,4 +43,12 @@ export class PagingInfoContainer extends Component {
   }
 }
 
-export default withSearch(PagingInfoContainer);
+export default withSearch(
+  ({ current, results, resultsPerPage, resultSearchTerm, totalResults }) => ({
+    current,
+    results,
+    resultsPerPage,
+    resultSearchTerm,
+    totalResults
+  })
+)(PagingInfoContainer);
diff --git a/packages/react-search-ui/src/containers/Result.js b/packages/react-search-ui/src/containers/Result.js
index 477cfb4..4a6c0a4 100644
--- a/packages/react-search-ui/src/containers/Result.js
+++ b/packages/react-search-ui/src/containers/Result.js
@@ -51,4 +51,6 @@ export class ResultContainer extends Component {
   }
 }
 
-export default withSearch(ResultContainer);
+export default withSearch(({ trackClickThrough }) => ({ trackClickThrough }))(
+  ResultContainer
+);
diff --git a/packages/react-search-ui/src/containers/Results.js b/packages/react-search-ui/src/containers/Results.js
index 1fc3fe4..2999d2d 100644
--- a/packages/react-search-ui/src/containers/Results.js
+++ b/packages/react-search-ui/src/containers/Results.js
@@ -62,4 +62,4 @@ export class ResultsContainer extends Component {
   }
 }
 
-export default withSearch(ResultsContainer);
+export default withSearch(({ results }) => ({ results }))(ResultsContainer);
diff --git a/packages/react-search-ui/src/containers/ResultsPerPage.js b/packages/react-search-ui/src/containers/ResultsPerPage.js
index d844f6b..5464c85 100644
--- a/packages/react-search-ui/src/containers/ResultsPerPage.js
+++ b/packages/react-search-ui/src/containers/ResultsPerPage.js
@@ -10,9 +10,7 @@ export class ResultsPerPageContainer extends Component {
     view: PropTypes.func,
     options: PropTypes.arrayOf(PropTypes.number),
     // State
-    results: PropTypes.arrayOf(PropTypes.object).isRequired,
     resultsPerPage: PropTypes.number.isRequired,
-    searchTerm: PropTypes.string.isRequired,
     // Actions
     setResultsPerPage: PropTypes.func.isRequired
   };
@@ -43,4 +41,7 @@ export class ResultsPerPageContainer extends Component {
   }
 }
 
-export default withSearch(ResultsPerPageContainer);
+export default withSearch(({ resultsPerPage, setResultsPerPage }) => ({
+  resultsPerPage,
+  setResultsPerPage
+}))(ResultsPerPageContainer);
diff --git a/packages/react-search-ui/src/containers/SearchBox.js b/packages/react-search-ui/src/containers/SearchBox.js
index 92c0c9b..447f224 100644
--- a/packages/react-search-ui/src/containers/SearchBox.js
+++ b/packages/react-search-ui/src/containers/SearchBox.js
@@ -213,4 +213,18 @@ export class SearchBoxContainer extends Component {
   }
 }
 
-export default withSearch(SearchBoxContainer);
+export default withSearch(
+  ({
+    autocompletedResults,
+    autocompletedSuggestions,
+    searchTerm,
+    setSearchTerm,
+    trackAutocompleteClickThrough
+  }) => ({
+    autocompletedResults,
+    autocompletedSuggestions,
+    searchTerm,
+    setSearchTerm,
+    trackAutocompleteClickThrough
+  })
+)(SearchBoxContainer);
diff --git a/packages/react-search-ui/src/containers/Sorting.js b/packages/react-search-ui/src/containers/Sorting.js
index 92f1709..e2b5701 100644
--- a/packages/react-search-ui/src/containers/Sorting.js
+++ b/packages/react-search-ui/src/containers/Sorting.js
@@ -30,8 +30,6 @@ export class SortingContainer extends Component {
     sortOptions: PropTypes.arrayOf(SortOption).isRequired,
     view: PropTypes.func,
     // State
-    results: PropTypes.arrayOf(PropTypes.object).isRequired,
-    searchTerm: PropTypes.string.isRequired,
     sortDirection: PropTypes.oneOf(["asc", "desc", ""]).isRequired,
     sortField: PropTypes.string.isRequired,
     // Actions
@@ -64,4 +62,8 @@ export class SortingContainer extends Component {
   }
 }
 
-export default withSearch(SortingContainer);
+export default withSearch(({ sortDirection, sortField, setSort }) => ({
+  sortDirection,
+  sortField,
+  setSort
+}))(SortingContainer);
diff --git a/packages/react-search-ui/src/index.js b/packages/react-search-ui/src/index.js
index 1a6b63e..3756c15 100644
--- a/packages/react-search-ui/src/index.js
+++ b/packages/react-search-ui/src/index.js
@@ -1,5 +1,6 @@
 export { default as withSearch } from "./withSearch";
-export { default as SearchConsumer } from "./SearchConsumer";
+export { default as SearchContext } from "./SearchContext";
+export { default as WithSearch } from "./WithSearchRenderProps";
 export { default as SearchProvider } from "./SearchProvider";
 
 export * from "./containers";
diff --git a/packages/react-search-ui/src/withSearch.js b/packages/react-search-ui/src/withSearch.js
index db6277c..ed7aad3 100644
--- a/packages/react-search-ui/src/withSearch.js
+++ b/packages/react-search-ui/src/withSearch.js
@@ -1,23 +1,95 @@
 import React from "react";
 
-import SearchConsumer from "./SearchConsumer";
+import SearchContext from "./SearchContext";
+
+function buildContextForProps(context) {
+  return {
+    ...context.driver.getState(),
+    ...context.driver.getActions()
+  };
+}
+
+/* For a given object execute mapContextToProps to pluck out the relevant
+properties */
+function giveMeJustWhatINeeded(stateOrContext, mapContextToProps, props) {
+  const mapContextToPropsToUse = props.mapContextToProps || mapContextToProps;
+  return mapContextToPropsToUse(stateOrContext, props) || {};
+}
 
 /**
- * This is a Higher Order Component that is used to expose (as `props`) all
- * state and Actions provided by the SearchProvider to "container"
- * components.
+ * This is a Higher Order Component that wraps a component and injects state and actions from Search UI, effectively
+ * "connecting" it to Search UI.
+ *
+ * Components using `withSearch` will be "Pure" components.
+ * It is important to understand the implications of using a PureComponent, as described here:
+ * https://reactjs.org/docs/optimizing-performance.html#examples
  *
- * `mapContextToProps` can be used to manipulate actions and state from context
- * before they are passed on to the container.
+ * @param Function mapContextToProps A function that accepts the context and allows you to pick the values to be passed as props
+ * into the component. This allows you to "select" which values from the context to use.
+ * @param Function Component
  */
-export default function withSearch(Component) {
-  return function WithSearch(props) {
-    const { mapContextToProps = context => context, ...rest } = props;
-
-    return (
-      <SearchConsumer>
-        {context => <Component {...mapContextToProps(context)} {...rest} />}
-      </SearchConsumer>
-    );
+export default function withSearch(mapContextToProps) {
+  if (!mapContextToProps) {
+    throw "withSearch requires a function to be provided which returns an object with at least one value.";
+  }
+
+  return function(Component) {
+    class WithSearch extends React.PureComponent {
+      constructor() {
+        super();
+        this.state = {};
+      }
+
+      componentDidMount() {
+        // Note that we're doing this in CDM rather than the constructor, since
+        // `this.context` is not yet available in the constructor
+        this.setState({
+          ...giveMeJustWhatINeeded(
+            buildContextForProps(this.context),
+            // eslint-disable-next-line react/prop-types
+            mapContextToProps,
+            this.props
+          ),
+          mounted: true
+        });
+        // Note that we subscribe to changes at the component level, rather than
+        // at the top level Provider, so that we are re-rendering the entire
+        // subtree when state changes in the Provider.
+        this.context.driver.subscribeToStateChanges(this.subscription);
+      }
+
+      componentWillUnmount() {
+        this.unmounted = true;
+        this.context.driver.unsubscribeToStateChanges(this.subscription);
+      }
+
+      subscription = state => {
+        if (this.unmounted) return;
+        this.setState(prevState =>
+          giveMeJustWhatINeeded(
+            {
+              // We pass prevState here instead of just state so that actions are
+              // persisted as well, which are not passed in the subscription param
+              ...prevState,
+              ...state
+            },
+            mapContextToProps,
+            this.props
+          )
+        );
+      };
+
+      render() {
+        if (!this.state.mounted) return null;
+
+        // eslint-disable-next-line react/prop-types
+        const { ...rest } = this.props;
+
+        return <Component {...this.state} {...rest} />;
+      }
+    }
+
+    WithSearch.contextType = SearchContext;
+    return WithSearch;
   };
 }
diff --git a/packages/search-ui-app-search-connector/package-lock.json b/packages/search-ui-app-search-connector/package-lock.json
index 5b4a4a8..e4371cf 100644
--- a/packages/search-ui-app-search-connector/package-lock.json
+++ b/packages/search-ui-app-search-connector/package-lock.json
@@ -1611,7 +1611,8 @@
         "ansi-regex": {
           "version": "2.1.1",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "aproba": {
           "version": "1.2.0",
@@ -1632,12 +1633,14 @@
         "balanced-match": {
           "version": "1.0.0",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "brace-expansion": {
           "version": "1.1.11",
           "bundled": true,
           "dev": true,
+          "optional": true,
           "requires": {
             "balanced-match": "^1.0.0",
             "concat-map": "0.0.1"
@@ -1652,17 +1655,20 @@
         "code-point-at": {
           "version": "1.1.0",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "concat-map": {
           "version": "0.0.1",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "console-control-strings": {
           "version": "1.1.0",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "core-util-is": {
           "version": "1.0.2",
@@ -1779,7 +1785,8 @@
         "inherits": {
           "version": "2.0.3",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "ini": {
           "version": "1.3.5",
@@ -1791,6 +1798,7 @@
           "version": "1.0.0",
           "bundled": true,
           "dev": true,
+          "optional": true,
           "requires": {
             "number-is-nan": "^1.0.0"
           }
@@ -1805,6 +1813,7 @@
           "version": "3.0.4",
           "bundled": true,
           "dev": true,
+          "optional": true,
           "requires": {
             "brace-expansion": "^1.1.7"
           }
@@ -1812,12 +1821,14 @@
         "minimist": {
           "version": "0.0.8",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "minipass": {
           "version": "2.2.4",
           "bundled": true,
           "dev": true,
+          "optional": true,
           "requires": {
             "safe-buffer": "^5.1.1",
             "yallist": "^3.0.0"
@@ -1836,6 +1847,7 @@
           "version": "0.5.1",
           "bundled": true,
           "dev": true,
+          "optional": true,
           "requires": {
             "minimist": "0.0.8"
           }
@@ -1916,7 +1928,8 @@
         "number-is-nan": {
           "version": "1.0.1",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "object-assign": {
           "version": "4.1.1",
@@ -1928,6 +1941,7 @@
           "version": "1.4.0",
           "bundled": true,
           "dev": true,
+          "optional": true,
           "requires": {
             "wrappy": "1"
           }
@@ -2013,7 +2027,8 @@
         "safe-buffer": {
           "version": "5.1.1",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "safer-buffer": {
           "version": "2.1.2",
@@ -2049,6 +2064,7 @@
           "version": "1.0.2",
           "bundled": true,
           "dev": true,
+          "optional": true,
           "requires": {
             "code-point-at": "^1.0.0",
             "is-fullwidth-code-point": "^1.0.0",
@@ -2068,6 +2084,7 @@
           "version": "3.0.1",
           "bundled": true,
           "dev": true,
+          "optional": true,
           "requires": {
             "ansi-regex": "^2.0.0"
           }
@@ -2111,12 +2128,14 @@
         "wrappy": {
           "version": "1.0.2",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "yallist": {
           "version": "3.0.2",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         }
       }
     },
@@ -2564,7 +2583,8 @@
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
       "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
-      "dev": true
+      "dev": true,
+      "optional": true
     },
     "is-finite": {
       "version": "1.0.2",



Test patch:
diff --git a/packages/react-search-ui/src/__tests__/SearchContext.test.js b/packages/react-search-ui/src/__tests__/SearchContext.test.js
new file mode 100644
index 0000000..67dc3eb
--- /dev/null
+++ b/packages/react-search-ui/src/__tests__/SearchContext.test.js
@@ -0,0 +1,16 @@
+import React from "react";
+import { mount } from "enzyme";
+
+import { SearchContext } from "..";
+
+it("Should be a context", () => {
+  const value = mount(
+    <SearchContext.Provider value={"hi"}>
+      <SearchContext.Consumer>
+        {value => <div>{value}</div>}
+      </SearchContext.Consumer>
+    </SearchContext.Provider>
+  );
+
+  expect(value.text()).toBe("hi");
+});
diff --git a/packages/react-search-ui/src/__tests__/SearchProvider.test.js b/packages/react-search-ui/src/__tests__/SearchProvider.test.js
index 08de144..3bfbd3a 100644
--- a/packages/react-search-ui/src/__tests__/SearchProvider.test.js
+++ b/packages/react-search-ui/src/__tests__/SearchProvider.test.js
@@ -1,3 +1,38 @@
-it("needs tests", () => {
-  expect(true).toBe(true);
+import React from "react";
+import { mount } from "enzyme";
+
+import { SearchProvider, WithSearch } from "../..";
+
+describe("SearchProvider", () => {
+  it("exposes state and actions to components", () => {
+    const wrapper = mount(
+      <SearchProvider
+        config={{
+          initialState: {
+            searchTerm: "test"
+          },
+          onSearch: () => {
+            return {
+              then: () => ({})
+            };
+          }
+        }}
+      >
+        <WithSearch
+          mapContextToProps={({ searchTerm, setSearchTerm }) => ({
+            searchTerm,
+            setSearchTerm
+          })}
+        >
+          {({ searchTerm, setSearchTerm }) => (
+            <div>
+              {searchTerm}
+              {typeof setSearchTerm}
+            </div>
+          )}
+        </WithSearch>
+      </SearchProvider>
+    );
+    expect(wrapper.text()).toEqual("testfunction");
+  });
 });
diff --git a/packages/react-search-ui/src/__tests__/WithSearchRenderProps.test.js b/packages/react-search-ui/src/__tests__/WithSearchRenderProps.test.js
new file mode 100644
index 0000000..8db3d1b
--- /dev/null
+++ b/packages/react-search-ui/src/__tests__/WithSearchRenderProps.test.js
@@ -0,0 +1,74 @@
+import React from "react";
+import { mount } from "enzyme";
+
+import { SearchProvider, WithSearch } from "../..";
+
+describe("WithSearch", () => {
+  it("exposes state and actions to components", () => {
+    const wrapper = mount(
+      <SearchProvider
+        config={{
+          initialState: {
+            searchTerm: "test"
+          },
+          onSearch: () => {
+            return {
+              then: () => ({})
+            };
+          }
+        }}
+      >
+        <WithSearch mapContextToProps={({ searchTerm }) => ({ searchTerm })}>
+          {({ searchTerm }) => <div>{searchTerm}</div>}
+        </WithSearch>
+      </SearchProvider>
+    );
+    expect(wrapper.text()).toEqual("test");
+  });
+
+  describe("mapContextToProps", () => {
+    function setup(mapContextToProps) {
+      return mount(
+        <SearchProvider
+          config={{
+            initialState: {
+              resultsPerPage: 90,
+              searchTerm: "test"
+            },
+            onSearch: () => {
+              return {
+                then: () => ({})
+              };
+            }
+          }}
+        >
+          <WithSearch mapContextToProps={mapContextToProps}>
+            {({ searchTerm, resultsPerPage, setResultsPerPage }) => (
+              <div>
+                {searchTerm}
+                {resultsPerPage}
+                {setResultsPerPage && typeof setResultsPerPage}
+              </div>
+            )}
+          </WithSearch>
+        </SearchProvider>
+      );
+    }
+
+    it("can inject state", () => {
+      const mapContextToProps = ({ resultsPerPage }) => ({
+        resultsPerPage
+      });
+      const wrapper = setup(mapContextToProps);
+      expect(wrapper.text()).toEqual("90");
+    });
+
+    it("can inject actions", () => {
+      const mapContextToProps = ({ setResultsPerPage }) => ({
+        setResultsPerPage
+      });
+      const wrapper = setup(mapContextToProps);
+      expect(wrapper.text()).toEqual("function");
+    });
+  });
+});
diff --git a/packages/react-search-ui/src/__tests__/withSearch.test.js b/packages/react-search-ui/src/__tests__/withSearch.test.js
new file mode 100644
index 0000000..962e511
--- /dev/null
+++ b/packages/react-search-ui/src/__tests__/withSearch.test.js
@@ -0,0 +1,190 @@
+import React from "react";
+import { mount } from "enzyme";
+
+import { SearchContext } from "..";
+import { withSearch } from "..";
+
+describe("withSearch", () => {
+  let mockDriver;
+  let callback;
+
+  beforeEach(() => {
+    mockDriver = {
+      getState: () => ({
+        searchTerm: "a search term",
+        resultSearchTerm: "another search term"
+      }),
+      getActions: () => ({
+        setSearchTerm: searchTerm => {
+          callback({
+            searchTerm
+          });
+        }
+      }),
+      subscribeToStateChanges: cb => {
+        callback = cb;
+      },
+      unsubscribeToStateChanges: jest.fn()
+    };
+  });
+
+  describe("driver subscription", () => {
+    function setup(mapContextToProps) {
+      const Component = withSearch(mapContextToProps)(
+        ({ searchTerm, setSearchTerm }) => {
+          return (
+            <div>
+              {searchTerm}, {setSearchTerm.name}
+            </div>
+          );
+        }
+      );
+
+      return mount(
+        <SearchContext.Provider value={{ driver: mockDriver }}>
+          <Component />
+        </SearchContext.Provider>
+      );
+    }
+
+    it("will subscribe to state updates", () => {
+      const element = setup(c => c);
+      mockDriver.getActions().setSearchTerm("New Term");
+      expect(element.text()).toEqual("New Term, setSearchTerm");
+    });
+
+    it("will maintain action properties on state updates when mapContextToProps parameter is passed", () => {
+      const element = setup(({ searchTerm, setSearchTerm }) => ({
+        searchTerm,
+        setSearchTerm
+      }));
+      mockDriver.getActions().setSearchTerm("New Term");
+      expect(element.text()).toEqual("New Term, setSearchTerm");
+    });
+
+    it("will unsubsribe on unmount", () => {
+      const element = setup(c => c);
+      element.unmount();
+      expect(mockDriver.unsubscribeToStateChanges.mock.calls.length).toEqual(1);
+    });
+  });
+
+  describe("mapContextToProps", () => {
+    function setup(mapContextToProps) {
+      const Component = withSearch(mapContextToProps)(
+        ({ searchTerm, resultSearchTerm, setSearchTerm, clap }) => {
+          return (
+            <div>
+              {searchTerm}
+              {resultSearchTerm}
+              {setSearchTerm && setSearchTerm.name}
+              {clap}
+            </div>
+          );
+        }
+      );
+
+      return mount(
+        <SearchContext.Provider value={{ driver: mockDriver }}>
+          <Component prop1="prop 1 value" />
+        </SearchContext.Provider>
+      );
+    }
+
+    it("will inject specified state", () => {
+      const element = setup(({ searchTerm }) => ({ searchTerm }));
+
+      expect(element.text()).toEqual("a search term");
+    });
+
+    it("will inject a specified action", () => {
+      const element = setup(({ setSearchTerm }) => ({ setSearchTerm }));
+
+      expect(element.text()).toEqual("setSearchTerm");
+    });
+
+    it("will not inject an  unspecified state", () => {
+      const element = setup(({ resultSearchTerm }) => ({ resultSearchTerm }));
+
+      expect(element.text()).toEqual("another search term");
+    });
+
+    it("will inject arbitrary state", () => {
+      const element = setup(() => ({ clap: "your hands" }));
+
+      expect(element.text()).toEqual("your hands");
+    });
+
+    it("will error if nothing is passed", () => {
+      expect(() => setup()).toThrow();
+    });
+
+    it("will inject nothing if the function injects nothing", () => {
+      const element = setup(() => {});
+
+      expect(element.text()).toEqual("");
+    });
+
+    it("accepts the current props as a second parameter", () => {
+      const element = setup((_, { prop1 }) => ({ searchTerm: prop1 }));
+
+      expect(element.text()).toEqual("prop 1 value");
+    });
+
+    it("will use mapContextToProps on state update", () => {
+      const element = setup(({ searchTerm }) => ({
+        searchTerm: searchTerm + " (updated)"
+      }));
+      mockDriver.getActions().setSearchTerm("a new search term");
+      expect(element.text()).toEqual("a new search term (updated)");
+    });
+  });
+
+  describe("mapContextToProps override", () => {
+    function setup(initialMapContextToProps, mapContextToProps) {
+      const Component = withSearch(initialMapContextToProps)(
+        ({ clap, searchTerm }) => {
+          return (
+            <div>
+              {searchTerm}
+              {clap}
+            </div>
+          );
+        }
+      );
+
+      return mount(
+        <SearchContext.Provider value={{ driver: mockDriver }}>
+          <Component mapContextToProps={mapContextToProps} />
+        </SearchContext.Provider>
+      );
+    }
+
+    it("should allow a component level prop that overrides mapContextToProps from setup", () => {
+      const element = setup(
+        () => {
+          return {
+            clap: "your hands"
+          };
+        },
+        ({ searchTerm }) => {
+          return {
+            searchTerm: searchTerm + " is now modified"
+          };
+        }
+      );
+      expect(element.text()).toEqual("a search term is now modified");
+    });
+
+    it("will use the mapContextToProps override on state update", () => {
+      const element = setup(
+        () => {},
+        ({ searchTerm }) => ({
+          searchTerm: searchTerm + " (updated)"
+        })
+      );
+      mockDriver.getActions().setSearchTerm("a new search term");
+      expect(element.text()).toEqual("a new search term (updated)");
+    });
+  });
+});