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

Saga -> Component communication (aka sagas and setState) #907

Closed
brettstack opened this issue Apr 12, 2017 · 57 comments
Closed

Saga -> Component communication (aka sagas and setState) #907

brettstack opened this issue Apr 12, 2017 · 57 comments

Comments

@brettstack
Copy link

brettstack commented Apr 12, 2017

I want to set form loading state (spinner icon, disable input) when the user submits the form, and clear that state when the action completes or fails. Currently I am storing this state in my store, which involves creating an action creator and reducer, which is annoying for a few reasons:

  1. It's a lot more work than simply calling this.setState
  2. It's more difficult to reason about
  3. I don't really want to store local component state in my store

Essentially I want to do this from within my form component:

handleFormSubmit() {
  this.setState({ isSaving: true })
  this.props.dispatchSaveForm({ formData: this.props.formData })
    .finally(() => this.setState({ isSaving: false }))
}
@brettstack
Copy link
Author

I haven't been able to find any discussion on this, but conceivably, I could do this:

handleFormSubmit() {
  this.setState({ isSaving: true })
  this.props.dispatchSaveForm({
    formData: this.props.formData,
    callback: () => this.setState({ isSaving: false })
  })
}

Any issues with this? Is there a better way?

@Andarist
Copy link
Member

Your action is losing its serialibility. Personally I prefer them being plain objects. The question is - what do u want (ui-wise) to do after saving is complete?

@brettstack
Copy link
Author

Willing to lose serializability for simplicity here. In this example, I want the form to become disabled and show a spinning icon. Of course I can achieve this by storing state in the store, but it requires a ridiculous number of steps for something that should be so simple.

@eirslett
Copy link

We use redux-wait-for-action as a compromise when we need do do hacks like this. it makes store.dispatch(...) return a Promise which you can .then() or .catch() on.

@Andarist
Copy link
Member

If u want to bypass the store I think indeed some Promise-based solution is the best way to achieve your goal. Interesting problem nevertheless, wondering if this could be somehow tackled by us on the library level, but no ideas yet. Any thoughts @yelouafi ?

@ms88privat
Copy link

ms88privat commented May 3, 2017

This problem is really old. Some years ago someone posted a similar solution to the callback style.

handleSubmit(data) {
    return new Promise((resolve, reject) => this.props.dispatchAction(data, resolve, reject))

This way you can even reject form SubmissionErrors.
It is working fine in my apps. Loosing serialibility is of course a drawback.

@raarts
Copy link

raarts commented Aug 5, 2017

I think the state does belong in the store, but if there's a way to address this:

but it requires a ridiculous number of steps for something that should be so simple.

that would be great.

@norbertpy
Copy link

norbertpy commented Oct 9, 2017

Any update on this? Some of the transient states do not really belong to redux store. Is there an easy way to do:

class App extends Component {
  state = {
    loading: false,
  }

  handleGetData = () => {
    this.setState({ loading: true });
    const data = this.props.getData(); // SAGA
    this.setState({ loading: false });
  }

  render () {
    const { loading } = this.state;
    return (
        <div>
          { loading && <span> loading... </span> }
          <button onClick={ this.handleGetData }> fetch </button>
       </div>
    );
  }
}

@eirslett
Copy link

eirslett commented Oct 9, 2017

loading state as true/false is maybe a bad example, since it's widely considered correct practice to store that in Redux: http://redux.js.org/docs/advanced/AsyncActions.html

@norbertpy
Copy link

A form getting submitted has nothing to do with redux store. I wanna show a popup before the request and close it after it's done. There is no point writing reducer, action creator, ... for that tiny thing. The Prophet himself seems to agree.

@eirslett
Copy link

eirslett commented Oct 9, 2017

This is how we do it with redux-wait-for-action, give it a try:

// action creators
const getData = (payload) => ({
  type: 'GET_DATA',
  [WAIT_FOR_ACTION]: 'GET_DATA_SUCCESS',
  [ERROR_ACTION]: 'GET_DATA_FAIL'
});

const getDataSuccess = payload => ({
  type: 'GET_DATA_SUCCESS',
  payload
)};

const getDataFail = error => ({
  type: 'GET_DATA_FAIL',
  error
});

// saga
function * handleGetData (action) {
  try {
    const data = fetchDataFromSomewhere();
    yield put(getDataSuccess(data)); // assume there's an action creator
  } catch (error) {
    yield put(getDataFail(error)); // assume there's an action creator
  }
}

// Inside your UI component (I think setState() returns Promises, they are async, I can't remember)
await this.setState({ loading: true });
const result = await this.props.getData() // <-- will be mapped to store.dispatch(getData())
await this.setState({ loading: false });

@norbertpy
Copy link

Thanks. But that's the same thing as writing redux ceremonial stuff.

Now I have two problems.

@brettstack
Copy link
Author

Right. The redux-wait-for-action approach still has a lot of extra code. This is what I'm currently doing:

// promise-dispatch.js
export default ({ dispatch, actionCreator, payload }) => new Promise((resolve, reject) => dispatch(actionCreator({ ...payload, resolve, reject })))
// In a React component
const response = await promiseDispatch({
  dispatch,
  actionCreator: widgetsPageLoad,
  payload: { foo: 'bar' }
})
this.setState({ loading: false })

Ideally this would just be

const response = await dispatch(widgetsPageLoad({ foo: 'bar' }))
this.setState({ loading: false })

@sam-rad
Copy link

sam-rad commented Oct 10, 2017

@brettstack, @norbertpy If you want generators with ease and not coupled to redux store, you can use co or simply async/await. It'll be like:

// logic.js
import co from 'co';

export const getData = () => 
  new Promise(res => setTimeout(() => res(), 2000));

export const getRestOfTheData = () => 
  new Promise(res => setTimeout(() => res(), 3000));

export const getAll = co.wrap(function* () {
  const d = yield getData();
  const r = yield getRestOfTheData();
  return { d, r };
});

And then in your component:

import { getAll } from './logic';

class App extends Component {
  state = { loading: false }
  handleGetData = () {
    this.setState({ loading: true });
    getAll()
      .then(data => this.setState({ loading: false, data: data }))
      .catch(e => this.setState({ loading: false, error: 'error' }));
  }
  render () {
        return (...);
  }
}

@Andarist
Copy link
Member

It would be possible I think to write some kind of sagaConnector HOC, which could glue ur trigger and success actions, inject appropriate handlers into ur wrapped component and effectively allow u to bypass redux's store and just 'react' directly with setState in ur component.

Should be fairly easy to implement this in user land using context API.

@deniss-y
Copy link

hey there! any updates on this point?

@Andarist
Copy link
Member

Unfortunately not, it is also not a priority right now for the core team of redux-saga. I really wish somebody could pitch in and provide at least API draft, that way it would be easier to work on this for anyone.

@salterok
Copy link

@brettstack @norbertpy I use helper function in one of my work projects. Seems like it solves your case.

There is middleware.run(saga, ...args) method described in docs which supposed to run dynamic saga.

So i do something like this:

// middleware.js
import createSagaMiddleware from "redux-saga";

const sagaMiddleware = createSagaMiddleware();

// 'sagaMiddleware' should be registered as middleware in 'createStore'
// otherwise you have no access to store 

export function execute(saga, ...args) {
    return sagaMiddleware.run(saga, ...args).done;
}

And in my component use exported execute function:

import * as React from "react";
import { execute } from "./middleware";

class ExampleComponent extends React.Component {
    async onLoadClicked() {
        this.setState({
            loading: true,
        });
        const result = await execute(mySagaHandler);
        this.setState({
            loading: false,
            result
        });
    }
    render() {
        return (...);
    }
}

Note that mySagaHandler can read/write to store and use other effects.

@mingca
Copy link

mingca commented Feb 21, 2018

I am much interested in this topic but it's fruitless. redux-saga is designed for side effects. it can track each event but can't track every event.

@a-type
Copy link

a-type commented Mar 7, 2018

I did some exploration into this problem with redux-facet. Specifically, a kind of hack which wraps / intercepts saga generator effects: https://github.com/Bandwidth/redux-facet/blob/master/src/facetSaga.js

The idea in this library is that every action gets tagged with a meta.facetName value so that it can be traced back to the component which dispatched it.

The facetSaga wrapper intercepts all put effects and references the original action which triggered the saga, copying its meta.facetName value onto the outgoing actions.

This makes it (kind of) easy to keep track of the thread from the REQUEST action to the COMPLETE (or FAILED) action.

The same technique might be useful in resolving a promise on the original action.

For instance, a generator wrapper could be written like so...

const smartActionCreator = (data) => {
  let resolve;
  let reject;
  // there's probably a way cleaner way to do this:
  const promise = new Promise((res, rej) => { resolve = res; reject = rej; });
  return {
    type: 'SOME_ACTION',
    payload: data,
    meta: { resolve, reject, promise },
  };
};

function trackCompletion(saga) {
  return function*(action) {
    // ignore 'dumb' actions without promises
    if (!action.meta.promise) {
      yield* saga(action);
      return;
    }

    const { resolve, reject } = action.meta;

    const iterator = saga(action);
    let lastResult;

    while (true) {
      let next;
      if (lastResult instanceof Error) {
        // an error was thrown, reject the action promise
        reject(lastResult);
        next = iterator.throw(lastResult);
      } else {
        next = iterator.next(lastResult);
      }

      if (next.done) {
        // the saga has completed!
        resolve(next.value);
        return next.value;
      }

      // keep generating values
      lastResult = yield next.value;
    }
  };
}

// usage

// using the saga wrapper
function* handleAction(action) { /* ... */ }
function* () {
  yield takeEvery('SOME_ACTION', trackCompletion(handleAction));
}

// dispatching the action and awaiting saga completion
const { meta: { promise } } = dispatch(smartActionCreator(data));
await promise;
// saga is complete.

Code has not been tested, just spitballing

This could probably be streamlined by doing the async action stuff in middleware; there are many solutions for that.

I'm thinking of ways that the redux-saga library could more directly support the kind of pattern seen above without having to do a generator-wrapper (though I don't think the wrapper is too complex by itself).

Perhaps a way to add middleware to the redux-saga library itself? Some place we could put the code above so that it would be applied to every saga? Of course, not every saga has an action as a parameter, so that might not make much sense. Perhaps this wrapper function is good enough.

@Andarist
Copy link
Member

Andarist commented Mar 9, 2018

The idea itself is ok, I'd like to enable connecting such pieces as components and sagas more tightly for some use cases, but a builtin mechanism just for this doesn't seem right - unless we figure out better API. Need to think about something more universal.

@a-type
Copy link

a-type commented Mar 12, 2018

I agree, I don't think it's a good idea to make this a built-in behavior.

I've been doing some more thinking on this problem. I think the approach may be flawed from the start. I'm starting to believe that a combination of 1) more case-specific sagas and 2) more usage of saga composition may provide a cleaner answer.

Consider the original form-submit use case. As boilerplate-y as it is to store loading state in Redux, if we consent to do so, we can write a pretty readable saga to achieve our aims...

// generic state reducer for 'raw' profile data. Can be combined in selectors
// to provide data to any view which deals with profiles
const defaultProfileState = null;
const profileReducer = (state = defaultProfileState, { type, payload }) => {
  switch (type) {
    case 'PROFILE/NEW_DATA': return payload.data;
    default: return state;
  }
};

// view state reducer for any state related to the profile edit form
const defaultProfileEditState = { loading: false, error: null };
const profileEditFormReducer = (state = defaultProfileEditState, { type, payload }) => {
  switch (type) {
    case 'PROFILE_EDIT_FORM/SUBMIT': return { ...state, loading: true };
    case 'PROFILE_EDIT_FORM/RESET': return defaultProfileEditState;
    case 'PROFILE_EDIT_FORM/SHOW_ERROR': return { ...state, error: payload.message };
    default: return state;
  }
};

