-
Notifications
You must be signed in to change notification settings - Fork 4.3k
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
Decide whether we need formal asynchronous state patterns #691
Comments
Big +1 for refx, especially if RxJS is not used. |
Pulling in some discussion from #769
|
@BE-Webdesign Could you point to some examples on how an RxJS approach (or observables generally) would look? A goal with refx was to treat side effects as almost incidental to the actions occurring in the system, behaving as observers of sorts. I'm curious to what extent this overlaps with a solution using observables proper. A key worry for me here is colocation. Thunks are at least nice in that actions and side effects occur in the same place. Since a reason many view Redux as daunting is due to its separation between components, actions, and reducers (and selectors, and action types), it's a fair argument to say that thunks improve developer experience by keeping things simpler at smaller scale.
@youknowriad Generators intrigue me. Ignoring the fact that they don't (and likely won't) see much usage and are therefore unfamiliar to most, they seem to be a good fit for what we need an asynchronous pattern for; namely, functions (action creators) which can async function* fetchPost( postId ) {
yield { type: 'FETCH_POST_START', postId };
try {
const response = await fetch( `/wp/posts/${ postId }` );
const post = await response.json();
yield { type: 'RECEIVE_POST', post };
yield { type: 'FETCH_POST_SUCCESS', postId };
} catch ( error ) {
yield { type: 'FETCH_POST_ERROR', postId, error };
}
} |
This is a choice from the library to make it explicit and in the same time allow yielding other values. So, to make it explicit if we should dispatch or not the yielded value, we have a In the same way, other helpers could serve other purposes. For example we could have a helper |
From a quick look, I really like https://github.com/aduth/refx: it seems to preserve most of the simplicity of thunks while improving testability. Increasing separation is a relatively minor concern, IMO far outweighed by improved testability. It also makes sense to me to locate all side-effects together in a similar place. I think JS generators are nicer in theory than in practice and would prefer that we don't introduce them here. I think Python got generators mostly right and JavaScript got them mostly wrong. They are pretty complicated, and feel half-baked and poorly integrated with the rest of ES6. For a small example, you can't use the |
I don't agree that JS generators are bad. I feel they are really powerful, but I think generators, in general, have a steep learning curve. I do think it's the nicest approach I've seen to handle side-effects and asynchronous code in Javascript, but in a wide project, I would argue against their use just because of the learning curve. It's not a feature used much by lambda developers. |
This would be using an extra library which turns the redux store into an observable, and adds the idea of an Epic, which is basically the pull equivalent to a redux Saga. // action creators; roughly using the same naming and conventions in gutenberg.
const savePost = ( edits, postId = '' ) => {
const isNew = ! postId;
const editsToSend = postId ? { id: postId, ...edits } : edits;
return { type: POST_REQUEST_UPDATE, edits: editsToSend, isNew };
};
const savePostFulfilled = ( post, isNew ) => ( { type: POST_REQUEST_UPDATE_SUCCESS, post, isNew } );
const savePostFailure = ( err, edits, isNew ) => {
return { type: POST_REQUEST_UPDATE_FAILURE, err, edits, isNew };
};
// epic
const updatePostEpic = action$ =>
action$.ofType( POST_REQUEST_UPDATE )
.mergeMap( action => {
return Observable.fromPromise( new wp.api.models.Post( action.edits ).save().done() )
.map( post => savePostFulfilled( post, action.isNew ) )
.catch( err => savePostFailure( err, action.edits, action.isNew ) )
// If the request failed try it again if online every second, if offline try again when online.
.retryWhen( err => window.navigator.online ?
Observable.timer( 1000 ) :
Observable.fromEvent( window, 'online' )
} );
// On the publish button click.
dispatch( savePost( state.editor.edits ) ); The advantages of using RxJS or a Saga based middleware are the ability to manage complexity more easily ( notice how trivial it is to add offline handling, you could even slap in local storage and cache handling stuff ). If we needed to throttle network requests, that is trivial as well ( simply chain the throttle call at the end of mergeMap ), not so much for thunks or refx. For right now, RxJS or Sagas are not necessary or adding much value ( I finally relent on my Rx crusade ). I like refx, because it is light weight ( both in learning, and in code length ) and helps split the side effects from the actual action creators, and in my opinion an easier solution than thunks. One advantage of thunks over refx is that the action flow is more explicit, which could make it easier to track flow as complexity grows. So either thunks or refx are the way to go in my book. If we get into heavy duty async, like collaboration tools, I think that is when Rx or something should be introduced. Rx should be a definite consideration if/when auto drafting behavior is introduced. |
Pulling in remarks from @BE-Webdesign at #769 (comment) :
I'd say a good bit of the simplicity in thunks is merely colocation, which is lost when you move effects into a separate file. But I agree that an action observer pattern is one which would set us on a better footing for the future. To the colocation point, I was recently reintroduced to the Redux Ducks convention which is a rather extreme take on colocation, while flexible in supporting a multitude of side effects patterns. |
With the Ducks pattern, as long as things are self containing, it would be pretty easy to add a |
Related: #594
We'll want to decide if we need to establish patterns around managing asynchronous state data, or if it's enough to pass around dispatch as an argument as included in #594. This is a relatively unsettled problem in Redux, with some consensus around thunks.
http://redux.js.org/docs/advanced/AsyncActions.html#async-action-creators
So far we've made a conscious effort at keeping concepts simple in our state, forgoing some patterns altogether such as constant action types, action creators, middlewares, and selectors. With this in mind we should be cautious about introducing any new patterns unless they can be demonstrably shown to provide benefit outweighing the overhead of their learning curve.
Quoting below a comment I'd shared previously at #594 (comment)
The text was updated successfully, but these errors were encountered: