Bindings for Algolia connectors (Higher order components)

I can’t figure out how to create bindings for a couple Algolia connectors, which are higher order components.

For example there’s the connectStateResults which let’s you access the search state.

I’ve used this in typescript like so:

import { StateResultsProvided } from "react-instantsearch-core"
import { connectStateResults } from "react-instantsearch-dom"

interface Props extends StateResultsProvided {
  expectedNumberOfHits: number
}

const StateResults: React.FC<Props> = ({
  children,
  searching,
  isSearchStalled,
  searchState,
  searchResults,
  expectedNumberOfHits,
}) => {
  ...content omitted
}

export const AlgoliaLoadingWrapper = connectStateResults(StateResults)

How do I achieve something similar in ReScript where I have my own component which takes a prop, in this case expectedNumberOfHits, and that gets added to the props from StateResultsProvived.

I’ve tried a few different approached without luck. My current iteration looks like this:

@module("react-instantsearch-dom")
external connectStateResults: React.element => React.element = "connectStateResults"

type refinementList = {key: array<string>}
type searchState = {query: option<string>, refinementList: option<Js.Dict.t<string>>}
type searchResults = {hits: option<array<Algolia.provision>>}

@react.component
let make = (
  ~children,
  ~expectedNumberOfHits,
  ~searching,
  ~isSearchStalled,
  ~searchState,
  ~searchResults,
) =>
  {
    ...content omitted
  }->connectStateResults

The main problem is understanding how I can create a component where I provide children and expectedNumberOfHits my self, but still have access to the other props provided by the wrapper connectStateResults

I’ve come a bit further by giving this type to my binding and setting make to my component sent through the binding:

@module("react-instantsearch-dom")
external connectHits: React.component<{
  "hits": option<array<Komplio.Algolia.provision>>,
  "provision": option<int>,
  "regulation_fragmentRefs": RescriptRelay.fragmentRefs<[> #ProvisionList_regulation]>,
  "user_data_fragmentRefs": RescriptRelay.fragmentRefs<[> #ProvisionList_user_preferences]>,
}> => React.element = "connectHits"

let make = provisionList->connectHits

And this works I can see that I get the right code in the generated file.
But then I now get an error at the call site and I’m unsure how to proceed:

(React.component<'props>, 'props) => React.element
This value might need to be wrapped in a function that takes an extra
parameter of type 'a

Here's the original error message
This has type: React.element
Somewhere wanted: React.component<'a> (defined as 'a => React.element)
ReScript

I got a start to something that works by using generic types on the mapping.

@react.component
let provisionList = (
  ~provision,
  ~regulation_fragmentRefs,
  ~user_data_fragmentRefs,
  ~hits: option<array<Algolia.provision>>=?,
) => {
  ... implementation details omitted
}

@module("react-instantsearch-dom")
external connectHits: 'a => 'a = "connectHits"

@react.component
let make = (~provision, ~regulation_fragmentRefs, ~user_data_fragmentRefs) =>
  {
    "hits": Some([]),
    "provision": provision,
    "regulation_fragmentRefs": regulation_fragmentRefs,
    "user_data_fragmentRefs": user_data_fragmentRefs,
  }
  |> provisionList->connectHits

This seems to work so far :blush:

Hmmm, also need to bind to a HOC, specifically the Auth0 withAuthenticationRequired function. Have you found any way to do this that doesn’t require generic types? Seems like there should be an idiomatic way to do this :grinning_face_with_smiling_eyes:

Still using this approach and it works quite nicely :blush:

1 Like

Why don’t you use the hooks api?

I would love to! :grinning_face_with_smiling_eyes: So do you mean:

  1. Bind to the HOC function using hooks? (I don’t know how this would work) OR
  2. Using Auth0’s hook to build my own withAuthenticationRequired component with the loginWithRedirect & isAuthenticated options? (which is what I was going to do).

They don’t expose withAuthenticationRequired from the hook, but the functionality can easily be recreated :slight_smile:

Yes please do that. The time spent on HoC could easily be used to build the idiomatic solution with hooks instead :grinning_face_with_smiling_eyes:

1 Like

Great idea @ryyppy :bulb:

In our app I’ve built a component around the hooks guide from Auth0 that looks like this:

// Auth0ProviderWithHistory.res

open Auth

module Auth0Provider = {
  @react.component @module("@auth0/auth0-react")
  external make: (
    ~domain: string,
    ~clientId: string,
    ~redirectUri: string,
    ~audience: string,
    ~onRedirectCallback: option<logoutType> => unit,
    ~useRefreshTokens: bool,
    ~cacheLocation: string,
    ~children: React.element,
  ) => React.element = "Auth0Provider"
}

@react.component
let make = (~children) => {
  let {isTesting} = EnvInfo.use()

  let onRedirectCallback = appState => {
    switch appState {
    | Some(appState) => RescriptReactRouter.push(appState.returnTo)
    | None => RescriptReactRouter.push(Window.pathname)
    }
  }

  <Auth0Provider
    domain=Env.auth0ClientDomain
    clientId=Env.auth0ClientId
    redirectUri=Window.origin
    audience=Env.auth0Audience
    onRedirectCallback={onRedirectCallback}
    useRefreshTokens=true
    cacheLocation={isTesting ? "localstorage" : "memory"}>
    {children}
  </Auth0Provider>
}

And with a binding file and some helper types defined in Auth.res (I’ve removed our specific user types and replaced all those with …, so just replace those with your own needs):

// Auth.res

type user = {
  ...
}

type claims = {
  ...
}
type auth0user = {
  "email": string,
  "name": string,
  "nickname": string,
  "picture": string,
  ...
}
type logoutType = {returnTo: string}
type auth0type = {
  user: option<auth0user>,
  getAccessTokenSilently: unit => Js.Promise.t<string>,
  isAuthenticated: bool,
  isLoading: bool,
  loginWithRedirect: unit => unit,
  logout: logoutType => unit,
}
%%private(@module("@auth0/auth0-react") external useAuth0: unit => auth0type = "useAuth0")

type auth = {
  user: option<user>,
  isLoading: bool,
  loginWithRedirect: unit => unit,
  logout: logoutType => unit,
  isAuthenticated: bool,
  getAccessTokenSilently: unit => Js.Promise.t<string>,
}

let use = () => {
  let {
    user: auth0user,
    isLoading,
    loginWithRedirect,
    logout,
    isAuthenticated,
    getAccessTokenSilently,
  } = useAuth0()

  switch (isLoading, isAuthenticated, auth0user) {
  | (false, true, Some(user)) => {
      {
        user: Some({...}),
        isLoading: isLoading,
        loginWithRedirect: loginWithRedirect,
        logout: logout,
        isAuthenticated: isAuthenticated,
        getAccessTokenSilently: getAccessTokenSilently,
      }
    }

  | _ => {
      user: None,
      isAuthenticated: isAuthenticated,
      isLoading: isLoading,
      loginWithRedirect: loginWithRedirect,
      logout: logout,
      getAccessTokenSilently: getAccessTokenSilently,
    }
  }
}

This has worked out ok for us so far.

3 Likes