@qlover/create-app 0.7.14 → 0.8.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 +27 -0
- package/dist/configs/_common/.gitignore.template +6 -0
- package/dist/configs/_common/.prettierignore +17 -5
- package/dist/configs/_common/.vscode/settings.json +6 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/templates/next-app/.env.template +1 -1
- package/dist/templates/next-app/README.en.md +130 -0
- package/dist/templates/next-app/README.md +114 -20
- package/dist/templates/next-app/config/Identifier/api.ts +5 -5
- package/dist/templates/next-app/config/Identifier/common/admint.table.ts +69 -0
- package/dist/templates/next-app/config/Identifier/common/common.ts +76 -0
- package/dist/templates/next-app/config/Identifier/common/index.ts +3 -0
- package/dist/templates/next-app/config/Identifier/{validator.ts → common/validators.ts} +5 -5
- package/dist/templates/next-app/config/Identifier/index.ts +2 -12
- package/dist/templates/next-app/config/Identifier/pages/index.ts +6 -0
- package/dist/templates/next-app/config/Identifier/pages/page.admin.home.ts +27 -0
- package/dist/templates/next-app/config/Identifier/pages/page.admin.locales.ts +266 -0
- package/dist/templates/next-app/config/Identifier/pages/page.admin.user.ts +293 -0
- package/dist/templates/{react-app/config/Identifier → next-app/config/Identifier/pages}/page.home.ts +15 -22
- package/dist/templates/next-app/config/Identifier/{page.login.ts → pages/page.login.ts} +28 -34
- package/dist/templates/next-app/config/Identifier/{page.register.ts → pages/page.register.ts} +30 -29
- package/dist/templates/next-app/config/adminNavs.ts +19 -0
- package/dist/templates/next-app/config/common.ts +22 -13
- package/dist/templates/next-app/config/i18n/HomeI18n.ts +5 -5
- package/dist/templates/next-app/config/i18n/admin18n.ts +61 -19
- package/dist/templates/next-app/config/i18n/i18nConfig.ts +2 -0
- package/dist/templates/next-app/config/i18n/i18nKeyScheam.ts +36 -0
- package/dist/templates/next-app/config/i18n/loginI18n.ts +22 -22
- package/dist/templates/next-app/config/i18n/register18n.ts +23 -24
- package/dist/templates/next-app/docs/en/api.md +387 -0
- package/dist/templates/next-app/docs/en/component.md +544 -0
- package/dist/templates/next-app/docs/en/database.md +496 -0
- package/dist/templates/next-app/docs/en/development-guide.md +727 -0
- package/dist/templates/next-app/docs/en/env.md +563 -0
- package/dist/templates/next-app/docs/en/i18n.md +287 -0
- package/dist/templates/next-app/docs/en/index.md +165 -0
- package/dist/templates/next-app/docs/en/page.md +457 -0
- package/dist/templates/next-app/docs/en/project-structure.md +176 -0
- package/dist/templates/next-app/docs/en/router.md +427 -0
- package/dist/templates/next-app/docs/en/theme.md +532 -0
- package/dist/templates/next-app/docs/en/validator.md +478 -0
- package/dist/templates/next-app/docs/zh/api.md +387 -0
- package/dist/templates/next-app/docs/zh/component.md +544 -0
- package/dist/templates/next-app/docs/zh/database.md +496 -0
- package/dist/templates/next-app/docs/zh/development-guide.md +727 -0
- package/dist/templates/next-app/docs/zh/env.md +563 -0
- package/dist/templates/next-app/docs/zh/i18n.md +287 -0
- package/dist/templates/next-app/docs/zh/index.md +165 -0
- package/dist/templates/next-app/docs/zh/page.md +457 -0
- package/dist/templates/next-app/docs/zh/project-structure.md +176 -0
- package/dist/templates/next-app/docs/zh/router.md +427 -0
- package/dist/templates/next-app/docs/zh/theme.md +532 -0
- package/dist/templates/next-app/docs/zh/validator.md +476 -0
- package/dist/templates/next-app/make/generateLocales.ts +19 -12
- package/dist/templates/next-app/migrations/schema/LocalesSchema.ts +15 -0
- package/dist/templates/next-app/migrations/sql/1694244000000.sql +11 -0
- package/dist/templates/next-app/package.json +7 -3
- package/dist/templates/next-app/public/locales/en.json +172 -207
- package/dist/templates/next-app/public/locales/zh.json +172 -207
- package/dist/templates/next-app/src/app/[locale]/admin/locales/page.tsx +153 -0
- package/dist/templates/next-app/src/app/[locale]/admin/users/page.tsx +48 -50
- package/dist/templates/next-app/src/app/[locale]/login/LoginForm.tsx +2 -2
- package/dist/templates/next-app/src/app/api/admin/locales/create/route.ts +34 -0
- package/dist/templates/next-app/src/app/api/admin/locales/import/route.ts +40 -0
- package/dist/templates/next-app/src/app/api/admin/locales/route.ts +42 -0
- package/dist/templates/next-app/src/app/api/admin/locales/update/route.ts +32 -0
- package/dist/templates/next-app/src/app/api/locales/json/route.ts +44 -0
- package/dist/templates/next-app/src/base/cases/AdminPageManager.ts +1 -13
- package/dist/templates/next-app/src/base/cases/Datetime.ts +18 -0
- package/dist/templates/next-app/src/base/cases/DialogErrorPlugin.ts +12 -6
- package/dist/templates/next-app/src/base/cases/ResourceState.ts +17 -0
- package/dist/templates/next-app/src/base/cases/TranslateI18nInterface.ts +25 -0
- package/dist/templates/next-app/src/base/cases/ZodColumnBuilder.ts +200 -0
- package/dist/templates/next-app/src/base/port/ZodBuilderInterface.ts +8 -0
- package/dist/templates/next-app/src/base/services/AdminLocalesService.ts +20 -0
- package/dist/templates/next-app/src/base/services/AdminPageEvent.ts +26 -0
- package/dist/templates/next-app/src/base/services/AdminPageScheduler.ts +42 -0
- package/dist/templates/next-app/src/base/services/ResourceService.ts +122 -0
- package/dist/templates/next-app/src/base/services/adminApi/AdminLocalesApi.ts +104 -0
- package/dist/templates/next-app/src/base/services/adminApi/AdminUserApi.ts +38 -5
- package/dist/templates/next-app/src/base/services/appApi/AppApiPlugin.ts +1 -1
- package/dist/templates/next-app/src/i18n/request.ts +30 -1
- package/dist/templates/next-app/src/server/PageParams.ts +2 -10
- package/dist/templates/next-app/src/server/port/DBBridgeInterface.ts +5 -0
- package/dist/templates/next-app/src/server/port/DBTableInterface.ts +2 -0
- package/dist/templates/next-app/src/server/port/LocalesRepositoryInterface.ts +43 -0
- package/dist/templates/next-app/src/server/repositorys/LocalesRepository.ts +197 -0
- package/dist/templates/next-app/src/server/services/ApiLocaleService.ts +122 -0
- package/dist/templates/next-app/src/server/sqlBridges/SupabaseBridge.ts +60 -11
- package/dist/templates/next-app/src/server/validators/ExtendedExecutorError.ts +6 -0
- package/dist/templates/next-app/src/server/validators/LocalesValidator.ts +131 -0
- package/dist/templates/next-app/src/server/validators/LoginValidator.ts +2 -5
- package/dist/templates/next-app/src/server/validators/PaginationValidator.ts +32 -16
- package/dist/templates/next-app/src/styles/css/antd-themes/pagination/_default.css +2 -1
- package/dist/templates/next-app/src/styles/css/antd-themes/pagination/dark.css +28 -29
- package/dist/templates/next-app/src/styles/css/antd-themes/pagination/pink.css +2 -1
- package/dist/templates/next-app/src/uikit/components/AdminLayout.tsx +17 -3
- package/dist/templates/next-app/src/uikit/components/BaseHeader.tsx +5 -4
- package/dist/templates/next-app/src/uikit/components/BaseLayout.tsx +5 -4
- package/dist/templates/next-app/src/uikit/components/BootstrapsProvider.tsx +3 -2
- package/dist/templates/next-app/src/uikit/components/ComboProvider.tsx +1 -1
- package/dist/templates/next-app/src/uikit/components/EditableCell.tsx +118 -0
- package/dist/templates/next-app/src/uikit/components/LogoutButton.tsx +5 -6
- package/dist/templates/next-app/src/uikit/components/ThemeSwitcher.tsx +1 -1
- package/dist/templates/next-app/src/uikit/components/With.tsx +2 -2
- package/dist/templates/next-app/src/uikit/components/localesImportButton/LocalesImportButton.tsx +62 -0
- package/dist/templates/next-app/src/uikit/components/localesImportButton/LocalesImportEvent.ts +28 -0
- package/dist/templates/next-app/src/uikit/components/localesImportButton/import.module.css +6 -0
- package/dist/templates/next-app/src/uikit/hook/useI18nInterface.ts +8 -14
- package/dist/templates/next-app/src/uikit/hook/useWarnTranslations.ts +25 -0
- package/dist/templates/react-app/.prettierignore +17 -0
- package/dist/templates/react-app/README.en.md +71 -54
- package/dist/templates/react-app/README.md +35 -18
- package/dist/templates/react-app/__tests__/__mocks__/BootstrapTest.ts +14 -0
- package/dist/templates/react-app/__tests__/__mocks__/MockAppConfit.ts +1 -1
- package/dist/templates/react-app/__tests__/__mocks__/MockDialogHandler.ts +2 -2
- package/dist/templates/react-app/__tests__/__mocks__/MockLogger.ts +1 -1
- package/dist/templates/react-app/__tests__/__mocks__/components/TestApp.tsx +45 -0
- package/dist/templates/react-app/__tests__/__mocks__/components/TestBootstrapsProvider.tsx +34 -0
- package/dist/templates/react-app/__tests__/__mocks__/components/TestRouter.tsx +46 -0
- package/dist/templates/react-app/__tests__/__mocks__/components/index.ts +12 -0
- package/dist/templates/react-app/__tests__/__mocks__/createMockGlobals.ts +1 -2
- package/dist/templates/react-app/__tests__/__mocks__/testIOC/TestIOC.ts +51 -0
- package/dist/templates/react-app/__tests__/__mocks__/testIOC/TestIOCRegister.ts +69 -0
- package/dist/templates/react-app/__tests__/setup/index.ts +1 -51
- package/dist/templates/react-app/__tests__/setup/setupGlobal.ts +51 -0
- package/dist/templates/react-app/__tests__/src/App.structure.test.tsx +115 -0
- package/dist/templates/react-app/__tests__/src/base/cases/AppConfig.test.ts +2 -2
- package/dist/templates/react-app/__tests__/src/base/cases/AppError.test.ts +1 -1
- package/dist/templates/react-app/__tests__/src/base/cases/DialogHandler.test.ts +3 -5
- package/dist/templates/react-app/__tests__/src/base/cases/I18nKeyErrorPlugin.test.ts +13 -2
- package/dist/templates/react-app/__tests__/src/base/cases/InversifyContainer.test.ts +1 -1
- package/dist/templates/react-app/__tests__/src/base/cases/PublicAssetsPath.test.ts +1 -1
- package/dist/templates/react-app/__tests__/src/base/cases/RequestLogger.test.ts +5 -5
- package/dist/templates/react-app/__tests__/src/base/cases/RequestStatusCatcher.test.ts +1 -2
- package/dist/templates/react-app/__tests__/src/base/cases/RouterLoader.test.ts +25 -15
- package/dist/templates/react-app/__tests__/src/base/services/I18nService.test.ts +29 -15
- package/dist/templates/react-app/__tests__/src/core/IOC.test.ts +19 -9
- package/dist/templates/react-app/__tests__/src/core/bootstraps/BootstrapClient.test.ts +153 -0
- package/dist/templates/react-app/__tests__/src/core/bootstraps/BootstrapsApp.test.ts +9 -7
- package/dist/templates/react-app/__tests__/src/main.integration.test.tsx +4 -5
- package/dist/templates/react-app/__tests__/src/main.test.tsx +4 -4
- package/dist/templates/react-app/__tests__/src/uikit/components/BaseHeader.test.tsx +68 -59
- package/dist/templates/react-app/config/IOCIdentifier.ts +8 -8
- package/dist/templates/react-app/config/Identifier/{common.error.ts → common/common.error.ts} +5 -5
- package/dist/templates/react-app/config/Identifier/{common.ts → common/common.ts} +9 -9
- package/dist/templates/react-app/config/Identifier/common/index.ts +2 -0
- package/dist/templates/react-app/config/Identifier/index.ts +1 -9
- package/dist/templates/react-app/config/Identifier/pages/index.ts +8 -0
- package/dist/templates/react-app/config/Identifier/{page.about.ts → pages/page.about.ts} +34 -26
- package/dist/templates/react-app/config/Identifier/{page.executor.ts → pages/page.executor.ts} +47 -39
- package/dist/templates/{next-app/config/Identifier → react-app/config/Identifier/pages}/page.home.ts +24 -23
- package/dist/templates/react-app/config/Identifier/pages/page.identifiter.ts +102 -0
- package/dist/templates/react-app/config/Identifier/{page.jsonStorage.ts → pages/page.jsonStorage.ts} +18 -11
- package/dist/templates/react-app/config/Identifier/{page.login.ts → pages/page.login.ts} +37 -27
- package/dist/templates/react-app/config/Identifier/{page.register.ts → pages/page.register.ts} +37 -25
- package/dist/templates/react-app/config/Identifier/{page.request.ts → pages/page.request.ts} +34 -44
- package/dist/templates/react-app/config/app.router.ts +66 -69
- package/dist/templates/react-app/config/i18n/PageI18nInterface.ts +51 -0
- package/dist/templates/react-app/config/i18n/aboutI18n.ts +42 -0
- package/dist/templates/react-app/config/i18n/executorI18n.ts +51 -0
- package/dist/templates/react-app/config/i18n/homeI18n.ts +24 -0
- package/dist/templates/react-app/config/i18n/i18nConfig.ts +30 -0
- package/dist/templates/react-app/config/i18n/identifiter18n.ts +30 -0
- package/dist/templates/react-app/config/i18n/jsonStorage18n.ts +27 -0
- package/dist/templates/react-app/config/i18n/login18n.ts +42 -0
- package/dist/templates/react-app/config/i18n/notFoundI18n.ts +34 -0
- package/dist/templates/react-app/config/i18n/register18n.ts +40 -0
- package/dist/templates/react-app/config/i18n/request18n.ts +41 -0
- package/dist/templates/react-app/config/theme.ts +14 -4
- package/dist/templates/react-app/docs/en/bootstrap.md +1670 -341
- package/dist/templates/react-app/docs/en/development-guide.md +1021 -345
- package/dist/templates/react-app/docs/en/env.md +1132 -278
- package/dist/templates/react-app/docs/en/i18n.md +858 -147
- package/dist/templates/react-app/docs/en/index.md +733 -104
- package/dist/templates/react-app/docs/en/ioc.md +1228 -287
- package/dist/templates/react-app/docs/en/playwright/e2e-tests.md +321 -0
- package/dist/templates/react-app/docs/en/playwright/index.md +19 -0
- package/dist/templates/react-app/docs/en/playwright/installation-summary.md +332 -0
- package/dist/templates/react-app/docs/en/playwright/overview.md +222 -0
- package/dist/templates/react-app/docs/en/playwright/quickstart.md +325 -0
- package/dist/templates/react-app/docs/en/playwright/reorganization-notes.md +340 -0
- package/dist/templates/react-app/docs/en/playwright/setup-complete.md +290 -0
- package/dist/templates/react-app/docs/en/playwright/testing-guide.md +565 -0
- package/dist/templates/react-app/docs/en/store.md +1194 -184
- package/dist/templates/react-app/docs/en/why-no-globals.md +797 -0
- package/dist/templates/react-app/docs/zh/bootstrap.md +1670 -341
- package/dist/templates/react-app/docs/zh/development-guide.md +1021 -345
- package/dist/templates/react-app/docs/zh/env.md +1132 -275
- package/dist/templates/react-app/docs/zh/i18n.md +858 -147
- package/dist/templates/react-app/docs/zh/index.md +717 -104
- package/dist/templates/react-app/docs/zh/ioc.md +1229 -287
- package/dist/templates/react-app/docs/zh/playwright/e2e-tests.md +321 -0
- package/dist/templates/react-app/docs/zh/playwright/index.md +19 -0
- package/dist/templates/react-app/docs/zh/playwright/installation-summary.md +332 -0
- package/dist/templates/react-app/docs/zh/playwright/overview.md +222 -0
- package/dist/templates/react-app/docs/zh/playwright/quickstart.md +325 -0
- package/dist/templates/react-app/docs/zh/playwright/reorganization-notes.md +340 -0
- package/dist/templates/react-app/docs/zh/playwright/setup-complete.md +290 -0
- package/dist/templates/react-app/docs/zh/playwright/testing-guide.md +565 -0
- package/dist/templates/react-app/docs/zh/store.md +1192 -184
- package/dist/templates/react-app/docs/zh/why-no-globals.md +797 -0
- package/dist/templates/react-app/e2e/App.spec.ts +319 -0
- package/dist/templates/react-app/e2e/fixtures/base.fixture.ts +40 -0
- package/dist/templates/react-app/e2e/main.spec.ts +20 -0
- package/dist/templates/react-app/e2e/utils/test-helpers.ts +19 -0
- package/dist/templates/react-app/eslint.config.mjs +247 -0
- package/dist/templates/react-app/makes/eslint-utils.mjs +195 -0
- package/dist/templates/react-app/makes/generateTs2LocalesOptions.ts +26 -0
- package/dist/templates/react-app/package.json +31 -3
- package/dist/templates/react-app/playwright.config.ts +79 -0
- package/dist/templates/react-app/public/locales/en/common.json +190 -179
- package/dist/templates/react-app/public/locales/zh/common.json +190 -179
- package/dist/templates/react-app/src/App.tsx +15 -42
- package/dist/templates/react-app/src/base/apis/AiApi.ts +5 -5
- package/dist/templates/react-app/src/base/apis/feApi/FeApi.ts +1 -1
- package/dist/templates/react-app/src/base/apis/feApi/FeApiAdapter.ts +1 -1
- package/dist/templates/react-app/src/base/apis/feApi/FeApiBootstarp.ts +8 -8
- package/dist/templates/react-app/src/base/apis/feApi/FeApiType.ts +1 -1
- package/dist/templates/react-app/src/base/apis/userApi/UserApi.ts +6 -6
- package/dist/templates/react-app/src/base/apis/userApi/UserApiAdapter.ts +1 -1
- package/dist/templates/react-app/src/base/apis/userApi/UserApiBootstarp.ts +12 -14
- package/dist/templates/react-app/src/base/apis/userApi/UserApiType.ts +1 -1
- package/dist/templates/react-app/src/base/cases/DialogHandler.ts +5 -2
- package/dist/templates/react-app/src/base/cases/I18nKeyErrorPlugin.ts +3 -3
- package/dist/templates/react-app/src/base/cases/InversifyContainer.ts +3 -3
- package/dist/templates/react-app/src/base/cases/RequestLanguages.ts +2 -2
- package/dist/templates/react-app/src/base/cases/RequestLogger.ts +4 -4
- package/dist/templates/react-app/src/base/cases/RequestStatusCatcher.ts +1 -1
- package/dist/templates/react-app/src/base/cases/ResourceState.ts +23 -0
- package/dist/templates/react-app/src/base/cases/RouterLoader.ts +4 -4
- package/dist/templates/react-app/src/base/cases/TranslateI18nInterface.ts +26 -0
- package/dist/templates/react-app/src/base/port/ExecutorPageBridgeInterface.ts +2 -3
- package/dist/templates/react-app/src/base/port/I18nServiceInterface.ts +1 -1
- package/dist/templates/react-app/src/base/port/IOCInterface.ts +36 -0
- package/dist/templates/react-app/src/base/port/JSONStoragePageBridgeInterface.ts +2 -1
- package/dist/templates/react-app/src/base/port/ProcesserExecutorInterface.ts +1 -1
- package/dist/templates/react-app/src/base/port/RequestPageBridgeInterface.ts +2 -2
- package/dist/templates/react-app/src/base/port/RouteServiceInterface.ts +9 -5
- package/dist/templates/react-app/src/base/port/UserServiceInterface.ts +1 -1
- package/dist/templates/react-app/src/base/services/I18nService.ts +29 -29
- package/dist/templates/react-app/src/base/services/IdentifierService.ts +143 -0
- package/dist/templates/react-app/src/base/services/ProcesserExecutor.ts +3 -3
- package/dist/templates/react-app/src/base/services/RouteService.ts +27 -8
- package/dist/templates/react-app/src/base/services/UserService.ts +8 -8
- package/dist/templates/react-app/src/base/types/Page.ts +14 -2
- package/dist/templates/react-app/src/base/types/global.d.ts +1 -1
- package/dist/templates/react-app/src/core/IOC.ts +5 -46
- package/dist/templates/react-app/src/core/bootstraps/{BootstrapApp.ts → BootstrapClient.ts} +44 -17
- package/dist/templates/react-app/src/core/bootstraps/BootstrapsRegistry.ts +14 -7
- package/dist/templates/react-app/src/core/bootstraps/IocIdentifierTest.ts +1 -1
- package/dist/templates/react-app/src/core/bootstraps/PrintBootstrap.ts +1 -1
- package/dist/templates/react-app/src/core/clientIoc/ClientIOC.ts +40 -0
- package/dist/templates/react-app/src/core/{IocRegisterImpl.ts → clientIoc/ClientIOCRegister.ts} +35 -24
- package/dist/templates/react-app/src/core/globals.ts +9 -9
- package/dist/templates/react-app/src/main.tsx +4 -4
- package/dist/templates/react-app/src/pages/404.tsx +6 -3
- package/dist/templates/react-app/src/pages/500.tsx +5 -2
- package/dist/templates/react-app/src/pages/NoRouteFound.tsx +5 -0
- package/dist/templates/react-app/src/pages/auth/Layout.tsx +9 -6
- package/dist/templates/react-app/src/pages/auth/LoginPage.tsx +46 -56
- package/dist/templates/react-app/src/pages/auth/RegisterPage.tsx +46 -58
- package/dist/templates/react-app/src/pages/base/AboutPage.tsx +35 -40
- package/dist/templates/react-app/src/pages/base/ExecutorPage.tsx +51 -51
- package/dist/templates/react-app/src/pages/base/HomePage.tsx +14 -15
- package/dist/templates/react-app/src/pages/base/IdentifierPage.tsx +70 -11
- package/dist/templates/react-app/src/pages/base/JSONStoragePage.tsx +24 -25
- package/dist/templates/react-app/src/pages/base/Layout.tsx +2 -2
- package/dist/templates/react-app/src/pages/base/RedirectPathname.tsx +3 -2
- package/dist/templates/react-app/src/pages/base/RequestPage.tsx +41 -59
- package/dist/templates/react-app/src/styles/css/antd-themes/{_default.css → _common/_default.css} +85 -0
- package/dist/templates/react-app/src/styles/css/antd-themes/{dark.css → _common/dark.css} +99 -0
- package/dist/templates/react-app/src/styles/css/antd-themes/_common/index.css +3 -0
- package/dist/templates/react-app/src/styles/css/antd-themes/{pink.css → _common/pink.css} +86 -0
- package/dist/templates/react-app/src/styles/css/antd-themes/index.css +4 -3
- package/dist/templates/react-app/src/styles/css/antd-themes/menu/_default.css +108 -0
- package/dist/templates/react-app/src/styles/css/antd-themes/menu/dark.css +67 -0
- package/dist/templates/react-app/src/styles/css/antd-themes/menu/index.css +3 -0
- package/dist/templates/react-app/src/styles/css/antd-themes/menu/pink.css +67 -0
- package/dist/templates/react-app/src/styles/css/antd-themes/pagination/_default.css +34 -0
- package/dist/templates/react-app/src/styles/css/antd-themes/pagination/dark.css +31 -0
- package/dist/templates/react-app/src/styles/css/antd-themes/pagination/index.css +3 -0
- package/dist/templates/react-app/src/styles/css/antd-themes/pagination/pink.css +36 -0
- package/dist/templates/react-app/src/styles/css/antd-themes/table/_default.css +44 -0
- package/dist/templates/react-app/src/styles/css/antd-themes/table/dark.css +43 -0
- package/dist/templates/react-app/src/styles/css/antd-themes/table/index.css +3 -0
- package/dist/templates/react-app/src/styles/css/antd-themes/table/pink.css +43 -0
- package/dist/templates/react-app/src/styles/css/page.css +4 -3
- package/dist/templates/react-app/src/styles/css/themes/_default.css +1 -0
- package/dist/templates/react-app/src/styles/css/themes/dark.css +1 -0
- package/dist/templates/react-app/src/styles/css/themes/pink.css +1 -0
- package/dist/templates/react-app/src/styles/css/zIndex.css +1 -1
- package/dist/templates/react-app/src/uikit/bridges/ExecutorPageBridge.ts +3 -3
- package/dist/templates/react-app/src/uikit/bridges/JSONStoragePageBridge.ts +2 -2
- package/dist/templates/react-app/src/uikit/bridges/NavigateBridge.ts +1 -1
- package/dist/templates/react-app/src/uikit/bridges/RequestPageBridge.ts +3 -3
- package/dist/templates/react-app/src/uikit/components/AppRouterProvider.tsx +35 -0
- package/dist/templates/react-app/src/uikit/components/BaseHeader.tsx +15 -11
- package/dist/templates/react-app/src/uikit/components/BaseRouteProvider.tsx +14 -11
- package/dist/templates/react-app/src/uikit/components/BaseRouteSeo.tsx +18 -0
- package/dist/templates/react-app/src/uikit/components/BootstrapsProvider.tsx +13 -0
- package/dist/templates/react-app/src/uikit/components/ClientSeo.tsx +62 -0
- package/dist/templates/react-app/src/uikit/components/ComboProvider.tsx +38 -0
- package/dist/templates/react-app/src/uikit/components/LanguageSwitcher.tsx +48 -27
- package/dist/templates/react-app/src/uikit/components/Loading.tsx +4 -2
- package/dist/templates/react-app/src/uikit/components/LocaleLink.tsx +4 -5
- package/dist/templates/react-app/src/uikit/components/LogoutButton.tsx +34 -11
- package/dist/templates/react-app/src/uikit/components/ProcessExecutorProvider.tsx +9 -5
- package/dist/templates/react-app/src/uikit/components/RouterRenderComponent.tsx +6 -3
- package/dist/templates/react-app/src/uikit/components/ThemeSwitcher.tsx +97 -40
- package/dist/templates/react-app/src/uikit/components/UserAuthProvider.tsx +5 -5
- package/dist/templates/react-app/src/uikit/components/With.tsx +17 -0
- package/dist/templates/react-app/src/uikit/contexts/BaseRouteContext.ts +17 -11
- package/dist/templates/react-app/src/uikit/contexts/IOCContext.ts +13 -0
- package/dist/templates/react-app/src/uikit/hooks/useAppTranslation.ts +26 -0
- package/dist/templates/react-app/src/uikit/hooks/useI18nGuard.ts +8 -11
- package/dist/templates/react-app/src/uikit/hooks/useI18nInterface.ts +25 -0
- package/dist/templates/react-app/src/uikit/hooks/useIOC.ts +35 -0
- package/dist/templates/react-app/src/uikit/hooks/useNavigateBridge.ts +3 -3
- package/dist/templates/react-app/src/uikit/hooks/useStrictEffect.ts +0 -1
- package/dist/templates/react-app/tsconfig.e2e.json +21 -0
- package/dist/templates/react-app/tsconfig.json +8 -1
- package/dist/templates/react-app/tsconfig.node.json +1 -1
- package/dist/templates/react-app/tsconfig.test.json +3 -1
- package/dist/templates/react-app/vite.config.ts +50 -34
- package/package.json +2 -1
- package/dist/configs/react-app/eslint.config.js +0 -94
- package/dist/templates/next-app/config/Identifier/common.error.ts +0 -41
- package/dist/templates/next-app/config/Identifier/common.ts +0 -69
- package/dist/templates/next-app/config/Identifier/page.about.ts +0 -181
- package/dist/templates/next-app/config/Identifier/page.admin.ts +0 -48
- package/dist/templates/next-app/config/Identifier/page.executor.ts +0 -272
- package/dist/templates/next-app/config/Identifier/page.identifiter.ts +0 -39
- package/dist/templates/next-app/config/Identifier/page.jsonStorage.ts +0 -72
- package/dist/templates/next-app/config/Identifier/page.request.ts +0 -182
- package/dist/templates/next-app/docs/env.md +0 -94
- package/dist/templates/next-app/src/base/cases/ChatAction.ts +0 -21
- package/dist/templates/next-app/src/base/cases/FocusBarAction.ts +0 -36
- package/dist/templates/next-app/src/base/cases/RequestState.ts +0 -20
- package/dist/templates/next-app/src/base/port/AdminPageInterface.ts +0 -85
- package/dist/templates/next-app/src/base/port/AsyncStateInterface.ts +0 -7
- package/dist/templates/next-app/src/base/services/AdminUserService.ts +0 -45
- package/dist/templates/next-app/src/uikit/components/ChatRoot.tsx +0 -17
- package/dist/templates/next-app/src/uikit/components/chat/ChatActionInterface.ts +0 -30
- package/dist/templates/next-app/src/uikit/components/chat/ChatFocusBar.tsx +0 -65
- package/dist/templates/next-app/src/uikit/components/chat/ChatMessages.tsx +0 -59
- package/dist/templates/next-app/src/uikit/components/chat/ChatWrap.tsx +0 -28
- package/dist/templates/next-app/src/uikit/components/chat/FocusBarActionInterface.ts +0 -19
- package/dist/templates/next-app/src/uikit/hook/useMountedClient.ts +0 -17
- package/dist/templates/next-app/src/uikit/hook/useStore.ts +0 -15
- package/dist/templates/react-app/__tests__/__mocks__/I18nService.ts +0 -13
- package/dist/templates/react-app/__tests__/src/App.test.tsx +0 -139
- package/dist/templates/react-app/config/Identifier/page.identifiter.ts +0 -39
- package/dist/templates/react-app/config/i18n.ts +0 -15
- package/dist/templates/react-app/docs/en/project-structure.md +0 -434
- package/dist/templates/react-app/docs/zh/project-structure.md +0 -434
- package/dist/templates/react-app/src/base/cases/RequestState.ts +0 -20
- package/dist/templates/react-app/src/base/port/AsyncStateInterface.ts +0 -7
- package/dist/templates/react-app/src/uikit/hooks/useDocumentTitle.ts +0 -15
- package/dist/templates/react-app/src/uikit/hooks/useStore.ts +0 -15
|
@@ -1,168 +1,370 @@
|
|
|
1
1
|
# Store State Management
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 📋 Table of Contents
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
- [Core Philosophy](#-core-philosophy) - Application layer notifies UI layer
|
|
6
|
+
- [What is Store](#-what-is-store) - State container
|
|
7
|
+
- [Why Need Store](#-why-need-store) - Solve communication problem
|
|
8
|
+
- [Core Problem](#-core-problem) - How application layer notifies UI layer
|
|
9
|
+
- [Implementation in the Project](#-implementation-in-the-project) - Practical guide
|
|
10
|
+
- [How to Use](#-how-to-use) - Service + Store + useStore
|
|
11
|
+
- [Testing](#-testing) - Independent testing and combination testing
|
|
12
|
+
- [Best Practices](#-best-practices) - 7 core practices
|
|
13
|
+
- [FAQ](#-faq) - Common questions
|
|
6
14
|
|
|
7
|
-
|
|
8
|
-
- Business logic centralized in Store management
|
|
9
|
-
- UI components only handle rendering and user interaction
|
|
10
|
-
- Logic dependency injection through IOC container
|
|
15
|
+
---
|
|
11
16
|
|
|
12
|
-
|
|
13
|
-
- Based on publish-subscribe pattern
|
|
14
|
-
- State changes automatically trigger UI updates
|
|
15
|
-
- Precise component re-rendering control
|
|
17
|
+
## 🎯 Core Philosophy
|
|
16
18
|
|
|
17
|
-
|
|
18
|
-
- Break down complex states into independent slices
|
|
19
|
-
- Each slice handles specific business domain
|
|
20
|
-
- Slices can be combined and communicate
|
|
19
|
+
> **🚨 Core Problem: How does the application layer (Service) notify the UI layer to update while maintaining separation?**
|
|
21
20
|
|
|
22
|
-
|
|
21
|
+
> **⭐ Solution: Service contains Store, publishes state through `emit`, UI subscribes to state through `useStore`!**
|
|
23
22
|
|
|
24
|
-
###
|
|
23
|
+
### Core Concept
|
|
25
24
|
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
```
|
|
26
|
+
┌──────────────────────────────────────────────┐
|
|
27
|
+
│ Problem: UI and logic are separated, │
|
|
28
|
+
│ but how do they communicate? │
|
|
29
|
+
│ │
|
|
30
|
+
│ Service (Application Layer) │
|
|
31
|
+
│ ├── Business logic │
|
|
32
|
+
│ └── Data processing │
|
|
33
|
+
│ ↓ How to notify? │
|
|
34
|
+
│ Component (UI Layer) │
|
|
35
|
+
│ └── UI rendering │
|
|
36
|
+
│ │
|
|
37
|
+
│ ❌ Problem: Service changed data, how does │
|
|
38
|
+
│ UI know? │
|
|
39
|
+
└──────────────────────────────────────────────┘
|
|
40
|
+
|
|
41
|
+
┌──────────────────────────────────────────────┐
|
|
42
|
+
│ Solution: Store as bridge │
|
|
43
|
+
│ │
|
|
44
|
+
│ Service (Application Layer) │
|
|
45
|
+
│ ├── Business logic │
|
|
46
|
+
│ ├── Store (State container) │
|
|
47
|
+
│ │ ├── state │
|
|
48
|
+
│ │ └── emit() (Publish state) │
|
|
49
|
+
│ │ │
|
|
50
|
+
│ │ ↓ Publish-Subscribe pattern │
|
|
51
|
+
│ │ │
|
|
52
|
+
│ └── useStore (Subscribe) │
|
|
53
|
+
│ ↓ │
|
|
54
|
+
│ Component (UI Layer) │
|
|
55
|
+
│ └── Auto-update UI │
|
|
56
|
+
│ │
|
|
57
|
+
│ ✅ Service publishes state via emit │
|
|
58
|
+
│ ✅ UI subscribes to state via useStore │
|
|
59
|
+
│ ✅ Maintain separation, decoupled │
|
|
60
|
+
└──────────────────────────────────────────────┘
|
|
61
|
+
```
|
|
30
62
|
|
|
31
|
-
|
|
32
|
-
protected emit(newState: T) {
|
|
33
|
-
this.state = newState;
|
|
34
|
-
this.listeners.forEach((listener) => listener(this.state));
|
|
35
|
-
}
|
|
63
|
+
---
|
|
36
64
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
65
|
+
## 🗂️ What is Store
|
|
66
|
+
|
|
67
|
+
Store is a **reactive state container** based on the **publish-subscribe pattern**.
|
|
68
|
+
|
|
69
|
+
### Simple Understanding
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
Store = State + Publish-Subscribe
|
|
73
|
+
|
|
74
|
+
Service owns Store
|
|
75
|
+
Service publishes state through Store.emit()
|
|
76
|
+
UI subscribes to state through useStore()
|
|
43
77
|
```
|
|
44
78
|
|
|
45
|
-
###
|
|
79
|
+
### Analogy
|
|
46
80
|
|
|
47
81
|
```
|
|
48
|
-
|
|
82
|
+
Store is like a radio station:
|
|
83
|
+
|
|
84
|
+
📻 Station (Store)
|
|
85
|
+
- Has program content (state)
|
|
86
|
+
- Can broadcast programs (emit)
|
|
87
|
+
- Listeners can tune in (subscribe)
|
|
88
|
+
|
|
89
|
+
🎤 Host (Service)
|
|
90
|
+
- Creates program content (business logic)
|
|
91
|
+
- Broadcasts via station (emit)
|
|
92
|
+
|
|
93
|
+
📱 Listener (UI Component)
|
|
94
|
+
- Tunes into station (useStore)
|
|
95
|
+
- Automatically reacts to new content (auto-update UI)
|
|
49
96
|
```
|
|
50
97
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## 🤔 Why Need Store
|
|
101
|
+
|
|
102
|
+
### Core Problem: How to communicate after UI and logic separation?
|
|
103
|
+
|
|
104
|
+
We've already separated UI and logic through IOC, but here's the problem:
|
|
56
105
|
|
|
57
|
-
|
|
106
|
+
#### ❌ Problem Example: Without Store
|
|
58
107
|
|
|
59
|
-
```
|
|
60
|
-
//
|
|
108
|
+
```typescript
|
|
109
|
+
// Service (Logic layer)
|
|
110
|
+
@injectable()
|
|
111
|
+
export class UserService {
|
|
112
|
+
private user: UserInfo | null = null;
|
|
113
|
+
|
|
114
|
+
async login(username: string, password: string) {
|
|
115
|
+
const response = await this.api.login({ username, password });
|
|
116
|
+
this.user = response.user; // ✅ Login successful, user updated
|
|
117
|
+
|
|
118
|
+
// ❌ Problem: How does UI know user has been updated?
|
|
119
|
+
// ❌ Service cannot notify UI
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// UI component
|
|
61
124
|
function UserProfile() {
|
|
62
|
-
|
|
63
|
-
|
|
125
|
+
const userService = useIOC('UserServiceInterface');
|
|
126
|
+
|
|
127
|
+
// ❌ Problem: How to get userService.user?
|
|
128
|
+
// ❌ How to trigger UI re-render when userService.user updates?
|
|
64
129
|
|
|
65
|
-
return <div>{user
|
|
130
|
+
return <div>{/* Cannot display user */}</div>;
|
|
66
131
|
}
|
|
132
|
+
|
|
133
|
+
// 😰😰😰 Problem Summary:
|
|
134
|
+
// 1. UI cannot access Service's internal state
|
|
135
|
+
// 2. UI doesn't know when Service state updates
|
|
136
|
+
// 3. Need to manually call a method to get state? (breaks separation principle)
|
|
137
|
+
// 4. Need to poll for state? (poor performance)
|
|
67
138
|
```
|
|
68
139
|
|
|
69
|
-
|
|
140
|
+
#### ✅ Solution: Use Store
|
|
70
141
|
|
|
71
142
|
```typescript
|
|
72
|
-
//
|
|
73
|
-
|
|
143
|
+
// Service (Logic layer)
|
|
144
|
+
@injectable()
|
|
145
|
+
export class UserService extends StoreInterface<UserState> {
|
|
74
146
|
constructor() {
|
|
75
147
|
super(() => ({
|
|
76
|
-
|
|
77
|
-
|
|
148
|
+
user: null,
|
|
149
|
+
loading: false
|
|
78
150
|
}));
|
|
79
151
|
}
|
|
80
152
|
|
|
81
|
-
login(
|
|
82
|
-
//
|
|
153
|
+
async login(username: string, password: string) {
|
|
154
|
+
// Set loading state
|
|
155
|
+
this.emit({ ...this.state, loading: true });
|
|
156
|
+
|
|
157
|
+
const response = await this.api.login({ username, password });
|
|
158
|
+
|
|
159
|
+
// ✅ Publish new state via emit, automatically notify all subscribers
|
|
83
160
|
this.emit({
|
|
84
|
-
|
|
85
|
-
|
|
161
|
+
user: response.user,
|
|
162
|
+
loading: false
|
|
86
163
|
});
|
|
87
164
|
}
|
|
88
165
|
}
|
|
89
166
|
|
|
90
|
-
//
|
|
91
|
-
|
|
167
|
+
// UI component
|
|
168
|
+
function UserProfile() {
|
|
169
|
+
const userService = useIOC('UserServiceInterface');
|
|
170
|
+
|
|
171
|
+
// ✅ Subscribe to state via useStore
|
|
172
|
+
const { user, loading } = useStore(userService);
|
|
173
|
+
|
|
174
|
+
// ✅ Component automatically re-renders when userService.emit()
|
|
175
|
+
|
|
176
|
+
if (loading) return <div>Loading...</div>;
|
|
177
|
+
return <div>{user?.name}</div>;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ✅✅✅ Advantages Summary:
|
|
181
|
+
// 1. UI can subscribe to Service state
|
|
182
|
+
// 2. UI automatically updates when Service state updates
|
|
183
|
+
// 3. Maintain separation (Service doesn't know which UIs are listening)
|
|
184
|
+
// 4. High performance (only subscribed components update)
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Comparison Summary
|
|
188
|
+
|
|
189
|
+
| Feature | Without Store | With Store |
|
|
190
|
+
| ----------------------- | -------------------------------------- | ------------------------------ |
|
|
191
|
+
| **State Access** | ❌ Cannot access internal state | ✅ Subscribe via useStore |
|
|
192
|
+
| **Update Notification** | ❌ UI doesn't know about state changes | ✅ emit automatically notifies |
|
|
193
|
+
| **UI Update** | ❌ Need manual trigger | ✅ Auto re-render |
|
|
194
|
+
| **Decoupling** | ❌ Service needs to know UI | ✅ Completely decoupled |
|
|
195
|
+
| **Performance** | ❌ Polling or global update | ✅ Precise subscriber updates |
|
|
196
|
+
| **Testability** | ❌ Hard to test state changes | ✅ Easy to test state |
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## ❓ Core Problem
|
|
201
|
+
|
|
202
|
+
### How does application layer notify UI layer while maintaining separation?
|
|
203
|
+
|
|
204
|
+
#### Problem Breakdown
|
|
205
|
+
|
|
206
|
+
1. **Application layer (Service) has state** - like user info, loading state
|
|
207
|
+
2. **UI layer needs to display this state** - show username, show loading animation
|
|
208
|
+
3. **Application layer state changes** - after login success, user info updates
|
|
209
|
+
4. **UI layer needs to auto-update** - after user info changes, UI automatically shows new name
|
|
210
|
+
5. **Maintain separation** - Service shouldn't directly manipulate UI, UI shouldn't directly access Service internals
|
|
211
|
+
|
|
212
|
+
#### Solution: Publish-Subscribe Pattern
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
// 1. Service defines state
|
|
216
|
+
interface UserState {
|
|
217
|
+
user: UserInfo | null;
|
|
218
|
+
loading: boolean;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// 2. Service extends StoreInterface
|
|
222
|
+
@injectable()
|
|
223
|
+
export class UserService extends StoreInterface<UserState> {
|
|
92
224
|
constructor() {
|
|
93
225
|
super(() => ({
|
|
94
|
-
|
|
95
|
-
|
|
226
|
+
user: null,
|
|
227
|
+
loading: false
|
|
96
228
|
}));
|
|
97
229
|
}
|
|
98
230
|
|
|
99
|
-
|
|
100
|
-
|
|
231
|
+
// 3. Service publishes state via emit
|
|
232
|
+
async login(username: string, password: string) {
|
|
233
|
+
this.emit({ ...this.state, loading: true }); // Publish: start loading
|
|
234
|
+
|
|
235
|
+
const response = await this.api.login({ username, password });
|
|
236
|
+
|
|
101
237
|
this.emit({
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
});
|
|
238
|
+
user: response.user,
|
|
239
|
+
loading: false
|
|
240
|
+
}); // Publish: loading complete, user logged in
|
|
105
241
|
}
|
|
242
|
+
|
|
243
|
+
// 4. Service doesn't need to know who's listening
|
|
244
|
+
// ✅ Completely decoupled
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// 5. UI subscribes to state via useStore
|
|
248
|
+
function LoginPage() {
|
|
249
|
+
const userService = useIOC('UserServiceInterface');
|
|
250
|
+
const { loading } = useStore(userService);
|
|
251
|
+
|
|
252
|
+
const handleLogin = () => {
|
|
253
|
+
userService.login('user', 'pass');
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
// 6. When Service emits new state, UI auto-updates
|
|
257
|
+
return (
|
|
258
|
+
<button onClick={handleLogin} disabled={loading}>
|
|
259
|
+
{loading ? 'Logging in...' : 'Login'}
|
|
260
|
+
</button>
|
|
261
|
+
);
|
|
106
262
|
}
|
|
107
263
|
```
|
|
108
264
|
|
|
109
|
-
|
|
265
|
+
#### Workflow
|
|
110
266
|
|
|
111
|
-
|
|
267
|
+
```
|
|
268
|
+
┌─────────────────────────────────────────────┐
|
|
269
|
+
│ Complete state update flow │
|
|
270
|
+
│ │
|
|
271
|
+
│ 1. User clicks button │
|
|
272
|
+
│ ↓ │
|
|
273
|
+
│ 2. UI calls Service method │
|
|
274
|
+
│ userService.login() │
|
|
275
|
+
│ ↓ │
|
|
276
|
+
│ 3. Service executes business logic │
|
|
277
|
+
│ - Call API │
|
|
278
|
+
│ - Process data │
|
|
279
|
+
│ ↓ │
|
|
280
|
+
│ 4. Service publishes new state via emit │
|
|
281
|
+
│ this.emit({ user: ..., loading: false })│
|
|
282
|
+
│ ↓ │
|
|
283
|
+
│ 5. Store notifies all subscribers │
|
|
284
|
+
│ listeners.forEach(listener => ...) │
|
|
285
|
+
│ ↓ │
|
|
286
|
+
│ 6. useStore receives notification │
|
|
287
|
+
│ Trigger component re-render │
|
|
288
|
+
│ ↓ │
|
|
289
|
+
│ 7. UI displays latest state │
|
|
290
|
+
│ Show username / Hide loading animation │
|
|
291
|
+
└─────────────────────────────────────────────┘
|
|
292
|
+
```
|
|
112
293
|
|
|
113
|
-
|
|
114
|
-
- **Lightweight**: No complex configuration, easy to use
|
|
115
|
-
- **High Performance**: Precise component updates, avoiding unnecessary renders
|
|
116
|
-
- **Modular**: Supports state slicing, convenient for managing large applications
|
|
117
|
-
- **IOC Integration**: Perfect integration with dependency injection system
|
|
294
|
+
---
|
|
118
295
|
|
|
119
|
-
##
|
|
296
|
+
## 🛠️ Implementation in the Project
|
|
120
297
|
|
|
121
|
-
### 1.
|
|
298
|
+
### 1. File Structure
|
|
122
299
|
|
|
123
|
-
|
|
300
|
+
```
|
|
301
|
+
src/
|
|
302
|
+
├── base/
|
|
303
|
+
│ ├── services/
|
|
304
|
+
│ │ ├── UserService.ts # Service extends StoreInterface
|
|
305
|
+
│ │ ├── RouteService.ts # Service extends StoreInterface
|
|
306
|
+
│ │ └── I18nService.ts # Service extends StoreInterface
|
|
307
|
+
│ └── port/
|
|
308
|
+
│ └── UserServiceInterface.ts # Service interface
|
|
309
|
+
└── uikit/
|
|
310
|
+
└── hooks/
|
|
311
|
+
└── useStore.ts (from @brain-toolkit/react-kit)
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
### 2. Store Base Class
|
|
124
315
|
|
|
125
|
-
|
|
316
|
+
Store system is based on `SliceStore` from `@brain-toolkit/react-kit`:
|
|
126
317
|
|
|
127
318
|
```typescript
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
//
|
|
138
|
-
|
|
319
|
+
// From @brain-toolkit/react-kit
|
|
320
|
+
export class SliceStore<T> {
|
|
321
|
+
protected state: T;
|
|
322
|
+
private listeners = new Set<(state: T) => void>();
|
|
323
|
+
|
|
324
|
+
constructor(stateFactory: () => T) {
|
|
325
|
+
this.state = stateFactory();
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Publish state
|
|
329
|
+
protected emit(newState: T) {
|
|
330
|
+
this.state = newState;
|
|
331
|
+
// Notify all subscribers
|
|
332
|
+
this.listeners.forEach((listener) => listener(this.state));
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Subscribe to state
|
|
336
|
+
subscribe(listener: (state: T) => void) {
|
|
337
|
+
this.listeners.add(listener);
|
|
338
|
+
// Return unsubscribe function
|
|
339
|
+
return () => this.listeners.delete(listener);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Get current state
|
|
343
|
+
getState(): T {
|
|
344
|
+
return this.state;
|
|
345
|
+
}
|
|
139
346
|
}
|
|
140
347
|
```
|
|
141
348
|
|
|
142
|
-
|
|
349
|
+
### 3. StoreInterface Base Class
|
|
350
|
+
|
|
351
|
+
Project's Store base class, provides additional utility methods:
|
|
143
352
|
|
|
144
353
|
```typescript
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
*
|
|
148
|
-
* Purpose: Abstract base class for all state stores
|
|
149
|
-
* Core Idea: Provide unified state management API, including reset and clone helper methods
|
|
150
|
-
* Main Function: Extend SliceStore, add resetState and cloneState utility methods
|
|
151
|
-
* Main Goal: Simplify store implementation and ensure consistency
|
|
152
|
-
*/
|
|
153
|
-
abstract class StoreInterface<
|
|
354
|
+
// From @qlover/corekit-bridge
|
|
355
|
+
export abstract class StoreInterface<
|
|
154
356
|
T extends StoreStateInterface
|
|
155
357
|
> extends SliceStore<T> {
|
|
156
358
|
constructor(protected stateFactory: () => T) {
|
|
157
359
|
super(stateFactory);
|
|
158
360
|
}
|
|
159
361
|
|
|
160
|
-
// Reset
|
|
362
|
+
// Reset state
|
|
161
363
|
resetState(): void {
|
|
162
364
|
this.emit(this.stateFactory());
|
|
163
365
|
}
|
|
164
366
|
|
|
165
|
-
// Clone
|
|
367
|
+
// Clone state (for updates)
|
|
166
368
|
cloneState(source?: Partial<T>): T {
|
|
167
369
|
const cloned = clone(this.state);
|
|
168
370
|
if (typeof cloned === 'object' && cloned !== null) {
|
|
@@ -173,149 +375,957 @@ abstract class StoreInterface<
|
|
|
173
375
|
}
|
|
174
376
|
```
|
|
175
377
|
|
|
176
|
-
###
|
|
378
|
+
### 4. State Interface
|
|
379
|
+
|
|
380
|
+
```typescript
|
|
381
|
+
// All state must implement this interface
|
|
382
|
+
export interface StoreStateInterface {
|
|
383
|
+
// Can define common properties here
|
|
384
|
+
// loading?: boolean;
|
|
385
|
+
// error?: Error | null;
|
|
386
|
+
}
|
|
387
|
+
```
|
|
177
388
|
|
|
178
|
-
|
|
389
|
+
---
|
|
390
|
+
|
|
391
|
+
## 📝 How to Use
|
|
392
|
+
|
|
393
|
+
### 1. Define State Interface
|
|
179
394
|
|
|
180
395
|
```typescript
|
|
181
|
-
//
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
role: string;
|
|
187
|
-
} | null = null;
|
|
396
|
+
// src/base/services/UserService.ts
|
|
397
|
+
export interface UserState extends StoreStateInterface {
|
|
398
|
+
user: UserInfo | null;
|
|
399
|
+
loading: boolean;
|
|
400
|
+
error: Error | null;
|
|
188
401
|
}
|
|
402
|
+
```
|
|
189
403
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
404
|
+
### 2. Service Extends StoreInterface
|
|
405
|
+
|
|
406
|
+
```typescript
|
|
407
|
+
// src/base/services/UserService.ts
|
|
408
|
+
import { StoreInterface } from '@qlover/corekit-bridge';
|
|
409
|
+
import { injectable, inject } from 'inversify';
|
|
410
|
+
|
|
411
|
+
@injectable()
|
|
412
|
+
export class UserService extends StoreInterface<UserState> {
|
|
413
|
+
constructor(
|
|
414
|
+
@inject(UserApi) private api: UserApi,
|
|
415
|
+
@inject(IOCIdentifier.AppConfig) private config: AppConfig
|
|
416
|
+
) {
|
|
417
|
+
// Initialize state
|
|
418
|
+
super(() => ({
|
|
419
|
+
user: null,
|
|
420
|
+
loading: false,
|
|
421
|
+
error: null
|
|
422
|
+
}));
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Business method: publish state via emit
|
|
426
|
+
async login(username: string, password: string) {
|
|
427
|
+
// 1. Start loading
|
|
428
|
+
this.emit({
|
|
429
|
+
...this.state,
|
|
430
|
+
loading: true,
|
|
431
|
+
error: null
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
try {
|
|
435
|
+
// 2. Call API
|
|
436
|
+
const response = await this.api.login({ username, password });
|
|
437
|
+
|
|
438
|
+
// 3. Success: publish new state
|
|
439
|
+
this.emit({
|
|
440
|
+
user: response.user,
|
|
441
|
+
loading: false,
|
|
442
|
+
error: null
|
|
443
|
+
});
|
|
444
|
+
} catch (error) {
|
|
445
|
+
// 4. Failure: publish error state
|
|
446
|
+
this.emit({
|
|
447
|
+
...this.state,
|
|
448
|
+
loading: false,
|
|
449
|
+
error: error as Error
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
async logout() {
|
|
455
|
+
this.emit({
|
|
456
|
+
user: null,
|
|
457
|
+
loading: false,
|
|
458
|
+
error: null
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Use cloneState to simplify updates
|
|
463
|
+
setUser(user: UserInfo) {
|
|
464
|
+
this.emit(this.cloneState({ user }));
|
|
194
465
|
}
|
|
195
466
|
}
|
|
196
467
|
```
|
|
197
468
|
|
|
198
|
-
|
|
469
|
+
### 3. UI Subscribes to State
|
|
199
470
|
|
|
200
|
-
|
|
471
|
+
```typescript
|
|
472
|
+
// src/pages/LoginPage.tsx
|
|
473
|
+
import { useStore } from '@brain-toolkit/react-kit/hooks/useStore';
|
|
474
|
+
import { useIOC } from '@/uikit/hooks/useIOC';
|
|
475
|
+
|
|
476
|
+
function LoginPage() {
|
|
477
|
+
const userService = useIOC('UserServiceInterface');
|
|
478
|
+
|
|
479
|
+
// ✅ Method 1: Subscribe to complete state
|
|
480
|
+
const { user, loading, error } = useStore(userService);
|
|
481
|
+
|
|
482
|
+
const handleLogin = async () => {
|
|
483
|
+
await userService.login('username', 'password');
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
if (loading) {
|
|
487
|
+
return <div>Loading...</div>;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
return (
|
|
491
|
+
<div>
|
|
492
|
+
{error && <div>Error: {error.message}</div>}
|
|
493
|
+
<button onClick={handleLogin}>Login</button>
|
|
494
|
+
</div>
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
### 4. Use Selectors (Performance Optimization)
|
|
201
500
|
|
|
202
501
|
```typescript
|
|
203
|
-
|
|
502
|
+
// src/pages/UserProfile.tsx
|
|
503
|
+
function UserProfile() {
|
|
504
|
+
const userService = useIOC('UserServiceInterface');
|
|
204
505
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
506
|
+
// ✅ Method 2: Only subscribe to needed state (better performance)
|
|
507
|
+
const user = useStore(userService, (state) => state.user);
|
|
508
|
+
|
|
509
|
+
// ✅ Only re-renders when user changes, loading changes won't trigger
|
|
510
|
+
|
|
511
|
+
return <div>{user?.name}</div>;
|
|
208
512
|
}
|
|
513
|
+
```
|
|
209
514
|
|
|
515
|
+
### 5. Define Selectors (Recommended)
|
|
516
|
+
|
|
517
|
+
```typescript
|
|
518
|
+
// src/base/services/UserService.ts
|
|
210
519
|
@injectable()
|
|
211
|
-
export class
|
|
212
|
-
|
|
213
|
-
super(() => ({
|
|
214
|
-
helloState: '',
|
|
215
|
-
tasks: []
|
|
216
|
-
}));
|
|
217
|
-
}
|
|
520
|
+
export class UserService extends StoreInterface<UserState> {
|
|
521
|
+
// ... other code
|
|
218
522
|
|
|
219
|
-
//
|
|
523
|
+
// ✅ Define selectors
|
|
220
524
|
selector = {
|
|
221
|
-
|
|
222
|
-
|
|
525
|
+
user: (state: UserState) => state.user,
|
|
526
|
+
loading: (state: UserState) => state.loading,
|
|
527
|
+
error: (state: UserState) => state.error,
|
|
528
|
+
isLoggedIn: (state: UserState) => state.user !== null
|
|
223
529
|
};
|
|
224
530
|
}
|
|
531
|
+
|
|
532
|
+
// Usage
|
|
533
|
+
function UserProfile() {
|
|
534
|
+
const userService = useIOC('UserServiceInterface');
|
|
535
|
+
|
|
536
|
+
// ✅ Use predefined selectors
|
|
537
|
+
const user = useStore(userService, userService.selector.user);
|
|
538
|
+
const isLoggedIn = useStore(userService, userService.selector.isLoggedIn);
|
|
539
|
+
|
|
540
|
+
return <div>{isLoggedIn ? user?.name : 'Please login'}</div>;
|
|
541
|
+
}
|
|
225
542
|
```
|
|
226
543
|
|
|
227
|
-
###
|
|
544
|
+
### 6. Real Project Examples
|
|
228
545
|
|
|
229
|
-
|
|
546
|
+
#### Example 1: UserService
|
|
230
547
|
|
|
231
|
-
```
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
548
|
+
```typescript
|
|
549
|
+
// src/base/services/UserService.ts
|
|
550
|
+
@injectable()
|
|
551
|
+
export class UserService extends UserAuthServiceInterface {
|
|
552
|
+
constructor(
|
|
553
|
+
@inject(UserApi) userApi: UserApi,
|
|
554
|
+
@inject(IOCIdentifier.AppConfig) appConfig: AppConfig,
|
|
555
|
+
@inject(IOCIdentifier.LocalStorageEncrypt) storage: Storage
|
|
556
|
+
) {
|
|
557
|
+
super(userApi, {
|
|
558
|
+
userStorage: {
|
|
559
|
+
key: appConfig.userInfoStorageKey,
|
|
560
|
+
storage: storage
|
|
561
|
+
},
|
|
562
|
+
credentialStorage: {
|
|
563
|
+
key: appConfig.userTokenStorageKey,
|
|
564
|
+
storage: storage
|
|
565
|
+
}
|
|
566
|
+
});
|
|
567
|
+
}
|
|
235
568
|
|
|
236
|
-
//
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
569
|
+
// ✅ UserService's base class contains store
|
|
570
|
+
override get store(): UserAuthStore<UserApiState> {
|
|
571
|
+
return super.store as UserAuthStore<UserApiState>;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
override async logout(): Promise<void> {
|
|
575
|
+
await super.logout();
|
|
576
|
+
// ✅ store automatically notifies UI
|
|
577
|
+
this.routerService.gotoLogin();
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// Usage
|
|
582
|
+
function Layout() {
|
|
583
|
+
const userService = useIOC(IOCIdentifier.UserServiceInterface);
|
|
584
|
+
|
|
585
|
+
// ✅ Subscribe to userService.store
|
|
586
|
+
useStore(userService.store);
|
|
587
|
+
|
|
588
|
+
if (userService.isAuthenticated()) {
|
|
589
|
+
return <Navigate to="/" replace />;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
return <Outlet />;
|
|
593
|
+
}
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
#### Example 2: RouteService
|
|
597
|
+
|
|
598
|
+
```typescript
|
|
599
|
+
// src/base/services/RouteService.ts
|
|
600
|
+
export class RouteService extends StoreInterface<RouterServiceState> {
|
|
601
|
+
constructor(
|
|
602
|
+
protected uiBridge: UIBridgeInterface<NavigateFunction>,
|
|
603
|
+
protected i18nService: I18nServiceInterface,
|
|
604
|
+
protected options: RouterServiceOptions
|
|
605
|
+
) {
|
|
606
|
+
super(
|
|
607
|
+
() => new RouterServiceState(options.routes, !!options.hasLocalRoutes)
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// ✅ Publish route changes via emit
|
|
612
|
+
override changeRoutes(routes: RouteConfigValue[]): void {
|
|
613
|
+
this.emit(this.cloneState({ routes }));
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
override goto(path: string, options?: NavigateOptions): void {
|
|
617
|
+
const composedPath = this.composePath(path);
|
|
618
|
+
this.uiBridge.getUIBridge()(composedPath, options);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// Usage
|
|
623
|
+
function AppRouterProvider() {
|
|
624
|
+
const routerService = useIOC(IOCIdentifier.RouteServiceInterface);
|
|
625
|
+
|
|
626
|
+
// ✅ Subscribe to routes changes
|
|
627
|
+
const routes = useStore(routerService, (state) => state.routes);
|
|
628
|
+
|
|
629
|
+
const router = createBrowserRouter(routes);
|
|
630
|
+
|
|
631
|
+
return <RouterProvider router={router} />;
|
|
632
|
+
}
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
#### Example 3: I18nService
|
|
636
|
+
|
|
637
|
+
```typescript
|
|
638
|
+
// src/base/services/I18nService.ts
|
|
639
|
+
export class I18nService extends StoreInterface<I18nServiceState> {
|
|
640
|
+
constructor(protected pathname: string) {
|
|
641
|
+
super(() => new I18nServiceState(i18n.language as I18nServiceLocale));
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
selector = {
|
|
645
|
+
loading: (state: I18nServiceState) => state.loading,
|
|
646
|
+
language: (state: I18nServiceState) => state.language
|
|
647
|
+
};
|
|
648
|
+
|
|
649
|
+
override async changeLanguage(lng: string): Promise<void> {
|
|
650
|
+
// ✅ Publish loading state
|
|
651
|
+
this.emit(this.cloneState({ loading: true }));
|
|
652
|
+
|
|
653
|
+
await i18n.changeLanguage(lng);
|
|
654
|
+
|
|
655
|
+
// ✅ Publish complete state
|
|
656
|
+
this.emit({
|
|
657
|
+
language: lng as I18nServiceLocale,
|
|
658
|
+
loading: false
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// Usage
|
|
664
|
+
function LanguageSwitcher() {
|
|
665
|
+
const i18nService = useIOC(IOCIdentifier.I18nServiceInterface);
|
|
666
|
+
|
|
667
|
+
// ✅ Only subscribe to loading state
|
|
668
|
+
const loading = useStore(i18nService, i18nService.selector.loading);
|
|
241
669
|
|
|
242
670
|
return (
|
|
243
|
-
<
|
|
244
|
-
|
|
245
|
-
|
|
671
|
+
<Select
|
|
672
|
+
value={i18n.language}
|
|
673
|
+
loading={loading}
|
|
674
|
+
onChange={(lng) => i18nService.changeLanguage(lng)}
|
|
675
|
+
/>
|
|
246
676
|
);
|
|
247
677
|
}
|
|
248
678
|
```
|
|
249
679
|
|
|
250
|
-
|
|
680
|
+
---
|
|
681
|
+
|
|
682
|
+
## 🧪 Testing
|
|
683
|
+
|
|
684
|
+
### Core Advantage: Store can be tested independently, UI can mock Store
|
|
251
685
|
|
|
252
|
-
|
|
686
|
+
#### 1. Test Service and Store (Logic Test)
|
|
253
687
|
|
|
254
688
|
```typescript
|
|
689
|
+
// __tests__/src/base/services/UserService.test.ts
|
|
690
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
691
|
+
import { UserService } from '@/base/services/UserService';
|
|
692
|
+
|
|
693
|
+
describe('UserService (Logic Test)', () => {
|
|
694
|
+
let userService: UserService;
|
|
695
|
+
let mockApi: any;
|
|
696
|
+
|
|
697
|
+
beforeEach(() => {
|
|
698
|
+
mockApi = {
|
|
699
|
+
login: vi.fn(),
|
|
700
|
+
getUserInfo: vi.fn()
|
|
701
|
+
};
|
|
702
|
+
|
|
703
|
+
userService = new UserService(mockApi, mockConfig, mockStorage);
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
it('should update store state when login success', async () => {
|
|
707
|
+
// ✅ Test state changes
|
|
708
|
+
mockApi.login.mockResolvedValue({
|
|
709
|
+
user: { name: 'John', id: 1 },
|
|
710
|
+
token: 'test-token'
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
// Subscribe to state changes
|
|
714
|
+
const states: any[] = [];
|
|
715
|
+
userService.subscribe((state) => {
|
|
716
|
+
states.push({ ...state });
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
// Call login
|
|
720
|
+
await userService.login('user', 'pass');
|
|
721
|
+
|
|
722
|
+
// ✅ Verify state change sequence
|
|
723
|
+
expect(states).toHaveLength(2);
|
|
724
|
+
|
|
725
|
+
// First emit: loading = true
|
|
726
|
+
expect(states[0]).toEqual({
|
|
727
|
+
user: null,
|
|
728
|
+
loading: true,
|
|
729
|
+
error: null
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
// Second emit: loading = false, user = John
|
|
733
|
+
expect(states[1]).toEqual({
|
|
734
|
+
user: { name: 'John', id: 1 },
|
|
735
|
+
loading: false,
|
|
736
|
+
error: null
|
|
737
|
+
});
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
it('should update store state when login fails', async () => {
|
|
741
|
+
mockApi.login.mockRejectedValue(new Error('Invalid credentials'));
|
|
742
|
+
|
|
743
|
+
const states: any[] = [];
|
|
744
|
+
userService.subscribe((state) => states.push({ ...state }));
|
|
745
|
+
|
|
746
|
+
await expect(userService.login('user', 'wrong')).rejects.toThrow();
|
|
747
|
+
|
|
748
|
+
// ✅ Verify error state
|
|
749
|
+
expect(states[1]).toEqual({
|
|
750
|
+
user: null,
|
|
751
|
+
loading: false,
|
|
752
|
+
error: expect.any(Error)
|
|
753
|
+
});
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
it('should emit logout state', () => {
|
|
757
|
+
// First set user logged in
|
|
758
|
+
userService.emit({
|
|
759
|
+
user: { name: 'John', id: 1 },
|
|
760
|
+
loading: false,
|
|
761
|
+
error: null
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
// Logout
|
|
765
|
+
userService.logout();
|
|
766
|
+
|
|
767
|
+
// ✅ Verify state reset
|
|
768
|
+
expect(userService.getState()).toEqual({
|
|
769
|
+
user: null,
|
|
770
|
+
loading: false,
|
|
771
|
+
error: null
|
|
772
|
+
});
|
|
773
|
+
});
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
// ✅✅✅ Advantages:
|
|
777
|
+
// 1. Don't need to render UI
|
|
778
|
+
// 2. Can test all state changes
|
|
779
|
+
// 3. Can verify emit call sequence
|
|
780
|
+
// 4. Tests run fast
|
|
781
|
+
```
|
|
782
|
+
|
|
783
|
+
#### 2. Test UI Component (UI Test)
|
|
784
|
+
|
|
785
|
+
```typescript
|
|
786
|
+
// __tests__/src/pages/LoginPage.test.tsx
|
|
787
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
788
|
+
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
789
|
+
import { LoginPage } from '@/pages/LoginPage';
|
|
790
|
+
import { IOCProvider } from '@/uikit/contexts/IOCContext';
|
|
791
|
+
|
|
792
|
+
describe('LoginPage (UI Test)', () => {
|
|
793
|
+
it('should show loading when login', async () => {
|
|
794
|
+
// ✅ Mock Service and Store
|
|
795
|
+
const mockStore = {
|
|
796
|
+
user: null,
|
|
797
|
+
loading: false,
|
|
798
|
+
error: null
|
|
799
|
+
};
|
|
800
|
+
|
|
801
|
+
const mockUserService = {
|
|
802
|
+
login: vi.fn().mockImplementation(() => {
|
|
803
|
+
// Simulate state change
|
|
804
|
+
mockStore.loading = true;
|
|
805
|
+
return Promise.resolve();
|
|
806
|
+
}),
|
|
807
|
+
subscribe: vi.fn(),
|
|
808
|
+
getState: () => mockStore
|
|
809
|
+
};
|
|
810
|
+
|
|
811
|
+
const mockIOC = (identifier: string) => {
|
|
812
|
+
if (identifier === 'UserServiceInterface') return mockUserService;
|
|
813
|
+
};
|
|
814
|
+
|
|
815
|
+
// ✅ Render component
|
|
816
|
+
const { rerender } = render(
|
|
817
|
+
<IOCProvider value={mockIOC}>
|
|
818
|
+
<LoginPage />
|
|
819
|
+
</IOCProvider>
|
|
820
|
+
);
|
|
821
|
+
|
|
822
|
+
// Click login button
|
|
823
|
+
const loginButton = screen.getByText('Login');
|
|
824
|
+
fireEvent.click(loginButton);
|
|
825
|
+
|
|
826
|
+
// ✅ Verify Service was called
|
|
827
|
+
expect(mockUserService.login).toHaveBeenCalled();
|
|
828
|
+
|
|
829
|
+
// Simulate state update
|
|
830
|
+
mockStore.loading = true;
|
|
831
|
+
rerender(
|
|
832
|
+
<IOCProvider value={mockIOC}>
|
|
833
|
+
<LoginPage />
|
|
834
|
+
</IOCProvider>
|
|
835
|
+
);
|
|
836
|
+
|
|
837
|
+
// ✅ Verify UI shows loading state
|
|
838
|
+
expect(screen.getByText('Loading...')).toBeInTheDocument();
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
it('should show error message when login fails', () => {
|
|
842
|
+
const mockStore = {
|
|
843
|
+
user: null,
|
|
844
|
+
loading: false,
|
|
845
|
+
error: new Error('Invalid credentials')
|
|
846
|
+
};
|
|
847
|
+
|
|
848
|
+
const mockUserService = {
|
|
849
|
+
login: vi.fn(),
|
|
850
|
+
subscribe: vi.fn(),
|
|
851
|
+
getState: () => mockStore
|
|
852
|
+
};
|
|
853
|
+
|
|
854
|
+
const mockIOC = (identifier: string) => {
|
|
855
|
+
if (identifier === 'UserServiceInterface') return mockUserService;
|
|
856
|
+
};
|
|
857
|
+
|
|
858
|
+
render(
|
|
859
|
+
<IOCProvider value={mockIOC}>
|
|
860
|
+
<LoginPage />
|
|
861
|
+
</IOCProvider>
|
|
862
|
+
);
|
|
863
|
+
|
|
864
|
+
// ✅ Verify error message displayed
|
|
865
|
+
expect(screen.getByText('Error: Invalid credentials')).toBeInTheDocument();
|
|
866
|
+
});
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
// ✅✅✅ Advantages:
|
|
870
|
+
// 1. Don't need real Service implementation
|
|
871
|
+
// 2. Can easily simulate various states
|
|
872
|
+
// 3. UI tests focus on UI logic
|
|
873
|
+
```
|
|
874
|
+
|
|
875
|
+
#### 3. Combination Testing (Integration Test)
|
|
876
|
+
|
|
877
|
+
```typescript
|
|
878
|
+
// __tests__/src/integration/UserLogin.test.tsx
|
|
879
|
+
import { describe, it, expect } from 'vitest';
|
|
880
|
+
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
881
|
+
import { LoginPage } from '@/pages/LoginPage';
|
|
882
|
+
import { UserService } from '@/base/services/UserService';
|
|
883
|
+
import { IOCProvider } from '@/uikit/contexts/IOCContext';
|
|
884
|
+
|
|
885
|
+
describe('User Login Flow (Integration Test)', () => {
|
|
886
|
+
it('should complete login flow', async () => {
|
|
887
|
+
// ✅ Use real Service and Store
|
|
888
|
+
const mockApi = {
|
|
889
|
+
login: vi.fn().mockResolvedValue({
|
|
890
|
+
user: { name: 'John', id: 1 },
|
|
891
|
+
token: 'test-token'
|
|
892
|
+
})
|
|
893
|
+
};
|
|
894
|
+
|
|
895
|
+
const userService = new UserService(mockApi, mockConfig, mockStorage);
|
|
896
|
+
|
|
897
|
+
const mockIOC = (identifier: string) => {
|
|
898
|
+
if (identifier === 'UserServiceInterface') return userService;
|
|
899
|
+
};
|
|
900
|
+
|
|
901
|
+
// ✅ Render real UI
|
|
902
|
+
render(
|
|
903
|
+
<IOCProvider value={mockIOC}>
|
|
904
|
+
<LoginPage />
|
|
905
|
+
</IOCProvider>
|
|
906
|
+
);
|
|
907
|
+
|
|
908
|
+
// ✅ Simulate user action
|
|
909
|
+
const loginButton = screen.getByText('Login');
|
|
910
|
+
fireEvent.click(loginButton);
|
|
911
|
+
|
|
912
|
+
// ✅ Verify loading state
|
|
913
|
+
await waitFor(() => {
|
|
914
|
+
expect(screen.getByText('Loading...')).toBeInTheDocument();
|
|
915
|
+
});
|
|
916
|
+
|
|
917
|
+
// ✅ Verify login success
|
|
918
|
+
await waitFor(() => {
|
|
919
|
+
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
|
|
920
|
+
expect(userService.getState().user).toEqual({
|
|
921
|
+
name: 'John',
|
|
922
|
+
id: 1
|
|
923
|
+
});
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
// ✅ Verify API was called
|
|
927
|
+
expect(mockApi.login).toHaveBeenCalled();
|
|
928
|
+
});
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
// ✅✅✅ Advantages:
|
|
932
|
+
// 1. Test real user flow
|
|
933
|
+
// 2. Verify Service and UI integration
|
|
934
|
+
// 3. Discover integration issues
|
|
935
|
+
```
|
|
936
|
+
|
|
937
|
+
### Testing Strategy Summary
|
|
938
|
+
|
|
939
|
+
```
|
|
940
|
+
┌────────────────────────────────────────┐
|
|
941
|
+
│ Testing Pyramid │
|
|
942
|
+
│ │
|
|
943
|
+
│ △ UI Tests (10%) │
|
|
944
|
+
│ ╱ ╲ │
|
|
945
|
+
│ ╱ ╲ Integration Tests (20%) │
|
|
946
|
+
│ ╱ ╲ │
|
|
947
|
+
│ ╱───────╲ │
|
|
948
|
+
│ ╱ ╲ Store + Service Tests (70%)│
|
|
949
|
+
│╱═══════════╲ │
|
|
950
|
+
│ │
|
|
951
|
+
│ Store Tests: Test state change logic │
|
|
952
|
+
│ Integration Tests: Test Service + UI │
|
|
953
|
+
│ UI Tests: Test UI interaction │
|
|
954
|
+
└────────────────────────────────────────┘
|
|
955
|
+
```
|
|
956
|
+
|
|
957
|
+
---
|
|
958
|
+
|
|
959
|
+
## 💎 Best Practices
|
|
960
|
+
|
|
961
|
+
### 1. ✅ Service Extends StoreInterface
|
|
962
|
+
|
|
963
|
+
```typescript
|
|
964
|
+
// ✅ Good practice: Service extends StoreInterface
|
|
255
965
|
@injectable()
|
|
256
|
-
class
|
|
257
|
-
|
|
966
|
+
export class UserService extends StoreInterface<UserState> {
|
|
967
|
+
constructor() {
|
|
968
|
+
super(() => ({
|
|
969
|
+
user: null,
|
|
970
|
+
loading: false
|
|
971
|
+
}));
|
|
972
|
+
}
|
|
258
973
|
|
|
259
|
-
|
|
260
|
-
this.emit({ ...this.state,
|
|
974
|
+
async login(username: string, password: string) {
|
|
975
|
+
this.emit({ ...this.state, loading: true });
|
|
976
|
+
// ...
|
|
261
977
|
}
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// ❌ Bad practice: Service doesn't extend StoreInterface
|
|
981
|
+
@injectable()
|
|
982
|
+
export class UserService {
|
|
983
|
+
private user: UserInfo | null = null;
|
|
984
|
+
|
|
985
|
+
// Problem: UI cannot subscribe to state
|
|
986
|
+
}
|
|
987
|
+
```
|
|
988
|
+
|
|
989
|
+
### 2. ✅ Use emit to Publish State
|
|
990
|
+
|
|
991
|
+
```typescript
|
|
992
|
+
// ✅ Good practice: Publish state via emit
|
|
993
|
+
async login(username: string, password: string) {
|
|
994
|
+
this.emit({ ...this.state, loading: true });
|
|
995
|
+
|
|
996
|
+
const response = await this.api.login({ username, password });
|
|
997
|
+
|
|
998
|
+
this.emit({
|
|
999
|
+
user: response.user,
|
|
1000
|
+
loading: false
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
// ❌ Bad practice: Directly modify state
|
|
1005
|
+
async login(username: string, password: string) {
|
|
1006
|
+
this.state.loading = true; // ❌ Won't notify subscribers
|
|
1007
|
+
|
|
1008
|
+
const response = await this.api.login({ username, password });
|
|
1009
|
+
|
|
1010
|
+
this.state.user = response.user; // ❌ Won't notify subscribers
|
|
1011
|
+
}
|
|
1012
|
+
```
|
|
1013
|
+
|
|
1014
|
+
### 3. ✅ Use cloneState to Simplify Updates
|
|
1015
|
+
|
|
1016
|
+
```typescript
|
|
1017
|
+
// ✅ Good practice: Use cloneState
|
|
1018
|
+
setUser(user: UserInfo) {
|
|
1019
|
+
this.emit(this.cloneState({ user }));
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
setLoading(loading: boolean) {
|
|
1023
|
+
this.emit(this.cloneState({ loading }));
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
// ⚠️ Also acceptable: Manual spread
|
|
1027
|
+
setUser(user: UserInfo) {
|
|
1028
|
+
this.emit({ ...this.state, user });
|
|
1029
|
+
}
|
|
1030
|
+
```
|
|
1031
|
+
|
|
1032
|
+
### 4. ✅ Define Selectors
|
|
1033
|
+
|
|
1034
|
+
```typescript
|
|
1035
|
+
// ✅ Good practice: Define selectors
|
|
1036
|
+
@injectable()
|
|
1037
|
+
export class UserService extends StoreInterface<UserState> {
|
|
1038
|
+
selector = {
|
|
1039
|
+
user: (state: UserState) => state.user,
|
|
1040
|
+
loading: (state: UserState) => state.loading,
|
|
1041
|
+
isLoggedIn: (state: UserState) => state.user !== null
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
// Usage
|
|
1046
|
+
const isLoggedIn = useStore(userService, userService.selector.isLoggedIn);
|
|
1047
|
+
|
|
1048
|
+
// ❌ Bad practice: Inline selectors
|
|
1049
|
+
const isLoggedIn = useStore(userService, (state) => state.user !== null);
|
|
1050
|
+
// Problem: Creates new function every render
|
|
1051
|
+
```
|
|
262
1052
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
1053
|
+
### 5. ✅ Use Selectors for Performance Optimization
|
|
1054
|
+
|
|
1055
|
+
```typescript
|
|
1056
|
+
// ✅ Good practice: Only subscribe to needed state
|
|
1057
|
+
function UserName() {
|
|
1058
|
+
const userService = useIOC('UserServiceInterface');
|
|
1059
|
+
|
|
1060
|
+
// Only subscribe to user, loading changes won't trigger re-render
|
|
1061
|
+
const user = useStore(userService, (state) => state.user);
|
|
1062
|
+
|
|
1063
|
+
return <span>{user?.name}</span>;
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
// ❌ Bad practice: Subscribe to complete state
|
|
1067
|
+
function UserName() {
|
|
1068
|
+
const userService = useIOC('UserServiceInterface');
|
|
1069
|
+
|
|
1070
|
+
// loading changes will also trigger re-render
|
|
1071
|
+
const { user, loading } = useStore(userService);
|
|
1072
|
+
|
|
1073
|
+
return <span>{user?.name}</span>;
|
|
1074
|
+
}
|
|
1075
|
+
```
|
|
1076
|
+
|
|
1077
|
+
### 6. ✅ Keep State Immutable
|
|
1078
|
+
|
|
1079
|
+
```typescript
|
|
1080
|
+
// ✅ Good practice: Create new object
|
|
1081
|
+
updateUser(changes: Partial<UserInfo>) {
|
|
1082
|
+
this.emit({
|
|
1083
|
+
...this.state,
|
|
1084
|
+
user: {
|
|
1085
|
+
...this.state.user,
|
|
1086
|
+
...changes
|
|
1087
|
+
}
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
// ❌ Bad practice: Directly modify object
|
|
1092
|
+
updateUser(changes: Partial<UserInfo>) {
|
|
1093
|
+
this.state.user.name = changes.name; // ❌ Direct modification
|
|
1094
|
+
this.emit(this.state); // ❌ Same reference, may not trigger update
|
|
1095
|
+
}
|
|
1096
|
+
```
|
|
1097
|
+
|
|
1098
|
+
### 7. ✅ Reasonably Divide State
|
|
1099
|
+
|
|
1100
|
+
```typescript
|
|
1101
|
+
// ✅ Good practice: Each Service manages its own state
|
|
1102
|
+
class UserService extends StoreInterface<UserState> {
|
|
1103
|
+
// Only manage user-related state
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
class ThemeService extends StoreInterface<ThemeState> {
|
|
1107
|
+
// Only manage theme-related state
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
class I18nService extends StoreInterface<I18nState> {
|
|
1111
|
+
// Only manage i18n-related state
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
// ❌ Bad practice: Global large Store
|
|
1115
|
+
class GlobalStore extends StoreInterface<GlobalState> {
|
|
1116
|
+
// Contains all state: user, theme, i18n, etc.
|
|
1117
|
+
// Problem: Any state change affects all subscribers
|
|
1118
|
+
}
|
|
1119
|
+
```
|
|
1120
|
+
|
|
1121
|
+
---
|
|
1122
|
+
|
|
1123
|
+
## ❓ FAQ
|
|
1124
|
+
|
|
1125
|
+
### Q1: Why not use Redux?
|
|
1126
|
+
|
|
1127
|
+
**A:**
|
|
1128
|
+
|
|
1129
|
+
| Feature | Redux | Store (SliceStore) |
|
|
1130
|
+
| ---------------------- | ------------------------------------- | ---------------------------- |
|
|
1131
|
+
| **Complexity** | ❌ High (Action, Reducer, Middleware) | ✅ Low (emit + subscribe) |
|
|
1132
|
+
| **Learning Curve** | ❌ Steep | ✅ Gentle |
|
|
1133
|
+
| **TypeScript Support** | ⚠️ Needs extra config | ✅ Native support |
|
|
1134
|
+
| **IOC Integration** | ⚠️ Needs extra work | ✅ Natural integration |
|
|
1135
|
+
| **Performance** | ✅ Good | ✅ Good |
|
|
1136
|
+
| **Use Case** | Large applications | Small to medium applications |
|
|
1137
|
+
|
|
1138
|
+
**Our Choice:**
|
|
1139
|
+
|
|
1140
|
+
- Project already uses IOC, don't need Redux's global state management
|
|
1141
|
+
- Each Service manages its own state, clearer
|
|
1142
|
+
- SliceStore is simple and powerful enough
|
|
1143
|
+
|
|
1144
|
+
### Q2: What's the difference between Store and React Context?
|
|
1145
|
+
|
|
1146
|
+
**A:**
|
|
1147
|
+
|
|
1148
|
+
| Feature | React Context | Store |
|
|
1149
|
+
| ----------------------- | ---------------------------------- | --------------------------------------- |
|
|
1150
|
+
| **Scope** | Component tree | Global (through IOC) |
|
|
1151
|
+
| **Performance** | ⚠️ Any value change re-renders all | ✅ Only subscribed value changes render |
|
|
1152
|
+
| **Selectors** | ❌ None | ✅ Yes |
|
|
1153
|
+
| **Service Integration** | ⚠️ Need manual | ✅ Natural integration |
|
|
1154
|
+
|
|
1155
|
+
**Recommendation:**
|
|
1156
|
+
|
|
1157
|
+
- Use Store to manage application state (Service state)
|
|
1158
|
+
- Use Context to manage UI state (like modals, temporary form data)
|
|
1159
|
+
|
|
1160
|
+
### Q3: How to avoid redundant renders?
|
|
1161
|
+
|
|
1162
|
+
**A:** Use selectors
|
|
1163
|
+
|
|
1164
|
+
```typescript
|
|
1165
|
+
// ❌ Problem: Subscribe to complete state
|
|
1166
|
+
const { user, loading, error } = useStore(userService);
|
|
1167
|
+
// loading changes will cause component re-render
|
|
1168
|
+
|
|
1169
|
+
// ✅ Solution: Only subscribe to needed state
|
|
1170
|
+
const user = useStore(userService, (state) => state.user);
|
|
1171
|
+
// Only user changes will cause re-render
|
|
1172
|
+
```
|
|
1173
|
+
|
|
1174
|
+
### Q4: Can I call emit outside Service?
|
|
1175
|
+
|
|
1176
|
+
**A:** Not recommended
|
|
1177
|
+
|
|
1178
|
+
```typescript
|
|
1179
|
+
// ❌ Bad practice
|
|
1180
|
+
function SomeComponent() {
|
|
1181
|
+
const userService = useIOC('UserServiceInterface');
|
|
1182
|
+
|
|
1183
|
+
// ❌ Directly call emit
|
|
1184
|
+
userService.emit({ user: newUser, loading: false });
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
// ✅ Good practice: Through Service method
|
|
1188
|
+
function SomeComponent() {
|
|
1189
|
+
const userService = useIOC('UserServiceInterface');
|
|
1190
|
+
|
|
1191
|
+
// ✅ Call Service method
|
|
1192
|
+
userService.setUser(newUser);
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
// In Service
|
|
1196
|
+
@injectable()
|
|
1197
|
+
export class UserService extends StoreInterface<UserState> {
|
|
1198
|
+
setUser(user: UserInfo) {
|
|
1199
|
+
this.emit(this.cloneState({ user }));
|
|
266
1200
|
}
|
|
1201
|
+
}
|
|
1202
|
+
```
|
|
1203
|
+
|
|
1204
|
+
**Reasons:**
|
|
1205
|
+
|
|
1206
|
+
- Maintain encapsulation
|
|
1207
|
+
- Easier to test
|
|
1208
|
+
- Business logic centralized in Service
|
|
1209
|
+
|
|
1210
|
+
### Q5: Store state update not working?
|
|
1211
|
+
|
|
1212
|
+
**A:** Check these points:
|
|
1213
|
+
|
|
1214
|
+
```typescript
|
|
1215
|
+
// ❌ Common mistake 1: Directly modify state
|
|
1216
|
+
this.state.loading = true; // Won't trigger update
|
|
1217
|
+
|
|
1218
|
+
// ✅ Correct: Use emit
|
|
1219
|
+
this.emit({ ...this.state, loading: true });
|
|
1220
|
+
|
|
1221
|
+
// ❌ Common mistake 2: Not creating new object
|
|
1222
|
+
const state = this.state;
|
|
1223
|
+
state.loading = true;
|
|
1224
|
+
this.emit(state); // Same reference, may not trigger update
|
|
1225
|
+
|
|
1226
|
+
// ✅ Correct: Create new object
|
|
1227
|
+
this.emit({ ...this.state, loading: true });
|
|
1228
|
+
|
|
1229
|
+
// ❌ Common mistake 3: Forgot to subscribe
|
|
1230
|
+
function MyComponent() {
|
|
1231
|
+
const userService = useIOC('UserServiceInterface');
|
|
1232
|
+
// Not calling useStore, cannot receive updates
|
|
1233
|
+
|
|
1234
|
+
return <div>{userService.getState().user?.name}</div>;
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
// ✅ Correct: Use useStore to subscribe
|
|
1238
|
+
function MyComponent() {
|
|
1239
|
+
const userService = useIOC('UserServiceInterface');
|
|
1240
|
+
const user = useStore(userService, (state) => state.user);
|
|
1241
|
+
|
|
1242
|
+
return <div>{user?.name}</div>;
|
|
1243
|
+
}
|
|
1244
|
+
```
|
|
1245
|
+
|
|
1246
|
+
### Q6: How to share state between Services?
|
|
267
1247
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
1248
|
+
**A:** Through IOC injection
|
|
1249
|
+
|
|
1250
|
+
```typescript
|
|
1251
|
+
// Service A
|
|
1252
|
+
@injectable()
|
|
1253
|
+
export class UserService extends StoreInterface<UserState> {
|
|
1254
|
+
// ...
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
// Service B depends on Service A
|
|
1258
|
+
@injectable()
|
|
1259
|
+
export class ProfileService {
|
|
1260
|
+
constructor(
|
|
1261
|
+
@inject('UserServiceInterface')
|
|
1262
|
+
private userService: UserService
|
|
1263
|
+
) {}
|
|
1264
|
+
|
|
1265
|
+
async updateProfile(data: ProfileData) {
|
|
1266
|
+
// ✅ Access UserService state
|
|
1267
|
+
const user = this.userService.getState().user;
|
|
1268
|
+
|
|
1269
|
+
// ✅ Can also subscribe to UserService state
|
|
1270
|
+
this.userService.subscribe((state) => {
|
|
1271
|
+
console.log('User state changed:', state);
|
|
1272
|
+
});
|
|
271
1273
|
}
|
|
272
1274
|
}
|
|
273
1275
|
```
|
|
274
1276
|
|
|
275
|
-
|
|
1277
|
+
---
|
|
276
1278
|
|
|
277
|
-
|
|
278
|
-
- Divide state by functional modules
|
|
279
|
-
- Avoid state redundancy
|
|
280
|
-
- Keep state flat
|
|
1279
|
+
## 📚 Related Documentation
|
|
281
1280
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
1281
|
+
- [Project Architecture Design](./index.md) - Understand overall architecture
|
|
1282
|
+
- [IOC Container](./ioc.md) - Dependency injection and UI separation
|
|
1283
|
+
- [Bootstrap Initializer](./bootstrap.md) - Application startup and initialization
|
|
1284
|
+
- [Testing Guide](./test-guide.md) - Detailed testing strategies
|
|
286
1285
|
|
|
287
|
-
|
|
288
|
-
- Define interfaces for all states
|
|
289
|
-
- Use TypeScript's type inference
|
|
290
|
-
- Avoid using any type
|
|
1286
|
+
---
|
|
291
1287
|
|
|
292
|
-
|
|
293
|
-
- Initialize store during Bootstrap phase
|
|
294
|
-
- Manage store instances through IOC container
|
|
295
|
-
- Use plugin system to extend functionality
|
|
1288
|
+
## 🎉 Summary
|
|
296
1289
|
|
|
297
|
-
|
|
1290
|
+
Core value of Store state management:
|
|
298
1291
|
|
|
299
|
-
|
|
1292
|
+
1. **Solve Communication Problem** 📡 - Application layer notifies UI layer while maintaining separation
|
|
1293
|
+
2. **Publish-Subscribe Pattern** 🔔 - Service emits, UI uses useStore
|
|
1294
|
+
3. **Auto-update UI** ⚡ - UI automatically re-renders when state changes
|
|
1295
|
+
4. **Maintain Decoupling** 🔗 - Service doesn't know which UIs are listening
|
|
1296
|
+
5. **Easy to Test** 🧪 - Store can be tested independently
|
|
1297
|
+
6. **Performance Optimization** 🚀 - Selectors only subscribe to needed state
|
|
1298
|
+
7. **Type Safety** 🔒 - Full TypeScript support
|
|
300
1299
|
|
|
301
|
-
|
|
1300
|
+
**Remember the core pattern:**
|
|
302
1301
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
1302
|
+
```typescript
|
|
1303
|
+
// 1. Service extends StoreInterface
|
|
1304
|
+
class MyService extends StoreInterface<MyState> {
|
|
1305
|
+
// 2. Publish state via emit
|
|
1306
|
+
doSomething() {
|
|
1307
|
+
this.emit({ ...this.state, data: newData });
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
306
1310
|
|
|
307
|
-
|
|
1311
|
+
// 3. UI subscribes to state via useStore
|
|
1312
|
+
function MyComponent() {
|
|
1313
|
+
const myService = useIOC('MyServiceInterface');
|
|
1314
|
+
const data = useStore(myService, (state) => state.data);
|
|
308
1315
|
|
|
309
|
-
|
|
1316
|
+
return <div>{data}</div>;
|
|
1317
|
+
}
|
|
1318
|
+
```
|
|
310
1319
|
|
|
311
|
-
|
|
312
|
-
- Check if dependencies are correctly set
|
|
313
|
-
- Consider using React.memo for component optimization
|
|
1320
|
+
**Core Principles:**
|
|
314
1321
|
|
|
315
|
-
|
|
1322
|
+
- ✅ Service publishes state via emit
|
|
1323
|
+
- ✅ UI subscribes to state via useStore
|
|
1324
|
+
- ✅ Use selectors for performance optimization
|
|
1325
|
+
- ✅ Keep state immutable
|
|
1326
|
+
- ✅ Each Service manages its own state
|
|
316
1327
|
|
|
317
|
-
|
|
1328
|
+
---
|
|
318
1329
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
- Ensure state types implement StoreStateInterface
|
|
1330
|
+
**Feedback:**
|
|
1331
|
+
If you have any questions or suggestions about Store state management, please discuss in the team channel or submit an Issue.
|