javascriptreactjsreduxredux-saga

Calling navigator.perrmissions.query from a generator


I am working with Redux Saga to perform actions like setting authorization and permissions once a user has logged in or has resumed a session. However, it appears that calling navigator.permissions.query from within the saga generator function causes an "illegal invocation" error to occur:

function* getUserLocation(){
  const result = yield call(navigator.permissions.query, { name: "geolocation" });
  switch (result.state) {
    // ...
  }
}

From what I've found on the web, this appears to be due to the context no longer being what is expected when the generator runs. I've considered binding the query function, however I am unsure what to bind it to. Is there possibly a solution to this other than simply calling the query function outside of a generator?

Thanks in advance.


Solution

  • You're losing the context (this) required by the navigator.permissions.query fn since redux-sagas call() only has the function reference here.

    A slight oddity in JS is that if you pass/store a reference to a function that lives inside some object, then call it (which call() does internally), it may not work if that function relies on this as that will actually evaluate to window instead of what was intended, which would be whatever navigator.permissions is.

    When you do a plain call like navigator.permissions.query(), that is what this is inside the query function (navigator.permissions). It is derived from the parent object as presented in the statement that executes the call itself.

    The generator isn't the root cause here. This code fails too with the same error:

    const queryFn = navigator.permissions.query
    queryfn()
    

    It's the nature of how the function is being passed as a direct reference and then called (in this case, internally by redux-saga). The actual call pattern itself matters in JS.

    This is often really quite surprising when first experienced. See this answer. It's a common and misunderstood "gotcha" in JS.

    You mentioned binding which is basically the answer, though after some reading of the docs, redux saga provides its own sugar around the core JS APIs. You could try:

    const result = yield call({ context: navigator.permissions, fn: navigator.permissions.query }, { name: "geolocation" });
    

    Though my immediate thought before looking at the docs was to do:

    const result = yield call(() => navigator.permissions.query({ name: "geolocation" }));
    

    Which is an ergonomic shortcut to achieve the same thing and a common approach in modern code. The call, bind and apply standard methods (and saga's functions on top of them) that were once very commonplace in situations like this are perhaps somewhat less common now since the above arrow syntax goes some way to replace many of their use cases with minimal/reduced syntactic overhead. It is not identical when considering the low-level execution path since you create a new anonymous/inlined wrapper fn here, but in this use case, the differences are likely irrelevant.

    Possibly, redux-saga has some debugging stuff that would have reduced granularity when doing this (redux-saga can't see the args to the query function), but that's something I have not explored. If so, you can go with the first snippet.