Remove memoise withColors; Simplify API.#6782
Conversation
Can you elaborate on what was not possible? |
| colors: get( settings, [ 'colors' ], DEFAULT_COLORS ), | ||
| }; | ||
| } ), | ||
| ( WrappedComponent ) => { |
There was a problem hiding this comment.
Style: The trailing parentheses placement on the previous line makes it difficult to discern this as the second argument of withSelect or the second entry in the array.
I'd suggest either:
withSelect( ( select ) => {
const settings = select( 'core/editor' ).getEditorSettings();
return {
colors: get( settings, [ 'colors' ], DEFAULT_COLORS ),
};
} ),Or:
withSelect(
( select ) => {
const settings = select( 'core/editor' ).getEditorSettings();
return {
colors: get( settings, [ 'colors' ], DEFAULT_COLORS ),
};
}
),4402706 to
da73bbc
Compare
|
Hi @aduth thank you for the review!
In this case: withColors( 'backgroundColor', 'customBackgroundColor', 'background-color' ), we can use one paramenter withColors( 'backgroundColor' ) and from the string backgroundColor we can compute the strings customBackgroundColor and background-color, they are just string appends and transformations without any map. In this case: withColors( 'textColor', 'customTextColor', 'color' ), no direct systematic string transformation can be applied that works exactly for this case and the previous case. From 'textColor' generating 'color' seems like a hardcoded map and not a systematic string transformation. |
|
Thanks for the explanation. It reveals a couple more questions:
withColors( {
backgroundColor: 'backgroundColor',
textColor: 'color',
} );
// Or: (in a somewhat `classnames` fashion)
withColors( 'backgroundColor', {
textColor: 'color',
} ); |
da73bbc to
553c9c5
Compare
|
Hi @aduth,
I'm not certain about this one, as far as I know in the paragraph we always used textColor + backgroundColor. In button, we used color at the start because we had no textColor. Color, was the background color of the button. When textColor was added I think we started using backgroundColor + textColor.
I like this suggestion, I added a new commit that allows us to use a "classnames" like syntax, and allows us to use the simple syntax e.g: withColors( 'backgroundColor' ); |
| } ), | ||
| edit: compose( [ | ||
| withColors( 'backgroundColor', { textColor: 'color' } ), | ||
| FallbackStyles, |
There was a problem hiding this comment.
Unrelated: I don't think this should be capitalized as though it were a component, since it's not. Confused me thinking we had incorrect usage. Something with a with prefix would be good to reflect it being a higher-order component (admitting there is a bit of confusion on higher-order-components vs. higher-order-component-creators like withFallbackStyles).
There was a problem hiding this comment.
It seems withFallbackStyles is a better name I will rename it in a separate PR.
There was a problem hiding this comment.
Filed #7355 so we don't forget (plus it's a good first bug).
core-blocks/button/edit.js
Outdated
| setTextColor: setColor( 'textColor', 'customTextColor' ), | ||
| }; | ||
| } )( ButtonEdit ); | ||
| export default compose( [ |
There was a problem hiding this comment.
We don't need compose?
export default withColors( 'backgroundColor', { textColor: 'color' } )( ButtonEdit );| * @param {Function} mapGetSetColorToProps Function that receives getColor, setColor, and props, | ||
| * and returns additional props to pass to the component. | ||
| * @param {...(object|string)} args The arguments can be strings or objects. If the argument is an object, | ||
| * it should contain the color attribute name as key and the color context as value. |
There was a problem hiding this comment.
Can we define somewhere what we mean by "color context" in practice? Each time I look at this, I forget what it's for. I see it's primarily used as part of the generated class name modifier e.g. has-green-color ?
There was a problem hiding this comment.
I extended the comment to describe the color context.
| ); | ||
| } | ||
| mergePropsAcc[ `set${ ufColorAttrName }` ] = | ||
| setColorValue( colors, colorAttributeName, customColorAttributeName, setAttributes ); |
There was a problem hiding this comment.
Rather than copying setAttributes from props into state, could instead we assign the prop callback as an instance-bound function?
Similar to what we do in withDispatch:
gutenberg/packages/data/src/index.js
Lines 384 to 393 in eb4fc18
Getting a general sense we might be trying to hard to optimize this with caching copies of relevantAttributes, colors, setAttributes. Maybe we could compare against prevState.mergeProps to see if the attribute value has changed, rather than doing the shallow equality before.
Something like:
return class extends Component {
constructor( props ) {
super( props );
this.state = {};
}
static getDerivedStateFromProps( { attributes, colors, setAttributes }, prevState ) {
return reduce( colorMap, ( mergePropsAcc, colorContext, colorAttributeName ) => {
const ufColorAttrName = upperFirst( colorAttributeName );
const customColorAttributeName = `custom${ ufColorAttrName }`;
const colorName = attributes[ colorAttributeName ];
const customColor = attributes[ customColorAttributeName ];
const colorValue = getColorValue( colors, colorName, customColor );
if ( get( mergePropsAcc, [ colorAttributeName, 'value' ] ) === colorValue ) {
return mergePropsAcc;
}
mergePropsAcc[ colorAttributeName ] = {
name: colorName,
class: getColorClass( colorContext, colorName ),
value: colorValue,
};
mergePropsAcc[ `set${ ufColorAttrName }` ] =
setColorValue( colors, colorAttributeName, customColorAttributeName, setAttributes );
return mergePropsAcc;
}, prevState );
}
render() {
return (
<WrappedComponent
{ ...{
...this.props,
colors: undefined,
...this.state,
} }
/>
);
}
};There was a problem hiding this comment.
Hi @aduth, I changed the code in the direction of your feedback.
582a640 to
1e75443
Compare
| 'withColors' | ||
| ); | ||
| createSetColor( colorAttributeName, customColorAttributeName ) { | ||
| return ( ( colorValue ) => { |
There was a problem hiding this comment.
There's a tab here after the space following return. Not sure why ESLint didn't pick it up.
There was a problem hiding this comment.
Nice catch I made my editor show tabs and spaces all the time, to avoid this problem in the future :)
| [ colorAttributeName ]: colorObj && colorObj.name ? colorObj.name : undefined, | ||
| [ customColorAttributeName ]: colorObj && colorObj.name ? undefined : colorValue, | ||
| } ); | ||
| } ).bind( this ); |
There was a problem hiding this comment.
Is bind valid for an arrow function? Is it needed?
There was a problem hiding this comment.
No needed, arrow functions cannot be rebinded and the bind for this is automatic. It was corrected 👍
| ); | ||
| const prevColorObject = prevState[ colorAttributeName ]; | ||
| const prevColorValue = get( prevColorObject, [ 'value' ] ); | ||
| if ( prevColorValue === colorValue && prevColorValue ) { |
There was a problem hiding this comment.
For what case do we need && prevColorValue ? Can we have a unit test?
There was a problem hiding this comment.
It is a trivial case and the condition is executed all the time, without && prevColorValue during the first execution, both prevColorValue and colorValue are undefined. So we would always execute newState[ colorAttributeName ] = prevColorObject; and never compute the object. I will add this as a comment to make clear why && prevColorValue is needed.
There was a problem hiding this comment.
I also updated the code to use && prevColorObject, this makes the condition more clear.
1a5c00a to
3e8aa09
Compare
tofumatt
left a comment
There was a problem hiding this comment.
I have my usual nitpicky feelings about abbreviated variables names and comments, but this looks good to me assuming little nitpicks are cleaned up.
Maybe @aduth wants to have another look too though, as he did the original reviews. 🤷♂️
| constructor() { | ||
| super( ...arguments ); | ||
| /** | ||
| * Even though, we don't expect setAttributes or colors to change memoizing it is essential. |
There was a problem hiding this comment.
There shouldn't be a comma after "though".
| * @param {...(object|string)} args The arguments can be strings or objects. If the argument is an object, | ||
| * it should contain the color attribute name as key and the color context as value. | ||
| * If the argument is a string the value should be the color attribute name, | ||
| * the color context is computed by applying a kebab case transform to the value. |
There was a problem hiding this comment.
Haha, I've never heard of kebab case 😄 🍖
| */ | ||
| import { getColorValue, getColorClass, setColorValue } from './utils'; | ||
| import { getColorValue, getColorClass } from './utils'; | ||
| import { default as withColorsDeprecated } from './with-colors-deprecated'; |
There was a problem hiding this comment.
Why is this import { default as withColorsDeprecated } from './with-colors-deprecated'; instead of just import withColorsDeprecated from './with-colors-deprecated';?
| } | ||
| createSetters() { | ||
| return reduce( colorMap, ( settersAcc, colorContext, colorAttributeName ) => { | ||
| const ufColorAttrName = upperFirst( colorAttributeName ); |
There was a problem hiding this comment.
ufColorAttrName is a lot of abbreviation–I'm not sure what it means and I tried to read above but it was hard to tell from context.
| ); | ||
| } | ||
| createSetters() { | ||
| return reduce( colorMap, ( settersAcc, colorContext, colorAttributeName ) => { |
There was a problem hiding this comment.
What does Acc stand for? Sorry, a lot of these abbreviations are totally new to me and might throw other new contributors off.
There was a problem hiding this comment.
There was a problem hiding this comment.
Ah, it would be nice to expand the variable name then, that is a lot more instructive 🤷♂️
There was a problem hiding this comment.
Also noting that we have a horrible inconsistency with our naming of the Array#reduce accumulator variable throughout the codebase, often also referred to as memo or result.
There was a problem hiding this comment.
I filed #7378 for it; setting it to accumulator everywhere works for me; we could do something like settersAccumulator here and elsewhere if we want more clarity and I think that'd be fine.
| }; | ||
| } | ||
|
|
||
| static getDerivedStateFromProps( { attributes, colors }, prevState ) { |
There was a problem hiding this comment.
I also think we have space for previous instead of prev 😉
| const prevColorObject = prevState[ colorAttributeName ]; | ||
| const prevColorValue = get( prevColorObject, [ 'value' ] ); | ||
| /** | ||
| * && prevColorObject checks that a previous color object was already computed. |
| if ( isFunction( args[ 0 ] ) ) { | ||
| deprecated( 'Using withColors( mapGetSetColorToProps ) ', { | ||
| version: '3.2', | ||
| alternative: 'withColors( colorAttributeName, { secondColorAttributeName: \'color-context\' }, ... )', |
There was a problem hiding this comment.
It would be nicer to use double-quotes here rather than escaping the single-quotes inside. Is that against the style rules? (If so: ouch. 😅)
|
|
||
| const colorMap = reduce( args, ( colorObj, arg ) => { | ||
| return { | ||
| ...colorObj, |
| } ), | ||
| edit: compose( [ | ||
| withColors( 'backgroundColor', { textColor: 'color' } ), | ||
| FallbackStyles, |
There was a problem hiding this comment.
Filed #7355 so we don't forget (plus it's a good first bug).
3e8aa09 to
3c7c372
Compare
|
Merging this so we have more time and testing before the next release. If there is any other improvement that we find would improve the code/API I will gladly open a follow-up PR. |
This PR simplify the withColors HOC so we can avoid the usage of memoize while still having a correct implementation without unnecessary rerenders.
It is based on a solution proposed by @aduth in #6686 (comment). Unfortunately passing a single parameter as suggested e.g.:
withColors( 'color' ),was not possible for all cases so this API uses three parameters. I think we may add a single parameter version in the future. It may handle this casewithColors( 'backgroundColor', 'customBackgroundColor', 'background-color' ),as everything can be derived from backgroundColor but not this case `withColors( 'textColor', 'customTextColor', 'color' ),. So having the three parameters version is a must.How has this been tested?
No noticeable changes are expected. Verify setting colors (and custom colors) in paragraph and button still work as expected. Use the Highlight updates browser feature and check no unexpected rerenders are happening (when compared to master).
Revert changes in paragraph and button and test things still work as before, (we are backcompatible)