// generic, reusable data saga which encapsulates updating a profile,
// including dispatching data-related actions to update the raw data in the store.
// returns the updated profile.
// this saga isn't directly connected to any `take` effects. It is only called
// by view-related sagas.
function* profileUpdateSaga(profileData) {
  // let's assume this throws an error on 4xx/5xx
  const apiResponse = yield call(sdk.updateProfile, profileData);
  // put a data-centric action to update the raw data in the store
  yield put(profileActions.newData(apiResponse.body);
  return apiResponse.body;
}

// view-specific saga which delegates profile update to the general saga,
// but also adds effects which relate directly to the profile edit form view.
function* profileEditFormSaga({ payload }) {
  try {
	const profile = yield call(profileUpdateSaga, payload.data);
	// we can freely include view-specific concerns here in a readable way
	yield put(profileEditFormActions.reset());
	// even other side-effects like routing, since this saga will only ever represent
	// the experience on the profile edit form view
	yield call(navigation.goTo, '/home'); // mock / generic navigation service
  } catch (err) {
    yield put(profileEditFormActions.showError(err.message));
  }
}; 

function* watchProfileEditFormSubmit() {
  yield takeLatest('PROFILE_EDIT_FORM/SUBMIT', profileEditFormSaga);
}

This may seem like redux-saga 101... cause it is... but I know I've personally gone really far down the rabbit hole in search of a way to effectively achieve this kind of pattern where we can have generic, reusable logic for common data and still have very specific and easy-to-follow logic for views. I think the desire to manage loading state within the component itself is coming from the same need which this pattern addresses: we want to have a place which can plainly manage highly specialized user flows which come from specific views without polluting our generic logic with edge-cases. Saga composition, based on my explorations, still seems like the best way to do this (even if it means we must move some state to Redux like loading or error alerts). It just seems like once you use redux-saga, it needs to be the exclusive way that you handle asynchronous operations. As a middleware which has no concept of the view, it can't easily be sewn together with view-based logic like other approaches (thunks, etc) can. Personally, I'm cool with that.

@ovaldi
Copy link

ovaldi commented Apr 5, 2018

Mark!

@Andarist
Copy link
Member

Andarist commented Apr 5, 2018

New library got released that might help some of you solving this problem - https://github.com/erikras/redux-promise-listener

@klis87
Copy link
Contributor

klis87 commented Apr 5, 2018

Also I am creator of library https://github.com/klis87/redux-saga-requests for simplification of AJAX requests - promises like those with Redux-thunk are built-in there. Disadvantage is that promisification is just for AJAX request, but usually this is what most people need anyway :)

@BlueAccords
Copy link

BlueAccords commented May 7, 2018

Somewhat related, but there's a similar issue here #161 that discusses trying to return a promise from an action dispatch in case anyone stumbles upon this issue and wants to look at possible solutions to their problem.

The referenced issue is specific to redux-saga + redux-form integration but still has some solutions worth looking at imo.

Additionally, this library https://github.com/diegohaz/redux-saga-thunk might be of interest as well as another potential solution.

@saboya
Copy link

saboya commented May 14, 2018

Keeping state in one place is what redux encourages.. It's up to each developer to decide if they belong there or not.

redux-thunk returns Promise from dispatch and that allows some stuff, sure. But if you landed here in redux-saga it's because you probably realized that it doesn't scale to large-scale apps. Each solution has its pros and cons, but trying to turn redux-saga into something it's not isn't going to solve design problems.

@HunderlineK
Copy link

HunderlineK commented Nov 17, 2018

There are multiple ways to achieve what you want (adding a middleware to creating and saving the promises as symbols in your store, etc.) but I'd argue you should only need to either dispatch actions or handle the asynchronous operation internally, and not both.

The essence of what you are trying to achieve is making your React component become aware of the asynchronous nature of the action you are dispatching, and, concretely, introducing promises (or observables, async/await, generators, etc.) into your component. If you are okay with introducing asynchronous operations into your component, then why not just postpone submitting the redux action until the action is resolved?

handleSubmit = (data) => {
 fetch(uri, { method: 'POST', body: JSON.stringify(data) })
  .then(payload => { this.props.action(payload); this.handle(payload) })
  .catch(error => { this.props.action(new Error(error)); this.handle(new Error(error)) })
}

The point is that if your store does not already contain the information that an asynchronous action has started and is pending and you do not want to add that information to the store because it is not needed at a global level then, by definition, there is no need to dispatch the action until the asynchronous operation has already been resolved/rejected.

@jochakovsky
Copy link

jochakovsky commented Dec 10, 2018

Here’s a scenario where Saga -> Component communication is required that I haven’t seen in this thread so far (though I may have missed it):

We are following the Redux SubApp pattern - in our case, I help maintain a “SubApp” that exists in a large SPA consisting of both React and non-React components. The props passed into the root SubApp component are the primary way that the SubApp communicates with the larger application. Unfortunately, the Redux SubApp example linked above has no examples where the SubApp accepts any callback props (or any props at all).

We need to call some callbacks passed to the root SubApp component after asynchronous activities are completed (eg. calling something like this.props.ready() after everything is loaded). Here are some approaches we’ve considered:

  1. Put the prop into Redux Saga context (if the prop is a function) at load using an undocumented createSagaMiddleware API, eg. createSagaMiddleware({context: {ready: this.props.ready}}). Works well enough if you can assume that the prop never changes. If it does change, then you have to update the Redux Saga context every time it changes, which is cumbersome.
  2. Put a reference to the root SubApp react component into the Redux Saga context, eg createSagaMiddleware({context: {rootComponent: this}}). It will always have the latest props, I think, but this seems incredibly hacky.
  3. (not Redux Saga specific) Update some state when we want the callback to be called - basically the top-voted answer to this StackOverflow question. The comparison between this.props and nextProps doesn’t seem ideal, it seems to be a React anti-pattern that should be avoided in React except in specific scenarios like animations. Plus, react-redux doesn’t guarantee that every state change results in a re-render, which may result in the app not calling the callback when expected - not an issue with this particular this.props.ready() example, but could be an issue with other examples.
  4. (not Redux Saga specific) Put the callback in the action, as suggested here, but then the action isn’t serializable, and if the root SubApp component is passed a new callback in the meantime, the action will still reference the old callback.
  5. (not Redux Saga specific) Put the callback in the Redux store, but then the Redux store isn’t serializable, and it has to be kept in sync with the latest props passed into the SubApp root component.

I’m at a bit of a loss of what the best approach is when following the SubApp pattern, any tips would be appreciated!

@saboya
Copy link

saboya commented Dec 10, 2018

Most of the issues I see are created because a lot of implementations rely on implicit design decisions / state.

If you have a SubApp, that implicitly says that there's something bigger that controls all of that, and you should be able to receive events / signals from it. If you can't do it from redux-saga or some connected component, I believe the cleanest approach would be to explicitly design some inter-app communication channel, possibly through a redux middleware, which would make the most sense for me.

I can't see how connecting a saga directly to a component would solve your issue.

@jochakovsky
Copy link

@saboya Yes, there is a BigApp, to use Redux's terminology, that controls all of that. We do already have an inter-app communication channel - the BigApp passes state and callbacks as props to each SubApp. If you're suggesting adding a second inter-app communication channel, maybe something using events, this seems to add a lot of complexity just to get around some redux/redux-saga limitations. From the SubApps perspective, calling a callback passed from the BigApp seems like a regular side-effect to me, but I don't see a clean way for Redux Saga to interact with the props passed in from the BigApp.

@saboya
Copy link

saboya commented Dec 11, 2018

You should handle it through the props passed to each SubApp then. Having the outermost component compare props changed and fire events accordingly.

@jochakovsky
Copy link

jochakovsky commented Dec 11, 2018

@saboya that kinda works for BigApp -> SubApp communication, although it suffers from the same issues described in the third point of my first comment. I'm more concerned about SubApp -> BigApp communication, for which neither Redux nor Redux Saga seem to have any solutions for.

@salterok
Copy link

@jochakovsky I've posted some solution earlier in this thread, which can be integrated with component even further.

// declared somewhere near store creation
function bindToSaga(fn) {
  return (...args) => sagaMiddleware.run(fn, ...args).done;
}

class Foo extends React.Component {
  constructor(props) {
    super(props);

    this.initialize = bindToSaga(this.initialize.bind(this));
  }

  *initialize(param1, param2) {
    yield put({
      type: "MAKE_STORE_HAPPY",
      payload: param1 + " " + param2,
    });

    // call function passed from parent
    const readyResult = yield call(this.props.ready);

    this.setState({
      title: "I'm ready!!!",
      verySpecialValue: readyResult,
    });

    // if someone is interested in result
    return {
      meow: true,
    }
  }

  render() {
    return (
      <button onClick={() => this.initialize("Hello", "world")}></button>
    )
  }
}

So this way you can use components state & props inside your saga.

@saboya
Copy link

saboya commented Dec 11, 2018

The pattern itself is not recommended for scenarios that share data between Apps:

This pattern is not recommended for parts of the same app that share data. However, it can be useful when the bigger app has zero access to the smaller apps' internals, and we'd like to keep the fact that they are implemented with Redux as an implementation detail. Each component instance will have its own store, so they won't “know” about each other.

So yeah, it's up to you to implement some middleware to share actions between stores.

@littlehome-eugene
Copy link

Is the following practice worth considering?

https://stackoverflow.com/a/42590347/433570 .

I spent all day dinking around with this stuff, switching from thunk to redux-saga

I too have a lot of code that looks like this

this.props.actions.login(credentials)
.then((success)=>redirectToHomePage)
.catch((error)=>alertError);

its possible to use thunk + saga

function login(params) {
  return (dispatch) => {
    return new Promise((resolve, reject) => {
      dispatch({
        type: ACTION_TYPES.LOGIN_REQUEST,
        params,
        resolve, 
        reject
      })
    }
  }
}

then over in saga land you can just do something like

function* login(action) {
  let response = yourApi.request('http://www.urthing.com/login')
  if (response.success) {
    action.resolve(response.success) // or whatever
  } else { action.reject() }
}

@charlax
Copy link

charlax commented Apr 1, 2019

Chiming in to provide some more use cases.

This becomes even worse when loading multiple objects in potentially different places. Where to store the loading state?

Imagine the following store:

interface Store {
  byId: Toaster[],
  byRoom: {[key: string]: ToasterID[]},
}

You could store the loading state on the Toaster object itself, but it does not belong there, so it's a hack:

interface Toaster {
  color: string,
  room: string,
  isLoading: boolean,  // Has nothing to do with the rest
}

You could have a loadingByID key in your state, but that does not scale very well...

What's more, I have reservations about store something very transient like the loading state in the redux store...

Same thing for errors. Errors can happen in multiple flows. Let's say you allow editing Toaster attributes using an inline design pattern. You need to store and display whether the save was successful. Where are you going to store that?

interface Toaster {
  color: string,
  colorIsSaving: boolean,
  colorError: string,
  colorIsSavingFromScreen1: boolean,
  // ...
}

Errors and loading state are transient IMO and have nothing to do in the store. They should be local to the component that triggered them - at least in most cases.

@vedmant
Copy link

vedmant commented Apr 1, 2019

@saboya I landed here only because some of the projects used redux-saga, I ended up just rewriting everything with redux-thunk and it solved all my issues, and I get rid of lots of boilerplate.

@charlax Yeah, absolutely agree with this!

@nimeshgurung
Copy link

nimeshgurung commented Apr 4, 2019

I ran into the same problem and came up with https://www.npmjs.com/package/redux-retry. Has worked fairly well for me so far, is it the right solution? that I can't tell.

@tkow
Copy link

tkow commented May 7, 2019

I'm really thinking this problem, and it may be hint of solution with for this though it have some problems,

const SUBSCRIBE = '@@Redux-Saga/SUBSCRIBE'

export const sagaSubscribe = (subscriber:()=>void) => ({
  type: SUBSCRIBE,
  payload: subscriber
})

export function subscribe<T extends any>(type = this.name,result?: T)  {
  return put({
    type: `${SUBSCRIBE}/${type}`,
    payload: result
  })
}

export function subscribeSaga (saga: Saga & {type: string}) {
  return function* () {
    yield fork(saga)
    while (true) {
      const callbackAction = yield take(SUBSCRIBE)
      if(callbackAction){
        const channel = yield actionChannel(`${SUBSCRIBE}/${saga.type}`)
        try {
          while (true) {
            const result = yield take(channel)
            callbackAction.payload(result.payload)
          }
        } finally {
          channel.close()
        }
      }
    }
  }
}

function * exampleSaga() {
  yield take(anyActonType)
  //... do something
  yield subscribe(exampleSaga.type, {
    result: 'result'
  })
}

exampleSaga.type = 'someId or maybe using exampleSaga.name'

function* rootSaga() {
  yield subscribeSaga(exampleSaga)
}

// ...components
componentDidMount() {
  dispatch(sagaSubscribe((result)=> {
    // can access any component props or state
  }))
}

this way barely obey the loosely-coupled rules of saga event and can be wrapped ,but this solution have the problems.

1. subscribe effect must be triggered if it doesn't match any sagas' action type.
2. It's fake of subscribe event of redux , so there is possibility to trigger when state update is not completed.
3. saga.type must be named if you don't use function.name, and actions 
4. It tough to wrap sagas to subscribe every time .

Though my code can be plugged in/out each saga , it may be good to set global subscribe listener in middleware or run independent saga as global subscriber because it doesn't need type is identical though we need to know the results of actions and handle them appropriately.

ex) Anytime you want to get results from subscriber, yield subscribe(result) when you want to invoke it is enough to do to component.

ADDITION

Finally I recommend you create listener if this your async process is not simple for extension.
If you pass success callback to action payload, you must compose multiple async process pipelines in component file though it is simple. It may be not good for the reason that value of saga is the composabillity and event passing model..

This can be adopted by ducks pattern saga, and combine multiple putted results no need to change independent fetched data saga.

// omitting  A/actionTypes.ts B/ actionTypes.ts
// this saga separated to definition A, B combination

function* exampleSaga() {
  while (true) {
    try {
      const action = yield take(FETCH_PARALLEL_LISTENER);
      if (typeof action.payload === 'function') {
        while (true) {
          const [_, unsubscribe] = yield race([
            all([
              take(actionTypesA.SUCCES_GET_RESULT), // this putted after fetch A data
              take(actionTypesB.SUCCES_GET_RESULT) // this putted after fetch B data
            ]),
            take(CLOSE_FETCH_PARALLEL_LISTENER)
          ]);
          if (unsubscribe) {
            break;
          }
          action.payload(...some result you want to use);
        }
      }
    } catch (e) {
      console.log(e);
    }
  }
}

onFetchParallelListener = (callback) => ({
   type: FETCH_PARALLEL_LISTENER,
   payload: callback
})

// your component 

componentDidMount () {
   onFetchParallelListener((result)=> {
       if(isError(result))  //...error process
       // ...success process
   })
}
//... someEvent
fetchParallelData()

@ithieund
Copy link

ithieund commented Sep 4, 2019

Somewhat related, but there's a similar issue here #161 that discusses trying to return a promise from an action dispatch in case anyone stumbles upon this issue and wants to look at possible solutions to their problem.

The referenced issue is specific to redux-saga + redux-form integration but still has some solutions worth looking at imo.

Additionally, this library https://github.com/diegohaz/redux-saga-thunk might be of interest as well as another potential solution.

I like this approach. It makes things simple :)

