-
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
Data Module: Add support for action creators as generators #6999
Conversation
editor/store/actions.js
Outdated
isCurrentPostPublished, | ||
} = select( 'core/editor' ); | ||
|
||
if ( ! isEditedPostSaveable() ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To access the state we're calling wp.data.select
directly. Another approach would be to inject a select
helper somehow into the generator:
export function* autosave( select ) {
const isSaveable = select( 'core/editor' ).isEditedPostSaveable(); // or just `select.isEditedPostSaveable()`
}
The downside of this approach is that it adds an extra argument to the action creator which is not ideal.
Maybe it's fine to rely on the global and mock the global in the tests. Thoughts?
Worth noting that this is a necessary consequence not so much of the move to generators, but of going from pure-function action creators to stateful/effectful action creators. Just pointing out the obvious — one of the ideas that Redux brought about was the limitation of responsibilities of action creators and other entities — so that we can weigh the trade-off we want to make, the upside of which I assume is to improve the developer experience of plugin authors. |
Also noting that with |
Agreed, and this may be good time to try to delineate what should be available to action creators:
|
Related Slack discussion (Core JS Meeting 2018-05-29): https://wordpress.slack.com/archives/C5UNMSU4R/p1527600900000259 |
(Mind the brain-dump 😄) The main thing I've been disappointed in with side effects implementations, Instead, most all of our effects in the editor are written as though they were a single set of steps otherwise inseparable from the original action. If I were to guess (even speaking for myself), the mindset here is a fixation on dispatching asynchronously, not as considering actions like network requests as side effects, and certainly not to the extent of other separate effects (notices, screen reader announcements, etc.) My hope with
With these in mind, I think it's failed in a number of ways. Take as an example the effect handler for saving a post. On the surface, I really only see two true side-effects:
With the above "hopes" as a framework, I'd then want (a) each to be its own effect handler, and (b) patterns to emerge for grouping like effects (e.g. all notices, optimistic behaviors). There is the important consideration of developer experience. Of course, as a developer, it's natural for me to think a save should involve all of the above steps, and in this sense I'd not fault the effect handler for being written the way it is. It should be a critical role of the tools we provide as the framework to guide the developer naturally toward the goals we're hoping to achieve with isolated side effects.
So, as all of the above applies to the proposal in this pull request, I've a few observations: Good:
Room for improvement:
To the last point, and in trying to ideate on a "best of all worlds" solution, what if we have side effects defined as associated by the action creator name, in a similar way to selectors: /* editor/store/actions.js */
function savePost( data ) {
return { type: 'SAVE_POST', data };
}
savePost.sideEffects = [
async function requestPostUpdate( action ) {
const { data } = action;
try {
const post = await apiRequest( { data } );
return savePostSuccess( post );
} catch ( error ) {
return savePostError( error );
}
},
];
function savePostSuccess( post ) {
return { type: 'SAVE_POST_SUCCESS', post };
}
savePostSuccess.sideEffects = [
resetPost,
];
/* editor/store/effects/notices.js */
import { dispatch, registerSideEffects } from '@wordpress/data';
registerSideEffects( 'core/editor', {
savePost: [
function dismissNotice() {
const { removeNotice } = dispatch( 'core/notices' );
removeNotice( 'SAVE_POST_NOTICE_ID' );
},
],
savePostSuccess: [
function createSaveSuccessNotice() {
const { createSuccessNotice } = dispatch( 'core/notices' );
createSuccessNotice( /* ... , */ { id: 'SAVE_POST_NOTICE_ID' } );
},
],
} ); Notes:
function savePost() {
return { type: 'SAVE_POST' };
}
savePost.sideEffects = {
before: [
function replaceWithPayload( action, selectors ) {
const { getCurrentPost, getPostEdits, getEditedPostContent } = selectors;
const post = getCurrentPost();
const edits = getPostEdits();
return {
...action,
data: {
...edits,
content: getEditedPostContent(),
id: post.id,
},
};
}
],
after: [
async function requestPostUpdate( action ) {
const { data } = action;
try {
const post = await apiRequest( { data } );
return savePostSuccess( post );
} catch ( error ) {
return savePostError( error );
}
},
],
}; And some notes on this:
To reiterate, I'm not trying to promote functional "purity" as much as the goals that these patterns exist to serve (maintenance/scalability benefits of grouping/isolating like/predictable behaviors). If we can introduce statefulness and/or generator yielding to action creators without sacrificing this objective, I'm okay with that. But I'm inclined to think it could promote bad practices (convoluted, unpredictable behaviors). |
I don’t have the talent to write such long responses :P I think the proposal is not great in terms of DevX, It’s really hard to understand the flow of things. It’s too difficult to follow what’s happening in these side effects.
The improvement I’d make to the generators proposal to solve this is to add composition function createSaveSuccessNotice( id ) {
return { type: ‘CREATE_NOTICE’, type: ‘success’, id };
}
function * savePostSuccess( post ) {
yield { type: 'SAVE_POST_SUCCESS', post };
yield createSaveSuccessNotice( 'SAVE_POST_NOTICE_ID' );
}
async function * requestPostUpdate( action ) {
const { data } = action;
try {
const post = await apiRequest( { data } );
yield savePostSuccess( post );
} catch ( error ) {
yield savePostError( error );
}
} We can even support multiple effects at the same time async function * requestPostUpdate( action ) {
yield parallel( [
effect1,
effect2
] );
} I think this “imperative-like” flow for side effects is better than indirection. It makes things easier to reason about. Whether these side-effects should be declared separately from the action creator, I don’t think it’s a good idea for two reasons:
Regarding access to selectors and actions from other stores. I think we should just call. |
But as has been demonstrated very clearly in
On the singular perspective of: What happens when we call But what about:
|
I don't understand the question, you call
I think the difference is that I'm fine with long action creators or long effects :). I'd isolate effects only when needed in two places. I don't really care about separating them just because it's conceptually separate effects.
I don't know 🤷♂️ This PR obviously doesn't address this but is it really a problem? I don't know the answer, I didn't felt the need for this personally. Wouldn't a simple I don't really care too much about the API we end up using, just sharing my thoughts. Actually, I'd prefer if we bake in |
The question was not so much "How do I create or remove notices", it's a question of: As someone merely reading the code, how can I see where the code notices currently occur? Put another way, let me pose the question: What notices do we have in Gutenberg right now? Once you've come up with an answer, think about the process you undertook to come to that conclusion. Is it better or worse to have notices behavior grouped together separately as a single set of related effects, or to closely associate them individually with e.g. saving, deleting, trashing (and every other future case where we need them)? I don't know the answer; just noting that perception of reasoning about is more complicated than just what occurs in a single action, but also making sense of the system as a whole.
And in just the same way, we could have the entire implementation of Gutenberg occur within
It's a problem in that we have the same need in the core implementation as is needed by plugins. But I may feel more strongly about this because I'm thinking of things like notices as incidental to the act of saving itself, as a separate behavior that occurs as a mere consequence of the action, in much the same way a plugin would want to hook in. |
As I look to resolve #7066, my first hunch was to move the setting of the URL from // edit-post/store/tbd.js
registerSideEffects( 'core/editor', {
resetPost: replaceURLForPost
} ); |
Thinking more about this, I think we're trying to solve two things that are different.
|
434d9c6
to
461758c
Compare
461758c
to
fae11a7
Compare
superseded by #8096 |
I feel like this the last missing piece to the data module. A way to replace the "effects" we have.
Since we support resolvers as action generators why don't we support actions as action generators 🤷♂️
Questions
wp.data.select
directly which works but makes testing those components a bit hard.