Skip to content
This repository has been archived by the owner on Dec 30, 2022. It is now read-only.

feat(Conditional): add connectStateResults connector #357

Merged
merged 7 commits into from
Sep 25, 2017
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 33 additions & 57 deletions docgen/src/guide/Conditional_display.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,89 +6,65 @@ category: guide
navWeight: 40
---

Using our connector and [`createConnector`](guide/Custom_connectors.html) approach, you can conditionally display content based on the search state.
When no results are found you might want to display some specific contents helping the user go back
to a search that was successful.

To help you do conditional rendering based on the `searchState` and the
`searchResults` of InstantSearch, we provide the [`connectStateResults`](connectors/connectStateResults.html) connector.

## Displaying content when the query is empty

```jsx
const Content = createConnector({
displayName: 'ConditionalQuery',
getProvidedProps(props, searchState) {
return {query: searchState.query};
},
})(({query}) => {
const content = query
? <div>The query {query} exists</div>
: <div>No query</div>;
return <div>{content}</div>;
});
const Content = connectStateResults(
({ searchState }) =>
Copy link
Contributor

@Haroenv Haroenv Sep 20, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if we can't call this: ({ state, results })

Copy link
Contributor Author

@mthuret mthuret Sep 20, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's called searchState and searchResults everywhere especially in the documentation when looking for the structure so I'd rather keep the same name. If a user wants to call them state or results, they can definitely do it.

searchState && searchState.query
? <div>
The query {searchState.query} exists
</div>
: <div>No query</div>
);
```

## Displaying content when there's no results
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we have an example where we use data from multiple indices? I think we said something like "searchResults, allSearchResults".?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I can add the code of the story for multi indices here 👍


```jsx
const content = createConnector({
displayName: 'ConditionalResults',
getProvidedProps(props, searchState, searchResults) {
const noResults = searchResults.results ? searchResults.results.nbHits === 0 : false;
return {query: searchState.query, noResults};
},
})(({noResults, query}) => {
const content = noResults
? <div>No results found for {query}</div>
: <div>Some results</div>;
return <div>{content}</div>;
});
const Content = connectStateResults(
({ searchState, searchResults }) =>
searchResults && searchResults.nbHits !== 0
? <div>Some results</div>
: <div>
No results has been found for {searchState.query}
</div>
);
```

## Displaying content when there's an error

```jsx
const content = createConnector({
displayName: 'ConditionalError',
getProvidedProps(props, searchState, searchResults) {
return {error: searchResults.error};
},
})(({error}) => {
const content = error
? <div>An error occurred: {error.message}</div>
: <div>Some results</div>;
return <div>{content}</div>;
});
const Content = connectStateResults(
({ error }) =>
error ? <div>Some error</div> : <div>No error</div>
);
```

## Displaying content when loading

In slow user network situations you might want to know when the search results are loading.

```jsx
const content = createConnector({
displayName: 'ConditionalError',
getProvidedProps(props, searchState, searchResults) {
return {loading: searchResults.searching};
},
})(({loading}) => {
const content = loading
? <div>We are loading</div>
: <div>Search finished</div>;
return <div>{content}</div>;
});
const Content = connectStateResults(
({ searching }) =>
searching ? <div>We are searching</div> : <div>Search finished</div>
);
```

Alternatively, if you're using the search in List feature then you can know when the search results are loading by doing:

```jsx
const content = createConnector({
displayName: 'ConditionalError',
getProvidedProps(props, searchState, searchResults) {
return {loading: searchResults.searchingForFacetValues};
},
})(({loading}) => {
const content = loading
? <div>We are loading</div>
: <div>Search finished</div>;
return <div>{content}</div>;
});
const Content = connectStateResults(
({ searchingForFacetValues }) =>
searchingForFacetValues ? <div>We are searching</div> : <div>Search finished</div>
);
```

