Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Subscribe on Epic #90

Closed
bsideup opened this issue Aug 3, 2016 · 12 comments
Closed

Subscribe on Epic #90

bsideup opened this issue Aug 3, 2016 · 12 comments

Comments

@bsideup
Copy link

bsideup commented Aug 3, 2016

Hi!

Since trunkservables are deprecated, how one can dispatch and subscribe?

i.e. in onEnter() of react-router I want to load some data asynchronously with redux-observable and then call callback to load the view (i.e. if user is authorized)

It's a bit unclear how to wait until Epic is completed (it was easy with trunkservables because they were returning an Observable)

Thanks!

@jayphelps
Copy link
Member

With Epics, this probably an anti-pattern. non-trivial side effects would belong in your Epics but obviously react-router was not designed with redux in mind so there probably isn't an ideal solution.

That said, it's possible to do this:

onEnter(_, _, callback) {
  const request = new Promise((resolve, reject) => {
    dispatch(({
      type: FETCH_SOMETHING
      meta: { resolve, reject }
    }))
  });

  request.then(() => {
    callback();
  });
}
const fetchSomethingEpic = action$ =>
  action$.ofType(FETCH_SOMETHING)
    .mergeMap(action =>
      api.fetchSomething() // whatever your API code is
        // resolve/reject the promise
        .do({
          next: action.meta.resolve,
          error: action.meta.reject
        })
        .map(fetchSomethingFulfilled)
        .catch(error => fetchSomethingRejected(error.xhr.message)) // whatever your error handling
    );

I would use this sort of approach very sparingly.

There may be other patterns that emerge later.

@jayphelps
Copy link
Member

One could write a middleware to handle this sort of thing magically.

Untested, but something like this:

const actionLifecycles = store => next => {
  const pending = {};

  return action => {
    let ret;

    if (action.meta && action.meta.lifecycle) {
      ret = new Promise((resolve, reject) => {
        const { lifecycle } = action.meta.lifecycle;
        pending[lifecycle.resolve] = resolve;
        pending[lifecycle.reject] = reject;
      });
    } else {
      ret = next(action);
    }

    if (pending[action.type]) {
      const resolveOrReject = pending[action.type];
      resolveOrReject(action);
    }

    return ret;
  };
};

Where you define what actions to listen for, to resolve or reject a promise.

const fetchSomething = () => ({
  type: FETCH_SOMETHING,
  meta: {
    lifecycle: {
      resolve: FETCH_SOMETHING_FULFILLED,
      reject: FETCH_SOMETHING_REJECTED
    }
  }
});

Allows you to use it like this:

dispatch(fetchSomething()).then(action => {
  // success
}, action => {
  // failure
});

Where get more confusing is what about two requests of the same action concurrently? What about cancellation?

@jayphelps
Copy link
Member

jayphelps commented Aug 4, 2016

One could write a middleware to handle this sort of thing magically.

Untested, but something like this:

const actionLifecyclesMiddleware = store => next => {
  const pending = {};

  return action => {
    let ret;

    if (action.meta && action.meta.lifecycle) {
      ret = new Promise((resolve, reject) => {
        const { lifecycle } = action.meta.lifecycle;
        pending[lifecycle.resolve] = resolve;
        pending[lifecycle.reject] = reject;
      });
      next(action);
    } else {
      ret = next(action);
    }

    if (pending[action.type]) {
      const resolveOrReject = pending[action.type];
      delete pending[action.type];
      resolveOrReject(action);
    }

    return ret;
  };
};

Where you define what actions to listen for, to resolve or reject a promise.

const fetchSomething = () => ({
  type: FETCH_SOMETHING,
  meta: {
    lifecycle: {
      resolve: FETCH_SOMETHING_FULFILLED,
      reject: FETCH_SOMETHING_REJECTED
    }
  }
});

Allows you to use it like this:

dispatch(fetchSomething()).then(action => {
  // success
}, action => {
  // failure
});

Where it gets more confusing is what about two requests of the same action concurrently..and what about cancellation?

@jayphelps
Copy link
Member

I believe the question has been answered so going to close due to inactivity but absolutely feel free add any other questions here or ideally on Stack Overflow for SEO 💃

@jfrolich
Copy link

Thanks @jayphelps. Very handy for implementing libraries that expect a promise. For simple uses cases this should work great.

@jiayihu
Copy link
Contributor

jiayihu commented Mar 9, 2017

I had the same issue and reimplemented @jayphelps's solution with some Typescript typings and it works like charm:

import { Middleware } from 'redux';

export interface IActionLifecycle {
  resolveType: string;
  rejectType: string;
}

