WordPress.org

WordPress Developer Blog

How to work effectively with the useSelect hook

How to work effectively with the useSelect hook

This article is about the useSelect React hook from the @wordpress/data library. This hook is commonly used by UI components, in the block editor and in custom blocks, to read data from the block editor data stores, and to be notified about data changes. The article offers several tips and tricks on how to use it in the most efficient way possible, and answers many subtle questions about how it really works.

You’ll get the most out of this article if you have some previous practical experience working with the @wordpress/data package and are familiar with how to use the data stores and how to select data and dispatch actions. There are excellent resources in the Block Editor Handbook, namely the @wordpress/data Reference and the Data Modules Reference. This article will help you deepen your understanding and to advance from an intermediate user to an expert.

Always declare useSelect dependencies

The useSelect hook has an optional last parameter that specifies an array of dependencies. It’s similar to how React hooks like useEffect or useMemo also have a parameter to declare dependencies.

You should always specify the dependency array. It’s optional only for backward-compatibility reasons, to continue supporting legacy code that was written before the dependency array parameter was added.

If you specify an empty dependency array on useSelect, what does that say?

useSelect( ( select ) => select( 'editor' ).isSidebarOpened(), [] );

The most important thing to note is that on each render, the useSelect callback function is different. The function body is the same, but the JavaScript value is different on each call. useSelect has no way to know that it’s the same function that will return the same result for the same parameters. Therefore, each time the component re-renders for any reason (local state has changed, a prop has changed, …), useSelect will need to recompute its return value, even if the underlying state hasn’t changed. That’s inefficient. If we knew that the state hasn’t changed, and that the callback function hasn’t changed either, we could reuse the previously calculated value.

The dependency array allows useSelect to reliably determine that it’s dealing with the same callback function. If the dependency array hasn’t changed since the previous call, it can reuse the previous function and the previous value computed by it.

If the callback function changes between re-renders, you need to declare the dependencies that trigger the change. For example, in the following code, the clientId of a block can change, and the callback function has a different meaning: it returns information for a different block.

useSelect( ( select ) => {
  return select( 'block-editor' ).getBlock( clientId );
}, [ clientId ] );

Large, busy data stores like core/block-editor have hundreds or thousands of subscribers that watch every change. Declaring dependencies allows these subscribers to skip execution of many useSelect callbacks and makes the block editor significantly faster.

Call selector functions inside the callback, not outside

Suppose that the site store has a getTitle() selector. It might be tempting to call it in a React component like this:

const { getTitle } = useSelect( ( select ) => select( 'site' ), [] );
return <h1>{ getTitle() }</h1>;

But this code is not reliable. The component will not be re-rendered when the store’s title state changes. It will keep showing the old value.

Why? useSelect doesn’t just read the desired values from the store, which is the immediately visible part of what it does. It also establishes a subscription to the store, observes changes in the store, and triggers a re-render of the calling component when the store state changes in a relevant way. What does “relevant” mean? The precise condition is: when the change causes the new return value of the useSelect callback to not be shallowly equal to the previous one.

What is shallow equality? When comparing complex structured values in JavaScript, in many cases it’s not very useful to use the === operator. This operator checks if the two values are references to the same object, i.e., to the same chunk of memory, but it doesn’t tell you whether two different values have the same content. To check for same content, you either look for:

  • shallow equality: compare the fields of an object or the elements of an array one by one, and check if they point to identical (===) values. This is very fast, and often sufficient. WordPress has a helper package called @wordpress/is-shallow-equal that implements this type of comparison.
  • deep equality: this kind of comparison checks the field values recursively, descending into nested objects and arrays, and comparing them against each other. It gives the most precise answer whether the content of two objects is the same, but the check can be very slow.

In our case, the return value is the { getTitle } object. The getTitle value is a function and it never changes. It’s still the same function. What changes is what the function returns when called, but that’s not what we are checking. The useSelect return value is always shallowly equal to the previous one.

Our React component will be updated to show the new getTitle() value only when a re-render is triggered by something else. When the component has some internal state that changes, a prop changes, or a parent component re-renders. That makes it harder to notice the bug because the getTitle() is updated even though the useSelect call is wrong. But rather randomly rather than reliably.

The fixed component looks like this:

const { title } = useSelect( ( select ) => ( {
  title: select( 'site' ).getTitle()
} ), [] );
return <h1>{ title }</h1>;

Here useSelect triggers a re-render whenever the old title and the new title are different, just as we’d expect.

But call them outside when you’re in an event handler

There is, however, one case where returning the selector function itself from useSelect is a good idea: when the selector is called inside an event handler. Then we want to read from the store the data that are valid at the time when the event handler is being executed. Not to use the potentially stale value read when the component was last rendered. Then this code works:

const { getTitle } = useSelect( ( select ) => select( 'site' ), [] );
function onClick() {
  recordAnalyticsEvent( 'click', { site: getTitle() } );
}
return <Button onClick={ onClick } />;

This code works, but we can do better! One suboptimal thing is that the useSelect call will establish a subscription to the site store, and the select( 'site' ) callback will be executed on every update in that store. But that’s all pointless work because the set of the store selectors is constant for the entire lifetime of the store. The getTitle 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 { getTitle } = useSelect( 'site' );

You can also write it this way, which is the same thing:

const { getTitle } = useRegistry().select( 'site' );

Here you retrieve the registry object and call its .select method.

This way of selecting data from the store is useful not only in event handlers, but also in other handlers triggered by external events, like timers or promise resolutions.

Inside the callback, select precisely the data you need

Now suppose the site store maintains values for several fields like title, theme and domain. And it provides a getSite() selector that returns an object with all these fields together. Then you might read the title value like this:

const site = useSelect( ( select ) => select( 'site' ).getSite(), [] );
return <h1>{ site.title }</h1>;

This code behaves correctly and doesn’t cause missed updates, like the getTitle example did. But its performance is suboptimal because it will re-render too often.

Although the component is interested only in the title value, useSelect doesn’t know that. It’s asked to return the entire getSite(), including the theme and domain values. And it will trigger a re-render whenever any of them changes, even when title remains the same.

A more performant version would be:

const { title } = useSelect( ( select ) => {
  const site = select( 'site' ).getSite();
  return { title: site.title };
}, [] );
return <h1>{ title }</h1>;

It is tempting to write the slower version because it may look more intuitive and elegant. The faster version is not as concise, and you’ll often find yourself struggling with an ESLint error that says “title is already declared in the upper scope” because you want to use a variable named title both inside the useSelect callback and also outside. But it’s faster nevertheless.

Another variation of the same principle is this component:

const { title } = useSelect( ( select ) => ( {
  title: select( 'site' ).getTitle()
} ), [] );
return <button disabled={ title.length === 0 }>Continue</button>;

This button will re-render every time title changes, for example, as you’re typing into an input field. But most of these re-renders will be wasted because the disabled prop will remain true. It’s better to calculate the boolean derived value inside the useSelect callback:

const { hasTitle } = useSelect( ( select ) => ( {
  hasTitle: select( 'site' ).getTitle().length > 0
} ), [] );return <button disabled={ ! hasTitle }>Continue</button>;

This re-renders only when it really needs to, i.e., when the disabled attribute is going to change.

The next section will warn about one caveat, where calculating a “derived value” should be moved not into the callback, but out of it.

Be careful about transforming data inside the callback

The following useSelect call will have surprising behavior. It will cause the component to re-render 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 calls .map on the tags array, and every such call returns a new array instance. Even though the raw tags array in the store hasn’t changed, and the returned array is semantically the same, the returned array is not equal (===) to the previous value. It is therefore 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.

The last two sections give you somewhat contradictory advice: either move a calculation inside the useSelect callback, or move it outside! To decide which one applies, you need to look at the type of the calculated value. If it’s a primitive type (number, string, boolean), then even different instances can be === equal to each other. But with object-like types (objects and arrays), different instances are never identical.

Prefer returning objects with value properties from the callback

You can select the title value in two slightly different ways. useSelect can either return directly the value:

const title = useSelect( ( select ) => select( 'site' ).getTitle(), [] );

Or it can return it wrapped as a property on an object.

const { title } = useSelect( ( select ) => ( {
  title: select( 'site' ).getTitle()
} ), [] );

The first version is certainly shorter, but is it a good idea to use it?

The answer is that the short version will almost always work, changes of title 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 on whether that library can compare values of your data type correctly. Consider this counterexample where the library will fail:

import eq from '@wordpress/is-shallow-equal';

class NumberBox {
  #value;
  constructor( value ) { this.#value = value; }
  get() { return this.#value; }
}

const one = new NumberBox( 1 );
const two = new NumberBox( 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 private, and Object.keys won’t see it, it will always return an [] empty array. The shallow compare function will use only this empty array when comparing values, and will come to a conclusion that all NumberBox instances are equal to each other.

If you store instances of NumberBox in your data store, useSelect might fail to see changed values.

There are many other ways how to “hide” data in objects so that they are not visible on their public enumerable properties. Private fields are by far not the only one.

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 you need to be aware of the limitations because one day it might backfire.

On the other hand, returning an object to be destructured is guaranteed to be always safe. And by the way, instead of an object, it’s also safe to return an array, and it will save you some typing:

const [ title ] = useSelect( ( select ) => [ select( 'site' ).getTitle() ], [] );

After all, an array is also an object, it just uses numeric keys (0) instead of strings (title).

Do all selections from the same store in one callback

Which one of the following is better?

const { title } = useSelect( ( select ) => ( {
  title: select( 'site' ).getTitle()
} ), [] );
const { theme } = useSelect( ( select ) => ( {
  theme: select( 'site' ).getTheme()
} ), [] );

or

const { title, theme } = useSelect( ( select ) => {
  const store = select( 'site' );
  return {
    title: store.getTitle(),
    theme: store.getTheme(),
  };
}, [] );

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 know how many store subscriptions are being created. Without care, their numbers can grow into thousands and tens of thousands. If possible, try to read as much data as you can in a minimum number or useSelect calls.

It’s more subtle when selecting from multiple stores

When your component selects from multiple stores, and if some selected values are used only conditionally, there are two facts to consider:

  1. Store subscriptions are granular, the useSelect hook subscribes to each store individually.
  2. 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( 'block-editor' ).hasSelection();
}, [] );

This code returns a boolean value that’s true when “the block sidebar is opened”. It’s a combination of two conditions: whether the editor sidebar is opened at all, and whether the editor sidebar shows a block sidebar UI (it can also show, e.g., a post sidebar UI when no 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-editor' ) call is not going to be executed. That means that the hook won’t subscribe to the block-editor store and the callback won’t be executed on a block-editor store update. Because these updates would be irrelevant anyway: they can’t change the return value. The block-editor 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 those 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( 'block-editor' ).hasSelection(), [] );
const showBlockSidebar = sidebarOpened && hasSelection;

This would be inefficient because the two values are not used independently. A React component re-render is triggered every time hasSelection changes, even though the showBlockSidebar value, the one that’s actually 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>sidebar: { sidebarOpened }</div>
    <div>selection: { hasSelection }</div>
  </>
);

Then you’ll be better off with two useSelect calls because each of them will do its own select from its own store, when that specific store updates, without wasting time on selecting from the other store.

Props to @bph, @welcher and @tyxla for providing feedback and reviewing this post.

Categories: ,

8 responses to “How to work effectively with the useSelect hook”

  1. Fabian Kägy Avatar
    Fabian Kägy

    Thanks for this deep dive 🙂 Lots of great nuggets of gold in here!

  2. Carsten Avatar

    Like Fabian already said: a super interesting post!

    I read it once, changed some lines of my code, following your great explanation and now I read it a second time to make sure I get every part of this delicious piece of documentation.

    Thanks for writing this detailed post, looking forward to the next of this kind.

  3. Kacper Avatar
    Kacper

    The post itself is very helpful, but the fact that WordPress team has chosen to develop Gutenberg using library that requires so much attention to quirks and caveats is so depressing. As this post shows, React is the worst kind of library – one that makes writing almost correct code easy, but writing 100% correct and performant code very difficult which is especially harmful in platform that aims to be open and easily extensible like WordPress. When I read post like this I wish 7 years ago WordPress team had decided to use something more developer-friendly to build Gutenberg like Vue or go with the vanilla js route 😞

    1. Bastian Avatar
      Bastian

      Agreed. Before Gutenberg, writing code for WordPress was way simpler and more straightforward. React’s learning curve and the nuances it entails make achieving optimal performance and correctness a daunting task.

  4. Anh Tran Avatar

    Interesting and very useful. I hope there will be more tutorials on using WordPress hooks in React like this. It would help using the built-in hooks instead of writing other ones.

  5. Lovro Hrust Avatar

    Although I am using useSelect from 2020, I didn’t think of all these nuances! Many thanks for this!

  6. Arhsad Kasar Avatar
    Arhsad Kasar

    Home work assignment

  7. Aditta Amin Avatar
    Aditta Amin

    How do I get Media Image by Attachment ID?

Leave a Reply

Your email address will not be published. Required fields are marked *