@wordpress/commands 1.55.0 → 1.56.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 1.56.0 (2026-09-23)
6
+
7
+ ### Bug Fixes
8
+
9
+ - `useCommandLoader`: stop unregistering and re-registering the loader when the `hook` option changes identity between renders ([#82819](https://github.com/WordPress/gutenberg/pull/82819)).
10
+
5
11
  ## 1.55.0 (2026-09-10)
6
12
 
7
13
  ### Internal
package/README.md CHANGED
@@ -6,13 +6,13 @@ Commands is a generic package that allows registering and modifying commands to
6
6
 
7
7
  There are two ways to register commands: static or dynamic. Both methods receive a command object as an argument, which provides:
8
8
 
9
- - `name`: A unique machine-readable name for the command
10
- - `label`: A human-readable label
11
- - `icon`: An SVG icon
12
- - `callback`: A callback function that is called when the command is selected
13
- - `category`: (Optional) The category of the command. See [Command categories](#command-categories) below
14
- - `context`: (Optional) The context of the command
15
- - `keywords`: (Optional) An array of keywords for search matching
9
+ - `name`: A unique machine-readable name for the command
10
+ - `label`: A human-readable label
11
+ - `icon`: An SVG icon
12
+ - `callback`: A callback function that is called when the command is selected
13
+ - `category`: (Optional) The category of the command. See [Command categories](#command-categories) below
14
+ - `context`: (Optional) The context of the command
15
+ - `keywords`: (Optional) An array of keywords for search matching
16
16
 
17
17
  ### Static commands
18
18
 
@@ -30,9 +30,9 @@ Static and dynamic commands can be contextual. This means that in a given contex
30
30
 
31
31
  At the moment, three contexts have been implemented:
32
32
 
33
- - `site-editor`: This is the context that is set when you are navigating in the site editor (sidebar visible).
34
- - `entity-edit`: This is the context that is set when you are editing a document (template, template part or page).
35
- - `block-selection-edit`: This is the context that is set when a block is selected.
33
+ - `site-editor`: This is the context that is set when you are navigating in the site editor (sidebar visible).
34
+ - `entity-edit`: This is the context that is set when you are editing a document (template, template part or page).
35
+ - `block-selection-edit`: This is the context that is set when a block is selected.
36
36
 
37
37
  As the usage of the Command Palette expands, more contexts will be added.
38
38
 
@@ -42,10 +42,10 @@ Attaching a command or command loader to a given context is as simple as adding
42
42
 
43
43
  Each command can be assigned a `category` that describes what kind of action it performs. Categories are used by the Command Palette to visually differentiate commands. The following categories are available:
44
44
 
45
- - `command`: Executes code or toggles a command (e.g., Add block, duplicating a block).
46
- - `view`: Navigates to an area in the admin or opens a panel (e.g., "Go to: Templates").
47
- - `edit`: Navigates to edit a document (e.g., editing a template, editing a page).
48
- - `action`: A generic fallback for commands that don't fit the other categories. This is also the default when an invalid category is provided.
45
+ - `command`: Executes code or toggles a command (e.g., Add block, duplicating a block).
46
+ - `view`: Navigates to an area in the admin or opens a panel (e.g., "Go to: Templates").
47
+ - `edit`: Navigates to edit a document (e.g., editing a template, editing a page).
48
+ - `action`: A generic fallback for commands that don't fit the other categories. This is also the default when an invalid category is provided.
49
49
 
50
50
  If no `category` is specified, the command will have `action` set. If an invalid value is provided, a warning is emitted in development mode and the category defaults to `action`.
51
51
 
@@ -55,9 +55,9 @@ A category can also supply a fallback icon, used when the command passes no `ico
55
55
 
56
56
  The Command Palette also offers a number of [selectors and actions](https://developer.wordpress.org/block-editor/reference-guides/data/data-core-commands/) to manipulate its state, which include:
57
57
 
58
- - Retrieving the registered commands and command loaders using the following selectors `getCommands` and `getCommandLoader`
59
- - Checking if the Command Palette is open using the `isOpen` selector.
60
- - Programmatically open or close the Command Palette using the `open` and `close` actions.
58
+ - Retrieving the registered commands and command loaders using the following selectors `getCommands` and `getCommandLoader`
59
+ - Checking if the Command Palette is open using the `isOpen` selector.
60
+ - Programmatically open or close the Command Palette using the `open` and `close` actions.
61
61
 
62
62
  See the [Commands Data](https://developer.wordpress.org/block-editor/reference-guides/data/data-core-commands/) documentation for more information.
63
63
 
@@ -89,7 +89,7 @@ Store definition for the commands namespace.
89
89
 
90
90
  _Related_
91
91
 
92
- - <https://github.com/WordPress/gutenberg/blob/HEAD/packages/data/README.md#createReduxStore>
92
+ - <https://github.com/WordPress/gutenberg/blob/HEAD/packages/data/README.md#createReduxStore>
93
93
 
94
94
  _Usage_
95
95
 
@@ -102,7 +102,7 @@ const { open: openCommandCenter } = useDispatch( commandsStore );
102
102
 
103
103
  _Type_
104
104
 
105
- - `Object`
105
+ - `Object`
106
106
 
107
107
  ### useCommand
108
108
 
@@ -128,12 +128,14 @@ useCommand( {
128
128
 
129
129
  _Parameters_
130
130
 
131
- - _command_ `import('../store/actions').WPCommandConfig`: command config.
131
+ - _command_ `import('../store/actions').WPCommandConfig`: command config.
132
132
 
133
133
  ### useCommandLoader
134
134
 
135
135
  Attach a command loader to the command palette. Used for dynamic commands.
136
136
 
137
+ The palette always calls the most recent `hook`. Changing the `hook` instance doesn't re-render the palette.
138
+
137
139
  _Usage_
138
140
 
139
141
  ```js
@@ -202,7 +204,7 @@ useCommandLoader( {
202
204
 
203
205
  _Parameters_
204
206
 
205
- - _loader_ `import('../store/actions').WPCommandLoaderConfig`: command loader config.
207
+ - _loader_ `import('../store/actions').WPCommandLoaderConfig`: command loader config.
206
208
 
207
209
  ### useCommands
208
210
 
@@ -240,7 +242,7 @@ useCommands( [
240
242
 
241
243
  _Parameters_
242
244
 
243
- - _commands_ `import('../store/actions').WPCommandConfig[]`: Array of command configs.
245
+ - _commands_ `import('../store/actions').WPCommandConfig[]`: Array of command configs.
244
246
 
245
247
  <!-- END TOKEN(Autogenerated API docs) -->
246
248
 
@@ -27,13 +27,21 @@ var import_data = require("@wordpress/data");
27
27
  var import_store = require("../store/index.cjs");
28
28
  function useCommandLoader(loader) {
29
29
  const { registerCommandLoader, unregisterCommandLoader } = (0, import_data.useDispatch)(import_store.store);
30
+ const currentHookRef = (0, import_element.useRef)(loader.hook);
31
+ (0, import_element.useEffect)(() => {
32
+ currentHookRef.current = loader.hook;
33
+ }, [loader.hook]);
34
+ const hook = (0, import_element.useCallback)(
35
+ (...args) => currentHookRef.current(...args),
36
+ []
37
+ );
30
38
  (0, import_element.useEffect)(() => {
31
39
  if (loader.disabled) {
32
40
  return;
33
41
  }
34
42
  registerCommandLoader({
35
43
  name: loader.name,
36
- hook: loader.hook,
44
+ hook,
37
45
  context: loader.context,
38
46
  category: loader.category
39
47
  });
@@ -42,7 +50,7 @@ function useCommandLoader(loader) {
42
50
  };
43
51
  }, [
44
52
  loader.name,
45
- loader.hook,
53
+ hook,
46
54
  loader.context,
47
55
  loader.category,
48
56
  loader.disabled,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/hooks/use-command-loader.js"],
4
- "sourcesContent": ["import { useEffect } from '@wordpress/element';\nimport { useDispatch } from '@wordpress/data';\nimport { store as commandsStore } from '../store';\n\n/**\n * Attach a command loader to the command palette. Used for dynamic commands.\n *\n * @param {import('../store/actions').WPCommandLoaderConfig} loader command loader config.\n *\n * @example\n * ```js\n * import { __ } from '@wordpress/i18n';\n * import { addQueryArgs } from '@wordpress/url';\n * import { useCommandLoader } from '@wordpress/commands';\n * import { page } from '@wordpress/icons';\n * import { useSelect } from '@wordpress/data';\n * import { store as coreStore } from '@wordpress/core-data';\n * import { useMemo } from '@wordpress/element';\n *\n * function usePageSearchCommandLoader( { search } ) {\n * // Retrieve the pages for the \"search\" term.\n * const { records, isLoading } = useSelect(\n * ( select ) => {\n * const { getEntityRecords } = select( coreStore );\n * const query = {\n * search: !! search ? search : undefined,\n * per_page: 10,\n * orderby: search ? 'relevance' : 'date',\n * };\n * return {\n * records: getEntityRecords( 'postType', 'page', query ),\n * isLoading: ! select( coreStore ).hasFinishedResolution(\n * 'getEntityRecords',\n * [ 'postType', 'page', query ]\n * ),\n * };\n * },\n * [ search ]\n * );\n *\n * // Create the commands.\n * const commands = useMemo( () => {\n * return ( records ?? [] ).slice( 0, 10 ).map( ( record ) => {\n * return {\n * name: record.title?.rendered + ' ' + record.id,\n * label: record.title?.rendered\n * ? record.title?.rendered\n * : __( '(no title)' ),\n * icon: page,\n * category: 'edit',\n * callback: ( { close } ) => {\n * const args = {\n * \t\t\t\t\t\t\tp: '/page',\n * \t\t\t\t\t\t\tpostId: record.id,\n * };\n * document.location = addQueryArgs( 'site-editor.php', args );\n * close();\n * },\n * };\n * } );\n * }, [ records ] );\n *\n * return {\n * commands,\n * isLoading,\n * };\n * }\n *\n * useCommandLoader( {\n * name: 'myplugin/page-search',\n * hook: usePageSearchCommandLoader,\n * } );\n * ```\n */\nexport default function useCommandLoader( loader ) {\n\tconst { registerCommandLoader, unregisterCommandLoader } =\n\t\tuseDispatch( commandsStore );\n\tuseEffect( () => {\n\t\tif ( loader.disabled ) {\n\t\t\treturn;\n\t\t}\n\t\tregisterCommandLoader( {\n\t\t\tname: loader.name,\n\t\t\thook: loader.hook,\n\t\t\tcontext: loader.context,\n\t\t\tcategory: loader.category,\n\t\t} );\n\t\treturn () => {\n\t\t\tunregisterCommandLoader( loader.name );\n\t\t};\n\t}, [\n\t\tloader.name,\n\t\tloader.hook,\n\t\tloader.context,\n\t\tloader.category,\n\t\tloader.disabled,\n\t\tregisterCommandLoader,\n\t\tunregisterCommandLoader,\n\t] );\n}\n"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,qBAA0B;AAC1B,kBAA4B;AAC5B,mBAAuC;AAwExB,SAAR,iBAAmC,QAAS;AAClD,QAAM,EAAE,uBAAuB,wBAAwB,QACtD,yBAAa,aAAAA,KAAc;AAC5B,gCAAW,MAAM;AAChB,QAAK,OAAO,UAAW;AACtB;AAAA,IACD;AACA,0BAAuB;AAAA,MACtB,MAAM,OAAO;AAAA,MACb,MAAM,OAAO;AAAA,MACb,SAAS,OAAO;AAAA,MAChB,UAAU,OAAO;AAAA,IAClB,CAAE;AACF,WAAO,MAAM;AACZ,8BAAyB,OAAO,IAAK;AAAA,IACtC;AAAA,EACD,GAAG;AAAA,IACF,OAAO;AAAA,IACP,OAAO;AAAA,IACP,OAAO;AAAA,IACP,OAAO;AAAA,IACP,OAAO;AAAA,IACP;AAAA,IACA;AAAA,EACD,CAAE;AACH;",
4
+ "sourcesContent": ["import { useCallback, useEffect, useRef } from '@wordpress/element';\nimport { useDispatch } from '@wordpress/data';\nimport { store as commandsStore } from '../store';\n\n/**\n * Attach a command loader to the command palette. Used for dynamic commands.\n *\n * The palette always calls the most recent `hook`. Changing the `hook` instance\n * doesn't re-render the palette.\n *\n * @param {import('../store/actions').WPCommandLoaderConfig} loader command loader config.\n *\n * @example\n * ```js\n * import { __ } from '@wordpress/i18n';\n * import { addQueryArgs } from '@wordpress/url';\n * import { useCommandLoader } from '@wordpress/commands';\n * import { page } from '@wordpress/icons';\n * import { useSelect } from '@wordpress/data';\n * import { store as coreStore } from '@wordpress/core-data';\n * import { useMemo } from '@wordpress/element';\n *\n * function usePageSearchCommandLoader( { search } ) {\n * // Retrieve the pages for the \"search\" term.\n * const { records, isLoading } = useSelect(\n * ( select ) => {\n * const { getEntityRecords } = select( coreStore );\n * const query = {\n * search: !! search ? search : undefined,\n * per_page: 10,\n * orderby: search ? 'relevance' : 'date',\n * };\n * return {\n * records: getEntityRecords( 'postType', 'page', query ),\n * isLoading: ! select( coreStore ).hasFinishedResolution(\n * 'getEntityRecords',\n * [ 'postType', 'page', query ]\n * ),\n * };\n * },\n * [ search ]\n * );\n *\n * // Create the commands.\n * const commands = useMemo( () => {\n * return ( records ?? [] ).slice( 0, 10 ).map( ( record ) => {\n * return {\n * name: record.title?.rendered + ' ' + record.id,\n * label: record.title?.rendered\n * ? record.title?.rendered\n * : __( '(no title)' ),\n * icon: page,\n * category: 'edit',\n * callback: ( { close } ) => {\n * const args = {\n * \t\t\t\t\t\t\tp: '/page',\n * \t\t\t\t\t\t\tpostId: record.id,\n * };\n * document.location = addQueryArgs( 'site-editor.php', args );\n * close();\n * },\n * };\n * } );\n * }, [ records ] );\n *\n * return {\n * commands,\n * isLoading,\n * };\n * }\n *\n * useCommandLoader( {\n * name: 'myplugin/page-search',\n * hook: usePageSearchCommandLoader,\n * } );\n * ```\n */\nexport default function useCommandLoader( loader ) {\n\tconst { registerCommandLoader, unregisterCommandLoader } =\n\t\tuseDispatch( commandsStore );\n\tconst currentHookRef = useRef( loader.hook );\n\tuseEffect( () => {\n\t\tcurrentHookRef.current = loader.hook;\n\t}, [ loader.hook ] );\n\n\t// Stable identity, so a hook rebuilt on every render does not re-register\n\t// the loader.\n\tconst hook = useCallback(\n\t\t( ...args ) => currentHookRef.current( ...args ),\n\t\t[]\n\t);\n\n\tuseEffect( () => {\n\t\tif ( loader.disabled ) {\n\t\t\treturn;\n\t\t}\n\t\tregisterCommandLoader( {\n\t\t\tname: loader.name,\n\t\t\thook,\n\t\t\tcontext: loader.context,\n\t\t\tcategory: loader.category,\n\t\t} );\n\t\treturn () => {\n\t\t\tunregisterCommandLoader( loader.name );\n\t\t};\n\t}, [\n\t\tloader.name,\n\t\thook,\n\t\tloader.context,\n\t\tloader.category,\n\t\tloader.disabled,\n\t\tregisterCommandLoader,\n\t\tunregisterCommandLoader,\n\t] );\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,qBAA+C;AAC/C,kBAA4B;AAC5B,mBAAuC;AA2ExB,SAAR,iBAAmC,QAAS;AAClD,QAAM,EAAE,uBAAuB,wBAAwB,QACtD,yBAAa,aAAAA,KAAc;AAC5B,QAAM,qBAAiB,uBAAQ,OAAO,IAAK;AAC3C,gCAAW,MAAM;AAChB,mBAAe,UAAU,OAAO;AAAA,EACjC,GAAG,CAAE,OAAO,IAAK,CAAE;AAInB,QAAM,WAAO;AAAA,IACZ,IAAK,SAAU,eAAe,QAAS,GAAG,IAAK;AAAA,IAC/C,CAAC;AAAA,EACF;AAEA,gCAAW,MAAM;AAChB,QAAK,OAAO,UAAW;AACtB;AAAA,IACD;AACA,0BAAuB;AAAA,MACtB,MAAM,OAAO;AAAA,MACb;AAAA,MACA,SAAS,OAAO;AAAA,MAChB,UAAU,OAAO;AAAA,IAClB,CAAE;AACF,WAAO,MAAM;AACZ,8BAAyB,OAAO,IAAK;AAAA,IACtC;AAAA,EACD,GAAG;AAAA,IACF,OAAO;AAAA,IACP;AAAA,IACA,OAAO;AAAA,IACP,OAAO;AAAA,IACP,OAAO;AAAA,IACP;AAAA,IACA;AAAA,EACD,CAAE;AACH;",
6
6
  "names": ["commandsStore"]
7
7
  }
@@ -1,16 +1,24 @@
1
1
  // packages/commands/src/hooks/use-command-loader.js
2
- import { useEffect } from "@wordpress/element";
2
+ import { useCallback, useEffect, useRef } from "@wordpress/element";
3
3
  import { useDispatch } from "@wordpress/data";
4
4
  import { store as commandsStore } from "../store/index.mjs";
5
5
  function useCommandLoader(loader) {
6
6
  const { registerCommandLoader, unregisterCommandLoader } = useDispatch(commandsStore);
7
+ const currentHookRef = useRef(loader.hook);
8
+ useEffect(() => {
9
+ currentHookRef.current = loader.hook;
10
+ }, [loader.hook]);
11
+ const hook = useCallback(
12
+ (...args) => currentHookRef.current(...args),
13
+ []
14
+ );
7
15
  useEffect(() => {
8
16
  if (loader.disabled) {
9
17
  return;
10
18
  }
11
19
  registerCommandLoader({
12
20
  name: loader.name,
13
- hook: loader.hook,
21
+ hook,
14
22
  context: loader.context,
15
23
  category: loader.category
16
24
  });
@@ -19,7 +27,7 @@ function useCommandLoader(loader) {
19
27
  };
20
28
  }, [
21
29
  loader.name,
22
- loader.hook,
30
+ hook,
23
31
  loader.context,
24
32
  loader.category,
25
33
  loader.disabled,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/hooks/use-command-loader.js"],
4
- "sourcesContent": ["import { useEffect } from '@wordpress/element';\nimport { useDispatch } from '@wordpress/data';\nimport { store as commandsStore } from '../store';\n\n/**\n * Attach a command loader to the command palette. Used for dynamic commands.\n *\n * @param {import('../store/actions').WPCommandLoaderConfig} loader command loader config.\n *\n * @example\n * ```js\n * import { __ } from '@wordpress/i18n';\n * import { addQueryArgs } from '@wordpress/url';\n * import { useCommandLoader } from '@wordpress/commands';\n * import { page } from '@wordpress/icons';\n * import { useSelect } from '@wordpress/data';\n * import { store as coreStore } from '@wordpress/core-data';\n * import { useMemo } from '@wordpress/element';\n *\n * function usePageSearchCommandLoader( { search } ) {\n * // Retrieve the pages for the \"search\" term.\n * const { records, isLoading } = useSelect(\n * ( select ) => {\n * const { getEntityRecords } = select( coreStore );\n * const query = {\n * search: !! search ? search : undefined,\n * per_page: 10,\n * orderby: search ? 'relevance' : 'date',\n * };\n * return {\n * records: getEntityRecords( 'postType', 'page', query ),\n * isLoading: ! select( coreStore ).hasFinishedResolution(\n * 'getEntityRecords',\n * [ 'postType', 'page', query ]\n * ),\n * };\n * },\n * [ search ]\n * );\n *\n * // Create the commands.\n * const commands = useMemo( () => {\n * return ( records ?? [] ).slice( 0, 10 ).map( ( record ) => {\n * return {\n * name: record.title?.rendered + ' ' + record.id,\n * label: record.title?.rendered\n * ? record.title?.rendered\n * : __( '(no title)' ),\n * icon: page,\n * category: 'edit',\n * callback: ( { close } ) => {\n * const args = {\n * \t\t\t\t\t\t\tp: '/page',\n * \t\t\t\t\t\t\tpostId: record.id,\n * };\n * document.location = addQueryArgs( 'site-editor.php', args );\n * close();\n * },\n * };\n * } );\n * }, [ records ] );\n *\n * return {\n * commands,\n * isLoading,\n * };\n * }\n *\n * useCommandLoader( {\n * name: 'myplugin/page-search',\n * hook: usePageSearchCommandLoader,\n * } );\n * ```\n */\nexport default function useCommandLoader( loader ) {\n\tconst { registerCommandLoader, unregisterCommandLoader } =\n\t\tuseDispatch( commandsStore );\n\tuseEffect( () => {\n\t\tif ( loader.disabled ) {\n\t\t\treturn;\n\t\t}\n\t\tregisterCommandLoader( {\n\t\t\tname: loader.name,\n\t\t\thook: loader.hook,\n\t\t\tcontext: loader.context,\n\t\t\tcategory: loader.category,\n\t\t} );\n\t\treturn () => {\n\t\t\tunregisterCommandLoader( loader.name );\n\t\t};\n\t}, [\n\t\tloader.name,\n\t\tloader.hook,\n\t\tloader.context,\n\t\tloader.category,\n\t\tloader.disabled,\n\t\tregisterCommandLoader,\n\t\tunregisterCommandLoader,\n\t] );\n}\n"],
5
- "mappings": ";AAAA,SAAS,iBAAiB;AAC1B,SAAS,mBAAmB;AAC5B,SAAS,SAAS,qBAAqB;AAwExB,SAAR,iBAAmC,QAAS;AAClD,QAAM,EAAE,uBAAuB,wBAAwB,IACtD,YAAa,aAAc;AAC5B,YAAW,MAAM;AAChB,QAAK,OAAO,UAAW;AACtB;AAAA,IACD;AACA,0BAAuB;AAAA,MACtB,MAAM,OAAO;AAAA,MACb,MAAM,OAAO;AAAA,MACb,SAAS,OAAO;AAAA,MAChB,UAAU,OAAO;AAAA,IAClB,CAAE;AACF,WAAO,MAAM;AACZ,8BAAyB,OAAO,IAAK;AAAA,IACtC;AAAA,EACD,GAAG;AAAA,IACF,OAAO;AAAA,IACP,OAAO;AAAA,IACP,OAAO;AAAA,IACP,OAAO;AAAA,IACP,OAAO;AAAA,IACP;AAAA,IACA;AAAA,EACD,CAAE;AACH;",
4
+ "sourcesContent": ["import { useCallback, useEffect, useRef } from '@wordpress/element';\nimport { useDispatch } from '@wordpress/data';\nimport { store as commandsStore } from '../store';\n\n/**\n * Attach a command loader to the command palette. Used for dynamic commands.\n *\n * The palette always calls the most recent `hook`. Changing the `hook` instance\n * doesn't re-render the palette.\n *\n * @param {import('../store/actions').WPCommandLoaderConfig} loader command loader config.\n *\n * @example\n * ```js\n * import { __ } from '@wordpress/i18n';\n * import { addQueryArgs } from '@wordpress/url';\n * import { useCommandLoader } from '@wordpress/commands';\n * import { page } from '@wordpress/icons';\n * import { useSelect } from '@wordpress/data';\n * import { store as coreStore } from '@wordpress/core-data';\n * import { useMemo } from '@wordpress/element';\n *\n * function usePageSearchCommandLoader( { search } ) {\n * // Retrieve the pages for the \"search\" term.\n * const { records, isLoading } = useSelect(\n * ( select ) => {\n * const { getEntityRecords } = select( coreStore );\n * const query = {\n * search: !! search ? search : undefined,\n * per_page: 10,\n * orderby: search ? 'relevance' : 'date',\n * };\n * return {\n * records: getEntityRecords( 'postType', 'page', query ),\n * isLoading: ! select( coreStore ).hasFinishedResolution(\n * 'getEntityRecords',\n * [ 'postType', 'page', query ]\n * ),\n * };\n * },\n * [ search ]\n * );\n *\n * // Create the commands.\n * const commands = useMemo( () => {\n * return ( records ?? [] ).slice( 0, 10 ).map( ( record ) => {\n * return {\n * name: record.title?.rendered + ' ' + record.id,\n * label: record.title?.rendered\n * ? record.title?.rendered\n * : __( '(no title)' ),\n * icon: page,\n * category: 'edit',\n * callback: ( { close } ) => {\n * const args = {\n * \t\t\t\t\t\t\tp: '/page',\n * \t\t\t\t\t\t\tpostId: record.id,\n * };\n * document.location = addQueryArgs( 'site-editor.php', args );\n * close();\n * },\n * };\n * } );\n * }, [ records ] );\n *\n * return {\n * commands,\n * isLoading,\n * };\n * }\n *\n * useCommandLoader( {\n * name: 'myplugin/page-search',\n * hook: usePageSearchCommandLoader,\n * } );\n * ```\n */\nexport default function useCommandLoader( loader ) {\n\tconst { registerCommandLoader, unregisterCommandLoader } =\n\t\tuseDispatch( commandsStore );\n\tconst currentHookRef = useRef( loader.hook );\n\tuseEffect( () => {\n\t\tcurrentHookRef.current = loader.hook;\n\t}, [ loader.hook ] );\n\n\t// Stable identity, so a hook rebuilt on every render does not re-register\n\t// the loader.\n\tconst hook = useCallback(\n\t\t( ...args ) => currentHookRef.current( ...args ),\n\t\t[]\n\t);\n\n\tuseEffect( () => {\n\t\tif ( loader.disabled ) {\n\t\t\treturn;\n\t\t}\n\t\tregisterCommandLoader( {\n\t\t\tname: loader.name,\n\t\t\thook,\n\t\t\tcontext: loader.context,\n\t\t\tcategory: loader.category,\n\t\t} );\n\t\treturn () => {\n\t\t\tunregisterCommandLoader( loader.name );\n\t\t};\n\t}, [\n\t\tloader.name,\n\t\thook,\n\t\tloader.context,\n\t\tloader.category,\n\t\tloader.disabled,\n\t\tregisterCommandLoader,\n\t\tunregisterCommandLoader,\n\t] );\n}\n"],
5
+ "mappings": ";AAAA,SAAS,aAAa,WAAW,cAAc;AAC/C,SAAS,mBAAmB;AAC5B,SAAS,SAAS,qBAAqB;AA2ExB,SAAR,iBAAmC,QAAS;AAClD,QAAM,EAAE,uBAAuB,wBAAwB,IACtD,YAAa,aAAc;AAC5B,QAAM,iBAAiB,OAAQ,OAAO,IAAK;AAC3C,YAAW,MAAM;AAChB,mBAAe,UAAU,OAAO;AAAA,EACjC,GAAG,CAAE,OAAO,IAAK,CAAE;AAInB,QAAM,OAAO;AAAA,IACZ,IAAK,SAAU,eAAe,QAAS,GAAG,IAAK;AAAA,IAC/C,CAAC;AAAA,EACF;AAEA,YAAW,MAAM;AAChB,QAAK,OAAO,UAAW;AACtB;AAAA,IACD;AACA,0BAAuB;AAAA,MACtB,MAAM,OAAO;AAAA,MACb;AAAA,MACA,SAAS,OAAO;AAAA,MAChB,UAAU,OAAO;AAAA,IAClB,CAAE;AACF,WAAO,MAAM;AACZ,8BAAyB,OAAO,IAAK;AAAA,IACtC;AAAA,EACD,GAAG;AAAA,IACF,OAAO;AAAA,IACP;AAAA,IACA,OAAO;AAAA,IACP,OAAO;AAAA,IACP,OAAO;AAAA,IACP;AAAA,IACA;AAAA,EACD,CAAE;AACH;",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wordpress/commands",
3
- "version": "1.55.0",
3
+ "version": "1.56.0",
4
4
  "description": "Handles the commands menu.",
5
5
  "author": "The WordPress Contributors",
6
6
  "license": "GPL-2.0-or-later",
@@ -20,8 +20,7 @@
20
20
  "url": "https://github.com/WordPress/gutenberg/issues"
21
21
  },
22
22
  "engines": {
23
- "node": ">=18.12.0",
24
- "npm": ">=8.19.2"
23
+ "node": ">=18.12.0"
25
24
  },
26
25
  "files": [
27
26
  "src",
@@ -43,16 +42,16 @@
43
42
  },
44
43
  "wpScript": true,
45
44
  "dependencies": {
46
- "@wordpress/base-styles": "^13.1.0",
47
- "@wordpress/components": "^40.1.0",
48
- "@wordpress/data": "^10.55.0",
49
- "@wordpress/element": "^8.7.0",
50
- "@wordpress/i18n": "^6.28.0",
51
- "@wordpress/icons": "^16.0.0",
52
- "@wordpress/keyboard-shortcuts": "^5.55.0",
53
- "@wordpress/keycodes": "^4.55.0",
54
- "@wordpress/preferences": "^4.55.0",
55
- "@wordpress/private-apis": "^1.55.0",
45
+ "@wordpress/base-styles": "^13.2.0",
46
+ "@wordpress/components": "^41.0.0",
47
+ "@wordpress/data": "^10.56.0",
48
+ "@wordpress/element": "^8.8.0",
49
+ "@wordpress/i18n": "^6.29.0",
50
+ "@wordpress/icons": "^17.0.0",
51
+ "@wordpress/keyboard-shortcuts": "^5.56.0",
52
+ "@wordpress/keycodes": "^4.56.0",
53
+ "@wordpress/preferences": "^4.56.0",
54
+ "@wordpress/private-apis": "^1.56.0",
56
55
  "clsx": "^2.1.1",
57
56
  "cmdk": "^1.0.0"
58
57
  },
@@ -63,5 +62,5 @@
63
62
  "publishConfig": {
64
63
  "access": "public"
65
64
  },
66
- "gitHead": "485f42ae8a1c58ceea18371a507fd4acfa86fbd8"
65
+ "gitHead": "56d8058eda6e30c4cc8bfc37818fa6ed972f7dc2"
67
66
  }
@@ -1,10 +1,13 @@
1
- import { useEffect } from '@wordpress/element';
1
+ import { useCallback, useEffect, useRef } from '@wordpress/element';
2
2
  import { useDispatch } from '@wordpress/data';
3
3
  import { store as commandsStore } from '../store';
4
4
 
5
5
  /**
6
6
  * Attach a command loader to the command palette. Used for dynamic commands.
7
7
  *
8
+ * The palette always calls the most recent `hook`. Changing the `hook` instance
9
+ * doesn't re-render the palette.
10
+ *
8
11
  * @param {import('../store/actions').WPCommandLoaderConfig} loader command loader config.
9
12
  *
10
13
  * @example
@@ -75,13 +78,25 @@ import { store as commandsStore } from '../store';
75
78
  export default function useCommandLoader( loader ) {
76
79
  const { registerCommandLoader, unregisterCommandLoader } =
77
80
  useDispatch( commandsStore );
81
+ const currentHookRef = useRef( loader.hook );
82
+ useEffect( () => {
83
+ currentHookRef.current = loader.hook;
84
+ }, [ loader.hook ] );
85
+
86
+ // Stable identity, so a hook rebuilt on every render does not re-register
87
+ // the loader.
88
+ const hook = useCallback(
89
+ ( ...args ) => currentHookRef.current( ...args ),
90
+ []
91
+ );
92
+
78
93
  useEffect( () => {
79
94
  if ( loader.disabled ) {
80
95
  return;
81
96
  }
82
97
  registerCommandLoader( {
83
98
  name: loader.name,
84
- hook: loader.hook,
99
+ hook,
85
100
  context: loader.context,
86
101
  category: loader.category,
87
102
  } );
@@ -90,7 +105,7 @@ export default function useCommandLoader( loader ) {
90
105
  };
91
106
  }, [
92
107
  loader.name,
93
- loader.hook,
108
+ hook,
94
109
  loader.context,
95
110
  loader.category,
96
111
  loader.disabled,