-
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
Block Editor: use shallow memo for prioritized inserter blocks #65737
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,7 @@ | ||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { useLayoutEffect, useMemo, useState } from '@wordpress/element'; | ||
import { useLayoutEffect, useState } from '@wordpress/element'; | ||
import { useRegistry } from '@wordpress/data'; | ||
import deprecated from '@wordpress/deprecated'; | ||
import isShallowEqual from '@wordpress/is-shallow-equal'; | ||
|
@@ -16,12 +16,21 @@ import { getLayoutType } from '../../layouts'; | |
|
||
const pendingSettingsUpdates = new WeakMap(); | ||
|
||
// Creates a memoizing caching function that remembers the last value and keeps returning it | ||
// as long as the new values are shallowly equal. Helps keep dependencies stable. | ||
function createShallowMemo() { | ||
let value; | ||
return ( newValue ) => { | ||
if ( value === undefined || ! isShallowEqual( value, newValue ) ) { | ||
value = newValue; | ||
} | ||
return value; | ||
}; | ||
} | ||
|
||
function useShallowMemo( value ) { | ||
const [ prevValue, setPrevValue ] = useState( value ); | ||
if ( ! isShallowEqual( prevValue, value ) ) { | ||
setPrevValue( value ); | ||
} | ||
return prevValue; | ||
const [ memo ] = useState( createShallowMemo ); | ||
return memo( value ); | ||
} | ||
|
||
/** | ||
|
@@ -77,10 +86,7 @@ export default function useNestedSettingsUpdate( | |
// otherwise if the arrays change length but the first elements are equal the comparison, | ||
// does not works as expected. | ||
const _allowedBlocks = useShallowMemo( allowedBlocks ); | ||
|
||
const _prioritizedInserterBlocks = useMemo( | ||
() => prioritizedInserterBlocks, | ||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
const _prioritizedInserterBlocks = useShallowMemo( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's a good idea to use function useMemoEx( callback, compare ) {
const memoRef = useRef( undefined );
const newValue = callback();
if ( memoRef.current === undefined || ! compare( memoRef.current, newValue ) ) {
memoRef.current = newValue;
}
return memoRef.current;
} I'm pretty sure we already have such a hook (memo with custom compare) defined somewhere. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's a good point 👍 I've seen you working on it in another repository. Feel free to open a PR, I think it can be improved separately. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @jsnajdr the implementation you're proposing is unsafe because you're reading/writing refs in render, which is forbidden by the React Gods (can mess things up in concurrent rendering trees). When doing render-time computations, you simply can't use refs. However, many use cases for render-time refs can be replaced by derived state, which is a technique consisting of calling a setState at render time, which causes the render function to instantly re-run synchronously, skipping all effects and other work of the kind. This is fine, as long as the state is guaranteed to settle, otherwise it can result in an endless loop. Can't find it right now, but the technique is officially listed somewhere in the official, updated docs. Running a component's render function is typically pretty much free, so it'd be rare if this technique is a problem for performance. Re-rendering is a tool, not a danger to avoid. It'd be another story imo if you found a specific perf problem somewhere that is caused by excessive re-rendering, that can happen but anything earlier than that feels like premature optimization (or even worsening, as sometimes re-rendering is the better option). That's my 2cents anyway :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Some context for the original Aside: The usehooks.com has also updated its implementation of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I made this about 3 years ago: https://codesandbox.io/p/sandbox/previous-vs-previous-value-t7t3y?file=%2Fsrc%2FApp.tsx There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't agree that breaking the rules to prevent renders without a clear performance issue is the pragmatic and less ideological way, I'd argue it's the opposite. That said, we can agree to disagree on that too :P Just hope I don't have a "told you so" moment a few months down the line. No hard feelings either way! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In the general case, I do agree that we shouldn't be disobeying the React Gods without a substantial upside, and without a mechanism to catch problems when things start to break. Especially because this seems to be the type of thing where the kinds of bugs that could possibly occur in the future are very subtle. I'm not privy enough of React internals to assess how much of a special exception this is to the general case. In the general case, just because an official hook adopts an implementation internally right now doesn't necessarily make it safe to imitate in userland. Could they rewrite the internal There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it's helpful to try to really understand why exactly reading/writing refs during render can be dangerous. The metaphors about rules written by gods are counterproductive, and also not completely innocent, because they suggest that we're dealing with some revealed truths that are inaccessible to reason. That's not true. One example of a bad component is this: function Foo( { value } ) {
const valueRef = useRef();
valueRef.current = value;
return <button onClick={ () => { console.log( valueRef.current ); } } />
} Here, if you render This means that an aborted render of But not all ref reads/writes are dangerous like this. The React docs contain this example that gives us a first piece of evidence that the rules are not absolute. function Video() {
const playerRef = useRef(null);
if (playerRef.current === null) {
playerRef.current = new VideoPlayer();
}
// ... Here we read and write a ref during render, but the docs say this is fine!
Indeed, if a tentative render that's going to be aborted initializes the Now let's check if function useMemo( create, deps ) {
const prevState = useRef();
if ( prevState.current === undefined || ! areDepsEqual( prevState.current[ 1 ], deps ) ) {
prevState.current = [ create(), deps ];
}
return prevState.current[ 0 ];
} Two things to note here:
That means that the That's why the native There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @jsnajdr Thanks for the explanation! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks y'all for the feedback! I agree with @jsnajdr and will move to ship this, and will keep an eye in the future. |
||
prioritizedInserterBlocks | ||
); | ||
|
||
|
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.
There is a side effect here at render time, comparable to reading/writing a ref, which makes this unsafe for concurrency and a future source of tearing.
The only reason this doesn't trigger a React compiler error is that their linter is not accounting for this more obscure way of setting and reading non-stateful values, but it is no different than a ref-based solution and the error applies all the same.