instantsearch: Refresh does not request pages in infinite hits again

Describe the bug 🐛

When updating the refresh token, infinite hits will not react to the refetch.

To Reproduce 🔍

Steps to reproduce the behavior:

  1. go to the example
  2. trigger next page
  3. refresh
  4. see no changes in the infinite hits, even if the index would have changed

The example’s index can be changed to an index you control where you make changes to make the effect clearer.

https://codesandbox.io/s/react-instantsearch-app-dq2ro

Expected behavior 💭

Infinite hits reacts to refresh, either by:

  1. clearing the cache completely and restarting on the current page
  2. clearing the cache and the state and restarting on page 0

Likely we should go for option 2, unless there’s a showPrevious.

Additional context

Add any other context about the problem here.

When refresh happens, we need to make sure the infinite hits internal cache is also invalidated.

On React Native, where the list component itself is stateful, we can not rely on the “key” hack, because it rerenders with an empty state when we simply clear the cache. What could be an option is:

  1. clear cache
  2. redo current search
  3. save in cache
  4. rerender

The problem is that you can’t do that as a user reasonably, since you don’t have access to the helper state.

A possible solution is:

In the function refresh in InstantSearchManager, emit an event to all widgets. Then in InfiniteHits, listen to that event, and clear the internal cache as we expect.

Another potential solution is to return a promise from the search which happens in refresh. This should allow people to rerender the InfiniteHits component manually.

Relevant pieces of code:

https://github.com/algolia/react-instantsearch/blob/ec9e0fbd6106d1c3e47f1dbfa6eaac3e20af6bd5/packages/react-instantsearch-core/src/core/createInstantSearchManager.js#L69-L72

Relevant issues:

https://github.com/algolia/react-instantsearch/issues/2464

About this issue

  • Original URL
  • State: open
  • Created 5 years ago
  • Reactions: 3
  • Comments: 61 (22 by maintainers)

Most upvoted comments

Yes, the connector will accept the cache prop as well @meck93. You can check that by using a custom cache which has some logging 😃

You can control the internal cache of InfiniteHits now.

import isEqual from 'react-fast-compare';

function getStateWithoutPage(state) {
  const { page, ...rest } = state || {};
  return rest;
}

function getInMemoryCache() {
  let cachedHits = undefined;
  let cachedState = undefined;
  return {
    read({ state }) {
      return isEqual(cachedState, getStateWithoutPage(state))
        ? cachedHits
        : null;
    },
    write({ state, hits }) {
      cachedState = getStateWithoutPage(state);
      cachedHits = hits;
    },
    clear() {
      cachedHits = undefined;
      cachedState = undefined;
    }
  };
}

const cache = getInMemoryCache();

<InfiniteHits 
  ...
  cache={cache}
/>

And later, you can call cache.clear() right before you refresh.

The internal cache of InfiniteHits should be cleared when refreshing. But, until it’s properly fixed, that can be another workaround.

Just dropping in to say I’ve encountered the same issue.

Hi all,

I was just wondering whether there was any update to this? I’m still having a little trouble “rolling my own” infinite hits using the API. My setup requires the ability to search, show no duplicated results, have infinite scroll etc. Everything with the ConnectInfiniteHits works except the problem stated above.

Thanks in advance, Alex

@alexpchin

do you have a working react native example with connectInfiniteHits that loads newly created items on refresh?

I have also encountered this issue. I use the pull to refresh of Flatlist with a callback which sets the refresh to true and remounts the <InfiniteHits>. The first time I pull it doesn’t show an updated list but the second time I pull then it shows the updated list.

update: as a workaround, I remount the <InfiniteHits> after a second using setTimeout and it seems to be working. Not ideal but at least it works for now.

In React InstantSearch Hooks refresh is a function returned from useInstantSearch, but it indeed doesn’t clear the infinite hits cache either. Sorry!

@allanohorn

I got refresh to work in my App by manually clearing the Algolia Client cache. I’m using a reactive variable titled searchVar with Apollo client, but any global state manager will work. My searchVar has this shape:

SearchVar.tsx

  import {SearchState} from 'react-instantsearch-core';

  export interface SearchVar {
  refresh: boolean;
  searchState: SearchState;
  }
SearchScreen.tsx

  const {refresh, searchState} = useReactiveVar(searchVar)

  useEffect(() => {
    if (refresh) {
      algoliaClient
        .clearCache()
        .then(() => {
         
          //  LOGIC TO REFRESH THE TOKEN 
          //  AND RESET THE SEARCH STATE TO AN EMPTY OBJECT

          searchVar({
            refresh: false,         
            searchState: {}
          });
        })
        .catch(err => console.log('[SearchScreen] refresh ERROR', err));
    }
  }, [refresh]);

