diff --git a/packages/core/admin/admin/src/StrapiApp.js b/packages/core/admin/admin/src/StrapiApp.js index 7b533eac45..a33a5fa690 100644 --- a/packages/core/admin/admin/src/StrapiApp.js +++ b/packages/core/admin/admin/src/StrapiApp.js @@ -11,8 +11,8 @@ import { BrowserRouter } from 'react-router-dom'; import Logo from './assets/images/logo-strapi-2022.svg'; import { LANGUAGE_LOCAL_STORAGE_KEY } from './components/LanguageProvider'; import Providers from './components/Providers'; -import { customFields, Plugin } from './core/apis'; -import configureStore from './core/store/configureStore'; +import { customFields, Plugin, Reducers } from './core/apis'; +import { configureStore } from './core/store/configureStore'; import { basename, createHook } from './core/utils'; import { INJECT_COLUMN_IN_TABLE, @@ -26,7 +26,7 @@ import App from './pages/App'; import languageNativeNames from './translations/languageNativeNames'; class StrapiApp { - constructor({ adminConfig, appPlugins, library, middlewares, reducers }) { + constructor({ adminConfig, appPlugins, library, middlewares }) { this.customConfigurations = adminConfig.config; this.customBootstrapConfiguration = adminConfig.bootstrap; this.configurations = { @@ -43,7 +43,7 @@ class StrapiApp { this.library = library; this.middlewares = middlewares; this.plugins = {}; - this.reducers = reducers; + this.reducers = Reducers({}); this.translations = {}; this.hooksDict = {}; this.admin = { diff --git a/packages/core/admin/admin/src/core/store/configureStore.js b/packages/core/admin/admin/src/core/store/configureStore.js deleted file mode 100644 index f881dbebcf..0000000000 --- a/packages/core/admin/admin/src/core/store/configureStore.js +++ /dev/null @@ -1,47 +0,0 @@ -import { applyMiddleware, combineReducers, compose, createStore } from 'redux'; - -const configureStore = (appMiddlewares, appReducers) => { - let composeEnhancers = compose; - - const middlewares = []; - - appMiddlewares.forEach((middleware) => { - middlewares.push(middleware()); - }); - - // If Redux Dev Tools are installed, enable them - if ( - process.env.NODE_ENV !== 'production' && - typeof window === 'object' && - window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ - ) { - composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({}); - } - - const store = createStore( - createReducer(appReducers, {}), - {}, - composeEnhancers(applyMiddleware(...middlewares)) - ); - - // Add a dictionary to keep track of the registered async reducers - store.asyncReducers = {}; - - // Create an inject reducer function - // This function adds the async reducer, and creates a new combined reducer - store.injectReducer = (key, asyncReducer) => { - store.asyncReducers[key] = asyncReducer; - store.replaceReducer(createReducer(appReducers, store.asyncReducers)); - }; - - return store; -}; - -const createReducer = (appReducers, asyncReducers) => { - return combineReducers({ - ...appReducers, - ...asyncReducers, - }); -}; - -export default configureStore; diff --git a/packages/core/admin/admin/src/core/store/configureStore.ts b/packages/core/admin/admin/src/core/store/configureStore.ts new file mode 100644 index 0000000000..25ad35501e --- /dev/null +++ b/packages/core/admin/admin/src/core/store/configureStore.ts @@ -0,0 +1,114 @@ +import { + configureStore, + StoreEnhancer, + Middleware, + Reducer, + combineReducers, + createSelector, + Selector, +} from '@reduxjs/toolkit'; +import { useDispatch, useStore, TypedUseSelectorHook, useSelector } from 'react-redux'; + +// @ts-expect-error no types, yet. +import rbacProviderReducer from '../../components/RBACProvider/reducer'; +// @ts-expect-error no types, yet. +import rbacManagerReducer from '../../content-manager/hooks/useSyncRbac/reducer'; +// @ts-expect-error no types, yet. +import cmAppReducer from '../../content-manager/pages/App/reducer'; +// @ts-expect-error no types, yet. +import editViewLayoutManagerReducer from '../../content-manager/pages/EditViewLayoutManager/reducer'; +// @ts-expect-error no types, yet. +import listViewReducer from '../../content-manager/pages/ListView/reducer'; +// @ts-expect-error no types, yet. +import editViewCrudReducer from '../../content-manager/sharedReducers/crudReducer/reducer'; +// @ts-expect-error no types, yet. +import appReducer from '../../pages/App/reducer'; + +const createReducer = ( + appReducers: Record, + asyncReducers: Record +) => { + return combineReducers({ + ...appReducers, + ...asyncReducers, + }); +}; + +/** + * @description Static reducers are ones we know, they live in the admin package. + */ +const staticReducers: Record = { + admin_app: appReducer, + rbacProvider: rbacProviderReducer, + 'content-manager_app': cmAppReducer, + 'content-manager_listView': listViewReducer, + 'content-manager_rbacManager': rbacManagerReducer, + 'content-manager_editViewLayoutManager': editViewLayoutManagerReducer, + 'content-manager_editViewCrudReducer': editViewCrudReducer, +} as const; + +const injectReducerStoreEnhancer: (appReducers: Record) => StoreEnhancer = + (appReducers) => + (next) => + (...args) => { + const store = next(...args); + + const asyncReducers: Record = {}; + + return { + ...store, + asyncReducers, + injectReducer: (key: string, asyncReducer: Reducer) => { + asyncReducers[key] = asyncReducer; + // @ts-expect-error we dynamically add reducers which makes the types uncomfortable. + store.replaceReducer(createReducer(appReducers, asyncReducers)); + }, + }; + }; + +/** + * @description This is the main store configuration function, injected Reducers use our legacy app.addReducer API, + * which we're trying to phase out. App Middlewares could potentially be improved...? + */ +const configureStoreImpl = ( + appMiddlewares: Array<() => Middleware>, + injectedReducers: Record +) => { + const coreReducers = { ...staticReducers, ...injectedReducers }; + + const store = configureStore({ + reducer: createReducer(coreReducers, {}), + devTools: process.env.NODE_ENV !== 'production', + middleware: (getDefaultMiddleware) => [ + ...getDefaultMiddleware(), + ...appMiddlewares.map((m) => m()), + ], + enhancers: [injectReducerStoreEnhancer(coreReducers)], + }); + + return store; +}; + +type Store = ReturnType & { + asyncReducers: Record; + injectReducer: (key: string, asyncReducer: Reducer) => void; +}; + +type RootState = ReturnType; +type AppDispatch = Store['dispatch']; + +const useTypedDispatch: () => AppDispatch = useDispatch; +const useTypedStore = useStore as () => Store; +const useTypedSelector: TypedUseSelectorHook = useSelector; + +const createTypedSelector = (selector: Selector) => + createSelector((state: RootState) => state, selector); + +export { + useTypedDispatch, + useTypedStore, + useTypedSelector, + configureStoreImpl as configureStore, + createTypedSelector, +}; +export type { RootState }; diff --git a/packages/core/admin/admin/src/index.js b/packages/core/admin/admin/src/index.js index 9c43564aad..e763d361cd 100644 --- a/packages/core/admin/admin/src/index.js +++ b/packages/core/admin/admin/src/index.js @@ -2,10 +2,9 @@ import { getFetchClient } from '@strapi/helper-plugin'; import { createRoot } from 'react-dom/client'; import appCustomisations from './app'; -import { Components, Fields, Middlewares, Reducers } from './core/apis'; +import { Components, Fields, Middlewares } from './core/apis'; // eslint-disable-next-line import/extensions import plugins from './plugins'; -import appReducers from './reducers'; window.strapi = { /** @@ -35,7 +34,6 @@ const library = { fields: Fields(), }; const middlewares = Middlewares(); -const reducers = Reducers({ appReducers }); const MOUNT_NODE = document.getElementById('app'); @@ -69,7 +67,6 @@ const run = async () => { adminConfig: customConfig, bootstrap: customConfig, middlewares, - reducers, }); await app.bootstrapAdmin(); diff --git a/packages/core/admin/admin/src/reducers.js b/packages/core/admin/admin/src/reducers.js deleted file mode 100644 index 912cbff289..0000000000 --- a/packages/core/admin/admin/src/reducers.js +++ /dev/null @@ -1,23 +0,0 @@ -import rbacProviderReducer from './components/RBACProvider/reducer'; -import rbacManagerReducer from './content-manager/hooks/useSyncRbac/reducer'; -import cmAppReducer from './content-manager/pages/App/reducer'; -import editViewLayoutManagerReducer from './content-manager/pages/EditViewLayoutManager/reducer'; -import listViewReducer from './content-manager/pages/ListView/reducer'; -import editViewCrudReducer from './content-manager/sharedReducers/crudReducer/reducer'; -import appReducer from './pages/App/reducer'; - -const contentManagerReducers = { - 'content-manager_app': cmAppReducer, - 'content-manager_listView': listViewReducer, - 'content-manager_rbacManager': rbacManagerReducer, - 'content-manager_editViewLayoutManager': editViewLayoutManagerReducer, - 'content-manager_editViewCrudReducer': editViewCrudReducer, -}; - -const reducers = { - admin_app: appReducer, - rbacProvider: rbacProviderReducer, - ...contentManagerReducers, -}; - -export default reducers; diff --git a/packages/core/admin/package.json b/packages/core/admin/package.json index 730feab1db..8aa29b1ee2 100644 --- a/packages/core/admin/package.json +++ b/packages/core/admin/package.json @@ -44,6 +44,7 @@ "@casl/ability": "6.5.0", "@pmmmwh/react-refresh-webpack-plugin": "0.5.10", "@radix-ui/react-toolbar": "1.0.4", + "@reduxjs/toolkit": "1.9.7", "@strapi/data-transfer": "4.14.4", "@strapi/design-system": "1.12.2", "@strapi/helper-plugin": "4.14.4",