-
Notifications
You must be signed in to change notification settings - Fork 4.2k
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
Packages: Refactor Common Block Code #16229
Conversation
…deps. - Add `createHigherOrderComponentWithMergeProps` for making it easier to make higher order components from hooks. - Add the `useCustomCompareDep`, `useShallowCompareDep`, and `useDeepCompareDep` hooks for making it easier to use non-primitives in hooks dependency arrays.
…or HOCs. - Added the `useAttributePicker` hook to abstract away common block code and used it to refactor the color higher order components. They are now written with the new exported hooks, `useColors` and `useCustomColors`.
Refactoring use-font-sizes.js export default function useFontSizes( fontSizeNames ) {
const fontSizes = useSelect(
( select ) => select( 'core/block-editor' ).getSettings().fontSizes || [],
[]
);
return useAttributePicker( {
names: fontSizeNames,
valuesEnum: fontSizes,
findEnumValue: ( fontSizeObject, newFontSizeValue ) =>
fontSizeObject.size === newFontSizeValue,
mapEnumValue: ( fontSizeObject ) => fontSizeObject.slug,
mapAttribute: ( fontSizeSlugOrCustomValue ) => {
const foundFontSizeObject = fontSizes.find(
( fontSizeObject ) => fontSizeObject.slug === fontSizeSlugOrCustomValue
) || { size: fontSizeSlugOrCustomValue };
return {
...foundFontSizeObject,
class:
foundFontSizeObject.slug &&
`has-${ kebabCase( foundFontSizeObject.slug ) }-font-size`,
};
},
} );
} with-font-sizes.js export default function withFontSizes( ...fontSizeNames ) {
return createHigherOrderComponentWithMergeProps(
() => useFontSizes( fontSizeNames ),
'withFontSizes'
);
} Relevant commit:
|
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.
I'm having some hard time reviewing this PR because it's big and because it's not clear how it achieves the goal.
Questions on my mind:
-
I have a hard time understanding how
useAttributePicker
works. What would be the minimal example of usage this hook? Is it just a low-level hook never meant to be used directly? In that case, is it a necessary abstraction or just a way to factorize code? -
How does
withColors
defer fromuseColor
? Can we have an example of a block refactored to useuseColor
? I think at the time of writing, they have the exact same functionality but I was wondering if we can push the abstraction further. How can we absorb more boilerplate? Say I have a block and I want to support block level colors. Should I have a hook that also returns the "element" to add to the block inspector (To clarify, I'm not sure it's a good idea but that's the kind of question I'd appreciate to see explored, the more we simplify for blocks to support "common features", the better is)?
Also, how can we split this PR into smaller PRs to ease reviews? |
Thanks for taking this on. I had a line of thought similar to Riad's, so I'll wait for some answers there before resuming the review. |
It's the result of consolidating the logic that was in const MyAlignableComponent = () => {
const attributePicker = useAttributePicker( {
names: [ 'topAlignment', 'bottomAlignment' ],
valuesEnum: [ 'left', 'right' ],
mapAttribute: ( alignment ) => `has-align-${ alignment }`, // has-align-left || has-align-right
} );
return (
<>
<div className={ attributePicker.topAlignment }>Top</div>
<button onClick={ () => attributePicker.setTopAlignment( 'left' ) }>
Left
</button>
<button onClick={ () => attributePicker.setTopAlignment( 'right' ) }>
Right
</button>
<div className={ attributePicker.bottomAlignment }>Bottom</div>
<button onClick={ () => attributePicker.setBottomAlignment( 'left' ) }>
Left
</button>
<button onClick={ () => attributePicker.setBottomAlignment( 'right' ) }>
Right
</button>
</>
);
};
Yeah, apart from the fact that one is a HOC and one is a hook, the API is the same. We could make There are surely lots of other block feature helpers I can come up with. I'll be mostly thinking about FSE for the moment, but some ideas that come to mind are: form <-> attribute binding, responsive helpers, pagination for huge attributes, tools for notifications and tooltips, hooks for any stateful WordPress API.
We can have a PR for each of the main hooks added here:
Should I break this into 3 PRs and add the |
Having myself a propensity to perhaps too eagerly generalise, I think we could approach this problem depth-first rather than breadth-first. What I mean by this is that, as Riad suggests, we should isolate the first use case—say, colors—and see how far we can go with a single dedicated hook for it. That entails studying current uses of Otherwise, we run the risk of merely porting one kind of enhancer (HoC) to the other (hook) and, with it, any usage issues.
Reiterating: by going deep with colours, then moving to fonts, we can build a better picture of how these should relate. To be clear: if in the end it turns out that we just power both hooks with a
I think this is a very good experiment!
Based on my previous feedback, I'd start with a PR just for colours, then work on fonts in an orthogonal PR, and then iterate in a PR dependent on the former two. |
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.
I left some comments related to the workaround colors. I guess it is a nice idea to separate this PR in multiple PR's. I think it would also be nice to not focus on the current API. It was built in a time where the UI was different and we did not had the number of different use cases we have now so it was expanded but always taking back-compatibility in consideration.
I think if we go for a hooks implementation we don't need to follow the current API, feel free to try a different one.
The objective seems to be to reduce the amount of code a block needs to implement color functionality. I thought about that problem and it seems a possibility is for the hook or HOC to return a react element with a PanelColorSettings component with all the props already passed to it, or to add context communication between useColors/withColors and PanelColorSettings. If the user just did the component would retrieve things from the context that were set by useColors/withColors, a context approach would also allow the contrast checking functionality to use it. These are just some ideas I have, feel free to follow a different path.
* | ||
* @param {string[]} attributePickerOptions.names The list of attributes to manage. | ||
* | ||
* @param {*[]} attributePickerOptions.valuesEnum The list of values attributes can be set to. |
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.
valuesEnum may make it look like we are referring to a pure enumeration e.g: one may expect it to be ['red', 'blue']
while its form is an array of objects [{ name: 'Red', slug: 'red', color: '#ff0000' },{ name: 'Blue', slug: 'blue', color: '#0000ff' }]
. Maybe we can call it valueSet?
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.
I think you can also have an ENUM of objects. Here is a good analogy for the difference between the two:
) || { color: colorSlugOrCustomValue }; | ||
|
||
let colorClass; | ||
if ( foundColorPaletteObject.slug ) { |
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.
This function seems complex mainly because of retriving the colorContextName. Would something like this work?
const foundColorType = find( colorTypes, ( colorType ) => !! colorType && !! colorType[ name ] );
const colorContextName = foundColorType[ name ] || name;
Given that the mapping may execute frequently we may also consider computing an object based on colorTypes whose keys are the name and whose value is the color context or even the class directly. The names passed to useAttributePicker would then be just the keys of that object.
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.
Yeah something in that style does read way better.
Given that the mapping may execute frequently we may also consider computing an object based on colorTypes whose keys are the name and whose value is the color context or even the class directly. The names passed to useAttributePicker would then be just the keys of that object.
Also might be a good idea.
* @param {*[]} [attributePickerOptions.mapAttributeDeps] List of items that, if they fail a shallow equality test, will | ||
* cause attributes to be mapped again. Useful if your `mapAttribute` has closures. | ||
* | ||
* @param {{ string: Function }} [attributePickerOptions.utilsToBind] Object with functions to bind to `valuesEnum` and merge into the |
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.
The relationship between utilsToBind and the other properties is not obvious, also the fact that we always bind valuesEnum seems really restrict, I may want to find to the selected value for example.
It's not hard for someone using this hook to create util functions and bind them to properties of the returned object (useMemo call where the returned object is a dependency, and return a new object that uses the returned object and adds the functions with binds to properties returned from useAttributePicker). Maybe we can remove utilsToBind to simplify this component.
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.
I thought binding to valuesEnum
could be common after seeing the current colors implementation. But I wasn't fully convinced by this API either. It feels like it's "forcing" the generalization.
Thanks for all the feedback everyone! 😄 I'll close this in favor of the new approaches proposed. |
I agree with @mcsf that we should focus on how can we streamline the use of specific block tools (like colors) so that the developer experience is tangibly better. That includes less boilerplate, less opportunities for messing up, easier to maintain, and also more flexible to tweak if needed. One example of the latter is the Alignments toolbar — we've wanted to group them all in a dropdown (instead of being a row in the toolbar) for certain blocks, and that is not trivial to do in the current setup without affecting all usages. |
cc @youknowriad
Relates to #15450
Description
This PR begins to explore a new way to share Common Block Code using React hooks.
In
@wordpress/block-editor
:It introduces a low level hook,
useAttributePicker
, that is very flexible and can be easily used to build higher level hooks likeuseColor
anduseCustomColor
which replaced the previous implementation ofwithColors
andcreateCustomColorsHOC
with a fraction of the code.use-colors.js
with-colors.js
In
@wordpress/compose
:It adds
createHigherOrderComponentWithMergeProps
(shown above) for making it easier to make higher order components from hooks. This will become a more and more common pattern as we replace the implementation of more and more HOCs with their hooks counterparts.It also adds the
useCustomCompareDep
,useShallowCompareDep
, anduseDeepCompareDep
hooks for making it easier to use non-primitives in hooks dependency arrays. This was needed foruseAttributePicker
.E.g.
Updates
How has this been tested?
All of the tests suites succeeded, but new unit tests have to be written for the new functions once consensus on the APIs is reached.
Types of Changes
New Features
useAttributePicker
hook to abstract away common block code and use it to refactor the color and font size higher order components. They are now written with the new exported hooks,useColors
,useCustomColors
, anduseFontSizes
.createHigherOrderComponentWithMergeProps
for making it easier to make higher order components from hooks.useCustomCompareDep
,useShallowCompareDep
, anduseDeepCompareDep
hooks for making it easier to use non-primitives in hooks dependency arrays.Checklist: