During a recent debugging session where I was trying to discover why e2e tests for code that uses @wordpress/data are failing, I found out that we often use the useSelect hook in a suboptimal way. One that either introduces outright bugs, or is slower and causes more React rerenders than it needs to.
In this post I’m describing several best practices that aim to prevent some common issues. Hopefully it’s helpful and gives you a better understanding of one of WordPress core JavaScript libraries.
Always call selector functions inside the callback
Suppose the onboard store has a getSiteTitle() selector. It might be tempting to call it in a React component like this:
function Title() {
const { getSiteTitle } = useSelect( ( select ) => select( 'onboard' ), [] );
return <h1>{ getSiteTitle() }</h1>;
}
But this code is buggy — the component will not be reliably rerendered when the store’s siteTitle state changes and will keep showing the old value.
Why? useSelect doesn’t just read the desired values from the store, which is the obvious and visible part of what it does. It also establishes a subscription to the store and triggers the rerender of the component when the relevant parts of the state change. What does “relevant” mean here? The precise condition is: when the new return value of useSelect is not shallowly equal to the previous one.
In our case, the return value is the { getSiteTitle } object and the getSiteTitle property value never changes. It’s still the same function. It’s only the return value of that function that changes, but that’s not what we are checking. The return value is always shallowly equal to the previous one.
The Title component will be rerendered with the new siteTitle value only when the rerender is triggered by something else. Maybe the component has some internal state that changes, a prop changes, or the rerender is triggered merely by a parent component rerendering.
The fixed component looks like this:
function Title() {
const { siteTitle } = useSelect( ( select ) => ( {
siteTitle: select( 'onboard' ).getSiteTitle()
} ), [] );
return <h1>{ siteTitle }</h1>;
}
Here useSelect triggers a rerender whenever the old siteTitle and new siteTitle are different, just as we’d expect.
Calling selectors inside event handlers
There is, however, one case where returning the selector function itself from useSelect makes sense: when the selector is called inside an event handler, to get data from the store that are valid at the time when the event handler is called (as opposed to the time when the component was last rendered. Then this code works:
const { getSiteTitle } = useSelect( ( select ) => select( 'onboard' ), [] );
function onClick() {
recordAnalyticsEvent( 'click', { site: getSiteTitle() } );
}
return <Button onClick={ onClick } />;
This code works, but we can do better! One thing that’s suboptimal is that the useSelect call will establish a subscription to the onboard store, and the select( 'onboard' ) callback will be executed on every update in that store. But that’s all pointless work because the set of the store’s selectors is constant for the entire lifetime of the store. The getSiteTitle function is guaranteed to be always the same.
useSelect has a special form where it just returns the set of selectors, without any reactivity:
const { getSiteTitle } = useSelect( 'onboard' );
You can also write this, which is exactly the same thing:
const { getSiteTitle } = useRegistry().select( 'onboard' );
This code is a simple map lookup in the store registry that returns the store’s selectors, nothing else.
Select the data you need inside the callback
Now suppose the onboard store maintains value for several fields like siteTitle, siteDesign and siteDomain. And it provides a getState() selector that returns an object will all these fields together. Then you might get the siteTitle value like this:
function Title() {
const { siteTitle } = useSelect( ( select ) => select( 'onboard' ).getState() );
return <h1>{ siteTitle }</h1>;
}
This code behaves correctly and doesn’t cause bugs with missed updates, like the getSiteTitle example, but its performance is suboptimal. Rerenders will be triggered too often.
Although the component is interested only in the siteTitle value, useSelect doesn’t know that. It’s asked to return the entire getState(), including the siteDesign and siteDomain values. And it will trigger a rerender whenever any of them changes, even when siteTitle remains the same.
A more performant version would be:
function Title() {
const { siteTitle } = useSelect( ( select ) => {
const state = select( 'onboard' ).getState();
return { siteTitle: state.siteTitle };
}, [] );
return <h1>{ siteTitle }</h1>;
}
I can see why the slower version may look more intuitive and elegant: the faster version is not as concise, you’ll often find yourself fighting with the “siteTitle is already declared in the upper scope” ESLint error. But it’s faster.
Another variation of the same principle is this component:
function ContinueButton() {
const { siteTitle } = useSelect( ( select ) => ( {
siteTitle: select( 'onboard' ).getSiteTitle()
}, [] );
return <button disabled={ siteTitle.length === 0 }>Continue</button>;
}
This button will rerender every time siteTitle changes, e.g., as you type into an input field. But most of these rerenders will be wasted, because the disabled prop will remain true. It’s better to calculate the boolean derived value inside the useSelect callback:
function ContinueButton() {
const { hasTitle } = useSelect( ( select ) => ( {
hasTitle: select( 'onboard' ).getSiteTitle().length > 0
}, [] );
return <button disabled={ ! hasTitle }>Continue</button>;
}
Prefer returning objects with properties from the callback
You could rewrite the Title example into a more concise form:
function Title() {
const siteTitle = useSelect( ( select ) => select( 'onboard' ).getState().siteTitle );
return <h1>{ siteTitle }</h1>;
}
Can I return the siteTitle value directly instead of the { siteTitle } object? Is it a good idea?
The answer is that this will almost always work, changes of siteTitle will almost always be correctly detected, but not 100% of the time.
The return values will be compared using the shallow-comparison function from @wordpress/is-shallow-equal, and it depends whether that library can compare values of your data type correctly. Consider this counterexample where the library will fail:
const { default: eq } = require( '@wordpress/is-shallow-equal' );
function get() {
return this.value;
}
function createBox( value ) {
const rv = { get };
Object.defineProperty( rv, 'value', { enumerable: false, value } );
return rv;
}
const one = createBox( 1 );
const two = createBox( 2 );
console.log( `Are ${ one.get() } and ${ two.get() } equal? ${ eq( one, two ) }` );
If you try to run this script in Node.js, you’ll see a surprising result:
Are 1 and 2 equal? true
The value property is semi-private, and Object.keys won’t return it. The shallow compare function will see only the get property which is always the same.
If you store instances of createBox in your data store, useSelect might fail to see changed values.
Another fun way how to implement objects with hidden private properties and trick @wordpress/is-shallow-equal is:
const valueMap = new WeakMap();
function get() {
return valueMap.get( this );
}
function createBox( value ) {
const rv = { get };
valueMap.set( rv, value );
return rv;
}
This technique is used by Babel to transpile JavaScript private class properties, so expect to see these weakmaps in your transpiled code soon 🙂
You might say that you only use nice objects and strings and numbers in your state and you’d be right. Returning them directly from useSelect will be fine. But shallow-comparing arbitrary objects can become messy and one day it might backfire. On the other hand, returning an object to be destructured is guaranteed to be always safe.
Be careful about transforming data inside the callback
The following useSelect call will have suprising behavior. It will cause the component to rerender on each update in the taxonomies store, even if the tags haven’t changed at all:
const { tagNames } = useSelect( ( select ) => {
const tags = select( 'taxonomies' ).getTags();
return { tagNames: tags.map( ( t ) => t.name ) };
}, [] );
This happens because every invocation of the callback returns a different array (the result of .map) even though the raw tags array in the store hasn’t changed. Because the returned array is not equal (===) to the previous value, it is detected as a change that needs to trigger a re-render.
The solution to this problem is to move the data transformation outside the useSelect hook, and wrap it in useMemo:
const { tags } = useSelect( ( select ) => ( {
tags: select( 'taxonomies' ).getTags(),
} ), [] );
const tagNames = useMemo( () => {
return tags.map( ( t ) => t.name );
}, [ tags ] );
This will cause a re-render only when the raw tags really change.
Do all selections from the same store in one callback
Which one of the following is better?
const { siteTitle } = useSelect( ( select ) => ( {
siteTitle: select( 'onboard' ).getSiteTitle()
}, [] );
const { siteDesign } = useSelect( ( select ) => ( {
siteDesign: select( 'onboard' ).getSiteDesign()
}, [] );
or
const { siteTitle, siteDesign } = useSelect( ( select ) => {
const store = select( 'onboard' );
return {
siteTitle: store.getSiteTitle(),
siteDesign: store.getSiteDesign(),
};
}, [] );
The answer is that the second one is faster and it also uses resources more economically. Two calls to useSelect will make the component establish two subscriptions to the data store. On each change, the subscription handler inside useSelect will be called twice, and at least one of the calls will be redundant and wasted.
The fact that every useSelect hook call establishes its own store subscription is a weak spot of the Redux architecture, performance-wise. If you’re writing a component that can get mounted many times in the editor, like when registering an editor.BlockEdit filter that wraps every instance of every block, you should be aware of how many store subscriptions are being created. Without care, their numbers can grow into thousands and tens of thousands.
What about selecting from multiple stores?
When your component selects from multiple stores, and if some of the selected values are used only conditionally, there are two facts to consider:
- Store subscriptions are granular, the
useSelecthook subscribes to each store individually. - Store subscriptions are established only when the corresponding
select( store )call is really executed.
For example, consider this useSelect call:
const showBlockSidebar = useSelect( ( select ) => {
const sidebarOpened = select( 'editor' ).isSidebarOpened();
if ( ! sidebarOpened ) {
return false;
}
return select( 'blocks' ).hasSelection();
}, [] );
This code returns a boolean value that says whether “the block sidebar is opened”. It’s a combination of two conditions: whether the editor sidebar is opened at all, and whether it should show a block sidebar UI (that happens only when a block is selected).
There are a few notable details about this hook call. First, If the sidebarOpened value, selected from the editor store, is false, the select( 'block' ) call is not going to be executed. That means that the hook won’t subscribe to the blocks store and the callback won’t be executed on a blocks store update. Because these updates would be irrelevant anyway, they can’t change the return value. The blocks store subscription will be established just-in-time only when the sidebarOpened value becomes true. This way we can optimize the number of store subscriptions and eliminate these that are guaranteed to be redundant.
Second, in this case we select from both stores in one useSelect hook. The alternative would be:
const sidebarOpened = useSelect( ( select ) => select( 'editor' ).isSidebarOpened(), [] );
const hasSelection = useSelect( ( select ) => select( 'blocks' ).hasSelection(), [] );
const showBlockSidebar = sidebarOpened && hasSelection;
This would be inefficient because the two values are not used independently. A React component re-render is triggered on every hasSelection change, even though the showBlockSidebar value, the only one that’s really used by the component, doesn’t change.
But you’ll want to prefer the two independent useSelect calls when the values are used independently to render the component, like:
return (
<div>
<div>sidebar: { sidebarOpened }</div>
<div>selection: { hasSelection }</div>
</div>
);
Then you’ll be better off with two useSelect calls because each of them will do its own select from its own store, on that specific store update, without wasting time on selecting from the other store.
Leave a comment