redux-observable you provided 'undefined' where a stream was expected
Asked Answered
M

4

14

I'm using the fbsdk to get user details in an ajax request. So it makes sense to do this in a redux-observable epic. The way the fbsdk request goes, it doesn't have a .map() and .catch() it takes the success and failure callbacks:

code:

export const fetchUserDetailsEpic: Epic<*, *, *> = (
  action$: ActionsObservable<*>,
  store
): Observable<CategoryAction> =>
  action$.ofType(FETCH_USER_DETAILS).mergeMap(() => {
    getDetails(store)
  })

const getDetails = store => {
  console.log(store)
  let req = new GraphRequest(
    '/me',
    {
      httpMethod: 'GET',
      version: 'v2.5',
      parameters: {
        fields: {
          string: 'email,first_name,last_name'
        }
      }
    },
    (err, res) => {
      if (err) {
        store.dispatch(fetchUserDetailsRejected(err))
      } else {
        store.dispatch(fetchUserDetailsFulfilled(res))
      }
    }
  )

  return new GraphRequestManager().addRequest(req).start()
}

It gives the error:

TypeError: You provided 'undefined' where a stream was expected. You can provide an Observable, Promise, Array, or Iterable.

How do I return an observable from the epic so this error goes away?

Attempt at bindCallback from this SO answer:

const getDetails = (callBack, details) => {
  let req = new GraphRequest(
    '/me',
    {
      httpMethod: 'GET',
      version: 'v2.5',
      parameters: {
        fields: {
          string: 'email,first_name,last_name'
        }
      }
    },
    callBack(details)
  )

  new GraphRequestManager().addRequest(req).start()
}

const someFunction = (options, cb) => {
  if (typeof options === 'function') {
    cb = options
    options = null
  }
  getDetails(cb, null)
}

const getDetailsObservable = Observable.bindCallback(someFunction)

export const fetchUserDetailsEpic: Epic<*, *, *> = (
  action$: ActionsObservable<*>
): Observable<CategoryAction> =>
  action$.ofType(FETCH_USER_DETAILS).mergeMap(() => {
    getDetailsObservable()
      .mergeMap(details => {
        return Observable.of(fetchUserDetailsFulfilled(details))
      })
      .catch(error => Observable.of(fetchUserDetailsRejected(error)))
  })

Getting the same error

Miyamoto answered 3/10, 2018 at 4:40 Comment(0)
A
4

Looking into source code of GraphRequestManager .start:

start(timeout: ?number) {
  const that = this;
  const callback = (error, result, response) => {
    if (response) {
      that.requestCallbacks.forEach((innerCallback, index, array) => {
        if (innerCallback) {
          innerCallback(response[index][0], response[index][1]);
        }
      });
    }
    if (that.batchCallback) {
      that.batchCallback(error, result);
    }
  };

  NativeGraphRequestManager.start(this.requestBatch, timeout || 0, callback);
}

As you can see it does return nothing, so effectively undefined. Rx mergeMap requires an instance of Observable or something compatible with it (more info).

Since you dispatch further actions, you can modify your original code like that:

export const fetchUserDetailsEpic: Epic<*, *, *> = (
  action$: ActionsObservable<*>,
  store
): Observable<CategoryAction> =>
  action$.ofType(FETCH_USER_DETAILS).do(() => { // .mergeMap changed to .do
    getDetails(store)
  })

const getDetails = store => {
  console.log(store)
  let req = new GraphRequest(
    '/me',
    {
      httpMethod: 'GET',
      version: 'v2.5',
      parameters: {
        fields: {
          string: 'email,first_name,last_name'
        }
      }
    },
    (err, res) => {
      if (err) {
        store.dispatch(fetchUserDetailsRejected(err))
      } else {
        store.dispatch(fetchUserDetailsFulfilled(res))
      }
    }
  )

  return new GraphRequestManager().addRequest(req).start()
}

To be honest I find your second attempt bit better / less coupled. To make it working you could do something like:

const getDetails = Observable.create((observer) => {
  let req = new GraphRequest(
    '/me',
    {
      httpMethod: 'GET',
      version: 'v2.5',
      parameters: {
        fields: {
          string: 'email,first_name,last_name'
        }
      }
    },
    (error, details) => {
      if (error) {
        observer.error(error)
      } else {
        observer.next(details)
        observer.complete()
      }
    }
  )

  new GraphRequestManager().addRequest(req).start()
})

export const fetchUserDetailsEpic: Epic<*, *, *> = (
  action$: ActionsObservable<*>
): Observable<CategoryAction> =>
  action$.ofType(FETCH_USER_DETAILS).mergeMap(() => {
    getDetails()
      .map(details => fetchUserDetailsFulfilled(details)) // regular .map should be enough here
      .catch(error => Observable.of(fetchUserDetailsRejected(error)))
  })
Amylo answered 7/10, 2018 at 18:34 Comment(0)
W
2

I don't remember well how was working redux-observable before using RxJS >= 6 but I'll try to help ;)

First, you don't need to dispatch yourself, redux-observable will do it for you. In this article, they show how it works under the hood, so they call dispatch, but you don't have to. In the new implementation, they removed store as a second argument in favor of a state stream:

const epic = (action$, store) => { ... //before
const epic = (action$, state$) => { ... //after

But most importantly, the problem you experience is that you don't return a stream of actions, but a single (dispatched) action. From their website:

It is a function which takes a stream of actions and returns a stream of actions.

So I think a quick solution would be to return observables from your callback:

(err, res) => {
  if (err) {
    return Observable.of(fetchUserDetailsRejected(err))
  }
  return Observable.of(fetchUserDetailsFulfilled(res))
}

I will update the answer based on your comments. Good luck!

Wreak answered 3/10, 2018 at 6:16 Comment(2)
Thanks I get the same error I think because it takes a long time to get to the code which returns those observables and it races off and returns undefined straight awayMiyamoto
It should not be linked to the time it takes to fullfill the request. I usually use the rxjs ajax to make my API calls (or before transformed Axios promises with fromPromise), so I have never dealt with callbacks. Have you checked this SO question? It seems related to your problem.Wreak
C
2

I beleive this seems the possible reason for undefined. You are returning undefined in mergeMap callback.

This

action$.ofType(FETCH_USER_DETAILS).mergeMap(() => {
    getDetails(store)
})

should be either

action$.ofType(FETCH_USER_DETAILS).mergeMap(() => getDetails(store))

or

action$.ofType(FETCH_USER_DETAILS).mergeMap(() => {
    return getDetails(store)
})
Confessional answered 5/10, 2018 at 18:53 Comment(1)
That callback is returning undefined. Anyway, I understand the problem now. Try wrapping getDetails function in a Promise, resolve/reject it when you get the response.Confessional
Z
2

It looks like @artur grzesiak has a correct answer, but for completeness this is how I think bindCallback can be used.

The only issue I have with Artur's answer is I don't think we need to catch the error in the epic, since fetchUserDetailsRejected is an error-handling action (presumably the reducer deals with it appropriately).

I used this reference RxJs Static Public Methods: bindCallback

Give it a function f of type f(x, callback) and it will return a function g that when called as g(x) will output an Observable.

// This callback receives the results and returns one or other action (non-observable)
const callback = (err, res) => {
  return err 
    ? fetchUserDetailsRejected(err)
    : fetchUserDetailsFulfilled(res)
}

// Here is the graph request uncluttered by concerns about the epic
const getDetails = (store, callback) => {
  console.log(store)
  let req = new GraphRequest(
    '/me',
    {
      httpMethod: 'GET',
      version: 'v2.5',
      parameters: {
        fields: {
          string: 'email,first_name,last_name'
        }
      }
    },
    callback
  )
  new GraphRequestManager().addRequest(req).start()
}

// This bound function wraps the action returned from callback in an Observable
const getDetails$ = Observable.bindCallback(getDetails).take(1)

// The epic definition using bound callback to return an Observable action
export const fetchUserDetailsEpic: Epic<*, *, *> = 
  (action$: ActionsObservable<*>, store): Observable<CategoryAction> =>
    action$.ofType(FETCH_USER_DETAILS).mergeMap(() => getDetails$(store))
Zn answered 12/10, 2018 at 10:59 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.