Then for the InstantSearch component I utilize a controlled searchState, onSearchStateChange, along with a refresh prop (but I’m not confident that actually does anything).

My InstantSearch looks like this:

      <InstantSearch
        searchClient={algoliaClient}
        indexName="items"
        searchState={searchState}
        onSearchStateChange={(newState: SearchState) => {

          //  UPDATE searchState BUT LEAVE REFRESH TOKEN UNTOUCHED
          searchVar({...searchVar(), searchState: newState});

        }}
        refresh={refresh}>

        <Header showHeader={showHeader} />

        <Animated.View style={animatedStyles.content}>
          <ConnectedSearchBox />
          <ConnectedSearchResults />
          <RefinementList />
        </Animated.View>

        <Footer showFooter={showFooter} />

      </InstantSearch>

let me know if that helps or if you have any questions

Hi @Haroenv I finally think I’ve sorted the issues. One of the issues was fixed with incorporating omit-deep-lodash to remove the page from all of the indices in the state (as I was using multi-index). Another was fixed by creating a cache using a key:

const storage = {};

function buildCache(key) {
  if (storage[key]) {
    return storage[key];
  }
  storage[key] = getInMemoryCache();
  return storage[key];
}

Then finally, your fix for algolia/react-instantsearch#3018 seemed to fix the stale hits that I was seeing.

I am just releasing a new version of my project, after I do that, I will create an example to share.

Thanks @alexpchin for providing your snippet!

Just for anyone looking, this is what I have so far:

import isEqual from 'react-fast-compare';
import AsyncStorage from '@react-native-async-storage/async-storage';

const getStateWithoutPage = (state) => {
  const { page, ...rest } = state || {};
  return rest;
};

const customCache = (key) => {
  return {
    clear() {
      AsyncStorage.removeItem(`cache${key}`);
    },
    read({ state }) {
      const cache = AsyncStorage.getItem(`cache${key}`);
      return cache && isEqual(cache.hits, getStateWithoutPage(state))
        ? cache.hits
        : null;
    },
    write({ hits, state }) {
      AsyncStorage.setItem(
        `cache${key}`,
        JSON.stringify({
          hits: hits,
          state: getStateWithoutPage(state),
        }),
      );
    },
  };
};

export const cache = (key) => customCache(key);

To be used with:

cache={cache('key')}

Hi @Haroenv

I did make a github repository a while ago: https://github.com/algolia/instantsearch.js/issues/5263 However, it is a little outdated now so I made a new one here:

https://github.com/alexpchin/react-instant-search-refresh

You will need to change the APP_ID and Index values to one where you can delete an object from an index to see the behaviour.

I was still looking what’s going on, but I don’t have a good react native sandbox and setup, so that took a while, and I had to work on other things. If you can recreate the bad behaviour in a GitHub example, that would help a lot!

Hi @Haroenv I’m just circling back to this. I’ve managed to get this work on the web by using:

<CustomHits
  cache={createInfiniteHitsSessionStorageCache("ais.tattoos")}
/>

Combined with:

import _objectWithoutProperties from "@babel/runtime/helpers/esm/objectWithoutProperties"
import isEqual from "react-fast-compare"

function getStateWithoutPage(state) {
  var _ref = state || {},
    // page = _ref.page,
    rest = _objectWithoutProperties(_ref, ["page"])

  return rest
}

function hasSessionStorage() {
  return (
    typeof window !== "undefined" &&
    typeof window.sessionStorage !== "undefined"
  )
}

export default function createInfiniteHitsSessionStorageCache(
  KEY = "ais.infiniteHits"
) {
  return {
    read: function read(_ref2) {
      var state = _ref2.state

      if (!hasSessionStorage()) {
        return null
      }

      try {
        var cache = JSON.parse(window.sessionStorage.getItem(KEY))
        return cache && isEqual(cache.state, getStateWithoutPage(state))
          ? cache.hits
          : null
      } catch (error) {
        if (error instanceof SyntaxError) {
          try {
            window.sessionStorage.removeItem(KEY)
          } catch (err) {
            // do nothing
          }
        }

        return null
      }
    },
    write: function write(_ref3) {
      var state = _ref3.state,
        hits = _ref3.hits

      if (!hasSessionStorage()) {
        return
      }

      try {
        window.sessionStorage.setItem(
          KEY,
          JSON.stringify({
            state: getStateWithoutPage(state),
            hits: hits,
          })
        )
      } catch (error) {
        // do nothing
      }
    },
  }
}

This allows me to create a different store for each page. However, I’m still a bit stuck on how best to implement this in react-native.

Do you have a guide for clearing the cache for multiple connectInfiniteHits and there no solution where we can clear the internal cache without having to create our own?

Thanks and Happy New Year!

You can remount the <InfiniteHits> component alone, at the same time as using refresh, which will clear its cache

Hi @Haroenv any plans to implement this?