Gutenberg 区块:useSelect
和 useDispatch
的妙用,打造自定义数据存储
大家好!今天,我们深入探讨 Gutenberg 区块开发中数据存储的关键:useSelect
和 useDispatch
。我们将不仅了解它们的基本用法,更要掌握如何利用它们与 WordPress 数据存储交互,甚至创建自定义的数据存储,从而构建更强大、更灵活的 Gutenberg 区块。
useSelect
:高效的数据提取器
useSelect
是 @wordpress/data
包提供的 React Hook,用于从 WordPress 数据存储中高效地提取数据。它的核心在于订阅机制:当数据存储中的数据发生变化时,useSelect
会自动重新执行选择器函数,更新组件的状态,从而实现组件的自动更新。
基本用法:
import { useSelect } from '@wordpress/data';
function MyComponent() {
const postTitle = useSelect(
(select) => {
const post = select('core/editor').getCurrentPost();
return post ? post.title.raw : '';
},
[] // 依赖项数组
);
return (
<div>
<h1>{postTitle}</h1>
</div>
);
}
代码解释:
-
import { useSelect } from '@wordpress/data';
: 引入useSelect
Hook。 -
useSelect((select) => { ... }, [])
: 调用useSelect
Hook,它接受两个参数:- 选择器函数
(select) => { ... }
: 这是一个函数,它接收一个select
对象作为参数。select
对象提供了一系列方法,用于从不同的数据存储中选择数据。在这个例子中,我们使用select('core/editor')
来访问core/editor
数据存储,该存储包含了当前编辑的 post 相关的信息。 - 依赖项数组
[]
: 这是一个可选的数组,用于指定选择器函数所依赖的值。当依赖项数组中的任何一个值发生变化时,选择器函数会被重新执行。如果依赖项数组为空,则选择器函数只会在组件挂载时执行一次,之后只会在数据存储发生变化时执行。
- 选择器函数
-
const post = select('core/editor').getCurrentPost();
: 使用select
对象从core/editor
数据存储中获取当前 post 对象。 -
return post ? post.title.raw : '';
: 如果 post 对象存在,则返回 post 的标题;否则返回一个空字符串。 -
<h1>{postTitle}</h1>
: 在组件中渲染 post 的标题。
更高级的用法:
useSelect
还可以接收第二个参数,即依赖项数组。这个数组告诉 useSelect
何时重新执行选择器函数。如果依赖项数组为空,则选择器函数只会在组件挂载时执行一次。如果依赖项数组包含某些变量,则当这些变量的值发生变化时,选择器函数会被重新执行。
import { useSelect } from '@wordpress/data';
import { useState } from '@wordpress/element';
function MyComponent() {
const [postId, setPostId] = useState(1);
const postTitle = useSelect(
(select) => {
const post = select('core').getEntityRecord('postType', 'post', postId);
return post ? post.title.raw : '';
},
[postId] // 依赖项数组:postId
);
return (
<div>
<h1>{postTitle}</h1>
<button onClick={() => setPostId(postId + 1)}>Next Post</button>
</div>
);
}
在这个例子中,useSelect
的依赖项数组包含 postId
变量。当 postId
的值发生变化时,选择器函数会被重新执行,从而更新 post 的标题。
性能优化:
useSelect
的一个重要优势是其内置的性能优化。它会对选择器函数的结果进行浅比较,只有当结果发生变化时才会触发组件的重新渲染。这可以有效地减少不必要的渲染,提高组件的性能。
useDispatch
:掌控数据流动的指挥官
useDispatch
同样是 @wordpress/data
包提供的 React Hook,用于获取数据存储的 dispatch
函数。dispatch
函数用于触发数据存储中的 actions,从而修改数据存储的状态。
基本用法:
import { useDispatch } from '@wordpress/data';
function MyComponent() {
const { updatePost } = useDispatch('core/editor');
const handleClick = () => {
updatePost({ title: 'New Title' });
};
return (
<button onClick={handleClick}>Update Title</button>
);
}
代码解释:
import { useDispatch } from '@wordpress/data';
: 引入useDispatch
Hook。const { updatePost } = useDispatch('core/editor');
: 调用useDispatch
Hook,它接收一个数据存储的名称作为参数,并返回一个包含该数据存储的所有 actions 的对象。在这个例子中,我们使用useDispatch('core/editor')
来访问core/editor
数据存储的 actions。const handleClick = () => { ... };
: 定义一个事件处理函数,当按钮被点击时,该函数会被调用。updatePost({ title: 'New Title' });
: 调用updatePost
action,并传递一个新的标题作为参数。这将更新当前 post 的标题。<button onClick={handleClick}>Update Title</button>
: 创建一个按钮,当按钮被点击时,调用handleClick
函数。
更高级的用法:
useDispatch
可以与 useSelect
结合使用,实现更复杂的数据交互。例如,我们可以使用 useSelect
从数据存储中获取当前 post 的 ID,然后使用 useDispatch
更新该 post 的标题。
import { useSelect, useDispatch } from '@wordpress/data';
function MyComponent() {
const postId = useSelect(
(select) => select('core/editor').getCurrentPostId(),
[]
);
const { editEntityRecord } = useDispatch('core');
const handleClick = () => {
editEntityRecord(
'postType',
'post',
postId,
{ title: 'Updated Title' }
);
};
return (
<button onClick={handleClick}>Update Title</button>
);
}
在这个例子中,我们首先使用 useSelect
从 core/editor
数据存储中获取当前 post 的 ID。然后,我们使用 useDispatch
从 core
数据存储中获取 editEntityRecord
action。最后,我们调用 editEntityRecord
action,并传递 post 的 ID 和新的标题作为参数。
注意事项:
useDispatch
返回的dispatch
函数是一个稳定的引用,这意味着它不会在每次渲染时都发生变化。这使得我们可以安全地将其传递给子组件,而不用担心子组件会不必要地重新渲染。- 在使用
useDispatch
时,需要确保传递给 actions 的数据是正确的类型。例如,如果 actions 期望接收一个字符串,则必须传递一个字符串,而不是一个数字或一个对象。
创建自定义数据存储:数据管理新思路
WordPress 数据存储 API 允许我们创建自定义的数据存储,从而更好地管理 Gutenberg 区块的数据。这在构建复杂区块或需要与其他系统集成时尤其有用。
步骤 1:定义数据存储
首先,我们需要定义数据存储。这包括定义数据存储的名称、状态、actions 和 selectors。
// my-plugin/src/data/index.js
import { registerStore } from '@wordpress/data';
const DEFAULT_STATE = {
items: [],
};
const actions = {
addItem(item) {
return {
type: 'ADD_ITEM',
item,
};
},
removeItem(itemId) {
return {
type: 'REMOVE_ITEM',
itemId,
};
},
};
const reducer = (state = DEFAULT_STATE, action) => {
switch (action.type) {
case 'ADD_ITEM':
return {
...state,
items: [...state.items, action.item],
};
case 'REMOVE_ITEM':
return {
...state,
items: state.items.filter((item) => item.id !== action.itemId),
};
default:
return state;
}
};
const selectors = {
getItems(state) {
return state.items;
},
getItem(state, itemId) {
return state.items.find((item) => item.id === itemId);
},
};
const storeConfig = {
reducer,
actions,
selectors,
persist: false, // 是否将数据存储持久化到本地存储
};
registerStore('my-plugin/items', storeConfig);
代码解释:
registerStore('my-plugin/items', storeConfig);
: 使用registerStore
函数注册数据存储。第一个参数是数据存储的名称,第二个参数是数据存储的配置对象。DEFAULT_STATE
: 定义数据存储的初始状态。actions
: 定义数据存储的 actions。每个 action 都是一个函数,它返回一个包含type
属性的对象。type
属性用于标识 action 的类型。reducer
: 定义数据存储的 reducer。reducer 是一个函数,它接收当前的状态和一个 action 作为参数,并返回一个新的状态。reducer 必须是一个纯函数,这意味着它不应该有任何副作用。selectors
: 定义数据存储的 selectors。每个 selector 都是一个函数,它接收当前的状态作为参数,并返回一个从状态中提取的数据。persist
: 指定是否将数据存储持久化到本地存储。如果设置为true
,则数据存储的状态会在浏览器关闭后仍然保留。
步骤 2:使用数据存储
在定义了数据存储之后,我们就可以在 Gutenberg 区块中使用它了。
import { useSelect, useDispatch } from '@wordpress/data';
function MyBlock() {
const items = useSelect((select) => select('my-plugin/items').getItems(), []);
const { addItem, removeItem } = useDispatch('my-plugin/items');
const handleAddItem = () => {
const newItem = {
id: Date.now(),
name: 'New Item',
};
addItem(newItem);
};
const handleRemoveItem = (itemId) => {
removeItem(itemId);
};
return (
<div>
<button onClick={handleAddItem}>Add Item</button>
<ul>
{items.map((item) => (
<li key={item.id}>
{item.name}
<button onClick={() => handleRemoveItem(item.id)}>Remove</button>
</li>
))}
</ul>
</div>
);
}
代码解释:
const items = useSelect((select) => select('my-plugin/items').getItems(), []);
: 使用useSelect
Hook 从my-plugin/items
数据存储中获取所有 items。const { addItem, removeItem } = useDispatch('my-plugin/items');
: 使用useDispatch
Hook 从my-plugin/items
数据存储中获取addItem
和removeItem
actions。handleAddItem
: 定义一个事件处理函数,当点击 "Add Item" 按钮时,该函数会被调用。该函数创建一个新的 item,并使用addItem
action 将其添加到数据存储中。handleRemoveItem
: 定义一个事件处理函数,当点击 "Remove" 按钮时,该函数会被调用。该函数使用removeItem
action 从数据存储中删除指定的 item。
步骤 3:插件入口文件
最后,需要在插件的入口文件中引入数据存储的定义。
// my-plugin/index.php
<?php
/**
* Plugin Name: My Plugin
*/
function my_plugin_register_block() {
wp_register_script(
'my-plugin-block',
plugins_url( 'build/index.js', __FILE__ ),
array( 'wp-blocks', 'wp-element', 'wp-editor', 'wp-components', 'wp-data' ),
filemtime( plugin_dir_path( __FILE__ ) . 'build/index.js' )
);
register_block_type( 'my-plugin/my-block', array(
'editor_script' => 'my-plugin-block',
) );
}
add_action( 'init', 'my_plugin_register_block' );
function my_plugin_enqueue_block_editor_assets() {
wp_enqueue_script(
'my-plugin-data',
plugins_url( 'src/data/index.js', __FILE__ ),
array( 'wp-data' ),
filemtime( plugin_dir_path( __FILE__ ) . 'src/data/index.js' )
);
}
add_action( 'enqueue_block_editor_assets', 'my_plugin_enqueue_block_editor_assets' );
代码解释:
my_plugin_enqueue_block_editor_assets
函数使用wp_enqueue_script
函数将src/data/index.js
文件加载到 Gutenberg 编辑器中。这确保了数据存储在 Gutenberg 编辑器中可用。
通过以上步骤,我们就成功创建了一个自定义的数据存储,并在 Gutenberg 区块中使用了它。
案例分析:构建一个可排序的列表区块
让我们通过一个更具体的案例来演示 useSelect
和 useDispatch
的强大功能:构建一个可排序的列表区块。
数据存储定义:
// src/data/sortable-list.js
import { registerStore } from '@wordpress/data';
const DEFAULT_STATE = {
items: [],
};
const actions = {
setItems(items) {
return {
type: 'SET_ITEMS',
items,
};
},
moveItem(fromIndex, toIndex) {
return {
type: 'MOVE_ITEM',
fromIndex,
toIndex,
};
},
};
const reducer = (state = DEFAULT_STATE, action) => {
switch (action.type) {
case 'SET_ITEMS':
return {
...state,
items: action.items,
};
case 'MOVE_ITEM':
const newItems = [...state.items];
const item = newItems.splice(action.fromIndex, 1)[0];
newItems.splice(action.toIndex, 0, item);
return {
...state,
items: newItems,
};
default:
return state;
}
};
const selectors = {
getItems(state) {
return state.items;
},
};
const storeConfig = {
reducer,
actions,
selectors,
};
registerStore('my-plugin/sortable-list', storeConfig);
区块组件:
// src/block/index.js
import { useSelect, useDispatch } from '@wordpress/data';
import { useBlockProps, InspectorControls } from '@wordpress/block-editor';
import { PanelBody, TextControl } from '@wordpress/components';
import { useState, useEffect } from '@wordpress/element';
function SortableListBlock({ attributes, setAttributes }) {
const { items: attributeItems } = attributes;
const [tempItems, setTempItems] = useState(attributeItems || []);
const { setItems, moveItem } = useDispatch('my-plugin/sortable-list');
const items = useSelect((select) => select('my-plugin/sortable-list').getItems(), [tempItems]);
const blockProps = useBlockProps();
useEffect(() => {
setItems(tempItems);
}, [tempItems, setItems]);
useEffect(() => {
setAttributes({ items: items });
}, [items, setAttributes]);
const handleItemChange = (index, value) => {
const newItems = [...tempItems];
newItems[index] = value;
setTempItems(newItems);
};
const handleMoveUp = (index) => {
moveItem(index, index - 1);
setTempItems([...items])
};
const handleMoveDown = (index) => {
moveItem(index, index + 1);
setTempItems([...items])
};
const handleAddItem = () => {
setTempItems([...tempItems, ""]);
}
const handleRemoveItem = (index) => {
const newItems = [...tempItems];
newItems.splice(index, 1);
setTempItems(newItems);
}
return (
<div {...blockProps}>
<InspectorControls>
<PanelBody title="List Items">
{tempItems.map((item, index) => (
<div key={index}>
<TextControl
label={`Item ${index + 1}`}
value={item}
onChange={(value) => handleItemChange(index, value)}
/>
<button onClick={() => handleMoveUp(index)} disabled={index === 0}>Up</button>
<button onClick={() => handleMoveDown(index)} disabled={index === tempItems.length - 1}>Down</button>
<button onClick={() => handleRemoveItem(index)}>Remove</button>
</div>
))}
<button onClick={handleAddItem}>Add Item</button>
</PanelBody>
</InspectorControls>
<ul>
{items.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
</div>
);
}
export default SortableListBlock;
代码解释:
useSelect
: 用于从my-plugin/sortable-list
数据存储中获取items
。useDispatch
: 用于获取setItems
和moveItem
actions。handleItemChange
: 当列表项的内容发生变化时,更新tempItems
状态。handleMoveUp
和handleMoveDown
: 调用moveItem
action 来移动列表项的位置,并更新tempItems
状态。handleAddItem
: 添加新的列表项到tempItems
状态。handleRemoveItem
: 从tempItems
状态删除列表项。- 使用
useEffect
同步tempItems
和items
和 Block的Attributes
注册区块:
// src/index.js
import { registerBlockType } from '@wordpress/blocks';
import SortableListBlock from './block';
import './data/sortable-list'; // 引入数据存储
registerBlockType('my-plugin/sortable-list-block', {
title: 'Sortable List',
icon: 'list-view',
category: 'common',
attributes: {
items: {
type: 'array',
default: [],
},
},
edit: SortableListBlock,
save: () => null, // 动态渲染,无需保存静态 HTML
});
这个案例展示了如何使用 useSelect
和 useDispatch
来管理区块的复杂状态,并实现交互式功能。
数据存储的优势与局限
优势:
- 状态管理集中化: 将区块的状态集中存储在数据存储中,方便管理和维护。
- 组件解耦: 区块组件不需要直接操作数据,而是通过 actions 来修改数据存储的状态,从而实现组件的解耦。
- 可测试性: 数据存储的 actions 和 reducers 都是纯函数,易于测试。
- 性能优化:
useSelect
具有内置的性能优化机制,可以减少不必要的渲染。
局限:
- 学习曲线: 掌握 WordPress 数据存储 API 需要一定的学习成本。
- 过度使用: 对于简单的区块,使用数据存储可能会显得过于复杂。
区块开发的精髓
通过深入学习 useSelect
和 useDispatch
,我们可以更好地理解 Gutenberg 区块开发的数据流,并能够构建更复杂、更强大的区块。自定义数据存储为我们提供了更大的灵活性,可以更好地管理区块的数据,并实现与其他系统的集成。选择合适的数据管理方式,才能写出更优秀的代码。