<div class="guide-nav">
Expand Down
3 changes: 3 additions & 0 deletions packages/react-instantsearch/connectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,6 @@ export {
export { default as connectSortBy } from './src/connectors/connectSortBy.js';
export { default as connectStats } from './src/connectors/connectStats.js';
export { default as connectToggle } from './src/connectors/connectToggle.js';
export {
default as connectStateResults,
} from './src/connectors/connectStateResults.js';
5 changes: 3 additions & 2 deletions packages/react-instantsearch/src/connectors/connectHits.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ import { getResults } from '../core/indexUtils';
* @example
* import React from 'react';
*
* import { connectHits, Highlight, InstantSearch } from 'react-instantsearch/dom';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was this a mistake previously?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

connectHits is from /connectors, so yes

*
* import { Highlight, InstantSearch } from 'react-instantsearch/dom';
* import { connectHits } from 'react-instantsearch/connectors';
* const CustomHits = connectHits(({ hits }) =>
* <div>
* {hits.map(hit =>
Expand Down
62 changes: 62 additions & 0 deletions packages/react-instantsearch/src/connectors/connectStateResults.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import createConnector from '../core/createConnector';
import { getResults } from '../core/indexUtils';

/**
* The `connectStateResults` connector provides a way to access the `searchState` and the `searchResults`
* of InstantSearch.
* For instance this connector allows you to create results/noResults or query/noQuery pages.
* @name connectStateResults
* @kind connector
* @providedPropType {object} searchState - The search state of the instant search component. <br/><br/> See: [Search state structure](https://community.algolia.com/react-instantsearch/guide/Search_state.html)
* @providedPropType {object} searchResults - The search results. <br/><br/> In case of multiple indices: if used under `<Index>`, results will be those of the corresponding index otherwise it'll be those of the root index See: [Search results structure](https://community.algolia.com/algoliasearch-helper-js/reference.html#searchresults)
* @providedPropType {object} allSearchResults - In case of multiple indices you can retrieve all the results
* @providedPropType {string} error - If the search failed, the error will be logged here.
* @providedPropType {boolean} searching - If there is a search in progress.
* @providedPropType {boolean} searchingForFacetValues - If there is a search in a list in progress.
* @example
* import React from 'react';
*
* import { InstantSearch, Hits } from 'react-instantsearch/dom';
* import { connectStateResults } from 'react-instantsearch/connectors';
*
* const Content = connectStateResults(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm having a little doubts about the name here. I think we can find something that describes intent rather than what it is.

connectConditional describes the use case better IMO

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be use in the future for future use case such as retrieving the userInfo field you can get with query rules (and probably a usage with analytics also). Then we need a name that could represent this. Cf discussion in the issue thread. If you have a better name you can still propose it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no API feature in this connector that has any conditional mean (no if/else) so naming it conditional would be not the best to me too.

* ({ searchState, searchResults }) =>
* searchResults && searchResults.nbHits !== 0
* ? <Hits/>
* : <div>
* No results has been found for {searchState.query}
* </div>
* );
* return (
* <WrapWithHits linkedStoryGroup="Conditional">
* <Content />
* </WrapWithHits>
* );
*
* export default function App() {
* return (
* <InstantSearch
* appId="latency"
* apiKey="6be0576ff61c053d5f9a3225e2a90f76"
* indexName="ikea"
* >
* <Content />
* </InstantSearch>
* );
* }
*/
export default createConnector({
displayName: 'AlgoliaStateResults',

getProvidedProps(props, searchState, searchResults) {
const results = getResults(searchResults, this.context);
return {
searchState,
searchResults: results,
allSearchResults: searchResults.results,
searching: searchResults.searching,
error: searchResults.error,
searchingForFacetValues: searchResults.searchingForFacetValues,
};
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/* eslint-env jest, jasmine */

import connect from './connectStateResults';
jest.mock('../core/createConnector');

let props;

describe('connectStateResults', () => {
describe('single index', () => {
const context = { context: { ais: { mainTargetedIndex: 'index' } } };
const getProvidedProps = connect.getProvidedProps.bind(context);
it('provides the correct props to the component', () => {
const searchState = { state: 'state' };
const error = 'error';
const searching = true;
const searchingForFacetValues = true;
const searchResults = {
results: { nbHits: 25, hits: [] },
error,
searching,
searchingForFacetValues,
};

props = getProvidedProps({}, searchState, searchResults);
expect(props).toEqual({
searchState,
searchResults: searchResults.results,
allSearchResults: searchResults.results,
error,
searching,
searchingForFacetValues,
});
});
});
describe('multi index', () => {
const context = {
context: {
ais: { mainTargetedIndex: 'first' },
multiIndexContext: { targetedIndex: 'first' },
},
};
const getProvidedProps = connect.getProvidedProps.bind(context);
it('provides the correct props to the component', () => {
const searchState = { state: 'state' };
const error = 'error';
const searching = true;
const searchingForFacetValues = true;
const searchResults = {
results: {
first: { nbHits: 25, hits: [] },
second: { nbHits: 25, hits: [] },
},
error,
searching,
searchingForFacetValues,
};

props = getProvidedProps({}, searchState, searchResults);
expect(props).toEqual({
searchState,
searchResults: searchResults.results.first,
allSearchResults: searchResults.results,
error,
searching,
searchingForFacetValues,
});
});
});
});
58 changes: 28 additions & 30 deletions stories/Conditional.stories.js
Original file line number Diff line number Diff line change
@@ -1,48 +1,46 @@
import React from 'react';
import { storiesOf } from '@storybook/react';
import { createConnector } from '../packages/react-instantsearch';
import { connectStateResults } from '../packages/react-instantsearch/connectors';
import { WrapWithHits } from './util';

const stories = storiesOf('Conditionals', module);

stories
.add('NoResults/HasResults', () => {
const Content = createConnector({
displayName: 'ConditionalResults',
getProvidedProps(props, searchState, searchResults) {
const noResults = searchResults.results
? searchResults.results.nbHits === 0
: false;
return { query: searchState.query, noResults };
},
})(({ noResults, query }) => {
const content = noResults ? (
<div>No results has been found for {query}</div>
) : (
<div>Some results</div>
);
return <div>{content}</div>;
});
const Content = connectStateResults(
({ searchState, searchResults }) =>
searchResults && searchResults.nbHits !== 0 ? (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

N.I.C.E

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks even nicer in future JS:

searchResults?.nbHits !== 0 ? 

<div>Some results</div>
) : (
<div>No results has been found for {searchState.query}</div>
)
);
return (
<WrapWithHits linkedStoryGroup="Conditional">
<Content />
</WrapWithHits>
);
})
.add('NoQuery/HasQuery', () => {
const Content = createConnector({
displayName: 'ConditionalQuery',
getProvidedProps(props, searchState) {
return { query: searchState.query };
},
})(({ query }) => {
const content = query ? (
<div>The query {query} exists</div>
) : (
<div>No query</div>
);
return <div>{content}</div>;
});
const Content = connectStateResults(
({ searchState }) =>
searchState && searchState.query ? (
<div>The query {searchState.query} exists</div>
) : (
<div>No query</div>
)
);
return (
<WrapWithHits linkedStoryGroup="Conditional">
<Content />
</WrapWithHits>
);
})
.add('NoLoading/HasQuery', () => {
const Content = connectStateResults(
({ searching }) =>
searching ? <div>searching</div> : <div>No searching</div>
);
return (
<WrapWithHits linkedStoryGroup="Conditional">
<Content />
Expand Down
Loading