algolia / react-instantsearch

⚡️ Lightning-fast search for React and React Native applications, by Algolia.
https://www.algolia.com/doc/guides/building-search-ui/what-is-instantsearch/react/
MIT License
1.97k stars 386 forks source link

Question: how to detect and handle errors? (especially apiKey) #1144

Closed ArnaudRinquin closed 1 year ago

ArnaudRinquin commented 6 years ago

We are building an SPA which uses Algolia for search-related features. While we periodically renew users apiKey, cases might happen where the key is expired (user reopens his laptop and starts searching right away before any renewal can be made).

I've searched through the documentation but could not find a way to hook an onError callback or even detect when there are similar issues.

Does anyone have pointers?

Haroenv commented 6 years ago

A regular error should be thrown when API key is incorrect. In that case you can use an Error boundary around the <InstantSearch />

see https://reactjs.org/docs/error-boundaries.html

samouss commented 6 years ago

You can also use the connector connectStateResults to access the error.

ArnaudRinquin commented 6 years ago

@Haroenv I tried using React error boundaries with success:

class SearchContainer extends React.Component {
  componentDidCatch(error) {
    debugger
    // renew searchToken
  }
  render() {
    const { searchToken, searchState, searchApplicationId } = this.props
    if (!searchToken) {
      return false
    }

    const query = cleanSearchQuery(searchState.get('query'))
    return (
      <InstantSearch
        appId={searchApplicationId}
        apiKey="obviously-invalid" //{searchToken}
        indexName="notes"
        root={{
          Root: 'div',
          props: {
            className: 'flexbox flexbox-no-grow flexbox-no-shrink',
            style: { width: 300 },
          },
        }}
        searchState={{ query, hitsPerPage: HITS_PER_PAGE }}
      >
        <SearchPanel />
      </InstantSearch>
    )
  }
}

I can see the 403s in the debug tools but the debugger isn't stopping and no logs show in my console. This isn't so surprising as I guess these network errors are asynchronous and can't be caught by React (unless InstantSearch specifically rethrows them in a way React can catch).

I coupled it with @samouss solution and it worked but it feels clumsy:

class SearchContainer extends Component {
  componentDidCatch(error, info) {
     debugger
     // renew searchToken
  }

  render() {
    const { searchToken, searchState, searchApplicationId } = this.props
    if (!searchToken) {
      return false
    }

    const query = cleanSearchQuery(searchState.get('query'))
    return (
      <InstantSearch
        appId={searchApplicationId}
        apiKey="obviously-invalid" //{searchToken}
        indexName="notes"
        root={{
          Root: 'div',
          props: {
            className: 'flexbox flexbox-no-grow flexbox-no-shrink',
            style: { width: 300 },
          },
        }}
        searchState={{ query, hitsPerPage: HITS_PER_PAGE }}
      >
        <SearchErrorCatcher />
        <SearchPanel />
      </InstantSearch>
    )
  }
}

const SearchErrorCatcher = connectStateResults(
  class SearchErrorCatcher extends Component {
    error = null

    componentWillReceiveProps(nextProps) {
      if (this.error && !nextProps.error) {
        this.error = null
      }
      if (!this.error && nextProps.error) {
        // only throw on new errors
        throw nextProps.error
      }
    }
    render() {
      return null
    }
  }
)
samouss commented 6 years ago

@ArnaudRinquin Thanks for sharing your solution. At the end you don't even need to re-throw the error. You can call a function that will renew the apiKey directly from willReceiveProps.

ArnaudRinquin commented 6 years ago

Oh I know I don't have to, but it feels cleaner.

Anyway, It'd be nice to have a proper Error handling as this solution seems clunky. @Haroenv don't you think the Error Boundary not working is a bug and should be addressed? An onError prop on the InstantSearch could work as well.

samouss commented 6 years ago

The error boundaries are used to catch error related to the rendering. This one is not related to the rendering, you can recover it independently from React (see Error Boundaries).

You are right a prop onError could also work, I like the approach with the declarative SearchErrorCatcher. It act a bit like an error boundaries at the end, it catch the network error of InstantSearch. We could provide a widget out of the box it will feels a bit more natural.

henrikhertler commented 2 years ago

Thanks for the work @ArnaudRinquin

const SearchErrorCatcher = connectStateResults(
  class SearchErrorCatcher extends Component {
    error = null

    componentWillReceiveProps(nextProps) {
      if (this.error && !nextProps.error) {
        this.error = null
      }
      if (!this.error && nextProps.error) {
        // only throw on new errors
        throw nextProps.error
      }
    }
    render() {
      return null
    }
  }
)

