algolia / react-instantsearch

⚡️ Lightning-fast search for React and React Native applications, by Algolia.
https://www.algolia.com/doc/guides/building-search-ui/what-is-instantsearch/react/
MIT License
1.97k stars 386 forks source link

React Native & Menu facet #121

Closed njt1982 closed 7 years ago

njt1982 commented 7 years ago

I'm trying to implement the Menu (https://community.algolia.com/react-instantsearch/widgets/Menu.html) on my app but getting a JS error about the element type being invalid (got undefined, likely forgot to export your component).

import { InstantSearch, Menu } from 'react-instantsearch/native';
import {
  connectSearchBox,
  connectInfiniteHits,
  connectMenu,
} from 'react-instantsearch/connectors';

const VirtualMenu = connectMenu(() => null);

...
...

  render() {
    return (
        <Container>
          <Drawer ref={(ref) => { this.drawer = ref; }}
            content={<FacetSidebar />}
            side="right"
            onClose={() => this.hideFacetDrawer()}>
            <InstantSearch
              appId="[ID GOES HERE]"
              apiKey={session.algolia_api_key}
              indexName="[INDEX GOES HERE]">
              <ConnectedSearchBox toggleFacetDrawer={() => this.toggleFacetDrawer() } />
              <VirtualMenu attributeName="search_type" defaultRefinement={'contact'} />
              <Menu attributeName="events" />
              <ConnectedHits navigation={this.props.navigation}/>
            </InstantSearch>
          </Drawer>
        </Container>
    );
  }

This all works fine if I remote Menu.

I'm clearly doing something wrong, but I cannot find any documentation about using a Menu facet/filter on react native. They all seem to import from the "dom" side of things, which doesn't work on native (or at least doesn't seem to). Ideally this Menu will eventually live under the FacetSidebar view.

Any ideas?

njt1982 commented 7 years ago

It doesn't look horrifically different to https://github.com/algolia/react-instantsearch/blob/master/stories/Menu.stories.js

njt1982 commented 7 years ago

ok so react-instantsearch/native doesn't export a Menu... So that explains that error, I think? https://github.com/algolia/react-instantsearch/blob/master/packages/react-instantsearch/native.js

njt1982 commented 7 years ago

So importing from dom causes this:

image

njt1982 commented 7 years ago

ok, current progress...

Instead of <Menu... /> I'm now doing <ConnectedEventsFilter attributeName="events" /> along with this:

class EventsFilter extends Component {
  render() {
    console.log(this);
    return (<Text>EventsFilter</Text>);
  }
}
const ConnectedEventsFilter = connectRefinementList(EventsFilter);

Pro points:

Con points:

I feel like I am missing something here (or, more accurately, several pages from the documentation are missing! ;) ).

I quite literally stumbled on this approach by finding this issue: https://github.com/algolia/instantsearch.js/issues/2069 Which involves @Arcade1080 and @Haroenv

Problem is, they are doing this:

// Modal Content
import CurrentRefinements from './currentrefinements';
import CategoryFilter from './categoryfilter';
import SizeFilter from './sizefilter';
import ApplyFilter from './applyfilter';

// Pass Components to Algolia Connectors
const ConnectedHits = connectInfiniteHits(Hits);
const ConnectedCurrentRefinements = connectCurrentRefinements(CurrentRefinements);
const ConnectedCategoryFilter = connectHierarchicalMenu(CategoryFilter);
const ConnectedSizeFilter = connectRefinementList(SizeFilter);
const ConnectedApplyFilter=  connectStats(ApplyFilter);

But I have no idea what is in ./categoryfilter - Does that subclass component with a custom renderer?!

IF this is the approach to take, what is the point of all those components and widgets?!

njt1982 commented 7 years ago

hmmm so this error: image

Seems to indicate that something is using non-react elements (https://stackoverflow.com/a/38238220/224707)...

It looks like using one of the provided components does not use React Native elements... https://github.com/algolia/react-instantsearch/blob/9afd0eff249c87379c66fefbdaf549483ea37272/packages/react-instantsearch/src/components/RefinementList.js

So does this mean we cannot use the provided widgets?

njt1982 commented 7 years ago

Ok so THIS lets me render a facet out using Native Base CheckBox's...

class CheckboxRefinementListComponent extends Component {
  render() {
    return (
      <List dataArray={this.props.items}
            renderRow={(row) => <ListItem><CheckBox checked={row.isRefined} /><Text>{row.label}</Text></ListItem>} />
    );
  }
}

const ConnectedCheckboxRefinementList = connectRefinementList(CheckboxRefinementListComponent);

But my problem now is trying to get it into a Drawer...

        <Container>
          <Drawer ref={(ref) => { this.drawer = ref; }}
            content={<Content bounces={false} style={{backgroundColor: 'white', padding: 15}}>
                       <ConnectedCheckboxRefinementList attributeName="events" />
                     </Content>}
            side="right"
            onClose={() => this.hideFacetDrawer()}>

            <InstantSearch
              appId="[ID]"
              apiKey="[KEY]"
              indexName="[INDEX]">

              <ConnectedSearchBox toggleFacetDrawer={() => this.toggleFacetDrawer() } />
              <VirtualMenu attributeName="search_type" defaultRefinement={'contact'} />

              <ConnectedHits navigation={this.props.navigation}/>
            </InstantSearch>
          </Drawer>
        </Container>

However, this throws errors due to the ConnectedCheckboxRefinementList not being able to pickup the context from the InstantSearch (as its outside it).

If I invert those wrappers (so Drawer is inside the InstantSearch), then the Drawer is not rendered properly.

mthuret commented 7 years ago

HI @njt1982,

Thank you for all this feedback. Actually we do not provide react native widgets yet, so you can only use connectors. It means that you need to implement your own Menu using the connectMenu connector. Also, our connectors can't be used outside an <InstantSearch/> instance. Because you're trying to implement a react native app you'll have to use multiple <InstantSearch/> instance and share the searchState between the instances by listening to the onSearchStateChange (a function you can provide to InstantSearch). Also, please note that to be applied a refinement needs to have its proper widgets mounted, so you'll need to use VirtualWidgets.

Here are some guides:

We are currently working on a getting started guide dedicated to React Native. It's not ready yet, but you can find the an in progress example of a complete react native application here to see those concepts in action: https://github.com/algolia/react-instantsearch/tree/docs/improve-react-native-example/packages/react-instantsearch/examples/react-native

njt1982 commented 7 years ago

Thanks @mthuret - the way I got around the nesting problem (at 1.30am last night!) was to add this to one of my elements inside InstantSearch ref={(ref) => { this.vm = ref; }} - then my Drawer content is rendered from a method which checks for the presense of this.vm. If its not present/set yet then it bails and returns null. If it is set, then it renders the custom widgets inside a wrapper which passes in some context as a property.

<DrawerContentWrapper ais={this.vm.context.ais} >

This is a custom component like this:

class DrawerContentWrapper extends Component {
  constructor(props) {
    super(props);
    this.state = {
      ais: props.ais
    };
  }

  getChildContext() {
    return {
      ais: this.state.ais
    };
  }

  render = () => this.props.children
}
DrawerContentWrapper.childContextTypes = {
  ais: PropTypes.object.isRequired,
  multiIndexContext: PropTypes.object,
}

I'm 99.9% sure this is a hack and not the "React Way". But it works ;) Or at least seems to so far...

I'm intrigued by the multiple InstantSearch wrappers approach and syncing the state between them, although tbh I think that feels like just as much of a hack solution as passing the context out of a nested stack and into a different sub-stack via properties and wrapper 😆 (and I mean "hack" in terms of "not a nice solution" as opposed to anything offensive).

I VERY nearly got the Drawer working inside the InstantSearch. The problem was the Hits section. If I manually rendered a List with some ListItems and Text (to fake some results), it all worked fine, but as soon as I put those fake results (or real results) inside a connected Hit component, they just wouldn't render and the Drawer stopped functioning properly.

mthuret commented 7 years ago

You should definitely not pass any context as props as its internal and can be changed but use another <InstantSearch/> instance inside your drawer :) You'll also have to use a VirtualSearchBox and a VirtualMenu inside the drawer.

In the example I gave you you'll find how we sync the searchState between different screen. Even though here it's just a drawer you can use the same strategy.

I'm not sure to get what happen when using the connectHits connector. Do you have any errors?

njt1982 commented 7 years ago

No errors, no - but it just didn't render the hits (a white area). I think it WAS rendering them somewhere, but the layout was all wrong. And the draw came in from the side as a 10px high bar and there was no grey overlay (or underlay?!). If I removed the hits, it all rendered fine (but obviously without any result set!). I messed around with all the flex stuff for hours, no luck.

I'll have a look at your examples. Thanks.

mthuret commented 7 years ago

Let me know how it goes. If you're still stuck maybe sharing a repo with the issue could be easier :)

njt1982 commented 7 years ago

@mthuret Thank you for the tips! Passing the searchState around has simplified things a lot. Fewer wrappers ;) Hopefully, if anyone else hits this issue before it's been documented properly, they'll find this thread ;)

However, I'm still not 100% sold on having to configure an InstantSearch in multiple places. It seems to mean "duplicating" settings like appId, appKey and indexName.

mthuret commented 7 years ago

You can have a configuration file with those in order to not repeat them everywhere :)

njt1982 commented 7 years ago

@mthuret the issue I have now is that I can search in the SearchBox and I can refine from my Drawer (and these refinements apply to the Hits)... However the query from the SearchBox is not being passed to the Refinements. I can see it's being propagated via state.searchState to the Drawer, am I also meant to pass that state into the connectedRefinementList's?

mthuret commented 7 years ago

@njt1982: I'm pretty sure you miss the virtual SearchBox under the <InstantSearch/> instance of the drawer. Having a refinement in the searchState is not enough to be applied, you'll also need the corresponding widget to be mounted.

mthuret commented 7 years ago

I'm closing this, feel free to reopen it @njt1982 if you still have issues.