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

Editor: Reduxify The EditorTitle component #8814

Merged
merged 10 commits into from
Jan 6, 2017

Conversation

youknowriad
Copy link
Contributor

@youknowriad youknowriad commented Oct 18, 2016

Testing instructions

  • Create a new post and edit its title
  • The title should update correctly
  • Do the same thing for different post types

cc @aduth

@youknowriad youknowriad added [Feature] Post/Page Editor The editor for editing posts and pages. [Status] Needs Review The PR is ready for review. This also triggers e2e canary tests and wp-desktop tests automatically. [Type] Janitorial labels Oct 18, 2016
@youknowriad youknowriad self-assigned this Oct 18, 2016
@matticbot
Copy link
Contributor

isNew,
post,
site,
ownProps
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are we adding ownProps here? it will come in as this.props.ownProps - is that the intention? also, did you know that ownProps automatically gets merged into the props through connect even if we never use them?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I know, the idea is that ownProps needs to be passed down the textarea. The previous way of doing this was to rely on ownProps automatically merged to the props and using omit to exclude the current component props.

And when using localize and connect etc... we get extra props that we don't want to pass down to the textarea (we'll get warnings). And it's hard to keep those ignored props up to date with this method. So, for now, localize adds three props (translate, moment and another one). Imagine someone adding another prop to localize, he won't think of fixing these kind of issues (adding the new Prop to omit). That's why I used this technique.

I could also replace the ...ownProps with explicit props passing (we use only tabIndex for now I think), but I see the component as an upgraded textarea, so It's "logic" to just pass all parent props down.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm sorry @youknowriad but I don't think I understand what you are saying here. what it appears like you are saying doesn't fit with what I think the code is saying.

what is the problem with localize() and why does it break things? also, one of my chief questions here is that we are forcing the component/children to change their logic here because if they formerly depended on this.props.value they will now have to call out to this.props.ownProps.value

Copy link
Contributor Author

@youknowriad youknowriad Oct 18, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, you are right! my answer was not so clear. Let me explain more. the PostEditor component renders the EditorTitle Component like this : <EditorTitle tabIndex="1" /> and the EditorTitle Component renders a textarea close to this : <Textarea {...props />.

The issue here, is to find the best way to compute the props that needs to be provided to the Textarea. In the example here, these props are { tabIndex: '1' }. We need to find the best way to find these exact props without passing any extra prop provided by the HoC components used on the EditorTitle.

The options are :
1- const props = omit( this.props, Object.keys( this.constructor.propTypes ) ) and make sure we define all the propTypes provided by HoC like localize even If we don't use them like the moment prop. Also If the localize HoC get updated and provides an extra prop, the EditorTitle component will have to be updated to "omit" the new prop.

2- using ownProps since we know these are the only props provided to the EditorTitle container and not computed by any HoC.

3- avoid using { ...props } and use only something like tabIndex={this.props.tabIndex}. This approach is fine but we change the behavior of the EditorTitle component since we can not provide extra props (for example style={someStyle} or whatever) to the textarea

I hope this is clearer.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I understand what you are saying now @youknowriad. it seems like an indication that bigger issues exist here.

we have plenty of places where we pass ...props down through several levels of nesting and that is almost always really confusing. I'm a huge advocate for explicit prop passing. props passed by localize shouldn't be passed along the chain either because if a child needs translate then it should be directly wrapped by localize

concerning passing other props I wonder if it's not more of a concern than it seems. why should we want to pass some new style to an underlying textarea inside a component? that actually breaks our React idioms, that parents should need to know nothing of the inner workings of their children. perhaps being explicit about a component's public API and keeping the nested children isolated from the grandparents can provide more robustness and reliability here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm ok with using explicit props. I think it's a good solution. My concern about it was that It changes the way this component works (and it was not the purpose of this PR, just a refactor).

From my understanding, this component was considered like a "decorator" of the textarea and this won't be the case anymore with explicit props but I'm ok with it.

I'll update the PR to reflect that :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we shouldn't be changing the behavior, but wouldn't this.props.ownProps also change it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it does not change it. If we compare the props passed down to the textarea before the refacto and after (with ownProps), they'll remain exactly the same.

@youknowriad
Copy link
Contributor Author

@dmsnell I update the RP with explicit props. It's probably better. We'll just pass down new props when needed and avoid the hassle of figuring out the right props to pass down.

@youknowriad
Copy link
Contributor Author

Any other thoughts about this @dmsnell tks :)

@dmsnell
Copy link
Member

dmsnell commented Nov 4, 2016

Sorry for missing this @youknowriad - I think it's probably ready to go. I haven't tested it so that's the catch. Please merge at your leisure or what on someone else to review if you prefer. Again, sorry - I'm a little swamped at the moment.

@dmsnell dmsnell added [Status] Ready to Merge and removed [Status] Needs Review The PR is ready for review. This also triggers e2e canary tests and wp-desktop tests automatically. labels Nov 4, 2016
@youknowriad
Copy link
Contributor Author

Don't worry @dmsnell I totally understand, we can all be overwhelmed :)

I'll test it deeply again and merge this up. Tks

@aduth
Copy link
Contributor

aduth commented Nov 4, 2016

@youknowriad Does this account for empty content checking that exists in the Flux implementation? Will explain shortly in follow-up, just wanted to catch before merge.

@aduth
Copy link
Contributor

aduth commented Nov 4, 2016

Specifically, see #5445 which is a similar refactoring that accounts for post having content. A post cannot be saved unless it has one of these fields not being empty: title, content, excerpt. The editor Publish and Save buttons should be disabled until one of these conditions is met. That is currently managed via PostEditStore.hasContent. If we were to update this component to set title in Redux state, we would need for this state to be considered too in whether Publish is active.

@aduth
Copy link
Contributor

aduth commented Nov 4, 2016

Steps to reproduce:

  1. Start new post
  2. Enter title

Expected: I can publish
Actual: I can't publish

@youknowriad
Copy link
Contributor Author

Oh! I missed that, thanks @aduth

I updated the PR by using the same behavior we had for the post dirty checking. Basically, to check if the post has content, we use the PostEditStore as well as a new redux selector editedPostHasContent. And after moving all the Editor's parts to Redux, we'll just stick with the selector.

Thoughts?