If I try your approche I get an TypeError:

Unhandled Runtime Error
TypeError: this.props.contextValue.store.getState is not a function

Call Stack
Connector.getProvidedProps
node_modules/react-instantsearch-core/dist/es/core/createConnector.js (186:0)
new Connector
node_modules/react-instantsearch-core/dist/es/core/createConnector.js (63:0)
constructClassInstance
node_modules/react-dom/cjs/react-dom.development.js (12880:0)
updateClassComponent
node_modules/react-dom/cjs/react-dom.development.js (17100:0)
beginWork
node_modules/react-dom/cjs/react-dom.development.js (18620:0)
HTMLUnknownElement.callCallback
node_modules/react-dom/cjs/react-dom.development.js (188:0)
Object.invokeGuardedCallbackDev
node_modules/react-dom/cjs/react-dom.development.js (237:0)
invokeGuardedCallback
node_modules/react-dom/cjs/react-dom.development.js (292:0)
beginWork$1
node_modules/react-dom/cjs/react-dom.development.js (23203:0)
performUnitOfWork
node_modules/react-dom/cjs/react-dom.development.js (22154:0)
workLoopSync
node_modules/react-dom/cjs/react-dom.development.js (22130:0)
performSyncWorkOnRoot
node_modules/react-dom/cjs/react-dom.development.js (21756:0)
eval
node_modules/react-dom/cjs/react-dom.development.js (11089:0)
unstable_runWithPriority
node_modules/scheduler/cjs/scheduler.development.js (653:0)
runWithPriority$1
node_modules/react-dom/cjs/react-dom.development.js (11039:0)
flushSyncCallbackQueueImpl
node_modules/react-dom/cjs/react-dom.development.js (11084:0)
flushSyncCallbackQueue
node_modules/react-dom/cjs/react-dom.development.js (11072:0)
flushPassiveEffectsImpl
node_modules/react-dom/cjs/react-dom.development.js (22883:0)
unstable_runWithPriority
node_modules/scheduler/cjs/scheduler.development.js (653:0)
runWithPriority$1
node_modules/react-dom/cjs/react-dom.development.js (11039:0)
flushPassiveEffects
node_modules/react-dom/cjs/react-dom.development.js (22820:0)
eval
node_modules/react-dom/cjs/react-dom.development.js (22699:0)
workLoop
node_modules/scheduler/cjs/scheduler.development.js (597:0)
flushWork
node_modules/scheduler/cjs/scheduler.development.js (552:0)
MessagePort.performWorkUntilDeadline
node_modules/scheduler/cjs/scheduler.development.js (164:0)

Any idea how to fix this?

I'm currently using:

Haroenv commented 2 years ago

You can't mix React InstantSearch dom and React InstantSearch hooks web @henrikhertler, there should be a clean error thrown but I guess it's not visible because you used the root from React InstantSearch dom

carlreid commented 2 years ago

@Haroenv What would the equivalent hook for connectStateResults be then? I can't find anything that matches it on the upgrade guide, or no widget in the list that seems to have a state that contains error.

I also can't find a hook that exposes searching either. Would I have to useConnector to connectStateResults from react-instantsearch-core? The example on how to do this, shows connecting to a connector from instantsearch.js/es/connectors, but it seems there is no connectStateResults connector in the connectors directory over there?

Maybe you can see, I'm a bit lost!

dhayab commented 2 years ago

Hi @carlreid, to handle errors, please follow this guide. It will show you how to add a middleware to InstantSearch and retrieve the error information.

Regarding searching, there is no strict equivalent in React InstantSearch Hooks, but you can use isSearchStalled returned by the useSearchBox() Hook. It will set to true when no results are returned after 200ms by default. If necessary, you can fine tune this value with stalledSearchDelay on <InstantSearch>.

carlreid commented 2 years ago

@dhayab Thanks, I'll check it out. Completely missed this "Conditional Display" section when I CTRL + F for errors 😅

Stalled isn't enough in the case we wish to show a spinner while it's searching, so could be nice if it made a return in the future, though not critical to have for now.

FabienMotte commented 1 year ago

Hey!

We're doing a round of clean up before migrating this repository to the new InstantSearch monorepo.

I'm going to close this issue since a solution was provided by @dhayab. We now have a Status API returned by the useInstantSearch() hook that could be useful for your use-case.