Gutenberg 区块:利用 useSelect
和 useDispatch
与数据存储交互
各位同学,今天我们来深入探讨 Gutenberg 区块开发中,如何利用 useSelect
和 useDispatch
这两个 React Hook 与 WordPress 数据存储进行交互。这两个 Hook 是 @wordpress/data
包提供的核心工具,允许我们在区块组件中轻松地读取和修改数据。
WordPress 数据存储体系概览
在深入 useSelect
和 useDispatch
之前,我们需要对 WordPress 数据存储体系有一个基本的了解。WordPress 使用基于 Redux 的数据存储来管理各种数据,例如:
- Posts: 文章、页面、自定义文章类型
- Taxonomies: 分类目录、标签、自定义分类法
- Settings: 站点设置、主题选项
- Users: 用户信息
- Editor: 编辑器状态、区块属性
每个数据类型都对应一个或多个 store,例如 core/editor
store 管理编辑器状态,core/block-editor
store 管理区块编辑器相关的数据。每个 store 都包含:
- State: 存储数据的 JavaScript 对象。
- Selectors: 函数,用于从 state 中提取数据。
- Actions: 函数,用于触发 state 的更新。
- Reducers: 函数,接收 action 并更新 state。
- Controls: 允许 action 发出副作用的中间件,例如 API 请求。
- Resolvers: 用于异步获取数据并将其添加到 state 的函数。
我们可以通过 @wordpress/data
包提供的 select
和 dispatch
函数访问这些 store 的 selectors 和 actions。useSelect
和 useDispatch
Hook 则是对这些函数的 React 友好的封装,允许我们在函数组件中更方便地使用它们。
useSelect
: 从数据存储中读取数据
useSelect
Hook 允许我们从数据存储中选择(select)特定的数据,并在数据发生变化时自动更新组件。它的基本语法如下:
import { useSelect } from '@wordpress/data';
function MyBlockComponent() {
const myData = useSelect( ( select ) => {
// 从数据存储中选择数据的逻辑
return select( 'my/store' ).getMyData();
}, [ /* 依赖项数组 */ ] );
// 使用 myData
return (
// JSX
);
}
参数:
- selector: 一个函数,接收一个
select
对象作为参数。select
对象包含访问所有注册 store 的 selector 函数的方法。 Selector 函数必须是纯函数,不能有副作用。 - dependencies: 一个可选的依赖项数组。只有当依赖项发生变化时,
useSelect
才会重新执行 selector 函数并更新组件。如果省略依赖项数组,selector 函数将在每次渲染时执行。
返回值:
- selector 函数的返回值。当 selector 函数返回的数据发生变化时,组件将重新渲染。
示例:
假设我们有一个名为 my/store
的自定义 store,它包含一个名为 getMyData
的 selector,用于获取存储在 state 中的数据。以下是如何使用 useSelect
从该 store 中读取数据:
import { useSelect } from '@wordpress/data';
function MyBlockComponent() {
const myData = useSelect( ( select ) => {
return select( 'my/store' ).getMyData();
}, [] ); // 空数组表示只在组件挂载时执行一次
return (
<div>
<p>My Data: {myData}</p>
</div>
);
}
在这个例子中,useSelect
Hook 会调用 select( 'my/store' ).getMyData()
函数,并将返回的值赋给 myData
变量。如果 my/store
的 state 发生变化,并且 getMyData
selector 返回的值与之前的不同,MyBlockComponent
组件将重新渲染,显示新的数据。
优化性能:
为了优化性能,我们应该尽量使用依赖项数组来控制 useSelect
Hook 的执行频率。只有当依赖项发生变化时,才重新执行 selector 函数。例如,如果我们只需要在 my/store
的 myData
属性发生变化时更新组件,我们可以将 myData
作为依赖项:
import { useSelect } from '@wordpress/data';
function MyBlockComponent() {
const myData = useSelect( ( select ) => {
return select( 'my/store' ).getMyData();
}, [ select( 'my/store' ).getMyData() ] );
return (
<div>
<p>My Data: {myData}</p>
</div>
);
}
然而,直接使用 select( 'my/store' ).getMyData()
作为依赖项通常不是最佳实践,因为它会导致每次渲染都执行 selector 函数,即使数据没有变化。更好的做法是将 selector 函数的结果赋值给一个变量,然后在依赖项数组中使用该变量:
import { useSelect } from '@wordpress/data';
import { useState } from '@wordpress/element';
function MyBlockComponent() {
const myData = useSelect( ( select ) => {
return select( 'my/store' ).getMyData();
}, [] );
const [previousData, setPreviousData] = useState(myData);
if (myData !== previousData) {
setPreviousData(myData);
}
return (
<div>
<p>My Data: {previousData}</p>
</div>
);
}
或者,也可以使用 useMemo
来缓存 selector 函数的结果:
import { useSelect } from '@wordpress/data';
import { useMemo } from '@wordpress/element';
function MyBlockComponent() {
const myData = useSelect( ( select ) => {
return select( 'my/store' ).getMyData();
}, [] );
const memoizedData = useMemo(() => myData, [myData]);
return (
<div>
<p>My Data: {memoizedData}</p>
</div>
);
}
从核心 store 中读取数据:
useSelect
也可以用来从 WordPress 核心 store 中读取数据。例如,我们可以使用 useSelect
从 core/editor
store 中获取当前的文章标题:
import { useSelect } from '@wordpress/data';
function MyBlockComponent() {
const title = useSelect( ( select ) => {
return select( 'core/editor' ).getEditedPostAttribute( 'title' );
}, [] );
return (
<div>
<p>Current Title: {title}</p>
</div>
);
}
在这个例子中,我们使用了 core/editor
store 的 getEditedPostAttribute
selector 来获取当前文章的 title
属性。
useDispatch
: 触发数据存储的更新
useDispatch
Hook 允许我们获取一个 dispatch 函数,用于触发数据存储的 action,从而更新 state。它的基本语法如下:
import { useDispatch } from '@wordpress/data';
function MyBlockComponent() {
const { myAction } = useDispatch( 'my/store' );
const handleClick = () => {
myAction( 'some data' );
};
return (
<button onClick={handleClick}>
Update Data
</button>
);
}
参数:
- storeName: 要获取 dispatch 函数的 store 的名称。
返回值:
- 一个对象,包含指定 store 的所有 action creator 函数。
示例:
假设我们有一个名为 my/store
的自定义 store,它包含一个名为 setMyData
的 action,用于更新存储在 state 中的数据。以下是如何使用 useDispatch
触发该 action:
import { useDispatch } from '@wordpress/data';
function MyBlockComponent() {
const { setMyData } = useDispatch( 'my/store' );
const handleClick = () => {
setMyData( 'new data' );
};
return (
<button onClick={handleClick}>
Update Data
</button>
);
}
在这个例子中,useDispatch( 'my/store' )
返回一个包含 setMyData
action creator 函数的对象。当我们点击按钮时,handleClick
函数会被调用,它会调用 setMyData( 'new data' )
来触发 setMyData
action,从而更新 my/store
的 state。
从核心 store 中触发 action:
useDispatch
也可以用来从 WordPress 核心 store 中触发 action。例如,我们可以使用 useDispatch
从 core/editor
store 中更新当前的文章标题:
import { useDispatch } from '@wordpress/data';
function MyBlockComponent() {
const { editPost } = useDispatch( 'core/editor' );
const handleClick = () => {
editPost( { title: 'New Title' } );
};
return (
<button onClick={handleClick}>
Update Title
</button>
);
}
在这个例子中,我们使用了 core/editor
store 的 editPost
action 来更新当前文章的 title
属性。
使用 useSelect
和 useDispatch
的组合:
useSelect
和 useDispatch
通常一起使用,以实现组件的双向数据绑定。例如,我们可以使用 useSelect
从 store 中读取数据,并使用 useDispatch
更新 store 中的数据。以下是一个完整的例子:
import { useSelect, useDispatch } from '@wordpress/data';
function MyBlockComponent() {
const myData = useSelect( ( select ) => {
return select( 'my/store' ).getMyData();
}, [] );
const { setMyData } = useDispatch( 'my/store' );
const handleChange = ( event ) => {
setMyData( event.target.value );
};
return (
<div>
<label htmlFor="my-data">My Data:</label>
<input
type="text"
id="my-data"
value={myData}
onChange={handleChange}
/>
</div>
);
}
在这个例子中,我们使用 useSelect
从 my/store
中读取 myData
,并将其显示在 input 框中。当 input 框的值发生变化时,handleChange
函数会被调用,它会调用 setMyData
action 来更新 my/store
中的 myData
。
最佳实践和注意事项
- 理解数据存储结构: 在使用
useSelect
和useDispatch
之前,请务必了解要交互的 store 的数据结构、selectors 和 actions。 - 避免过度渲染: 使用依赖项数组来控制
useSelect
Hook 的执行频率,避免不必要的渲染。可以使用useMemo
来缓存 selector 函数的结果。 - 使用纯函数: Selector 函数必须是纯函数,不能有副作用。
- 正确处理异步操作: 如果 action 触发了异步操作,例如 API 请求,请确保正确处理 loading 状态和错误处理。
- 测试: 对使用
useSelect
和useDispatch
的组件进行单元测试,以确保数据的正确读取和更新。 - 避免直接修改 state: 始终通过 actions 来更新 state。直接修改 state 可能会导致不可预测的行为。
- 利用 resolvers 异步加载数据: 对于初始化或者需要异步加载的数据,使用resolvers可以有效的管理数据加载过程,并且可以利用
hasFinishedResolution
判断数据是否加载完成,避免不必要的渲染。
示例代码:创建一个简单的计数器区块
以下是一个完整的示例代码,演示如何创建一个简单的计数器区块,使用 useSelect
和 useDispatch
来读取和更新计数器的值:
// index.js (主插件文件)
import { registerBlockType } from '@wordpress/blocks';
import { useBlockProps, InspectorControls } from '@wordpress/block-editor';
import { PanelBody, Button } from '@wordpress/components';
import { useSelect, useDispatch } from '@wordpress/data';
import { createReduxStore, register } from '@wordpress/data';
// 创建一个简单的 Redux Store
const COUNTER_STORE_NAME = 'my-plugin/counter';
const counterReducer = (state = { count: 0 }, action) => {
switch (action.type) {
case 'INCREMENT':
return { ...state, count: state.count + 1 };
case 'DECREMENT':
return { ...state, count: state.count - 1 };
default:
return state;
}
};
const counterStore = createReduxStore(COUNTER_STORE_NAME, {
reducer: counterReducer,
});
register(counterStore);
registerBlockType('my-plugin/counter-block', {
title: 'Counter Block',
icon: 'smiley',
category: 'common',
attributes: {
// 可以添加一些区块级别的配置,例如是否显示计数器
showCounter: {
type: 'boolean',
default: true,
},
},
edit: ({ attributes, setAttributes }) => {
const { showCounter } = attributes;
const count = useSelect((select) => select(COUNTER_STORE_NAME).getState().count, []);
const { dispatch } = useDispatch(COUNTER_STORE_NAME);
const increment = () => {
dispatch({ type: 'INCREMENT' });
};
const decrement = () => {
dispatch({ type: 'DECREMENT' });
};
const blockProps = useBlockProps();
return (
<div {...blockProps}>
<InspectorControls>
<PanelBody title="Counter Settings">
<Button isPrimary onClick={() => setAttributes({ showCounter: !showCounter })}>
{showCounter ? 'Hide Counter' : 'Show Counter'}
</Button>
</PanelBody>
</InspectorControls>
{showCounter && (
<>
<p>Count: {count}</p>
<Button onClick={increment}>Increment</Button>
<Button onClick={decrement}>Decrement</Button>
</>
)}
</div>
);
},
save: ({ attributes }) => {
const { showCounter } = attributes;
const blockProps = useBlockProps.save();
return (
<div {...blockProps}>
{showCounter && <p>Counter Block Content (Dynamic)</p>}
</div>
);
},
});
步骤解释:
- 创建 Redux Store: 创建了一个名为
my-plugin/counter
的 Redux store,包含一个 reducer 用于处理INCREMENT
和DECREMENT
action。 - 注册 Store: 使用
register
函数将 store 注册到 WordPress 数据存储系统中。 - 注册 Block Type: 使用
registerBlockType
函数注册一个名为my-plugin/counter-block
的 Gutenberg 区块。 - 使用
useSelect
: 在edit
函数中使用useSelect
Hook 从 store 中读取count
的值。 - 使用
useDispatch
: 在edit
函数中使用useDispatch
Hook 获取dispatch
函数,用于触发INCREMENT
和DECREMENT
action。 - Inspector Controls: 添加了 Inspector Controls,允许用户控制是否显示计数器。
- 显示计数器和按钮: 如果
showCounter
属性为 true,则显示计数器的值和递增/递减按钮。 - Save 函数:
save
函数返回区块的静态 HTML,该 HTML 将在前端显示。
完整表格总结:
Hook/Function | 描述 | 参数 | 返回值 | 用途 |
---|---|---|---|---|
useSelect |
从 WordPress 数据存储中选择数据,并在数据发生变化时自动更新组件。 | selector :一个函数,接收一个 select 对象作为参数,用于从数据存储中选择数据。 dependencies :一个可选的依赖项数组,只有当依赖项发生变化时,useSelect 才会重新执行 selector 函数并更新组件。 |
selector 函数的返回值。 | 用于在区块组件中读取 WordPress 数据存储中的数据,例如文章标题、分类目录、标签、站点设置等。可以订阅数据变化,当数据发生变化时自动更新组件。可以用于实现区块的动态渲染,例如根据文章标题动态显示区块内容。 |
useDispatch |
获取一个 dispatch 函数,用于触发数据存储的 action,从而更新 state。 | storeName :要获取 dispatch 函数的 store 的名称。 |
一个对象,包含指定 store 的所有 action creator 函数。 | 用于在区块组件中触发 WordPress 数据存储的 action,例如更新文章标题、分类目录、标签、站点设置等。可以用于实现区块的交互功能,例如点击按钮更新数据。可以用于实现区块的异步操作,例如发送 API 请求更新数据。 |
select |
用于从数据存储中选择数据的函数。 | storeName :要选择数据的 store 的名称。 |
一个对象,包含指定 store 的所有 selector 函数。 | 在 useSelect Hook 的 selector 函数中使用,用于从数据存储中选择数据。 |
dispatch |
用于触发数据存储的 action 的函数。 | action :要触发的 action 对象。 |
无返回值。 | 在 useDispatch Hook 返回的 action creator 函数中使用,用于触发数据存储的 action。 |
结论
useSelect
和 useDispatch
是 Gutenberg 区块开发中不可或缺的工具。它们允许我们轻松地与 WordPress 数据存储进行交互,实现区块的动态渲染和交互功能。通过理解这两个 Hook 的基本原理和最佳实践,我们可以开发出更强大、更灵活的 Gutenberg 区块。 熟练掌握这两个Hook, 可以让你以更高效和可维护的方式来开发 Gutenberg 区块,与WordPress的数据存储进行交互,实现更加复杂和动态的功能。