@piyushbaj
Copy link

HI , you can handle loader centrically , let say you have loader component , there you can attach your loader component to the reducer , so on the bases of reducer loading state you can handle easily.
you can use redux - thunk and also you can use redux saga here , every time you just need to dispatch the action to the reducer only .
For more info please do subscribe my youtube channel
Muo sigma classes :- https://www.youtube.com/channel/UCD8bHKf3eRE0bAWShADWAYw

Thanks and regards

@piyushbaj
Copy link

and also we have something like finally in redux saga , so using this you can also clear the reducer state for loading issue .
but i am using like this , i made two dispatch one for start_loading and another for stop_loading
, call after success or error in saga

Thanks and regards

@stephoune
Copy link

Hi there !
Any updates ?
What is the best solution for this problem ?
Thank's !

@Lexe003
Copy link

Lexe003 commented Apr 29, 2020

Not sure if it's a bad practice, but you can give your action a callback function which you can fire from sagas.

  • ComponentX
    dispatch(actionX("par1", "par2", () => { ..doSomthing } )

  • ActionX

export function actionX(param1, param2, callback) {
  return {
    type: "ACTION_X",
    payload: {
      param1,
      param2,
      callback
    }
  };
}

Sagas:

export function* doSomthingSaga({ payload }) {
// Callback setLoading(true)?
  try {
    const data = yield call(XService.updateDto, payload.param1, payload.param2);
    yield put(actionZ(data));
    payload.callback(); // callback when success? setLoading(false)
  } catch (error) {
    // Fire a callback here if somthing goes wrong
  }
}

@orpheus
Copy link

orpheus commented Apr 30, 2020

I was planning on handling this issue with a useEffect, but I agree that almost the first thing I noticed from switching from thunks to sagas was that I couldn't just await the action/saga in my component like I could with a thunk and do a .then or .catch on it to display a snackbar etc.

To get around this I was just going to do something like
dispatch(someActionToFireAnApiCall(data))

then in my component listen for the async state changes

useEffect(() => {
    if (apiSuccess) {
        setSnackbar({ message: "Updated" })
        clearApiState()
    }
    if (apiError) {
       setSnackbar({ message: "Failed" })
       clearApiState()
    }
}, [apiError, apiSuccess, setSnackbar, clearApiState])

depending on your specific use case, you could call the clearApiState anywhere you need. The main idea being to just listen for the loading statuses that the saga will dispatch.

as OP mentioned it would be most ideal to just do something like

dispatch(someSagaTriggeringAction).then().catch().finally()

or it's async/await equivalent

@sethen
Copy link

sethen commented May 1, 2020

The only way that I have ever used sagas were to dispatch an action and then respond to a state change in my React component since .then and async/await are unavailable or not feasible.

This is not ideal, but this is the trade off with Thunks vs. Sagas. I would much rather have an API like Thunks though and be able to then off of something to perform additional operations.

The entire JS ecosystem has embraced promises for many years now and async/await makes that much easier to use. It makes all the sense in the world to bring that to sagas somehow.

@zfeed
Copy link

zfeed commented Oct 29, 2020

I assume this problem can be solved by connecting your sagas to external I/O i.e. to your component's state

This way you don't need to keep local component's state in Redux state (also you can get rid of using redux in your app at all if there is no need for some specific redux capabilities (for example middlewares))

    const [state, dispatch] = useReducer(reducer, initialState);
    const channel = useMemo(() => stdChannel(), []);

    const channelDispatch = useCallback(
        (action) => {
            // firstly we should update the state and only then put to the channel. It's how saga's middleware is supposed to work in redux
           // https://github.com/redux-saga/redux-saga/issues/148
            dispatch(action);
            channel.put(action);
        },
        [channel, dispatch]
    );

    // run saga on component mount and cancel on unmount
   // channel always stays the same as it's memorised
    useEffect(() => {
        const saga = runSaga(
            {
                channel,
                dispatch
            },
            tableSaga
        );

        return () => saga.cancel();
    }, [channel]);

    // an example of how we can use saga in component
    useEffect(() => {
        const action = requestAction({ userId });
        channelDispatch(action);
    }, [userId, channelDispatch]);

@zfeed
Copy link

zfeed commented Oct 29, 2020

I assume this problem can be solved by connecting your sagas to external I/O i.e. to your component's state

Strange, but git's history says that solution (mention runSaga in the docs) for this problem has been existing since 2016. Am I missing something?

3deff3f#diff-3807b7428d45d9fecae6faa3d1a39952e15915fa07e2dac4122e8d866bfc4590

@lpcuong2106
Copy link

lpcuong2106 commented Jul 27, 2021

How I can listen dispatch failed to setModal suitable state in the local component's state?

const handleSubmit = (note) => {
  //call api inactive service
   dispatch({
          type: 'services/inactiveService',
          note,
    })
    if(dispatch_error){
       setModal(true);
    }
   if(dispatch_sucess){
      setModal(false);
   }
}

@neurosnap
Copy link
Member

There's a lot going on in this issue and a lot has changed in the redux ecosystem since it was originally created. Having said that I built a library https://github.com/neurosnap/saga-query that attempts to reduce boilerplate around loading states and using sagas in general.

@neurosnap
Copy link
Member

Circling back, I'm going to close this issue for now. If anyone is interested in continuing the discussion, I can reopen and chat with you about it! Just know, at this point in time, a primary goal for this library is maintenance. Any breaking changes or big new features should probably live in a superset library.

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

No branches or pull requests