/**
 * Middleware which allows to chain async actions as Promises.
 * @see https://github.com/redux-observable/redux-observable/issues/90
 * @example
 * const fetchSomething = () => ({
 *  type: FETCH_SOMETHING,
 *  meta: {
 *    lifecycle: {
 *      resolve: FETCH_SOMETHING_FULFILLED,
 *      reject: FETCH_SOMETHING_REJECTED
 *    }
 *  }
 * });
 *
 * Then you can use the action as following:
 * fetchSomething().then(action => doSomething(action))
 */
const middleware: Middleware = (store) => (next) => {
  const pending: { [key: string]: Function } = {};

  return (action: IAction) => {
    let returned;

    if (action.meta && action.meta.lifecycle) {
      returned = new Promise((resolve, reject) => {
        const lifecycle: IActionLifecycle = action.meta.lifecycle;
        const pendingResolves = pending[lifecycle.resolveType];
        const pendingRejections = pending[lifecycle.rejectType];

        pending[lifecycle.resolveType] = resolve;
        pending[lifecycle.rejectType] = reject;
      });
      next(action);
    } else {
      returned = next(action);
    }

    // This part is called later when the success/error action is dispatched
    if (pending[action.type]) {
      const actionCallback = pending[action.type];
      delete pending[action.type];
      actionCallback(action);
    }

    return returned;
  };
};

export default middleware;

@ShineOfFire
Copy link

ShineOfFire commented Apr 9, 2018

That is perfect for my project. Thank you i'll create a repository for help anyone want use it easily.

For my solution i create middleware like in this issues :

/**
  * Create Middleware
**/
const observableToPromise = store => next => {
  let pending = {}
  return action => {
    let ret = next(action)
    if (action.meta && action.meta.lifecycle) {
      ret = new Promise((resolve, reject) => {
        pending[action.meta.lifecycle.resolve] = resolve
        pending[action.meta.lifecycle.reject] = reject
      })
      next(action)
    }

    // Success/Error action is dispatched
    if (pending[action.type]) {
      const resolveOrReject = pending[action.type]
      delete pending[action.type]
      resolveOrReject(action)
    }
    return ret
  }
}

export default observableToPromise


/**
  * On configureStore dev and prod
**/

function configureStore (baseHistory) {
  const routingMiddleware = routerMiddleware(baseHistory)
  const middleware = applyMiddleware(
   ...,
    observableToPromise // Add your observable here
  )

/**
  * Action Creator for fetching
**/

export function actionRequest (arg) {
  return {
    type: ACTION_LOADING,
    payload: arg || {},
    meta: {
      lifecycle: {
        resolve: ACTION_SUCCESS,
        reject: ACTION_FAILED
      }
    }
  }
}

export function actionSuccess (data) {
  return {
    type: ACTION_SUCCESS,
    payload: {
      results: data
    }
  }
}

export function actionFailed (error) {
  return {
    type: ACTION_FAILED,
    payload: new Error('fetch:' + ACTION_FAILED, error),
    error: error.message
  }
}

/**
  * Epic Observable
**/

import { Observable } from 'rxjs/Observable'
import { ACTION_LOADING } from '../constants/ActionTypes'

import {
  actionSuccess,
  actionFailed
} from '../actions/action'
import { apiUri } from '../../../config/app.config'
import { $http } from '../services/http'

export default function fetchAction (action$) {
  return action$.ofType(ACTION_LOADING)
    .mergeMap(action => Observable
      .from(
        $http.get(apiUri)
      )
      .map(
        data => actionSuccess(data),
        error => actionFailed(error)
      )
      .catch(error => Observable.of(actionFailed(error)))
    )
}

Like this i can use promise on my router react and chains my promises, workly perfectly.

@cannalee90
Copy link

@jayphelps

const { lifecycle } = action.meta.lifecycle;

doesn't it occur error? I think it should be

const { lifecycle } = action.meta

or do I misunderstand something?

@ShineOfFire
Copy link

ShineOfFire commented Apr 12, 2018

@cannalee90 yeah that it not possible for this case.
if you need i resolve that with my code :p just above

@cannalee90
Copy link

@ShineOfFire you mean am I wrong?

I just want to know I understand this issue correctly because like you said, I want to chains my promises like this:

    this.props.fetchGistAll()
    .then(() => {
        this.props.history.push('/');
    })

@ShineOfFire
Copy link

ShineOfFire commented Apr 12, 2018

@cannalee90 it work just your props fetchGistAll is surrounded by a dispatch show me your code

cannalee90 added a commit to cannalee90/flash-card that referenced this issue Apr 13, 2018
redux-observable에서 비동기 액션을 수행할 경우 특정 액션이 언제 끝나는지 모름.
간단한 방법으론 callback 함수를 같이 넣어주는 방식이 있는데, 코드가 지저분해진다.

redux-observable/redux-observable#90 를 참조해서 작성했다.
다음에는 kesakiyo 버젼으로 적용해볼 예정
@ShineOfFire
Copy link

I was find problem in my code, the middleware is called 2 times if action is not a promise

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants