-
Notifications
You must be signed in to change notification settings - Fork 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
React 18: Fix @automattic/global-styles package #77319
Conversation
ba2b3c3
to
4b85d57
Compare
4b85d57
to
7c64d8e
Compare
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.
Overall, this looks good to me. I really like the gutenberg-bridge approach, which should ideally make it easier to change things in the future. I tested all the features you mentioned in the PR, and they are all testing great for me! Do we use any of these in the editor via ETK?
One thing I'm worried about is that the unlock API package version is from a newer release than the other Gutenberg packages. However, the unlock package doesn't have dependencies on other packages in Gutenberg, so I think it will be ok.
I think that most of the CI failures are from the underlying React PR and not from this one.
}, | ||
[ setUserConfig ] | ||
); | ||
return [ !! true, userConfig, setConfig ]; |
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.
What's the purpose of !! true
here?
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 indicate the config is ready. We don't want to fetch the current user config from the server at the current stage, so it's always ready at the begining
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 Noah was suggesting that we could remove the !!
boolean casting, since it's already a boolean.
import type { GlobalStylesObject } from '../types'; | ||
|
||
const { unlock } = __dangerousOptInToUnstableAPIsOnlyForCoreModules( | ||
'I know using unstable features means my plugin or theme will inevitably break on the next WordPress release.', |
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 hadn't realized they added this message, but I'm definitely a fan since it forces us to be explicit about it 😅
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.
Yup, I love it too 😉 Great tactics from a devex perspective. 👍
} | ||
}; | ||
|
||
return mergeWith( {}, base, user, mergeTreesCustomizer ); |
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.
We would like to avoid lodash if possible, but this could be a use case which is too difficult to replicate natively.
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.
Yes, we have to take time to see how to support mergeWith
natively but I think it's not the goal of this PR. So, I'd prefer to do it later 🙂
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'd also love to not see new Lodash usage introduced.
We've been using deepmerge
as a replacement for merge
and mergeWith
in Gutenberg and it mostly works well. In this case, it appears to be a simple scenario where we want to disable merging arrays and just have them overriden. This can be achieved with something like:
import deepmerge from 'deepmerge';
import { isPlainObject } from 'is-plain-object';
deepmerge( obj1, obj2, {
isMergeableObject: isPlainObject,
} );
and if you want to avoid using is-plain-object
for some reason, you could do something like (untested):
import deepmerge from 'deepmerge';
deepmerge( obj1, obj2, {
isMergeableObject: ( value ) => ! Array.isArray( value ),
} );
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.
Updated! TIL: deepmerge
👍
// return user; | ||
const { user, setUserConfig } = useContext( GlobalStylesContext ); | ||
useEffect( () => { | ||
if ( ! enabled ) { |
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.
Note: this file changed on trunk, so it might need to be refreshed. (I resolved the merge conflict in the base PR to maintain compatibility with this branch)
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.
Rebased. Thanks for noting that!
69a48b5
to
ce2e393
Compare
ce2e393
to
335e26e
Compare
} | ||
}; | ||
|
||
return mergeWith( {}, base, user, mergeTreesCustomizer ); |
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'd also love to not see new Lodash usage introduced.
We've been using deepmerge
as a replacement for merge
and mergeWith
in Gutenberg and it mostly works well. In this case, it appears to be a simple scenario where we want to disable merging arrays and just have them overriden. This can be achieved with something like:
import deepmerge from 'deepmerge';
import { isPlainObject } from 'is-plain-object';
deepmerge( obj1, obj2, {
isMergeableObject: isPlainObject,
} );
and if you want to avoid using is-plain-object
for some reason, you could do something like (untested):
import deepmerge from 'deepmerge';
deepmerge( obj1, obj2, {
isMergeableObject: ( value ) => ! Array.isArray( value ),
} );
@@ -1,7 +1,6 @@ | |||
// import { GlobalStylesContext } from '@wordpress/edit-site/build-module/components/global-styles/context'; | |||
// import { mergeBaseAndUserConfigs } from '@wordpress/edit-site/build-module/components/global-styles/global-styles-provider'; | |||
import { isEmpty, mapValues } from 'lodash'; |
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 bet those Lodash usages should be pretty straightforward to remove. Let me know if I can aid with suggestions.
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.
Done!
Button: ( | ||
<Button | ||
variant="link" | ||
target="__blank" |
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 assume you meant:
target="__blank" | |
target="_blank" |
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.
Fixed. But I found there are lots of __blank
in our codebase 😂
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.
Bummer. It's a good opportunity to fix them 😅 Fixed them all in #77484
Button: ( | ||
<Button | ||
variant="link" | ||
target="__blank" |
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.
target="__blank" | |
target="_blank" |
<ExternalLink href={ localizeUrl( 'https://wordpress.com/support/com-vs-org/' ) } /> | ||
<ExternalLink | ||
href={ localizeUrl( 'https://wordpress.com/support/com-vs-org/' ) } | ||
children={ null } |
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.
Why is that necessary? children
will be undefined
by default.
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 problem is that some components require children via TS, which is hard to support in some cases (like when interpolating)
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.
Makes sense, thanks.
Intuitively, I would suggest resolving this by altering the type to make the children
prop optional, but a double-check at the ExternalLink
code makes me think twice about it. Why do we have an external link without any text? Sounds like it could be an accessibility issue.
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.
Actually, it has as it used by createInterpolateElement
function to replace the custom-defined tag with the specified component, and there is the text between the tag that is the text for the ExternalLink
. For example,
// Input
createInterpolateElement(
'{{a}}Learn more{{/a}}',
{ a: <ExternalLink /> }
);
// Output
<ExternalLink>Learn more</ExternalLink>
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.
Cool! Then (sorry for going in circles 😅 ), I'd ask the following question: what do you feel is the best in this case - passing children={ null }
, ignoring the TS rule, or fixing the ExternalLink
type to support an optional children
prop`? If you picked one of those, why did you prefer that choice?
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 the ExternalLink
still needs the children
so we should not make it optional. So, I'd prefer to pass children={ null }
or even use ts-expect-error
to bypass the rule and see whether we can fix this issue via createInterpolateElement
as it should let TS know it will pass the children to that component. However, I'm not whether it's possible or not 😓
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.
Sounds good, thanks for confirming, let's keep the children={ null }
then 👍 No need to overcomplicate things.
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 also asked about this in Slack a couple weeks ago (it's a broader issue uncovered in the react 18 update): p1684448743933079-slack-CK1QYS6P6
<ExternalLink href={ localizeUrl( 'https://wordpress.org/support/forums/' ) } /> | ||
<ExternalLink | ||
href={ localizeUrl( 'https://wordpress.org/support/forums/' ) } | ||
children={ null } |
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.
Why is that necessary? children
will be undefined
by default.
{} as GlobalStylesObject | ||
) | ||
); | ||
}, [ globalStyles ] ); |
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.
Is there a chance globalStyles
will be a new object on every re-render and setUserConfig
will be called every time? Should we memoize the config merging and call setUserConfig
only if the memoized version changes?
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.
We handle this case outside when using this hook.
Should we memoize the config merging and call setUserConfig only if the memoized version changes?
How could we detect the memoized version changes? If the globalStyles
is a new object on every re-render, then the memoized version will be changed as well.
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 was mostly thinking out loud and leaving this for your consideration, but if it's handled already, no need to worry about it 👍
import type { GlobalStylesObject } from '../types'; | ||
|
||
const { unlock } = __dangerousOptInToUnstableAPIsOnlyForCoreModules( | ||
'I know using unstable features means my plugin or theme will inevitably break on the next WordPress release.', |
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.
Yup, I love it too 😉 Great tactics from a devex perspective. 👍
textAlign: 'center', | ||
} } | ||
> | ||
{ translate( 'Default' ) } |
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.
Note that such simple strings often needs a context for the translators to know how to translate it in various languages.
8fb2e94
to
5eef674
Compare
I'll go ahead and merge this into the main React 18 PR so that we have all the fixes in one place. |
Thank you! |
I've been doing a lot more work on the React 18 PR, and in an effort to split out much as possible from that PR, I've pinned the WordPress packages to a lower version (versions from around December 2022). The benefit is that we won't need to bundle this fix into the React 18 PR, but we'll still need to land it in the PR after that when we update the WordPress packages to the current versions. So I restored the branch in anticipation of that |
Reopened to merge onto the wordpress package updates: #78715 |
Related to #77048, #77046 -- this change fixes compatibility with WordPress packages after the react 18 update
Proposed Changes
gutenberg-bridge
folder inside the@automattic/global-styles
package andmergeBaseAndUserConfigs
function as Gutenberg doesn't export itwithExperimentalBlockEditorProvider
to wrap the component withExperimentalBlockEditorProvider
and eliminate the compatStyles. The reason is thecompatStyles
are not required on Calypso and it will cause warnings since some ofcompatStyles
might not have anid
so that we cannot use it as key when rendering.@wordpress/private-apis
to ensure we have the consistent__private
symbol and avoid we're not able to unlock the private APIs due to the unequal private symbol. Or is dedupe enough?There is a warning from Gutenberg below, but it looks like GB won't encounter this one 🤔
Warning: validateDOMNesting(...): <svg> cannot appear as a child of <head>.
Testing Instructions
Theme Showcase
/themes/<your_site>
Design Picker
/setup?siteSlug=<your_site>
Pattern Assembler
/setup?siteSlug=<your_site>
Pre-merge Checklist