Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[react-instantsearch-hooks] RefinementList search state cleared on un-mount even when using useRefinementList to create a virtual widget #5240

Closed
jamespeilow opened this issue Jun 29, 2022 · 12 comments
Labels
Library: React InstantSearch ≥ 7 Issues in any of the react-instantsearch@7 packages (formerly named react-instantsearch-hooks)

Comments

@jamespeilow
Copy link

🐛 Bug description

When placing a <RefinementList> component inside a modal component that mounts and un-mounts its content, the current refinements are cleared even when using the useRefinementsList hook to create a virtual refinement list widget.

🔍 Bug reproduction

Minimal example set up on Codesandbox, using the Algolia docs example as a reference.

Steps to reproduce the behavior:

  1. Go to https://codesandbox.io/s/snowy-field-22wyku?file=/src/App.js
  2. Click on the "Show DIalog" button to open the modal
  3. Apply a refinement (notice that the hits have updated)
  4. Close the modal by clicking outside the modal body
  5. The refinement has reset and all results show (refinement state has been cleared)

Live reproduction:

https://codesandbox.io/s/snowy-field-22wyku?file=/src/App.js

💭 Expected behavior

Current refinement search state should be preserved when the <RefinementList> component inside the modal component is unmounted on close.

Environment

  • React InstantSearch Hooks version: 6.29.0
  • React version: 18.0.0
  • Browser: Chrome 103.0.5060.53
  • OS: macOS

Thanks

@Haroenv
Copy link
Contributor

Haroenv commented Jun 29, 2022

Unfortunately this isn't yet possible. There's two possible workarounds:

  1. if you're making a custom widget anyway, make the layout like this:
function RefinementList() {
  useRefinementList()
  
  return <Dialog />
}
  1. use a dialog that only hides via css, but keeps the refinement in the tree

We don't yet have a mechanism to prevent a widget from cleaning up when it unmounts, even when there's another widget of the same type in the tree, as it's impossible to detect both are intended to be synced

@Haroenv Haroenv closed this as completed Jun 29, 2022
@Haroenv Haroenv reopened this Jun 29, 2022
@Haroenv
Copy link
Contributor

Haroenv commented Jun 29, 2022

Here's how to use the internal ui component to avoid recreating the whole rendering and still put the dialog inside the refinement list conceptually: https://codesandbox.io/s/autumn-leftpad-3j3nmp?file=/src/App.js

@Haroenv Haroenv closed this as completed Jun 29, 2022
@Haroenv Haroenv reopened this Jun 29, 2022
@RobbyUitbeijerse
Copy link

RobbyUitbeijerse commented Jul 22, 2022

We are running into the same issue. We use a drawer like structure on mobile, while keeping the desktop UI components mounted. The drawer is a component from @chakra-ui/react, of which we can't control the mounting state (it gets unmounted on close)
https://chakra-ui.com/docs/components/drawer

The drawer opens if you click a filter button, the user can apply refinements within the drawer but once the drawer closes, all filters cleared (as the drawer simply renders all the filters within). It feels like unexpected behaviour, as I would expect the lib to keep track of the mounted widgets

We'll look intro restructuring our component tree for now.

[edit] please note that both the desktop and mobile UI use hooks

The documentation actually seems to suggest that the @jamespeilow is pointing out should actually work: https://www.algolia.com/doc/guides/building-search-ui/widgets/customize-an-existing-widget/react-hooks/#building-a-virtual-widget-with-hooks

@fatlinesofcode
Copy link

fatlinesofcode commented Sep 21, 2022

had another look the connect example from here which uses a modal https://www.algolia.com/doc/guides/building-search-ui/going-further/native/react/#create-a-modal

it can also be done in hooks by using const { setUiState } = useInstantSearch() and wrapping the modal in another InstantSearch instance

const VirtualUiState = ({ searchState }) => {
  const { setUiState } = useInstantSearch();
  useEffect(() => {
    setUiState(searchState);
  }, [setUiState, searchState]);

  return null;
};
const VirtualRefinementList = ({ attribute, searchState }) => {
  useRefinementList({ attribute });
 return null
};
...
const [searchState, setSearchState] = useState({})
const handleStateChange = ({ uiState, setUiState }) => {
  const nextState = {...uiState} // or custom merge
  setSearchState(nextState);
  setUiState(nextState);
}
...
<InstantSearch searchClient={searchClient} indexName="instant_search" >
<VirtualUiState searchState={searchState} />
<VirtualRefinementList searchState={searchState} attribute={attribute} /> 
{filterVisible && (
                <InstantSearch
                  searchClient={searchClient}
                  indexName="instant_search"
                  onStateChange={handleStateChange}
                  initialUiState={searchState}
                >
                  <Panel header={attribute}>
                    <RefinementList attribute={attribute} />
                  </Panel>
                </InstantSearch>
              )}
</InstantSearch>

working version here https://codesandbox.io/s/snowy-leftpad-cbln77?file=/src/App.tsx:954-1145

@Haroenv the hooks documentation should be updated with this example

@fatlinesofcode
Copy link

fatlinesofcode commented Sep 23, 2022

We've fixed this issue in our app with this wrapper component for a widget to prevent the unloading state behaviour

const WidgetUiStateProvider = ({
  indexName,
  children,
  searchClient,
  syncWithRootUiState = true,
}) => {
  const {
    uiState: rootUiState,
    setUiState: setRootUiState,
  } = useInstantSearch();

  const handleStateChange = ({ uiState, setUiState }) => {
    if (syncWithRootUiState) {
      setUiState(uiState);
      setRootUiState(uiState);
    }
  };

  return (
    <InstantSearch
      indexName={indexName}
      searchClient={searchClient}
      onStateChange={handleStateChange}
      initialUiState={rootUiState}
    >
      {children}
    </InstantSearch>
  );
};

it can be used like so

<InstantSearch searchClient={searchClient} indexName="instant_search">
          <Configure hitsPerPage={8} />
         <VirtualRefinementList attribute={attribute} />
...

{filterVisible && (
                <WidgetUiStateProvider
                  searchClient={searchClient}
                  indexName="instant_search"
                >
                  <Panel header={attribute}>
                    <RefinementList attribute={attribute} />
                  </Panel>
                </WidgetUiStateProvider>
              )}

...
</InstantSearch>

https://codesandbox.io/s/solitary-mountain-d47veh?file=/src/App.tsx:2708-3066

@brensch
Copy link

brensch commented Sep 30, 2022

This is the fiddliest thing I've dealt with today. Thankyou @fatlinesofcode for the hint, would not have figured this out without your help. A note on my struggles implementing it in my code:

  • You need to do the unmount in the same function containing the widgetuistateprovider (ie don't unmount in a custom refinementlist)
  • The virtualrefinementlist needs to be mounted always.

@CMLivingston
Copy link

To anyone who doesn't want the added complexity of maintaining search state and virtual widgets just to get a drawer working w/ React on mobile, Material UI drawer has the ability to hide without unmounting, so you can use regular refinement lists without any of the workarounds above. Funny enough, I love Chakra UI and my entire project uses it except for just this single drawer on mobile breakpoints 😝.

Material UI drawer: https://mui.com/material-ui/react-drawer/

<Drawer
  anchor={'left'}
  open={isDrawerIsOpen}
  onClose={handleClose}
  variant="temporary"
  ModalProps={{
    keepMounted: true,
  }}
>

@bline108
Copy link

bline108 commented Dec 1, 2022

I've ran into this same issue but a little different. First, I am only hiding the refinements. If a refinement has a transformItems function set, it loses its state on hide, none of my other refinements lose state. I have not tried the state provider example from @fatlinesofcode yet but will see if that works this weekend.

@bline108
Copy link

bline108 commented Dec 1, 2022

So I just had time to test this and the state handler @fatlinesofcode proposed does not solve the issue of RefinementList with transformItems function set losing it's state when hiding.
My transformItems is only setting the label like:

        <RefinementList
          transformItems={
            (items) => items.map((item) => ({
              ...item,
              label: RefinementTypeMap.get(item.label) || item.label
            }))
          }
          attribute="type" />

As soon as the refinements are hidden, it loses the selection however all my other refinements without transformItems do not.
Any suggestions appreciated.

@sarahdayan sarahdayan added the Library: React InstantSearch ≥ 7 Issues in any of the react-instantsearch@7 packages (formerly named react-instantsearch-hooks) label Dec 22, 2022
@sarahdayan sarahdayan transferred this issue from algolia/react-instantsearch Dec 22, 2022
@AntoineDuComptoirDesPharmacies
Copy link

AntoineDuComptoirDesPharmacies commented Feb 24, 2023

In the past, we were able to avoid clean on unMount by deleting the cleanUp function of the custom connector.

Is there any way, in react-instantsearch-hooks, with custom connectors to avoid cleaning process ?

We tried to override the dispose method without success, the state is still reset.

EDIT

Nevermind, we were wrong and successfuly reach this objective, overriding dispose method.

import { connectSortBy } from 'instantsearch.js/es/connectors';

const connectNoCleanupSortBy = (renderFn, unmountFn = () => {}) => {
  const connector = connectSortBy(renderFn, unmountFn);

  const overridenConnector = widgetParams => {
    const newInstance = connector(widgetParams);

    newInstance.dispose = () => {
      unmountFn();
    };

    return newInstance;
  };

  return overridenConnector;
};

@timhonders
Copy link

We have a similar issue with react-native an a Modal with refinements.

So i made a similar solution like @AntoineDuComptoirDesPharmacies for useRefinementList

import connectRefinementList from 'instantsearch.js/es/connectors/refinement-list/connectRefinementList.js';
import { useConnector } from 'react-instantsearch-hooks';

const useRefinementList = (props, additionalWidgetProperties) => {

    const overridenConnectRefinementList = (renderFn, unmountFn) => {
        const connector = connectRefinementList(renderFn, unmountFn);

        return (props) => {
            const newInstance = connector(props)
            newInstance.dispose = () => {
                unmountFn();
            };

            return newInstance;
        };
    }

    return useConnector(overridenConnectRefinementList, props, additionalWidgetProperties);
}

export default useRefinementList;

@Haroenv
Copy link
Contributor

Haroenv commented Aug 9, 2024

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Library: React InstantSearch ≥ 7 Issues in any of the react-instantsearch@7 packages (formerly named react-instantsearch-hooks)
Projects
None yet
Development

No branches or pull requests

10 participants