管理面板 API:Redux 存储
页面摘要: 在
register期间使用addReducers()向 Redux store 添加自定义状态。然后使用useSelector读取状态,用useDispatch更新它,并用useStore订阅变更。admin_app切片暴露主题、语言区域、权限和身份验证数据。
Strapi 的管理面板使用全局 Redux store 来管理应用状态。插件可以访问该 store 以读取状态、分发 action 并订阅状态变更。这使得插件能够与核心管理功能(如主题设置、语言偏好和身份验证状态)交互。
在深入阅读本页概念之前,请确保你已经:
- 创建了一个 Strapi 插件,
- 阅读并理解了 Admin Panel API 的基础知识。
Store 概览
Redux store 通过 React Redux 的 Provider 自动提供给所有插件组件。该 store 包含若干切片(slice):
admin_app:核心管理状态,包括主题、语言、权限和身份验证令牌adminApi:用于管理端点的 RTK Query API 状态- 插件特定的切片:由插件添加的额外 reducer
添加自定义 reducer
Reducer 是 Redux reducer,可用于在组件之间共享状态。在以下情况下,reducer 会很有用:
- 应用在许多地方需要大量应用状态。
- 应用状态被频繁更新。
- 更新该状态的逻辑可能比较复杂。
可以在 register 生命周期期间,使用 addReducers() 函数将 reducer 添加到插件接口。
reducer 以如下语法声明为一个对象:
JavaScript
import { exampleReducer } from './reducers'
import pluginId from './pluginId'
const reducers = {
// Reducer Syntax
[`${pluginId}_exampleReducer`]: exampleReducer
}
export default {
register(app) {
app.addReducers(reducers)
},
bootstrap() {},
};
TypeScript
import type { StrapiApp } from '@strapi/admin/strapi-admin';
import { exampleReducer } from './reducers';
import pluginId from './pluginId';
const reducers = {
[`${pluginId}_exampleReducer`]: exampleReducer,
};
export default {
register(app: StrapiApp) {
app.addReducers(reducers);
},
bootstrap() {},
};
使用 useSelector 读取状态
在插件组件中访问 Redux 状态最常见的方式是使用来自 react-redux 的 useSelector hook:
JavaScript
import { useSelector } from 'react-redux';
const HomePage = () => {
// Read current theme
const currentTheme = useSelector(
(state) => state.admin_app?.theme?.currentTheme
);
// Read current locale
const currentLocale = useSelector(
(state) => state.admin_app?.language?.locale
);
// Read authentication status
const isAuthenticated = useSelector((state) => !!state.admin_app?.token);
// Read available locales
const availableLocales = useSelector(
(state) => state.admin_app?.language?.localeNames || {}
);
return (
<div>
<p>Current Theme: {currentTheme}</p>
<p>Current Locale: {currentLocale}</p>
<p>Authenticated: {isAuthenticated ? 'Yes' : 'No'}</p>
</div>
);
};
TypeScript
import { useSelector } from 'react-redux';
const HomePage = () => {
// Read current theme
const currentTheme = useSelector(
(state: any) => state.admin_app?.theme?.currentTheme
);
// Read current locale
const currentLocale = useSelector(
(state: any) => state.admin_app?.language?.locale
);
// Read authentication status
const isAuthenticated = useSelector((state: any) => !!state.admin_app?.token);
// Read available locales
const availableLocales = useSelector(
(state: any) => state.admin_app?.language?.localeNames || {}
);
return (
<div>
<p>Current Theme: {currentTheme}</p>
<p>Current Locale: {currentLocale}</p>
<p>Authenticated: {isAuthenticated ? 'Yes' : 'No'}</p>
</div>
);
};
可用的状态属性
admin_app 切片包含以下状态属性:
| Property | Type | Description |
|---|---|---|
theme.currentTheme | string | 当前主题('light'、'dark' 或 'system') |
theme.availableThemes | string[] | 可用主题名称数组 |
language.locale | string | 当前语言区域代码(例如 'en'、'fr') |
language.localeNames | object | 将语言区域代码映射到显示名称的对象 |
token | string \| null | 身份验证令牌 |
permissions | object | 用户权限对象 |
分发 action
要更新 Redux store,请使用 useDispatch hook:
下面的示例为了演示目的,向核心管理状态(主题、语言区域)分发了 action。在实践中,大多数插件应该向它们自己的自定义 reducer 分发 action,而不是修改全局管理状态。
JavaScript
import { useSelector, useDispatch } from 'react-redux';
const HomePage = () => {
const dispatch = useDispatch();
const currentTheme = useSelector(
(state) => state.admin_app?.theme?.currentTheme
);
const handleToggleTheme = () => {
const newTheme =
currentTheme === 'light'
? 'dark'
: currentTheme === 'dark'
? 'system'
: 'light';
dispatch({
type: 'admin/setAppTheme',
payload: newTheme,
});
};
const handleChangeLocale = (locale) => {
dispatch({
type: 'admin/setLocale',
payload: locale,
});
};
return (
<div>
<button onClick={handleToggleTheme}>
Toggle Theme (Current: {currentTheme})
</button>
<button onClick={() => handleChangeLocale('en')}>Set English</button>
</div>
);
};
TypeScript
import { useSelector, useDispatch } from 'react-redux';
const HomePage = () => {
const dispatch = useDispatch();
const currentTheme = useSelector(
(state: any) => state.admin_app?.theme?.currentTheme
);
const handleToggleTheme = () => {
const newTheme =
currentTheme === 'light'
? 'dark'
: currentTheme === 'dark'
? 'system'
: 'light';
dispatch({
type: 'admin/setAppTheme',
payload: newTheme,
} as any);
};
const handleChangeLocale = (locale: string) => {
dispatch({
type: 'admin/setLocale',
payload: locale,
} as any);
};
return (
<div>
<button onClick={handleToggleTheme}>
Toggle Theme (Current: {currentTheme})
</button>
<button onClick={() => handleChangeLocale('en')}>Set English</button>
</div>
);
};
可用的 action
admin_app 切片提供以下 action:
| Action type | Payload type | Description |
|---|---|---|
admin/setAppTheme | string | 设置主题('light'、'dark' 或 'system') |
admin/setAvailableThemes | string[] | 更新 admin_app 中的 theme.availableThemes |
admin/setLocale | string | 设置语言区域(例如 'en'、'fr') |
admin/setToken | string \| null | 设置身份验证令牌 |
admin/login | { token: string, persist?: boolean } | 带令牌和持久化选项的登录 action |
admin/logout | void | 登出 action(无负载) |
分发 action 时,请使用 Redux Toolkit 的 action 类型格式:'sliceName/actionName'。admin 切片的名称为 'admin',因此 action 遵循 'admin/actionName' 模式。
访问 store 实例
对于高级用例,你可以使用 useStore hook 直接访问 store 实例:
JavaScript
import { useStore } from 'react-redux';
import { useEffect } from 'react';
const App = () => {
const store = useStore();
useEffect(() => {
const state = store.getState();
console.log('Redux Store State:', state);
const unsubscribe = store.subscribe(() => {
const currentState = store.getState();
console.log('Store state changed:', {
theme: currentState.admin_app?.theme?.currentTheme,
locale: currentState.admin_app?.language?.locale,
timestamp: new Date().toISOString(),
});
});
return () => {
unsubscribe();
};
}, [store]);
return <div>My Plugin</div>;
};
TypeScript
import { useStore } from 'react-redux';
import { useEffect } from 'react';
const App = () => {
const store = useStore();
useEffect(() => {
const state = store.getState();
console.log('Redux Store State:', state);
const unsubscribe = store.subscribe(() => {
const currentState = store.getState();
console.log('Store state changed:', {
theme: currentState.admin_app?.theme?.currentTheme,
locale: currentState.admin_app?.language?.locale,
timestamp: new Date().toISOString(),
});
});
return () => {
unsubscribe();
};
}, [store]);
return <div>My Plugin</div>;
};
完整示例
以下示例组合了本页描述的全部 3 种模式(useSelector、useDispatch、useStore):
JavaScript
import { Main } from '@strapi/design-system';
import { Button, Box, Typography, Flex } from '@strapi/design-system';
import { useSelector, useDispatch, useStore } from 'react-redux';
import { useEffect, useState } from 'react';
const HomePage = () => {
const dispatch = useDispatch();
const store = useStore();
// Reading state
const currentTheme = useSelector(
(state) => state.admin_app?.theme?.currentTheme
);
const currentLocale = useSelector(
(state) => state.admin_app?.language?.locale
);
const isAuthenticated = useSelector((state) => !!state.admin_app?.token);
const availableLocales = useSelector(
(state) => state.admin_app?.language?.localeNames || {}
);
// Dispatching actions
const handleToggleTheme = () => {
const newTheme =
currentTheme === 'light'
? 'dark'
: currentTheme === 'dark'
? 'system'
: 'light';
dispatch({ type: 'admin/setAppTheme', payload: newTheme });
};
const handleChangeLocale = (locale) => {
dispatch({ type: 'admin/setLocale', payload: locale });
};
// Subscribing to store changes
const [storeChangeCount, setStoreChangeCount] = useState(0);
const [lastChange, setLastChange] = useState('');
useEffect(() => {
const unsubscribe = store.subscribe(() => {
setStoreChangeCount((prev) => prev + 1);
setLastChange(new Date().toLocaleTimeString());
});
return () => unsubscribe();
}, [store]);
return (
<Main>
<Box padding={8}>
<Typography variant="alpha" as="h1">
Redux Store Examples
</Typography>
<Flex direction="column" gap={4} paddingTop={6}>
<Box padding={4} background="neutral100" hasRadius>
<Typography variant="omega" fontWeight="bold" paddingBottom={2}>
Reading state
</Typography>
<Flex direction="column" gap={2}>
<Typography variant="omega">
Current Theme: <strong>{currentTheme || 'system'}</strong>
</Typography>
<Typography variant="omega">
Current Locale: <strong>{currentLocale || 'en'}</strong>
</Typography>
<Typography variant="omega">
Authentication Status:{' '}
<strong>
{isAuthenticated ? 'Authenticated' : 'Not Authenticated'}
</strong>
</Typography>
</Flex>
</Box>
<Box padding={4} background="neutral100" hasRadius>
<Typography variant="omega" fontWeight="bold" paddingBottom={2}>
Dispatching actions
</Typography>
<Flex direction="row" gap={2} wrap="wrap">
<Button onClick={handleToggleTheme} variant="secondary">
Toggle Theme
</Button>
{Object.keys(availableLocales).map((locale) => (
<Button
key={locale}
onClick={() => handleChangeLocale(locale)}
variant={currentLocale === locale ? 'default' : 'tertiary'}
>
Set {availableLocales[locale] || locale}
</Button>
))}
</Flex>
</Box>
<Box padding={4} background="neutral100" hasRadius>
<Typography variant="omega" fontWeight="bold" paddingBottom={2}>
Subscribing to store changes
</Typography>
<Flex direction="column" gap={2}>
<Typography variant="omega">
Store has changed <strong>{storeChangeCount}</strong> time(s)
</Typography>
{lastChange && (
<Typography variant="omega">
Last change at: <strong>{lastChange}</strong>
</Typography>
)}
</Flex>
</Box>
</Flex>
</Box>
</Main>
);
};
export { HomePage };
TypeScript
import { Main } from '@strapi/design-system';
import { Button, Box, Typography, Flex } from '@strapi/design-system';
import { useSelector, useDispatch, useStore } from 'react-redux';
import { useEffect, useState } from 'react';
const HomePage = () => {
const dispatch = useDispatch();
const store = useStore();
// Reading state
const currentTheme = useSelector(
(state: any) => state.admin_app?.theme?.currentTheme
);
const currentLocale = useSelector(
(state: any) => state.admin_app?.language?.locale
);
const isAuthenticated = useSelector((state: any) => !!state.admin_app?.token);
const availableLocales = useSelector(
(state: any) => state.admin_app?.language?.localeNames || {}
);
// Dispatching actions
const handleToggleTheme = () => {
const newTheme =
currentTheme === 'light'
? 'dark'
: currentTheme === 'dark'
? 'system'
: 'light';
dispatch({ type: 'admin/setAppTheme', payload: newTheme } as any);
};
const handleChangeLocale = (locale: string) => {
dispatch({ type: 'admin/setLocale', payload: locale } as any);
};
// Subscribing to store changes
const [storeChangeCount, setStoreChangeCount] = useState(0);
const [lastChange, setLastChange] = useState<string>('');
useEffect(() => {
const unsubscribe = store.subscribe(() => {
setStoreChangeCount((prev) => prev + 1);
setLastChange(new Date().toLocaleTimeString());
});
return () => unsubscribe();
}, [store]);
return (
<Main>
<Box padding={8}>
<Typography variant="alpha" as="h1">
Redux Store Examples
</Typography>
<Flex direction="column" gap={4} paddingTop={6}>
<Box padding={4} background="neutral100" hasRadius>
<Typography variant="omega" fontWeight="bold" paddingBottom={2}>
Reading state
</Typography>
<Flex direction="column" gap={2}>
<Typography variant="omega">
Current Theme: <strong>{currentTheme || 'system'}</strong>
</Typography>
<Typography variant="omega">
Current Locale: <strong>{currentLocale || 'en'}</strong>
</Typography>
<Typography variant="omega">
Authentication Status:{' '}
<strong>
{isAuthenticated ? 'Authenticated' : 'Not Authenticated'}
</strong>
</Typography>
</Flex>
</Box>
<Box padding={4} background="neutral100" hasRadius>
<Typography variant="omega" fontWeight="bold" paddingBottom={2}>
Dispatching actions
</Typography>
<Flex direction="row" gap={2} wrap="wrap">
<Button onClick={handleToggleTheme} variant="secondary">
Toggle Theme
</Button>
{Object.keys(availableLocales).map((locale) => (
<Button
key={locale}
onClick={() => handleChangeLocale(locale)}
variant={currentLocale === locale ? 'default' : 'tertiary'}
>
Set {availableLocales[locale] || locale}
</Button>
))}
</Flex>
</Box>
<Box padding={4} background="neutral100" hasRadius>
<Typography variant="omega" fontWeight="bold" paddingBottom={2}>
Subscribing to store changes
</Typography>
<Flex direction="column" gap={2}>
<Typography variant="omega">
Store has changed <strong>{storeChangeCount}</strong> time(s)
</Typography>
{lastChange && (
<Typography variant="omega">
Last change at: <strong>{lastChange}</strong>
</Typography>
)}
</Flex>
</Box>
</Flex>
</Box>
</Main>
);
};
export { HomePage };
最佳实践
- 使用
useSelector读取状态。 优先使用useSelector而非直接访问 store。它会自动订阅更新,并在所选状态变化时重新渲染组件。 - 清理订阅。 务必在
useEffect清理函数中取消 store 订阅,以防止内存泄漏。 - 考虑类型安全。 对于插件中的 Redux 状态访问,请使用带插件本地类型(例如
RootState和AppDispatch)的react-reduxhook。如果你使用 Strapi 管理工具,请从@strapi/admin/strapi-admin(而非@strapi/admin)导入它们。在它们被明确记录为稳定之前,避免依赖未文档化的类型化 Redux hook 作为 Strapi 公共 API 的一部分。 - 避免不必要的分发。 仅在需要更新状态时才分发 action。读取状态不需要分发 action。
- 尊重核心状态。 修改核心管理状态(如主题或语言区域)时要小心,因为这会影响整个管理面板。考虑你的插件应该修改全局状态还是维护自己的本地状态。
要向 Redux store 添加你自己的状态,请参阅上文的 添加自定义 reducer。