Skip to content
This repository has been archived by the owner on Nov 17, 2017. It is now read-only.

Make two separate queries in same Component #18

Closed
harshmaur opened this issue Jul 25, 2016 · 23 comments
Closed

Make two separate queries in same Component #18

harshmaur opened this issue Jul 25, 2016 · 23 comments

Comments

@harshmaur
Copy link

How can I have two search queries made at the same time and retrieve the results? Do I need to maintain Redux state to store the results? or is there any other way?

@vvo
Copy link
Contributor

vvo commented Jul 25, 2016

Hi @harshmaur, can you provide us an example use case for that? Maybe explain here what you want to show as a web UI.

@harshmaur
Copy link
Author

harshmaur commented Jul 25, 2016

I am using algolia's search to also populate my single page results. On every single page, I show a list of SAME products present on various eCommerce website to facilitate a price comparison.

Below the price comparison, I show similar products which needs another query.

Here is the ui for it.
page

@vvo
Copy link
Contributor

vvo commented Jul 25, 2016

@harshmaur You should be able to achieve this by having two helper instances and two Provider.

var mainHelper = helper();
var similarProductsHelper = helper();

mainHelper.on('change', function(state) {
  similarProductsHelper.setQuery(state.query).search();
});

const app = <div>
  <Provider helper=mainHelper/>
  <Provider helper=similarProductsHelper/>
</div>

(pseudo code)

@harshmaur
Copy link
Author

This would help me perform two queries however how do I access each state in my component? I am using connect as mentioned in #16 by @Morhaus

import {connect as reduxConnect} from 'react-redux';
import {connect as algoliaConnect} from 'react-algoliasearch-helper';

function mapAlgoliaStateToProps(state) {
    return {
      results: state.searchResults
    };
  }

const ReduxConnected = reduxConnect()(ProductSingle);

export default algoliaConnect(mapAlgoliaStateToProps)(ReduxConnected);

@vvo
Copy link
Contributor

vvo commented Jul 26, 2016

You will not be able to access both states in the same component at the same time, you will only always access one or another given where you put the Results component, see:

var mainHelper = helper();
var similarProductsHelper = helper();

mainHelper.on('change', function(state) {
  similarProductsHelper.setQuery(state.query).search();
});

const app = <div>
  <Provider helper=mainHelper>
    <Hits/> // have access to the state for mainHelper Provider
  </Provider>
  <Provider helper=similarProductsHelper>
    <Hits/>  // have access to the state for similarProductsHelper Provider
  </Provider>
</div>

Is that clearer?

@harshmaur
Copy link
Author

harshmaur commented Jul 26, 2016

Makes sense now. Thanks!

@harshmaur
Copy link
Author

Hi @vvo I analysed my whole application and I see that having multiple providers will make structuring the application quite complex. For instance, I have been Adding the provider in my Root.js file wrapping all the components and containers inside it. Its very difficult to wrap a particular section of my container or the container itself.

Consider a scenario where I have the same similar products component twice on the same page. Now since the component is same the helper state will be the same however I want to be rendering different products inside of it and thus making two queries. Then i will have to create two separate components and reusability is lost.

I am not sure what is the best way to approach the problem.

@harshmaur harshmaur reopened this Jul 27, 2016
@vvo
Copy link
Contributor

vvo commented Jul 27, 2016

@harshmaur You could then always have a single and create a custom widget that would connect to the query and use yourself either the https://github.com/algolia/algoliasearch-client-js or https://community.algolia.com/algoliasearch-helper-js/ to display hits from another index. This should work indeed.

@harshmaur
Copy link
Author

@vvo I could not understand. Which custom widget are you talking about? Also I do not see any query object available to me and I have one index only.

@alexkirsz
Copy link
Contributor

Hey.

I've written a small proof of concept for having a helper whose state is derived from another helper's state.

class ChildHelperProvider extends React.Component {
  constructor(props) {
    super();

    this.helper = algoliasearchHelper(client);
    this.updateState(props);
  }

  componentWillReceiveProps(nextProps) {
    this.updateState(nextProps);
  }

  updateState(props) {
    this.helper.setState(props.configure(props.searchParameters)).search();
  }

  render() {
    return (
      <Provider helper={this.helper}>
        {this.props.children}
      </Provider>
    );
  }
}

ChildHelperProvider.propTypes = {
  searchParameters: PropTypes.object.isRequired,
  configure: PropTypes.func.isRequired,
  children: PropTypes.node,
};

const ConnectedChildHelperProvider = connect(state => ({
  searchParameters: state.searchParameters
}))(ChildHelperProvider);

// Use it like this (where Results is a connected component):
<Provider helper={helper}>
  <div>
    <Results />
    {/* Also show results for the next page */}
    <ConnectedChildHelperProvider configure={s => s.setPage(s.page + 1)}>
      <Results />
    </ConnectedChildHelperProvider>
  </div>
</Provider>

The ChildHelperProvider component connects to the current helper's state, and creates a new helper with custom configured state that it exposes to its children. Every time the parent helper's state changes, the child helper's state will be changed as well and a new search will be triggered. You could also choose to listen directly on the helper's change and search events, but I've taken the simplest path.

Note that this child helper's state should not be modified directly. If you need to modify it, you should provide a new configure prop to the ChildHelperProvider.

Note also that the argument of the configure prop is a SearchParameters, not an AlgoliaSearchHelper, so the API slightly differs. The main difference between the two is that a SearchParameters is immutable, while a helper isn't.

@harshmaur Could that be useful in your case?

@harshmaur
Copy link
Author

Yes, I was able to do a second query properly however I am not able to access the new state inside the ConnectedChildHelperProvider.

My Container is like this. ProductSingle is already wrapped in the Provider from Root.js
I have created the ChildHelperProvider in another file and exported it.

class ProductSingle extends React.Component {

  constructor(props) {
    super();
    this.previousHelperState = props.helper.getState();
  }

  componentDidMount() {
    const {helper} = this.props;
    helper.setQueryParameter('distinct', false).search();
  }

  componentWillUnmount() {
    this.props.helper.setState(this.previousHelperState).search();
  }


  render(){
    let {results} = this.props;

    return(
      <div>

            {/* Comparison Data.. I get proper results from the first query */}

            {results.hits &&
              <Container>
                <Wrapper title="Compare Prices">
                  <CompareList data={results.hits} />
                </Wrapper>
              </Container>
            }

            {/*Similar Products... I dont get results from the new query in "results"  */}
            <ConnectedChildHelperProvider
              configure={s => s
                .removeFacetRefinement("slug")
                .addFacetRefinement('category_slug', results.hits[0].category_slug)
                .addNumericRefinement('price', '>=', _.toInteger(results.hits[0].price * (1-0.05) ))
                .addNumericRefinement('price', '<=', _.toInteger(results.hits[0].price * (1+0.05)))
                .setQueryParameter('distinct', true)
              }>
              <Container>
                <Wrapper title="Similar Products">
                  <Carousel settings = {data.CarouselSettings.tuples}>
                    {
                      _.map(results.hits, (item, i)=> {
                        return (
                          <ProductTuple key={i} item={item} />
                        );
                      })
                    }
                  </Carousel>
                </Wrapper>
              </Container>
            </ConnectedChildHelperProvider>
        </div>
      );
    }
  }
  ProductSingle.propTypes = {
    results: PropTypes.object,
    helper: PropTypes.object,
  };



  function mapAlgoliaStateToProps(state) {
    console.log(state.searchResults);
    return {
      results: state.searchResults
    };
  }


  const ReduxConnected = reduxConnect(null, null)(ProductSingle);

  export default algoliaConnect(mapAlgoliaStateToProps)(ReduxConnected);

@alexkirsz
Copy link
Contributor

alexkirsz commented Jul 28, 2016

Hey.

_.map(results.hits, (item, i)=> {
  return (
    <ProductTuple key={i} item={item} />
  );
})

In this context, results represents the search results of your initial helper. You'll need to create a new connected component that lives inside your ConnectedChildHelperProvider, so that it can receive the new results as props.

function MyCarousel({hits}) {
  if (!hits) {
    return null;
  }
  return (
    <Carousel settings = {data.CarouselSettings.tuples}>
      {
        _.map(hits, (item, i)=> {
          return (
            <ProductTuple key={i} item={item} />
          );
        })
      }
    </Carousel>
  );
}

const ConnectedCarousel = connect(state => ({hits: state.searchResults && state.searchResults.hits}))(MyCarousel);

And then:

<ConnectedChildHelperProvider
  configure={s => s
    .removeFacetRefinement("slug")
    .addFacetRefinement('category_slug', results.hits[0].category_slug)
    .addNumericRefinement('price', '>=', _.toInteger(results.hits[0].price * (1-0.05) ))
    .addNumericRefinement('price', '<=', _.toInteger(results.hits[0].price * (1+0.05)))
    .setQueryParameter('distinct', true)
  }>
  <Container>
    <Wrapper title="Similar Products">
      <ConnectedCarousel />
    </Wrapper>
  </Container>
</ConnectedChildHelperProvider>

@harshmaur
Copy link
Author

Instead of creating another component I am thinking of using contextTypes since I have already connected the ChildHelperProvider.

Here is my file.

import React, {PropTypes} from 'react';
import {Provider, connect} from 'react-algoliasearch-helper';
import algoliasearchHelper from 'algoliasearch-helper';
import algoliasearch from 'algoliasearch';


const client = algoliasearch('appID', 'key');


class ChildHelperProvider extends React.Component {



  constructor(props) {
    super();

    this.helper = algoliasearchHelper(client, 'indexname', {
      facets: ['category_slug', 'instock']
    });

    this.updateState(props);
  }


  getChildContext() {
    return {childResults: this.props.childResults};
  }

  componentWillReceiveProps(nextProps) {
    this.updateState(nextProps);
  }

  updateState(props) {
    this.helper.setState(props.configure(props.searchParameters)).search();
  }

  render() {
    return (
      <Provider helper={this.helper}>
        {this.props.children}
      </Provider>
    );
  }
}

ChildHelperProvider.propTypes = {
  searchParameters: PropTypes.object.isRequired,
  configure: PropTypes.func.isRequired,
  children: PropTypes.node,
  childResults: PropTypes.object
};

ChildHelperProvider.childContextTypes = {
  childResults: PropTypes.object
};

function mapStateToProps(state) {
  // console.log(state.searchParameters);
  return {
    searchParameters: state.searchParameters,
    childResults: state.searchResults && state.searchResults
  };
}

export const ConnectedChildHelperProvider  = connect(mapStateToProps)(ChildHelperProvider);

When I console.log the childResults I get the old state only. However my network tab shows there was a new search queried.

Moreover when I try to access it in my ProductSingle Container, my contextType childResults returns undefined. It should atleast give me results for the previous query since I can log that.

@alexkirsz
Copy link
Contributor

The ChildHelperProvider is connected to the old query, so it receives search parameters and search results from the old query. However, through the <Provider> it renders, it exposes new search parameters and new search results to its children. This is why you should make another connected component and render it inside your ChildHelperProvider.

@harshmaur
Copy link
Author

Wohoo! Works great!!! Thanks for the awesome support @vvo @Morhaus

@harshmaur
Copy link
Author

A new requirement has come up and I am unable to figure out again.

It is basically that I want to pass disjunctiveFacets from my first search to my childHelper. How can we do that?

@harshmaur harshmaur reopened this Aug 26, 2016
@harshmaur
Copy link
Author

harshmaur commented Aug 26, 2016

I have tried passing it as props to the constructor and instantiating the helper with disjunctive facets, however when I try to see the searchParameters of the new state in the child container, it shows the disjunctiveFacets that I have set for the main helper.

My constructor looks like this

constructor(props) {
    super(props);
    let disjunctiveFacets = [];
    if (props.disjunc) {
      disjunctiveFacets = props.disjunc;
    }

    this.helper = algoliasearchHelper(client, MAININDEX, {
      facets: ['category_slug', 'instock'],
      disjunctiveFacets : disjunctiveFacets
    });

    this.updateState(props);
  }

@alexkirsz
Copy link
Contributor

Hey.

You could do

<ChildHelperProvider
  configure={searchParameters =>
    searchParameters
      .addFacet('category_slug')
      .addFacet('instock')
      .addDisjunctiveFacet('my_disjunctive_facet')
  }
/>

@harshmaur
Copy link
Author

Hi,

Yes I can, but my problem is not that, if I am not wrong, .addDisjunctiveFacet would only work if I have mentioned that FacetFilter during initialisation of helper.

Currently my main helper does not have anything in disjunctiveFacets. However I want to add this to my second search.

@alexkirsz
Copy link
Contributor

Ah yes, my bad. The add{Facet, DisjunctiveFacet} API hasn't made it in the helper yet.

For now, you should be able to do:

    searchParameters
      .setQueryParameters({
        facets: searchParameters.facets.concat(['category_slug', 'instock']),
        disjunctiveFacets: searchParameters.disjunctiveFacets.concat(['my_disjunctive_facet']), 
      });

@harshmaur
Copy link
Author

Works!!! Thanks a lot!

@harshmaur
Copy link
Author

Another Problem,

You mentioned earlier that, helper state from child container should not be modified directly, instead it should be done via configure props.

I have a usecase where I have to trigger a change in my helper when someone clicks on an item in my child container.

What is the best way to tackle this? Do you think I should use redux and create actions for it?

@harshmaur harshmaur reopened this Aug 26, 2016
@harshmaur
Copy link
Author

I figured, that I am not modifying the state, just making a .search or .getState.

@vvo vvo closed this as completed Aug 29, 2016
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants