react-instantsearch
react-instantsearch copied to clipboard
Add a widget to retrieve a single hit
Use case
You have an instant search page with results and wants to add a link on each one of them to redirect to a detailed page with only one result.
Need
We need a new widget that will be able to fetch one result (through getObject).
Proposal
TBD
Just browsing through issues to see if anyone was asking for this. This would be a great feature to have. What I actually came looking for was a connector that would allow retrieving a list of objects. My use case is to be able to render a list of featured products and use algolia as the source of truth to pull out product data. Something like this:
const Hits = ({ hits }) => (
<div>
{hits.map(hit => <div>{hit.name}</div>)}
</div>
);
const RecommendedProducts = connectGetObjects(Hits);
const HomePage = () => (
<div>
<RecommendedProducts
ids={["myObjectID1", "myObjectID2", "myObjectID3"]}
/>
</div>
);
I see that we can do it currently with the algoliasearch
sdk. Is there a way to get that functionality through createConnector
? Or some other means?
Hi @shalomvolchok,
Why not using another <InstantSearch/>
to display those records? You'll need to add a featured
attribute on them and configure your second instance to actually retrieve only items that are featured.
Otherwise you could use directly algoliasearch
to fetch those records using getObject. You can also pass this client to <InstantSearch/>
for being used by the instance.
Getting single objects can now be done by doing this until we provide a connector for retrieving specific hits:
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import algoliasearch from 'algoliasearch/lite';
const client = algoliasearch('your_app_id', 'your_search_api_key');
const index = client.initIndex('your_index_name');
const schema = {
color: 'blue',
/* default values for the Hit component */
};
class Hit extends Component {
constructor(props) {
super(props);
this.state = {
...schema,
loaded: false,
};
}
static propTypes = {
objectID: PropTypes.string.isRequired,
};
componentWillMount() {
index.getObject(this.props.objectID).then(content =>
this.setState(prevState => ({
...prevState,
...content,
loaded: true,
}))
);
}
render() {
const { color } = this.state;
return (
<div>
{color}
</div>
);
}
}
Then you can map over Hit
components.
However, to get multiple results, it might be best to find the criterium for those results, and simply query for that. You can also do that with the Configure widget for getting a very specific refinement.
Thanks for the quick replies!
@mthuret, I want to be able to pass in a dynamic list that could include any of the products in Algolia. If I understand your suggestion, you're saying to tag the results in Algolia but that seems like it would be a static solution?
@Haroenv This could work for now, I have something similar that I started testing.
@mthuret @Haroenv Right now I have a template that is meant to serve multiple projects, so the algolia index is dynamic. I do this on a root component and it works great for using search across the app. I was hoping to be able to hook into this already instantiated client/index. As I understand the sdk is doing caching and other optimizations. If I instantiate algoliasearch
again, I'm assuming I'll get another non-shared instance? I can do that for now, but just wanted to see if there are any other options.
@mthuret I see what you are saying that I can pass algoliasearch
into <InstantSearch algoliaClient={client}/>
, this could be useful to allow for a shared instance. But then I'd still need to pass that instantiated client around my app. I looked at what algolia already has in the context (ais), but doesn't seem to be a usable client. Or is there a way for me to pull the client out of the context?
Unless there are some other options it seems the best workaround for now would be to pass algoliasearch
in <InstantSearch/>
and then add the instantiated client to the context myself. Does that seem reasonable for now?
How often those featured
items are changing? To me it can be dynamic, the same way you'll have to know the objectId on the front-end side, you can actually update those records on the server side.
Regarding the client you'll instantiate it once, and then InstantSearch will use it. Then I don't think there's a need to play with the context. Just pass it along as a prop.
Obviously when the single hit feature will be developed there'll no need to play with an extra client.
Well I'd like to have the functionality to change them independently from the index. For example, so that we could a/b test one list of featured items vs another. Or to show a list of recently viewed products to a specific user.
I'll pass the client into InstantSearch and down as props for now.
Thanks for the help :) And love the way this library and Algolia work. Great service and great pattern, fun to work with.
I’m not near to a computer now, but I think you can do something like this:
<Configure
filters={featuredIDS.reduce(
(filters, id) => `${filters} OR objectID:${id}`,
''
)}
/>
And then use that configure in a separate <Index>
for featured items.
@Haroenv I tested your suggestion with the code below and it totally seems to do what I need. I assume something like this is what you had in mind? Thanks!
import { connectHits } from "react-instantsearch/connectors";
import { Configure, Index, Hits } from "react-instantsearch/dom";
const RenderHits = ({ hits }) => (
<div>
{hits.map(hit => <div>{hit.name}</div>)}
</div>
);
const ConnectedRenderHits = connectHits(RenderHits);
const RecommendedProducts = ({ ids }) => (
<Index indexName="index_name">
<Configure
filters={ids
.map(id => `objectID:${id}`)
.reduce((filters, id) => `${filters} OR ${id}`)}
/>
<ConnectedRenderHits />
</Index>
);
const HomePage = () => (
<div>
<RecommendedProducts
ids={["myObjectID1", "myObjectID2", "myObjectID3"]}
/>
</div>
);
Indeed, that’s what I was suggesting! I’d write
<Configure
filters={ids.reduce((filters, id) => `${filters} OR objectID:${id}`)}
/>
instead though, a bit more obvious 🎉
I tried that first, but I got an error from Algolia because in the first reduce iteration filters
is just an ID and it doesn't get objectID:
in front of it. Maybe a simple way to fix that, but first thing to my mind was just add the map...
Ah interesting, could you share that in a codepen, seems like a bug 👍
edit: I get it now, your version seems good
@Haroenv I didn't know that we could filter by objectID, I thought it was not working. That's cool!
Then I think @shalomvolchok that you can also skip the Configure + map/filter step by just using a VirtualRefinementList or a VirtualMenu. Something like:
const FilterByIds = connectRefinementList(() => null);
<FilterByIds
attributeName="objectID"
defaultRefinement={['xxx', 'xxx']}
/> //use Menu for a list of items
It even seems that directly using a Menu or RefinementList is possible as nothing is displayed.
That's a nice use case using the multi indices feature 👍
@Haroenv https://codepen.io/anon/pen/MoWYPG?editors=0010#0
@mthuret ok, let me have a try with that as well...
not that it’s a big improvement, but
ids.map(id => `objectID:${id}`).join(' OR ')
also works. Whatever you find more legible
OK, pulled all that into a HOC and ended up with this. Seems to satisfy all my use cases.
import React, { Component } from "react";
import {
connectHits,
connectRefinementList
} from "react-instantsearch/connectors";
import { Index } from "react-instantsearch/dom";
const FilterByIds = connectRefinementList(() => null);
export default WrappedComponent => {
const ListFromSearch = connectHits(props => {
const { ids, hits } = props;
// order the hits by original list order
const orderedHits = hits
.sort(
(a, b) =>
ids.findIndex(id => id === a.objectID) -
ids.findIndex(id => id === b.objectID)
)
.filter(Boolean);
return <WrappedComponent {...props} hits={orderedHits} />;
});
return props => {
const { ids, indexName } = props;
return (
<Index indexName={indexName}>
<FilterByIds attributeName="objectID" defaultRefinement={ids} />
<ListFromSearch {...props} />
</Index>
);
};
};
I use the HOC like this:
const RecommendedProducts = getObjectsConnector(({ hits }) => (
<div>
{hits.map(hit => <div>{hit.name}</div>)}
</div>
));
const HomePage = () => (
<RecommendedProducts
indexName="index_name"
ids={["myObjectID1", "myObjectID2", "myObjectID3"]}
/>
);
How does that seem?
Hmmm... Maybe I misunderstood. I was thinking that <Index>
could give me another instance of the same index to play with. But do I need a completely different index_name
? Also, it seems that I can only have one list, a second list seems to refine the ids from the first. Also, my search seems to refine my featured products list... Have I really missed something here and this just needs a little fine tuning? Or should I go back to using algoliasearch
and getObject
directly?
I think I was a little too fast here... indeed you can't use the multi indices API because its made to target different indices. In your case you should use another <InstantSearch/>
instance.
Like this:
<InstantSearch>
<Hits />
<InstantSearch>
<FilterByIds />
<Hits />
</InstantSearch>
</InstantSearch>
OK, thanks for the reply. I'm out of the office today, but I will give it a try in the morning and confirm I've got it working.
Yes, I can confirm that got everything working. Appreciate the help :+1:
That's cool @shalomvolchok! Happy coding :)
I'm doing a hacky workaround similar to yours approach, but in my case I want to be possible display different widget based on a search filter.
HOC looks like:
<HomeProducts
title='Feature Sails'
subtitle='View more sails →'
filters='category:sails AND provider:lpwind'
hitsPerPage={3}
/>
<HomeProducts
title='Feature Boards'
subtitle='View more Boards →'
filters='category:boards AND provider:wewind'
hitsPerPage={3}
/>
so I defined a HomeProducts components. It's encapsulating a Configure
and connectHits
from Algolia, using a custom markup.
The only problem I found is, even I have two instances of the components using a different filter, the component render the same results.

Looks like the last Configure
is being apply for all HomeProducts
instances.
Also using Index
as external wrapper inside the component didnt' works as I expected, probably I need to apply InstantSearch
as well.
Related? https://github.com/algolia/react-instantsearch/issues/28
any idea how to do that?
edit: yes, apply InstantSearch
wrappers works like a charm:
export default ({title, filters, hitsPerPage}) => (
<InstantSearch
appId='LIRIRIRLRI'
apiKey='LERERLERE'
indexName='windsurf'
>
<Configure filters={filters} hitsPerPage={hitsPerPage} />
<HomeProducts title={title} />
</InstantSearch>
)
Now I have to find an strategy for don't populate appId
and apiKey
around all the code 🤔
@Kikobeats the example HOC I posted in full didn't end up actually working as expected. What @mthuret suggested, that did work, was to wrap every different instance of search results with its own <InstantSearch/>
. Without this I got the same results as you are seeing and other behavior I didn't want. So basically, I think something like this should work for you:
<div>
<InstantSearch appId="app_id" apiKey="api_key" indexName="index">
<HomeProducts
title="Feature Sails"
subtitle="View more sails →"
filters="category:sails AND provider:lpwind"
hitsPerPage={3}
/>
</InstantSearch>
<InstantSearch appId="app_id" apiKey="api_key" indexName="index">
<HomeProducts
title="Feature Boards"
subtitle="View more Boards →"
filters="category:boards AND provider:wewind"
hitsPerPage={3}
/>
</InstantSearch>
</div>
Hey guys, are you tracking this issue?
I saw new 4.1.0 beta version is shipping SSR (that's awesome, thanks folks 🎉 )
I think that this issue is higly related with create unique URL per each database record and expose it using SSR to be possible optimize content for SEO.
Hey,
I build a simple solution that works like a charm:
Live demo at next.windtoday.co
Basically, I'm injecting the ObjectID
into filters
field at <Configure/>
:
https://github.com/windtoday/windtoday-app/blob/v3/components/App.js#L116
Then, if the objectID is present I determinate render a Single Hit view component: https://github.com/windtoday/windtoday-app/blob/v3/components/App.js#L126
This single hit view component needs to use <connectHits/>
just for get the hits
(actually just get the first element) as prop:
https://github.com/windtoday/windtoday-app/blob/v3/components/SingleHit/index.js#L119
and that's it. It was more easy than I thought at first time.
Awesome @Kikobeats! Great example that others can follow!
We might still want to add a widget that will hide a bit this :)
Hi @Kikobeats ... you have no idea how much reading through this has helped me... quick question regarding
filters="category:boards AND provider:wewind"
How should I do to look for nested attributes? Would it be ok to do it like this?
<Configure filters='line_items[0].shipped_status: pending'/>
normally that should work @romafederico 👍
@romafederico i agree, just following this discussion helped me understand algolia from react perspective better. continue to be impressed by the flexibility. thanks @mthuret for planting the seed of idea and @Haroenv for that very nice last bit that exchanged reduce for a simple mapping. brilliant.
Getting single objects can now be done by doing this until we provide a connector for retrieving specific hits:
import PropTypes from 'prop-types'; import React, { Component } from 'react'; import algoliasearch from 'algoliasearch/lite'; const client = algoliasearch('your_app_id', 'your_search_api_key'); const index = client.initIndex('your_index_name'); const schema = { color: 'blue', /* default values for the Hit component */ }; class Hit extends Component { constructor(props) { super(props); this.state = { ...schema, loaded: false, }; } static propTypes = { objectID: PropTypes.string.isRequired, }; componentWillMount() { index.getObject(this.props.objectID).then(content => this.setState(prevState => ({ ...prevState, ...content, loaded: true, })) ); } render() { const { color } = this.state; return ( <div> {color} </div> ); } }
Then you can map over
Hit
components.However, to get multiple results, it might be best to find the criterium for those results, and simply query for that. You can also do that with the Configure widget for getting a very specific refinement.
I know that this is an old post, but the import
at the top is wrong. "getObject" seems to work only with import algoliasearch from "algoliasearch";
and not with import algoliasearch from "algoliasearch/lite";
that code sample was written a long time ago, when algoliasearch/lite
still shipped with getObject. Since version 4 it no longer does that. I'd recommend you go with the objectID filter (maybe in an index widget) though, since that way, if you're also searching on the same page, it will not cost you a new network request