Search code examples
react-nativereact-reduxredux-observablereact-native-fbsdk

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


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


Solution

  • 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)))
      })