Improve performance of custom refinement list

Hey :wave:

I’ve followed the example from the docs about creating a connected refinement list. However, I find that the performance of the implementation is a sub par user experience, as the user’s input aren’t reflected until after the network returns with an answer from Algolia:


Therefore, I thought I’d reflect the checkbox state locally in the component, and perform the search in the background. This provides the user with a much better UX, as the widgets update instantly when pressed. I’ll sprinkle this with a loading indicator later, but here’s an example:

The problem is, that this doesn’t take into account the fact that the filters can be modified outside of this component, either through <CurrentRefinements /> as is the case in the gif above, or through <ClearRefinements />

The code from the gif above:

const RefinementList: React.FC<Props & RefinementListProvided & RefinementListExposed> = ({
  items,
  refine,
  searchable,
  searchForItems,
  label,
  currentRefinement,
}) => {
  // Handle checkboxes
  const [checkboxState, setCheckboxState] = React.useState<{ [key: string]: boolean }>({});
  const handleCheckboxChange = (name: string, event: React.ChangeEvent<HTMLInputElement>) => {
    setCheckboxState({ ...checkboxState, [name]: event.target.checked });
  };

  // Works, except when updating the search state from other widgets.
  useEffect(() => {
    const chosenFilters = Object.keys(checkboxState).filter(key => checkboxState[key]);
    refine(chosenFilters);
  }, [checkboxState]); // eslint-disable-line react-hooks/exhaustive-deps

  return (
    <Grid>
      <FormLabel component="menu">{label}</FormLabel>
      <FormGroup className={classes.formControl}>
        {items.map(item => (
          <FormControlLabel
            control={
              <Checkbox
                checked={checkboxState[item.label]}
                onChange={event => {
                  handleCheckboxChange(item.label, event);
                }}
              />
            }
            key={item.objectID ? item.objectID : item.label}
            label={`${item.label} - ${item.count}`}
          />
        ))}
      </FormGroup>
    </Grid>
  );
};

export const ARRefinementList = memo(connectRefinementList(RefinementList));

I’ve tried different useEffects to map the currentRefinement prop to the local state, but haven’t had any success. I feel like this is the closest thing to being correct:

// TODO: Fix so local state reflects remote state, in case of changes from other widgets
  useEffect(() => {
    const chosenFilters = Object.keys(checkboxState).filter(key => checkboxState[key]);
    // If refinements have been changed from facets panel or "clear refinements" widget, we should reflect that in local state
    const mapped = items.filter(item => item.isRefined).map(refined => refined.label);
    const differenceBetweenLocalStateAndAlgoliaState = difference(chosenFilters, mapped);
    if (differenceBetweenLocalStateAndAlgoliaState.length > 0) {
      const newLocalStateValue = mapped.reduce((accumulator, key) => {
        return { ...accumulator, [key]: true };
      }, {});
      setCheckboxState(newLocalStateValue);
    }
  }, [checkboxState, items]);

but the reduce always produces {}, as mapped.length is always 0. I’ve tried using currentRefinement instead of mapping items as well, but currentRefinement is also always empty on re-render for some reason.

I’ve messed around with this for quite some time now, and thought to myself that I can’t be the only one with this challenge - surely very few applications/developers will accept the UX of the implementation in the docs on slower networks, so there must be a previous implementation of optimistic UI updates in React using InstantSearch that I can draw inspiration from? Or perhaps you have something working internally?

Thanks! :smile:

Unfortunately we do not have this implementation in the default of React InstantSearch, just like the example in the documentation. The visible refinements will always be those that are actually applied, not those that are “going to be” applied. As you noticed there’s quite a few circular reasons why simply applying from “local state” is complicated.

Can you maybe try generating something based on the searchState?

Hey @haroen ,

Thanks for chiming in! :smile:

Maybe I wasn’t clear enough, it ended up being a wall of text I posted - sorry :smile: I don’t care much if the visible refinements in the refinements list aren’t reflected until the network returns a response. My goal is

  • for the UI to be responsive, even in bad network conditions. This means that checkboxes should be checked/unchecked as soon as the user presses them, and then the state of the hits and CurrentRefinements will update once the network returns a response.
  • if the user alters the search state through other widgets, i.e. ClearRefinements or by removing a refinement in CurrentRefinements, this change should be reflected in the checkboxes as well - if not immediately (although preferable), then at least once the network returns.

Algolia must have had previous requests regarding this from other customers? Depending on network performance for UI state is bad practice when developing web clients to say the least. The problem isn’t only for refinement lists, it’s for all widgets.

What do you mean by

Can you maybe try generating something based on the searchState?

? :smile:

This is a fundamental difference between how InstantSearch.js and React InstantSearch compute their UIs. It’ll be tricky to change this behavior now.

You can see that with a low-network connection, an InstantSearch.js app updates the refinements state right away while a React InstantSearch app waits for a network response.

At some point we’d like to reuse the InstantSearch.js codebase in React InstantSearch to align these behaviors.

I think what @haroen was suggesting was to use searchState and onSearchStateChange to control when the state is updated, and therefore the UI. You might want to give it a try but I don’t expect it to be trivial.

So you’re saying that in order to achieve UI with acceptable performance with less-than-optimal network conditions, I can’t really use React Instantsearch?

I literally just wrote this comment in another thread. I know you are just trying to help and I appreciate it, but I must say that I’m rather disappointed and disheartened about using Algolia right now.

Hi @jm1,

Thanks for continuing to dig into this. We also don’t want you to keep utilizing your time in a way that is not efficient for your project.

Right now, we do not have any other suggestions different from what @haroen and @francoischalifour suggested. I’m sorry about that.

If you do have any other feedback or want to post these bugs as GH issues, please do not hesitate to do so.

Thanks for your continued feedback.