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
Comments
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? |
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? |
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. |
We use redux-wait-for-action as a compromise when we need do do hacks like this. it makes |
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 ? |
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. |
I think the state does belong in the store, but if there's a way to address this:
that would be great. |
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>
);
}
} |
|
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. |
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 }); |
Thanks. But that's the same thing as writing redux ceremonial stuff.
|
Right. The // 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 }) |
@brettstack, @norbertpy If you want generators with ease and not coupled to redux store, you can use co or simply // 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 (...);
}
} |
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 Should be fairly easy to implement this in user land using context API. |
hey there! any updates on this point? |
Unfortunately not, it is also not a priority right now for the core team of |
@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 |
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. |
I did some exploration into this problem with The idea in this library is that every action gets tagged with a The 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. |
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. |
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. |
Mark! |
New library got released that might help some of you solving this problem - https://github.com/erikras/redux-promise-listener |
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 :) |
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. |
Keeping state in one place is what
|
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?
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. |
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
I’m at a bit of a loss of what the best approach is when following the SubApp pattern, any tips would be appreciated! |
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 I can't see how connecting a saga directly to a component would solve your issue. |
@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. |
You should handle it through the props passed to each SubApp then. Having the outermost component compare props changed and fire events accordingly. |
@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. |
@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. |
The pattern itself is not recommended for scenarios that share data between Apps:
So yeah, it's up to you to implement some middleware to share actions between |
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
its possible to use thunk + saga
then over in saga land you can just do something like
|
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 interface Toaster {
color: string,
room: string,
isLoading: boolean, // Has nothing to do with the rest
} You could have a 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 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. |
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. |
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.
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. ADDITIONFinally I recommend you create listener if this your async process is not simple for extension. 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() |
I like this approach. It makes things simple :) |
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. Thanks and regards |
and also we have something like finally in redux saga , so using this you can also clear the reducer state for loading issue . Thanks and regards |
Hi there ! |
Not sure if it's a bad practice, but you can give your action a callback function which you can fire from sagas.
Sagas:
|
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 To get around this I was just going to do something like then in my component listen for the async state changes
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
or it's |
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 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 The entire JS ecosystem has embraced promises for many years now and |
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]); |
Strange, but git's history says that solution (mention 3deff3f#diff-3807b7428d45d9fecae6faa3d1a39952e15915fa07e2dac4122e8d866bfc4590 |
How I can listen
|
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. |
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. |
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:
this.setState
Essentially I want to do this from within my form component:
The text was updated successfully, but these errors were encountered: