管理面板 API:Redux 存储

页面摘要: 在 register 期间使用 addReducers() 向 Redux store 添加自定义状态。然后使用 useSelector 读取状态,用 useDispatch 更新它,并用 useStore 订阅变更。admin_app 切片暴露主题、语言区域、权限和身份验证数据。

Strapi 的管理面板使用全局 Redux store 来管理应用状态。插件可以访问该 store 以读取状态、分发 action 并订阅状态变更。这使得插件能够与核心管理功能(如主题设置、语言偏好和身份验证状态)交互。

WARNING

在深入阅读本页概念之前,请确保你已经:

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 切片包含以下状态属性:

PropertyTypeDescription
theme.currentThemestring当前主题('light'、'dark' 或 'system')
theme.availableThemesstring[]可用主题名称数组
language.localestring当前语言区域代码(例如 'en'、'fr')
language.localeNamesobject将语言区域代码映射到显示名称的对象
tokenstring \| null身份验证令牌
permissionsobject用户权限对象

分发 action

要更新 Redux store,请使用 useDispatch hook:

NOTE

下面的示例为了演示目的,向核心管理状态(主题、语言区域)分发了 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 typePayload typeDescription
admin/setAppThemestring设置主题('light'、'dark' 或 'system')
admin/setAvailableThemesstring[]更新 admin_app 中的 theme.availableThemes
admin/setLocalestring设置语言区域(例如 'en'、'fr')
admin/setTokenstring \| null设置身份验证令牌
admin/login{ token: string, persist?: boolean }带令牌和持久化选项的登录 action
admin/logoutvoid登出 action(无负载)
NOTE

分发 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-redux hook。如果你使用 Strapi 管理工具,请从 @strapi/admin/strapi-admin(而非 @strapi/admin)导入它们。在它们被明确记录为稳定之前,避免依赖未文档化的类型化 Redux hook 作为 Strapi 公共 API 的一部分。
  • 避免不必要的分发。 仅在需要更新状态时才分发 action。读取状态不需要分发 action。
  • 尊重核心状态。 修改核心管理状态(如主题或语言区域)时要小心,因为这会影响整个管理面板。考虑你的插件应该修改全局状态还是维护自己的本地状态。
TIP

要向 Redux store 添加你自己的状态,请参阅上文的 添加自定义 reducer。