@youknowriad youknowriad added [Status] Needs Review The PR is ready for review. This also triggers e2e canary tests and wp-desktop tests automatically. and removed [Status] Needs Author Reply labels Nov 7, 2016
* @param {Number} postId Post ID
* @return {Boolean} Whether the edited post has content or not
*/
export const editedPostHasContent = createSelector(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this selector be simplified to:

const editedPost = getEditedPost( siteId, postId );
return (
    some( [ 'title', 'excerpt' ], ( field ) => !! editedPost[ field ].trim() ) ||
    ! isPostContentEmpty( editedPost.content )
);

Notably leveraging getEditedPost instead of recreating that behavior.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tks @aduth I was not aware getEditedPost was handling the merge thing 👍

@dmsnell
Copy link
Member

dmsnell commented Nov 8, 2016

If this is a memorized selector, as via createSelector(), but the return value depends on a property of something not in the parameter list, won't the memorization lead to bugs?

Post content changes but code is requesting the same site/post ids, memorized selector thusly returns previous value of emptyPost even though it's no longer empty…

@youknowriad
Copy link
Contributor Author

If I understand correctly the behavior of createSelector there's no bug here, since the cache will be used only if we have the same sub-state (computed from the declared dependencies : the second parameter of createSelector) and also the exact same arguments.

https://github.com/Automattic/wp-calypso/tree/fcf789ebb43b7d8fe9a912d8fa82d5bee66573b5/client/lib/create-selector

@youknowriad
Copy link
Contributor Author

youknowriad commented Nov 8, 2016

At least, if it's not the case, then we should update the example on the README there

@dmsnell
Copy link
Member

dmsnell commented Nov 8, 2016

there's no bug here, since the cache will be used only if we have the same sub-state

is it the case then that we are "flushing" the memorized cache any time any post changes or is added/removed? is the memorized selector even helping us that much?

did we have performance issues with this title that led us to forego the simplicity of a straightforward selector?

@youknowriad
Copy link
Contributor Author

@dmsnell to be honest, I have not experienced specific performance any issue but I used this approach to mimic what has been done for isEditedPostDirty selector which is used in the exact same way as this selector.

So yes, I'm ok to drop the memoization unless we experience performance issues.
Any thought on this @aduth ?

@aduth
Copy link
Contributor

aduth commented Nov 8, 2016

is it the case then that we are "flushing" the memorized cache any time any post changes or is added/removed?

Yes, this is true. The members of the second argument of createSelector are compared between calls and if any is not strictly equal to its previous value, the cache is cleared.

is the memorized selector even helping us that much?

The most benefit comes from those selectors used often and perform heavy logic. Since connect selectors are run very frequently, it can save hundreds or thousands of function invocations (multiplied by number of multiple components in the same screen), especially if we don't expect the state to change very often.

Arguments from the "de facto" solution reselect: https://github.com/reactjs/reselect#motivation-for-memoized-selectors

The hope was for createSelector to be unobtrusive (simple API). With a memory overhead of storing the cache, it's not always appropriate, though I feel it's a nice optimization. In this case, I could see it going either way. I personally don't think it's an inappropriate use of createSelector.

@dmsnell
Copy link
Member

dmsnell commented Nov 8, 2016

it can save hundreds or thousands of function invocations

from the code of the memorized selector @aduth it looked like it might not save us any function calls. it looked like this one was simply checking if a string is empty while the memorized selector also makes several function calls

@aduth
Copy link
Contributor

aduth commented Nov 8, 2016

simply checking if a string is empty

There's more going on in the selector than just that, but sure, there's a balance to be had between the logic of the memoize function and the selector itself.

* @return {Boolean} true if the site's permalinks are editable
*/
export function areSitePermalinksEditable( state, siteId ) {
const site = getRawSite( state, siteId );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be good to leverage getSiteOption here I think.

@youknowriad youknowriad force-pushed the update/reduxify-editor-title branch from ff8b073 to b90fe74 Compare December 7, 2016 15:51
@youknowriad
Copy link
Contributor Author

Rebased and fixed the last notes :)

Copy link
Contributor

@aduth aduth left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Observed strange behavior that may not be specifically related to these changes but more apparent.

  1. Start new post
  2. Enter title
  3. Before post saves, open drafts drawer and select another post
  4. Accept warning that changes will be lost
  5. Observe draft loads correctly
  6. Start new post

Expected: Fresh post
Actual: Title from step 2 is included

* @param {Number} postId Post ID
* @return {Boolean} Whether the edited post has content or not
*/
export function editedPostHasContent( state, siteId, postId ) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally the has (or is, or get) would come first in the selector name as an indicator of the type of value it returns... but I'm struggling to come up with a better name 😄

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tend to agree, but here it's hard to follow this rule and have something meaningful. Naming is hard 😅

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I support putting has in the middle. reads much easier for A has B functions

@youknowriad
Copy link
Contributor Author

@aduth The strange behaviour regarding the remaining title edit while switching post is not directly related to this PR. Fix is here #10177

@youknowriad youknowriad force-pushed the update/reduxify-editor-title branch from b90fe74 to dd97399 Compare January 3, 2017 15:05
@youknowriad
Copy link
Contributor Author

@aduth I rebase this, and since #10177 has been merged, we may be good to go here?

Copy link
Contributor

@aduth aduth left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looked over code and took it for another spin, all appears to work well 👍

I'd much rather the new selectors (and maybe even updated selectors) be moved to state/selectors, but not opposed to merging as-is.

return (
!! editedPost &&
(
some( [ 'title', 'excerpt' ], ( field ) => editedPost[ field ] && !! editedPost[ field ].trim() ) ||
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to change, but I like how Lodash methods handle the falsey case for you gracefully. In this case it could be simplified to:

some( [ 'title', 'expect' ], ( field ) => trim( editedPost[ field ] ) )

!! editedPost &&
(
some( [ 'title', 'excerpt' ], ( field ) => editedPost[ field ] && !! editedPost[ field ].trim() ) ||
/*
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: Per guidelines, multi-line comments which are not docblocks should use // (reference)

* @param {string} content Raw post content
* @return {Boolean} Whether it's considered empty
*/
export function isEmptyContent( content ) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious how we might handle these types of utility functions once selectors are all moved to the state/selectors directory. I personally don't really like them all that much, but seem an unfortunate necessity in some cases. Here, since isEmptyContent is probably only needed by the editedPostHasContent selector, I might imagine it could live alongside the selector in its state/selectors/edited-post-has-content.js file (exported only for purposes of testing, but not to be used elsewhere).

return false;
}

return /\/\%postname\%\/?/.test( permalinkStructure );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Guessing this is partly legacy, but I don't think we need to escape % in the RegExp.

@aduth aduth added [Status] Ready to Merge and removed [Status] Needs Review The PR is ready for review. This also triggers e2e canary tests and wp-desktop tests automatically. labels Jan 4, 2017
@youknowriad
Copy link
Contributor Author

youknowriad commented Jan 5, 2017

@aduth Valuable notes above, I'm moving editedPostHasContent and areSitePermalinksEditable to the global selectors directory before merging.

expect( require( '../' + kebabCase( key ) ) ).to.equal( selector );
const module = require( '../' + kebabCase( key ) );
const defaultExport = module.default ? module.default : module;
expect( defaultExport ).to.equal( selector );
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@aduth I had to update this test to allow global selectors to export "utils" functions for test purpose.

Copy link
Contributor

@aduth aduth Jan 6, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@aduth I had to update this test to allow global selectors to export "utils" functions for test purpose.

Makes sense 👍 If/when we drop babel-plugin-add-module-exports (npm) this can be simplified to:

expect( require( '../' + kebabCase( key ) ).default ).to.equal( selector );

@aduth
Copy link
Contributor

aduth commented Jan 6, 2017

Still tests well for me after latest changes 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Feature] Post/Page Editor The editor for editing posts and pages. [Type] Janitorial
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants