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

how to use multiple instant search client in the same form? #194

Open
weiklr opened this issue Feb 5, 2024 · 5 comments
Open

how to use multiple instant search client in the same form? #194

weiklr opened this issue Feb 5, 2024 · 5 comments

Comments

@weiklr
Copy link

weiklr commented Feb 5, 2024

Description

Hi, we were wondering how do we have multiple instant search clients (Typesense Wrapper int he screenshots) co-existing and working in the same form.

One instant search client points to an employee index in the participants field. Another one only instantiates in the modal and is pointing to the receipts collection.
We found that the the 'query_by' in the employee index gets overriden by the receipts server adapter options when the modal is activated and then closed.

both instantsearch clients uses their own server adpater options object. How do we prevent overrides like this?

image (1)

initial correct payload:
image (4)

After modal is activated and closed, we saw this console error:
image (2)

employee index payload after modal is open and closed:
Notice how filename is in query_by but this shouldn't be the case, and should be the same as initial correct payload
image (3)

@jasonbosco
Copy link
Member

jasonbosco commented Feb 5, 2024

If these are separate InstantSearch instances, then you want to use separate typesense-instantsearch-adapter instances with their own settings as well, so there's no interaction between these two instances.

If these are the same InstantSearch instances, then you you want to use this mechanism to specify settings for each collection using collectionSpecificSearchParameters: https://github.com/typesense/typesense-instantsearch-adapter?tab=readme-ov-file#index

@weiklr
Copy link
Author

weiklr commented Feb 6, 2024

hmm i think there are considered separate instantsearch instances. We created a which contains an InstantSearch react component for each of these components.

sample code:

TypesenseWrapper:
const fullTypesenseAdapterOptions = generateTypesenseInstantsearchAdapterOptions();
const typesenseInstantSearchAdapter = new TypesenseInstantsearchAdapter(fullTypesenseAdapterOptions);
const searchClient = typesenseInstantSearchAdapter.searchClient;

const TypesenseWrapper = () => {
 /** THIN LAYER AROUND INSTANTSEARCH **/
      <InstantSearch
        searchClient={searchClient}
        indexName={indexName}
        {...instantSearchProps}
        future={{ preserveSharedStateOnUnmount: true }}
      >
        <Error />
        {children}
      </InstantSearch>
}

generateTypesenseInstantsearchAdapterOptions - function that returns a new typesense instance search adapter object:

const generateTypesenseInstantsearchAdapterOptions = (): TypesenseInstantsearchAdapterOptions => {
  // Search settings for full search
  return {
    server: {
      apiKey: '',
      nodes: [
        {
          host: ApiSettings.HOST,
          port: ApiSettings.PORT,
          protocol: ApiSettings.PROTOCOL,
        },
      ],
    },
    additionalSearchParameters: {
      query_by: 'q',
      facet_by: '',
      sort_by: '',
    },
  };
};

do you know how do i check if a typesense instantsearch adapter is its own instance or a shared one? cos from this code it seems like it should always create a new instance everytime.

@jasonbosco
Copy link
Member

To create a separate typesense-instantsearch-adapter instance, you want to instantiate two separate instances of the class.

const typesenseInstantSearchAdapterEmployees = new TypesenseInstantsearchAdapter(typesenseAdapterOptionsForEmployees);

const typesenseInstantSearchAdapterReceipts = new TypesenseInstantsearchAdapter(typesenseAdapterOptionsForReceipts);

And then in each Instantsearch instance:

<InstantSearch
        searchClient={typesenseInstantSearchAdapterEmployees.searchClient}
        indexName={'employees'}
        {...instantSearchProps}
        future={{ preserveSharedStateOnUnmount: true }}
      >
        <Error />
        {children}
      </InstantSearch>


<InstantSearch
        searchClient={typesenseInstantSearchAdapterReceipts.searchClient}
        indexName={'receipts'}
        {...instantSearchProps}
        future={{ preserveSharedStateOnUnmount: true }}
      >
        <Error />
        {children}
      </InstantSearch>

@weiklr
Copy link
Author

weiklr commented Feb 7, 2024

Hi Jason,
Thanks, we made our TypesenseWrapper such that it now generates new typesenseInstantSearchAdapter instances using useMemo ,which should work in the same principle as your example.

However, we are also using onStateChange instantSearch props to manipulate the uiState and noticed the receipt typesense wrapper instance still references employee index even though they should be 2 separate instances already.

here's the typesensewrapper code for your reference:

const TypesenseWrapper2: React.FC<ITypesenseWrapper> = (props) => {
  const { children, onStateChange, initialUiState, typesenseAdapterOptions, scopedKeyUrl } = props;


  // created to create stable reference for useEffects
  const { data: typesenseServerConfig } = useQuery({
    queryKey: ['typesenseServerConfig'],
    queryFn: async () => await getTypesenseServerConfig(scopedKeyUrl),
    refetchOnWindowFocus: false,
  });

  const typesenseInstantsearchAdapter: TypesenseInstantsearchAdapter | null =
    useMemo((): TypesenseInstantsearchAdapter | null => {
      const {
        key: apiKey,
        index,
        host,
        port,
        protocol,
      } = typesenseServerConfig ?? {
        key: '',
        host: 'HOST',
        port: 1234,
        protocol: 'https'
      };

      if (!apiKey) return null;
      
      return new TypesenseInstantsearchAdapter({
        server: {
          apiKey,
          nodes: [{ host, port, protocol }],
        },
        additionalSearchParameters: { ...typesenseAdapterOptions.additionalSearchParameters },
        collectionSpecificSearchParameters: { ...typesenseAdapterOptions.collectionSpecificSearchParameters },
      });
    }, [typesenseServerConfig, typesenseAdapterOptions]);

  // Get the index name for instant search
  const instantSearchIndexName = useMemo(() => typesenseServerConfig?.index ?? '', [typesenseServerConfig]);

  const genInitialUiState = {
    [instantSearchIndexName]: { ...initialUiState },
  } as unknown as UiState;
  // Create a instant search client

  const instantSearchClient = useMemo(
    () => typesenseInstantsearchAdapter?.searchClient,
    [typesenseInstantsearchAdapter]
  );

  // Function that runs when users close idle modal and continue to use the page

  const instantSearchOnStateChange: InstantSearchProps['onStateChange'] = ({ uiState, setUiState }) => {
    if (!onStateChange) {
      return;
    }
    console.log('instant index:', instantSearchIndexName);
    onStateChange({ uiState, setUiState }, instantSearchIndexName);
  };

  const instantSearchProps = { onStateChange: instantSearchOnStateChange, initialUiState: genInitialUiState };
  if (!typesenseInstantsearchAdapter || !instantSearchClient) {
    return null;
  }

  return (
    <>
      <InstantSearch
        searchClient={instantSearchClient}
        indexName={instantSearchIndexName}
        {...instantSearchProps}
        future={{ preserveSharedStateOnUnmount: true }}
      >
        <Error />
        {children}
      </InstantSearch>
)

how we use this wrapper -
receipts typesensewrapper

<TypesenseWrapper2
            scopedKeyUrl={SCOPED_KEY_URL}
            typesenseAdapterOptions={getTypesenseAdapterOptions()}
            onStateChange={onTypesenseStateChange2}
          >
            <AddReceiptModal isOpen onToggleModal={handleToggleReceiptModal} onUpdateReceipts={handleUpdateReceipts} />
</TypesenseWrapper2>

employee typesensewrapper as hoc. note that both are using different typesenseAdapterOptions object already.

export const withEmpListData = <T,>(Component: React.ComponentType<T>): React.FC<T> => {
  const WrappedComponent: React.FC<any> = (props) => (
    <TypesenseWrapper2
      scopedKeyUrl={SCOPED_KEY_URL}
      typesenseAdapterOptions={typesenseAdapterOptions}
      onStateChange={onTypesenseStateChange}
    >
      <Component {...props} />
    </TypesenseWrapper2>
  );

  WrappedComponent.displayName = `WithEmpListData(${Component.displayName ?? Component.name})`;

  return WrappedComponent;
};

now the issue is when i click on receipts modal, i still see that it's referencing employee index when it should be referencing receipt index when onStateChange is invoked. on subsequent clicks it behaves correctly. screenshot as follows:

image

@jasonbosco
Copy link
Member

I'm not too familiar with React, so I can't speak to the use of useMemo. But it sounds like the instantsearch-adapter instances are still being shared somehow...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants