From effad216aa39113d4a74a53c39065f9aebd45018 Mon Sep 17 00:00:00 2001 From: Ben Chypak Date: Mon, 20 Jan 2025 12:48:57 -0800 Subject: [PATCH 1/8] Initial commit Added extension state via Zustand --- .../src/hooks/use-extension-store.ts | 16 +++++++ .../src/pages/store-locator/index.tsx | 12 +++++- .../src/setup-app.ts | 20 +++++++++ .../pwa-kit-extension-sdk/package-lock.json | 43 ++++++++++++++++--- packages/pwa-kit-extension-sdk/package.json | 3 +- .../src/react/classes/ApplicationExtension.ts | 11 +++++ .../components/withApplicationExtensions.tsx | 15 ++++++- .../src/react/hooks/index.ts | 1 + .../hooks/useApplicationExtensionsStore.tsx | 38 ++++++++++++++++ .../app/pages/home.tsx | 7 +++ 10 files changed, 156 insertions(+), 10 deletions(-) create mode 100644 packages/extension-chakra-store-locator/src/hooks/use-extension-store.ts create mode 100644 packages/pwa-kit-extension-sdk/src/react/hooks/useApplicationExtensionsStore.tsx diff --git a/packages/extension-chakra-store-locator/src/hooks/use-extension-store.ts b/packages/extension-chakra-store-locator/src/hooks/use-extension-store.ts new file mode 100644 index 0000000000..de8b25d4d5 --- /dev/null +++ b/packages/extension-chakra-store-locator/src/hooks/use-extension-store.ts @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2024, salesforce.com, inc. + * All rights reserved. + * SPDX-License-Identifier: BSD-3-Clause + * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +import {useStore as useExtensionsStore} from '@salesforce/pwa-kit-extension-sdk/react' +import extensionMeta from '../../extension-meta.json' + +/** + * This hook returns the store for the current application extension. + */ +// export const useExtensionStore = () => useExtensionsStore((state: Record) => state[extensionMeta.id]) + +export const useExtensionStore = () => useExtensionsStore((state: Record) => state.state[extensionMeta.id] || {}) \ No newline at end of file diff --git a/packages/extension-chakra-store-locator/src/pages/store-locator/index.tsx b/packages/extension-chakra-store-locator/src/pages/store-locator/index.tsx index 8c509374b2..1b850f7b9b 100644 --- a/packages/extension-chakra-store-locator/src/pages/store-locator/index.tsx +++ b/packages/extension-chakra-store-locator/src/pages/store-locator/index.tsx @@ -5,13 +5,23 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ -import React from 'react' +import React, {useEffect} from 'react' import {Box, Container} from '@chakra-ui/react' import {StoreLocatorContent} from '../../components/content' +import {useExtensionStore} from '../../hooks/use-extension-store' +// import {useStore as useExtensionsStore} from '@salesforce/pwa-kit-extension-sdk/react' + + const StoreLocatorPage = () => { + const {setCounter, counter} = useExtensionStore() + return ( + Count: {counter}
+ { ...routes ] } + + getStoreSlice(): {sliceName: string; sliceInitializer: any} { + return { + sliceName: StoreLocatorExtension.id, + sliceInitializer: (set: any) => ({ + counter: 0, + setCounter: () => + set((state: any) => ({ + state: { + ...state.state, + ['@salesforce/extension-chakra-store-locator']: { + ...state.state['@salesforce/extension-chakra-store-locator'], + counter: state.state['@salesforce/extension-chakra-store-locator'].counter + 1 + } + + }, + })) + }) + } + } } export default StoreLocatorExtension diff --git a/packages/pwa-kit-extension-sdk/package-lock.json b/packages/pwa-kit-extension-sdk/package-lock.json index 4f9808a3f3..bf76d6798e 100644 --- a/packages/pwa-kit-extension-sdk/package-lock.json +++ b/packages/pwa-kit-extension-sdk/package-lock.json @@ -15,7 +15,8 @@ "handlebars": "^4.7.8", "hoist-non-react-statics": "^3.3.2", "lodash.merge": "^4.6.2", - "resolve": "^1.22.8" + "resolve": "^1.22.8", + "zustand": "5.0.3" }, "devDependencies": { "@babel/core": "^7.21.3", @@ -2048,7 +2049,7 @@ "version": "15.7.14", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==", - "dev": true + "devOptional": true }, "node_modules/@types/qs": { "version": "6.9.17", @@ -2066,7 +2067,7 @@ "version": "18.3.18", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz", "integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==", - "dev": true, + "devOptional": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -2916,7 +2917,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true + "devOptional": true }, "node_modules/debug": { "version": "4.4.0", @@ -4310,7 +4311,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true + "devOptional": true }, "node_modules/jsesc": { "version": "3.1.0", @@ -4389,7 +4390,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, + "devOptional": true, "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, @@ -4829,7 +4830,7 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "dev": true, + "devOptional": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -5972,6 +5973,34 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true + }, + "node_modules/zustand": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.3.tgz", + "integrity": "sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg==", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } } } } diff --git a/packages/pwa-kit-extension-sdk/package.json b/packages/pwa-kit-extension-sdk/package.json index ff16085c6f..eb9351f231 100644 --- a/packages/pwa-kit-extension-sdk/package.json +++ b/packages/pwa-kit-extension-sdk/package.json @@ -43,7 +43,8 @@ "handlebars": "^4.7.8", "hoist-non-react-statics": "^3.3.2", "lodash.merge": "^4.6.2", - "resolve": "^1.22.8" + "resolve": "^1.22.8", + "zustand": "5.0.3" }, "devDependencies": { "@babel/core": "^7.21.3", diff --git a/packages/pwa-kit-extension-sdk/src/react/classes/ApplicationExtension.ts b/packages/pwa-kit-extension-sdk/src/react/classes/ApplicationExtension.ts index 4e85290ffa..8cf5721ab3 100644 --- a/packages/pwa-kit-extension-sdk/src/react/classes/ApplicationExtension.ts +++ b/packages/pwa-kit-extension-sdk/src/react/classes/ApplicationExtension.ts @@ -65,4 +65,15 @@ export class ApplicationExtension< public beforeRouteMatch(routes: RouteProps[]): RouteProps[] { return routes } + + /** + * Default implementation of getStoreSlice. This method should be overridden by the extension. + * @returns The slice name and initializer for the extension's store slice. + */ + public getStoreSlice(): {sliceName: string; sliceInitializer: any} { + return { + sliceName: this.constructor.name, + sliceInitializer: () => ({}) + } + } } diff --git a/packages/pwa-kit-extension-sdk/src/react/components/withApplicationExtensions.tsx b/packages/pwa-kit-extension-sdk/src/react/components/withApplicationExtensions.tsx index a063c2ff36..3b4b46e871 100644 --- a/packages/pwa-kit-extension-sdk/src/react/components/withApplicationExtensions.tsx +++ b/packages/pwa-kit-extension-sdk/src/react/components/withApplicationExtensions.tsx @@ -11,6 +11,7 @@ import hoistNonReactStatics from 'hoist-non-react-statics' // Local import {applyHOCs} from '../utils' +import {useStore} from '../hooks/useApplicationExtensionsStore' // Types import {ApplicationExtension} from '../classes/ApplicationExtension' @@ -45,12 +46,24 @@ const withApplicationExtensions = ( WrappedComponent: React.ComponentType, options: withApplicationExtensionsOptions ) => { - const hocs: GenericHocType[] = options.applicationExtensions + const {applicationExtensions} = options + + // Get all application extension higher-order components (HOCs) + const hocs: GenericHocType[] = applicationExtensions .filter((applicationExtension) => applicationExtension.isEnabled()) // It's counterintuitive: reversing the list is necessary, as we build the React tree from innermost .reverse() .map((extension) => extension.extendApp.bind(extension) as GenericHocType) .filter(Boolean) + + // Inject store slices into the global store. + applicationExtensions.forEach((extension) => { + const {sliceName, sliceInitializer} = extension.getStoreSlice() + + // Because there extensions have unique slice names, we can safely add them to the global store. + useStore.getState().addSlice(sliceName, sliceInitializer) + }) + const withApplicationExtensionsProvider: GenericHocType = (WrappedComponent) => { const WithApplicationExtensionsProvider = (props: any) => ( diff --git a/packages/pwa-kit-extension-sdk/src/react/hooks/index.ts b/packages/pwa-kit-extension-sdk/src/react/hooks/index.ts index d8d6a0eda4..347cb47412 100644 --- a/packages/pwa-kit-extension-sdk/src/react/hooks/index.ts +++ b/packages/pwa-kit-extension-sdk/src/react/hooks/index.ts @@ -6,3 +6,4 @@ */ export * from './useApplicationExtensions' +export * from './useApplicationExtensionsStore' diff --git a/packages/pwa-kit-extension-sdk/src/react/hooks/useApplicationExtensionsStore.tsx b/packages/pwa-kit-extension-sdk/src/react/hooks/useApplicationExtensionsStore.tsx new file mode 100644 index 0000000000..d126d26ba6 --- /dev/null +++ b/packages/pwa-kit-extension-sdk/src/react/hooks/useApplicationExtensionsStore.tsx @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2025, salesforce.com, inc. + * All rights reserved. + * SPDX-License-Identifier: BSD-3-Clause + * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ +import {create} from 'zustand'; +import {StateCreator} from 'zustand'; +import {devtools} from 'zustand/middleware'; + +export type SliceInitializer = (set: (partial: any) => void, get: () => BaseStore) => T; + +interface BaseStore { + state: Record; + addSlice: (sliceName: string, sliceInitializer: SliceInitializer) => void; + getSlice: (sliceName: string) => T | undefined; +} + +export const useStore = create()( + devtools((set, get) => ({ + state: {}, + + // Dynamically add a slice + addSlice: (sliceName: string, sliceInitializer: SliceInitializer) => { + set((state) => ({ + state: { + ...state.state, + [sliceName]: sliceInitializer(set, get), + }, + })); + }, + + // Retrieve a slice + getSlice: (sliceName: string): T | undefined => { + return get().state[sliceName]; + }, + })) +); diff --git a/packages/template-typescript-minimal/app/pages/home.tsx b/packages/template-typescript-minimal/app/pages/home.tsx index c14110a188..fadeeaaff4 100644 --- a/packages/template-typescript-minimal/app/pages/home.tsx +++ b/packages/template-typescript-minimal/app/pages/home.tsx @@ -12,6 +12,10 @@ import {useApplicationExtensions} from '@salesforce/pwa-kit-extension-sdk/react' import HelloTS from '../components/hello-typescript' import HelloJS from '../components/hello-javascript' +import {useStore as useExtensionsStore} from '@salesforce/pwa-kit-extension-sdk/react' +import {Link} from 'react-router-dom' +export const useExtensionStore = () => useExtensionsStore((state: Record) => state.state['@salesforce/extension-chakra-store-locator'] || {}) + interface Props { value: number } @@ -85,6 +89,7 @@ h1 { const Home = ({value}: Props) => { const [counter, setCounter] = useState(0) const applicationExtensions = useApplicationExtensions() + const {setCounter: setMyCounter} = useExtensionStore() useEffect(() => { const interval = setInterval(() => { @@ -114,6 +119,8 @@ const Home = ({value}: Props) => {
Support! + + Store Locator
From 0f0b794b2ef5e36c4899675d53538596f4aa5926 Mon Sep 17 00:00:00 2001 From: Ben Chypak Date: Mon, 20 Jan 2025 13:39:07 -0800 Subject: [PATCH 2/8] Abstracted the id of the extension into the HOC --- .../src/pages/store-locator/index.tsx | 4 +- .../src/setup-app.ts | 42 +++++++++-------- .../src/react/classes/ApplicationExtension.ts | 11 +---- .../components/withApplicationExtensions.tsx | 6 ++- .../hooks/useApplicationExtensionsStore.tsx | 46 +++++++++---------- .../app/pages/home.tsx | 10 +++- 6 files changed, 62 insertions(+), 57 deletions(-) diff --git a/packages/extension-chakra-store-locator/src/pages/store-locator/index.tsx b/packages/extension-chakra-store-locator/src/pages/store-locator/index.tsx index 1b850f7b9b..a51bb57571 100644 --- a/packages/extension-chakra-store-locator/src/pages/store-locator/index.tsx +++ b/packages/extension-chakra-store-locator/src/pages/store-locator/index.tsx @@ -5,13 +5,11 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ -import React, {useEffect} from 'react' +import React from 'react' import {Box, Container} from '@chakra-ui/react' import {StoreLocatorContent} from '../../components/content' import {useExtensionStore} from '../../hooks/use-extension-store' -// import {useStore as useExtensionsStore} from '@salesforce/pwa-kit-extension-sdk/react' - const StoreLocatorPage = () => { const {setCounter, counter} = useExtensionStore() diff --git a/packages/extension-chakra-store-locator/src/setup-app.ts b/packages/extension-chakra-store-locator/src/setup-app.ts index a7642b0d93..47c87c7ef7 100644 --- a/packages/extension-chakra-store-locator/src/setup-app.ts +++ b/packages/extension-chakra-store-locator/src/setup-app.ts @@ -58,24 +58,30 @@ class StoreLocatorExtension extends ApplicationExtension { ] } - getStoreSlice(): {sliceName: string; sliceInitializer: any} { - return { - sliceName: StoreLocatorExtension.id, - sliceInitializer: (set: any) => ({ - counter: 0, - setCounter: () => - set((state: any) => ({ - state: { - ...state.state, - ['@salesforce/extension-chakra-store-locator']: { - ...state.state['@salesforce/extension-chakra-store-locator'], - counter: state.state['@salesforce/extension-chakra-store-locator'].counter + 1 - } - - }, - })) - }) - } + getSliceInitializer(): any { + return (set: any) => ({ + counter: 0, + setCounter: () => + set((state: any) => { + console.log('state: ', state) + console.log('state: ', {}) + return { + state: { + ...state.state, + ['@salesforce/extension-chakra-store-locator']: { + ...state.state['@salesforce/extension-chakra-store-locator'], + counter: state.state['@salesforce/extension-chakra-store-locator'].counter + 1 + } + + }, + } + }) + // setCounter: (state: any) => { + // return { + // counter: state.counter + 1 + // } + // } + }) } } diff --git a/packages/pwa-kit-extension-sdk/src/react/classes/ApplicationExtension.ts b/packages/pwa-kit-extension-sdk/src/react/classes/ApplicationExtension.ts index 8cf5721ab3..53360b1ce7 100644 --- a/packages/pwa-kit-extension-sdk/src/react/classes/ApplicationExtension.ts +++ b/packages/pwa-kit-extension-sdk/src/react/classes/ApplicationExtension.ts @@ -66,14 +66,7 @@ export class ApplicationExtension< return routes } - /** - * Default implementation of getStoreSlice. This method should be overridden by the extension. - * @returns The slice name and initializer for the extension's store slice. - */ - public getStoreSlice(): {sliceName: string; sliceInitializer: any} { - return { - sliceName: this.constructor.name, - sliceInitializer: () => ({}) - } + public getSliceInitializer(): any { + return () => ({}) } } diff --git a/packages/pwa-kit-extension-sdk/src/react/components/withApplicationExtensions.tsx b/packages/pwa-kit-extension-sdk/src/react/components/withApplicationExtensions.tsx index 3b4b46e871..9640de997e 100644 --- a/packages/pwa-kit-extension-sdk/src/react/components/withApplicationExtensions.tsx +++ b/packages/pwa-kit-extension-sdk/src/react/components/withApplicationExtensions.tsx @@ -58,10 +58,12 @@ const withApplicationExtensions = ( // Inject store slices into the global store. applicationExtensions.forEach((extension) => { - const {sliceName, sliceInitializer} = extension.getStoreSlice() + const sliceInitializer = extension.getSliceInitializer() // Because there extensions have unique slice names, we can safely add them to the global store. - useStore.getState().addSlice(sliceName, sliceInitializer) + useStore + .getState() + .addSlice((extension.constructor as typeof ApplicationExtension).id, sliceInitializer) }) const withApplicationExtensionsProvider: GenericHocType = (WrappedComponent) => { diff --git a/packages/pwa-kit-extension-sdk/src/react/hooks/useApplicationExtensionsStore.tsx b/packages/pwa-kit-extension-sdk/src/react/hooks/useApplicationExtensionsStore.tsx index d126d26ba6..e6c334e00b 100644 --- a/packages/pwa-kit-extension-sdk/src/react/hooks/useApplicationExtensionsStore.tsx +++ b/packages/pwa-kit-extension-sdk/src/react/hooks/useApplicationExtensionsStore.tsx @@ -4,35 +4,35 @@ * SPDX-License-Identifier: BSD-3-Clause * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ -import {create} from 'zustand'; -import {StateCreator} from 'zustand'; -import {devtools} from 'zustand/middleware'; +import {create} from 'zustand' +import {StateCreator} from 'zustand' +import {devtools} from 'zustand/middleware' -export type SliceInitializer = (set: (partial: any) => void, get: () => BaseStore) => T; +export type SliceInitializer = (set: (partial: any) => void, get: () => BaseStore) => T interface BaseStore { - state: Record; - addSlice: (sliceName: string, sliceInitializer: SliceInitializer) => void; - getSlice: (sliceName: string) => T | undefined; + state: Record + addSlice: (sliceName: string, sliceInitializer: SliceInitializer) => void + getSlice: (sliceName: string) => T | undefined } export const useStore = create()( - devtools((set, get) => ({ - state: {}, + devtools((set, get) => ({ + state: {}, - // Dynamically add a slice - addSlice: (sliceName: string, sliceInitializer: SliceInitializer) => { - set((state) => ({ - state: { - ...state.state, - [sliceName]: sliceInitializer(set, get), + // Dynamically add a slice + addSlice: (sliceName: string, sliceInitializer: SliceInitializer) => { + set((state) => ({ + state: { + ...state.state, + [sliceName]: sliceInitializer(set, get) + } + })) }, - })); - }, - // Retrieve a slice - getSlice: (sliceName: string): T | undefined => { - return get().state[sliceName]; - }, - })) -); + // Retrieve a slice + getSlice: (sliceName: string): T | undefined => { + return get().state[sliceName] + } + })) +) diff --git a/packages/template-typescript-minimal/app/pages/home.tsx b/packages/template-typescript-minimal/app/pages/home.tsx index fadeeaaff4..e7aa61cb7d 100644 --- a/packages/template-typescript-minimal/app/pages/home.tsx +++ b/packages/template-typescript-minimal/app/pages/home.tsx @@ -14,7 +14,11 @@ import HelloJS from '../components/hello-javascript' import {useStore as useExtensionsStore} from '@salesforce/pwa-kit-extension-sdk/react' import {Link} from 'react-router-dom' -export const useExtensionStore = () => useExtensionsStore((state: Record) => state.state['@salesforce/extension-chakra-store-locator'] || {}) +export const useExtensionStore = () => + useExtensionsStore( + (state: Record) => + state.state['@salesforce/extension-chakra-store-locator'] || {} + ) interface Props { value: number @@ -119,7 +123,9 @@ const Home = ({value}: Props) => {
Support! - + Store Locator
From cfb906ec4644829a20fffb0fe8ee6aa09dc8d318 Mon Sep 17 00:00:00 2001 From: Ben Chypak Date: Mon, 20 Jan 2025 15:10:07 -0800 Subject: [PATCH 3/8] Clean up test code --- .../src/pages/store-locator/index.tsx | 13 +++--- .../src/setup-app.ts | 40 +++++++++---------- .../hooks/useApplicationExtensionsStore.tsx | 15 ++++++- .../app/pages/home.tsx | 20 +++++----- 4 files changed, 50 insertions(+), 38 deletions(-) diff --git a/packages/extension-chakra-store-locator/src/pages/store-locator/index.tsx b/packages/extension-chakra-store-locator/src/pages/store-locator/index.tsx index a51bb57571..c95c777273 100644 --- a/packages/extension-chakra-store-locator/src/pages/store-locator/index.tsx +++ b/packages/extension-chakra-store-locator/src/pages/store-locator/index.tsx @@ -12,14 +12,17 @@ import {StoreLocatorContent} from '../../components/content' import {useExtensionStore} from '../../hooks/use-extension-store' const StoreLocatorPage = () => { - const {setCounter, counter} = useExtensionStore() + const {incrementCounter, decrementCounter, counter} = useExtensionStore() return ( - Count: {counter}
- + Count: {counter}
+ + { (component: React.ComponentType) => withStoreLocator(component, config), (component: React.ComponentType) => withOptionalCommerceSdkReactProvider(component, config), - (component: React.ComponentType) => withOptionalChakra(component) + (component: React.ComponentType) => withOptionalChakra(component), + + // Do we really want to do this, of should it automatically be done behind the scenes? + // The positives of doing this here is that we have some flexibility on having or not having state management for a given extension. + // Also we as lumping all the react cook into one place. + // The negatives is that it's a little more verbose. + // (component: React.ComponentType) => withExtensionStore(component, {initialState: {}}) ] return applyHOCs(App, HOCs) @@ -59,28 +65,18 @@ class StoreLocatorExtension extends ApplicationExtension { } getSliceInitializer(): any { - return (set: any) => ({ + // set: will set the state of the store for this extension slice. + // get: will get the state of the store for this extension slice. + return (set: any, get: any) => ({ counter: 0, - setCounter: () => - set((state: any) => { - console.log('state: ', state) - console.log('state: ', {}) - return { - state: { - ...state.state, - ['@salesforce/extension-chakra-store-locator']: { - ...state.state['@salesforce/extension-chakra-store-locator'], - counter: state.state['@salesforce/extension-chakra-store-locator'].counter + 1 - } - - }, - } - }) - // setCounter: (state: any) => { - // return { - // counter: state.counter + 1 - // } - // } + incrementCounter: () => + set((state: any) => ({ + counter: state.counter + 1 + })), + decrementCounter: () => + set((state: any) => ({ + counter: state.counter - 1 + })) }) } } diff --git a/packages/pwa-kit-extension-sdk/src/react/hooks/useApplicationExtensionsStore.tsx b/packages/pwa-kit-extension-sdk/src/react/hooks/useApplicationExtensionsStore.tsx index e6c334e00b..007075e7cd 100644 --- a/packages/pwa-kit-extension-sdk/src/react/hooks/useApplicationExtensionsStore.tsx +++ b/packages/pwa-kit-extension-sdk/src/react/hooks/useApplicationExtensionsStore.tsx @@ -5,7 +5,6 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ import {create} from 'zustand' -import {StateCreator} from 'zustand' import {devtools} from 'zustand/middleware' export type SliceInitializer = (set: (partial: any) => void, get: () => BaseStore) => T @@ -25,7 +24,19 @@ export const useStore = create()( set((state) => ({ state: { ...state.state, - [sliceName]: sliceInitializer(set, get) + // [sliceName]: sliceInitializer(set, get) + // Here we have a modified version of the "set" that sets only the slice. + [sliceName]: sliceInitializer((action: any) => { + set((state: any) => ({ + state: { + ...state.state, + [sliceName]: { + ...state.state[sliceName], + ...action(state.state[sliceName]) + } + } + })) + }, get) } })) }, diff --git a/packages/template-typescript-minimal/app/pages/home.tsx b/packages/template-typescript-minimal/app/pages/home.tsx index e7aa61cb7d..f6ed0126ca 100644 --- a/packages/template-typescript-minimal/app/pages/home.tsx +++ b/packages/template-typescript-minimal/app/pages/home.tsx @@ -14,11 +14,6 @@ import HelloJS from '../components/hello-javascript' import {useStore as useExtensionsStore} from '@salesforce/pwa-kit-extension-sdk/react' import {Link} from 'react-router-dom' -export const useExtensionStore = () => - useExtensionsStore( - (state: Record) => - state.state['@salesforce/extension-chakra-store-locator'] || {} - ) interface Props { value: number @@ -93,7 +88,10 @@ h1 { const Home = ({value}: Props) => { const [counter, setCounter] = useState(0) const applicationExtensions = useApplicationExtensions() - const {setCounter: setMyCounter} = useExtensionStore() + const {counter: myCounter, incrementCounter, decrementCounter} = useExtensionsStore( + (state: Record) => + state.state['@salesforce/extension-chakra-store-locator'] || {} + ) useEffect(() => { const interval = setInterval(() => { @@ -123,10 +121,14 @@ const Home = ({value}: Props) => {
Support! - + - Store Locator + /store-locator
From 5533e417b8c9bd853356f4d7f0c0b5962389138b Mon Sep 17 00:00:00 2001 From: Ben Chypak Date: Tue, 21 Jan 2025 11:18:19 -0800 Subject: [PATCH 4/8] Use store hoc added notes --- .../src/hooks/use-extension-store.ts | 2 - .../src/setup-app.ts | 56 ++++++++++++++++++- .../src/react/components/index.ts | 1 + .../components/withApplicationExtensions.tsx | 16 +++--- .../src/react/components/withStore.tsx | 33 +++++++++++ 5 files changed, 96 insertions(+), 12 deletions(-) create mode 100644 packages/pwa-kit-extension-sdk/src/react/components/withStore.tsx diff --git a/packages/extension-chakra-store-locator/src/hooks/use-extension-store.ts b/packages/extension-chakra-store-locator/src/hooks/use-extension-store.ts index de8b25d4d5..49c273854d 100644 --- a/packages/extension-chakra-store-locator/src/hooks/use-extension-store.ts +++ b/packages/extension-chakra-store-locator/src/hooks/use-extension-store.ts @@ -11,6 +11,4 @@ import extensionMeta from '../../extension-meta.json' /** * This hook returns the store for the current application extension. */ -// export const useExtensionStore = () => useExtensionsStore((state: Record) => state[extensionMeta.id]) - export const useExtensionStore = () => useExtensionsStore((state: Record) => state.state[extensionMeta.id] || {}) \ No newline at end of file diff --git a/packages/extension-chakra-store-locator/src/setup-app.ts b/packages/extension-chakra-store-locator/src/setup-app.ts index 1c39165424..db445c8562 100644 --- a/packages/extension-chakra-store-locator/src/setup-app.ts +++ b/packages/extension-chakra-store-locator/src/setup-app.ts @@ -10,7 +10,7 @@ import React from 'react' import {RouteProps} from 'react-router-dom' // Platform Imports -import {ApplicationExtension} from '@salesforce/pwa-kit-extension-sdk/react' +import {ApplicationExtension, withStore} from '@salesforce/pwa-kit-extension-sdk/react' import {applyHOCs} from '@salesforce/pwa-kit-extension-sdk/react/utils' // Local Imports @@ -47,7 +47,50 @@ class StoreLocatorExtension extends ApplicationExtension { // The positives of doing this here is that we have some flexibility on having or not having state management for a given extension. // Also we as lumping all the react cook into one place. // The negatives is that it's a little more verbose. - // (component: React.ComponentType) => withExtensionStore(component, {initialState: {}}) + // Right now we aren't injecting the store into the props of the components which is what you would normally do with an HOC. So we + // might want to do that. Instead we are just using the HOC to inject the store slice into the global store. + (component: React.ComponentType) => withStore( + component, + { + sliceInitializer: (set: any, get: any) => ({ + counter: 0, + incrementCounter: () => + set((state: any) => ({ + counter: state.counter + 1 + })), + decrementCounter: () => + set((state: any) => ({ + counter: state.counter - 1 + })) + }) + } + ) + // NOTE: We can also simplify the signature of this HOC by making the options more readable. But the caveat is that we don't have + // direct access to the "global store" and only the current slice. + // To get around this, we can pass in set/setAll/get/getAll functions to the action function. But that might look ugly and it doesn't + // align with the return value being the new state. + // Alternatively we can rely on telling our developers to use the store of a given extension by accessing the global store via the use store + // hook exported via the SDK. This is a little more verbose but it's more explicit. + // , + // (component: React.ComponentType) => withStore( + // component, + // { + // initialState: { + // counter: 0, + // incrementCounter: (state: any) => ({ + // counter: state.counter + 1 + // }), + // decrementCounter: (state: any) => ({ + // counter: state.counter - 1 + // }) + + // } + // } + // ) + + // Lets also talk about doing something like this: + // const extension = useApplicationExtension(extensionMeta.id) + // extension.openModal() ] return applyHOCs(App, HOCs) @@ -78,6 +121,15 @@ class StoreLocatorExtension extends ApplicationExtension { counter: state.counter - 1 })) }) + // return () => ({ + // counter: 0, + // incrementCounter: (set: any, get: any) => ({ + // counter: get().counter + 1 + // }), + // decrementCounter: (state: any) => ({ + // counter: get().counter - 1 + // }) + // }) } } diff --git a/packages/pwa-kit-extension-sdk/src/react/components/index.ts b/packages/pwa-kit-extension-sdk/src/react/components/index.ts index 67c056d1d0..e682cd2b53 100644 --- a/packages/pwa-kit-extension-sdk/src/react/components/index.ts +++ b/packages/pwa-kit-extension-sdk/src/react/components/index.ts @@ -6,3 +6,4 @@ */ export {default as withApplicationExtensions} from './withApplicationExtensions' +export {default as withStore} from './withStore' diff --git a/packages/pwa-kit-extension-sdk/src/react/components/withApplicationExtensions.tsx b/packages/pwa-kit-extension-sdk/src/react/components/withApplicationExtensions.tsx index 9640de997e..152444bb05 100644 --- a/packages/pwa-kit-extension-sdk/src/react/components/withApplicationExtensions.tsx +++ b/packages/pwa-kit-extension-sdk/src/react/components/withApplicationExtensions.tsx @@ -56,15 +56,15 @@ const withApplicationExtensions = ( .map((extension) => extension.extendApp.bind(extension) as GenericHocType) .filter(Boolean) - // Inject store slices into the global store. - applicationExtensions.forEach((extension) => { - const sliceInitializer = extension.getSliceInitializer() + // // Inject store slices into the global store. + // applicationExtensions.forEach((extension) => { + // const sliceInitializer = extension.getSliceInitializer() - // Because there extensions have unique slice names, we can safely add them to the global store. - useStore - .getState() - .addSlice((extension.constructor as typeof ApplicationExtension).id, sliceInitializer) - }) + // // Because there extensions have unique slice names, we can safely add them to the global store. + // useStore + // .getState() + // .addSlice((extension.constructor as typeof ApplicationExtension).id, sliceInitializer) + // }) const withApplicationExtensionsProvider: GenericHocType = (WrappedComponent) => { const WithApplicationExtensionsProvider = (props: any) => ( diff --git a/packages/pwa-kit-extension-sdk/src/react/components/withStore.tsx b/packages/pwa-kit-extension-sdk/src/react/components/withStore.tsx new file mode 100644 index 0000000000..86a1600981 --- /dev/null +++ b/packages/pwa-kit-extension-sdk/src/react/components/withStore.tsx @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2024, salesforce.com, inc. + * All rights reserved. + * SPDX-License-Identifier: BSD-3-Clause + * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +// Third-Party +import React from 'react' +// Local +import {useStore} from '../hooks/useApplicationExtensionsStore' + +// Types +import {ApplicationExtension} from '../classes/ApplicationExtension' + +// Local Types +type withStoreOptions = { + id?: string, + sliceInitializer?: any +} + +const withStore = (WrappedComponent: React.ComponentType, options: withStoreOptions) => { + const {id = '@salesforce/extension-chakra-store-locator', sliceInitializer} = options + + // Because there extensions have unique slice names, we can safely add them to the global store. + useStore + .getState() + .addSlice(id, sliceInitializer) + + return WrappedComponent +} + +export default withStore From 1e0ca31e690e8dc457e590f6d303f0e0e909867a Mon Sep 17 00:00:00 2001 From: Ben Chypak Date: Tue, 21 Jan 2025 13:32:29 -0800 Subject: [PATCH 5/8] Actions via extension methods. --- .../src/pages/store-locator/index.tsx | 8 +++++ .../src/setup-app.ts | 35 ++++++------------- .../components/withApplicationExtensions.tsx | 10 ------ .../src/react/components/withStore.tsx | 4 +-- 4 files changed, 20 insertions(+), 37 deletions(-) diff --git a/packages/extension-chakra-store-locator/src/pages/store-locator/index.tsx b/packages/extension-chakra-store-locator/src/pages/store-locator/index.tsx index c95c777273..85414fb2d6 100644 --- a/packages/extension-chakra-store-locator/src/pages/store-locator/index.tsx +++ b/packages/extension-chakra-store-locator/src/pages/store-locator/index.tsx @@ -10,13 +10,21 @@ import {Box, Container} from '@chakra-ui/react' import {StoreLocatorContent} from '../../components/content' import {useExtensionStore} from '../../hooks/use-extension-store' +import {useApplicationExtension} from '@salesforce/pwa-kit-extension-sdk/react' + +import StoreLocatorExtension from '../../setup-app' const StoreLocatorPage = () => { const {incrementCounter, decrementCounter, counter} = useExtensionStore() + const extension = useApplicationExtension('@salesforce/extension-chakra-store-locator') + return ( Count: {counter}
+ diff --git a/packages/extension-chakra-store-locator/src/setup-app.ts b/packages/extension-chakra-store-locator/src/setup-app.ts index db445c8562..0eddb7dd1b 100644 --- a/packages/extension-chakra-store-locator/src/setup-app.ts +++ b/packages/extension-chakra-store-locator/src/setup-app.ts @@ -7,10 +7,11 @@ // Third-Party import React from 'react' +import {unstable_batchedUpdates} from 'react-dom' import {RouteProps} from 'react-router-dom' // Platform Imports -import {ApplicationExtension, withStore} from '@salesforce/pwa-kit-extension-sdk/react' +import {ApplicationExtension, withStore, useStore} from '@salesforce/pwa-kit-extension-sdk/react' import {applyHOCs} from '@salesforce/pwa-kit-extension-sdk/react/utils' // Local Imports @@ -23,6 +24,7 @@ import StoreLocatorPage from './pages/store-locator' import {logger} from './logger' import extensionMeta from '../extension-meta.json' + class StoreLocatorExtension extends ApplicationExtension { static readonly id = extensionMeta.id @@ -107,29 +109,14 @@ class StoreLocatorExtension extends ApplicationExtension { ] } - getSliceInitializer(): any { - // set: will set the state of the store for this extension slice. - // get: will get the state of the store for this extension slice. - return (set: any, get: any) => ({ - counter: 0, - incrementCounter: () => - set((state: any) => ({ - counter: state.counter + 1 - })), - decrementCounter: () => - set((state: any) => ({ - counter: state.counter - 1 - })) - }) - // return () => ({ - // counter: 0, - // incrementCounter: (set: any, get: any) => ({ - // counter: get().counter + 1 - // }), - // decrementCounter: (state: any) => ({ - // counter: get().counter - 1 - // }) - // }) + incrementCounter() { + const nonReactCallback = () => { + unstable_batchedUpdates(() => { + useStore.getState().getSlice(extensionMeta.id).incrementCounter() + }) + } + + nonReactCallback() } } diff --git a/packages/pwa-kit-extension-sdk/src/react/components/withApplicationExtensions.tsx b/packages/pwa-kit-extension-sdk/src/react/components/withApplicationExtensions.tsx index 152444bb05..8d081ac79a 100644 --- a/packages/pwa-kit-extension-sdk/src/react/components/withApplicationExtensions.tsx +++ b/packages/pwa-kit-extension-sdk/src/react/components/withApplicationExtensions.tsx @@ -56,16 +56,6 @@ const withApplicationExtensions = ( .map((extension) => extension.extendApp.bind(extension) as GenericHocType) .filter(Boolean) - // // Inject store slices into the global store. - // applicationExtensions.forEach((extension) => { - // const sliceInitializer = extension.getSliceInitializer() - - // // Because there extensions have unique slice names, we can safely add them to the global store. - // useStore - // .getState() - // .addSlice((extension.constructor as typeof ApplicationExtension).id, sliceInitializer) - // }) - const withApplicationExtensionsProvider: GenericHocType = (WrappedComponent) => { const WithApplicationExtensionsProvider = (props: any) => ( diff --git a/packages/pwa-kit-extension-sdk/src/react/components/withStore.tsx b/packages/pwa-kit-extension-sdk/src/react/components/withStore.tsx index 86a1600981..a4e24006d8 100644 --- a/packages/pwa-kit-extension-sdk/src/react/components/withStore.tsx +++ b/packages/pwa-kit-extension-sdk/src/react/components/withStore.tsx @@ -7,12 +7,10 @@ // Third-Party import React from 'react' + // Local import {useStore} from '../hooks/useApplicationExtensionsStore' -// Types -import {ApplicationExtension} from '../classes/ApplicationExtension' - // Local Types type withStoreOptions = { id?: string, From ec2dbfb1311626ca53391c3b91d78a94ec65920a Mon Sep 17 00:00:00 2001 From: Ben Chypak Date: Tue, 21 Jan 2025 13:56:18 -0800 Subject: [PATCH 6/8] Clean up and file renaming --- .../src/hooks/use-extension-store.ts | 4 +- .../src/pages/store-locator/index.tsx | 10 +--- .../src/setup-app.ts | 49 ++----------------- .../src/react/components/index.ts | 4 +- ....tsx => withApplicationExtensionStore.tsx} | 4 +- .../components/withApplicationExtensions.tsx | 1 - .../hooks/useApplicationExtensionsStore.tsx | 2 +- .../app/pages/home.tsx | 8 ++- 8 files changed, 15 insertions(+), 67 deletions(-) rename packages/pwa-kit-extension-sdk/src/react/components/{withStore.tsx => withApplicationExtensionStore.tsx} (86%) diff --git a/packages/extension-chakra-store-locator/src/hooks/use-extension-store.ts b/packages/extension-chakra-store-locator/src/hooks/use-extension-store.ts index 49c273854d..aba15fc639 100644 --- a/packages/extension-chakra-store-locator/src/hooks/use-extension-store.ts +++ b/packages/extension-chakra-store-locator/src/hooks/use-extension-store.ts @@ -5,10 +5,10 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ -import {useStore as useExtensionsStore} from '@salesforce/pwa-kit-extension-sdk/react' +import {useApplicationExtensionsStore} from '@salesforce/pwa-kit-extension-sdk/react' import extensionMeta from '../../extension-meta.json' /** * This hook returns the store for the current application extension. */ -export const useExtensionStore = () => useExtensionsStore((state: Record) => state.state[extensionMeta.id] || {}) \ No newline at end of file +export const useExtensionStore = () => useApplicationExtensionsStore((state: Record) => state.state[extensionMeta.id] || {}) \ No newline at end of file diff --git a/packages/extension-chakra-store-locator/src/pages/store-locator/index.tsx b/packages/extension-chakra-store-locator/src/pages/store-locator/index.tsx index 85414fb2d6..090e2d3ce4 100644 --- a/packages/extension-chakra-store-locator/src/pages/store-locator/index.tsx +++ b/packages/extension-chakra-store-locator/src/pages/store-locator/index.tsx @@ -7,24 +7,16 @@ import React from 'react' import {Box, Container} from '@chakra-ui/react' -import {StoreLocatorContent} from '../../components/content' +import {StoreLocatorContent} from '../../components/content' import {useExtensionStore} from '../../hooks/use-extension-store' -import {useApplicationExtension} from '@salesforce/pwa-kit-extension-sdk/react' - -import StoreLocatorExtension from '../../setup-app' const StoreLocatorPage = () => { const {incrementCounter, decrementCounter, counter} = useExtensionStore() - const extension = useApplicationExtension('@salesforce/extension-chakra-store-locator') - return ( Count: {counter}
- diff --git a/packages/extension-chakra-store-locator/src/setup-app.ts b/packages/extension-chakra-store-locator/src/setup-app.ts index 0eddb7dd1b..81e08e6149 100644 --- a/packages/extension-chakra-store-locator/src/setup-app.ts +++ b/packages/extension-chakra-store-locator/src/setup-app.ts @@ -11,7 +11,7 @@ import {unstable_batchedUpdates} from 'react-dom' import {RouteProps} from 'react-router-dom' // Platform Imports -import {ApplicationExtension, withStore, useStore} from '@salesforce/pwa-kit-extension-sdk/react' +import {ApplicationExtension, withApplicationExtensionStore} from '@salesforce/pwa-kit-extension-sdk/react' import {applyHOCs} from '@salesforce/pwa-kit-extension-sdk/react/utils' // Local Imports @@ -44,16 +44,11 @@ class StoreLocatorExtension extends ApplicationExtension { (component: React.ComponentType) => withOptionalCommerceSdkReactProvider(component, config), (component: React.ComponentType) => withOptionalChakra(component), - - // Do we really want to do this, of should it automatically be done behind the scenes? - // The positives of doing this here is that we have some flexibility on having or not having state management for a given extension. - // Also we as lumping all the react cook into one place. - // The negatives is that it's a little more verbose. - // Right now we aren't injecting the store into the props of the components which is what you would normally do with an HOC. So we - // might want to do that. Instead we are just using the HOC to inject the store slice into the global store. - (component: React.ComponentType) => withStore( + // TODO: Remove after cleaning up the API + (component: React.ComponentType) => withApplicationExtensionStore( component, { + id: '@salesforce/extension-chakra-store-locator', sliceInitializer: (set: any, get: any) => ({ counter: 0, incrementCounter: () => @@ -67,32 +62,6 @@ class StoreLocatorExtension extends ApplicationExtension { }) } ) - // NOTE: We can also simplify the signature of this HOC by making the options more readable. But the caveat is that we don't have - // direct access to the "global store" and only the current slice. - // To get around this, we can pass in set/setAll/get/getAll functions to the action function. But that might look ugly and it doesn't - // align with the return value being the new state. - // Alternatively we can rely on telling our developers to use the store of a given extension by accessing the global store via the use store - // hook exported via the SDK. This is a little more verbose but it's more explicit. - // , - // (component: React.ComponentType) => withStore( - // component, - // { - // initialState: { - // counter: 0, - // incrementCounter: (state: any) => ({ - // counter: state.counter + 1 - // }), - // decrementCounter: (state: any) => ({ - // counter: state.counter - 1 - // }) - - // } - // } - // ) - - // Lets also talk about doing something like this: - // const extension = useApplicationExtension(extensionMeta.id) - // extension.openModal() ] return applyHOCs(App, HOCs) @@ -108,16 +77,6 @@ class StoreLocatorExtension extends ApplicationExtension { ...routes ] } - - incrementCounter() { - const nonReactCallback = () => { - unstable_batchedUpdates(() => { - useStore.getState().getSlice(extensionMeta.id).incrementCounter() - }) - } - - nonReactCallback() - } } export default StoreLocatorExtension diff --git a/packages/pwa-kit-extension-sdk/src/react/components/index.ts b/packages/pwa-kit-extension-sdk/src/react/components/index.ts index e682cd2b53..93a82bba23 100644 --- a/packages/pwa-kit-extension-sdk/src/react/components/index.ts +++ b/packages/pwa-kit-extension-sdk/src/react/components/index.ts @@ -1,9 +1,9 @@ /* - * Copyright (c) 2024, salesforce.com, inc. + * Copyright (c) 2025, salesforce.com, inc. * All rights reserved. * SPDX-License-Identifier: BSD-3-Clause * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ export {default as withApplicationExtensions} from './withApplicationExtensions' -export {default as withStore} from './withStore' +export {default as withApplicationExtensionStore} from './withApplicationExtensionStore' diff --git a/packages/pwa-kit-extension-sdk/src/react/components/withStore.tsx b/packages/pwa-kit-extension-sdk/src/react/components/withApplicationExtensionStore.tsx similarity index 86% rename from packages/pwa-kit-extension-sdk/src/react/components/withStore.tsx rename to packages/pwa-kit-extension-sdk/src/react/components/withApplicationExtensionStore.tsx index a4e24006d8..464393185e 100644 --- a/packages/pwa-kit-extension-sdk/src/react/components/withStore.tsx +++ b/packages/pwa-kit-extension-sdk/src/react/components/withApplicationExtensionStore.tsx @@ -9,7 +9,7 @@ import React from 'react' // Local -import {useStore} from '../hooks/useApplicationExtensionsStore' +import {useApplicationExtensionsStore} from '../hooks/useApplicationExtensionsStore' // Local Types type withStoreOptions = { @@ -21,7 +21,7 @@ const withStore = (WrappedComponent: React.ComponentType, options: withSt const {id = '@salesforce/extension-chakra-store-locator', sliceInitializer} = options // Because there extensions have unique slice names, we can safely add them to the global store. - useStore + useApplicationExtensionsStore .getState() .addSlice(id, sliceInitializer) diff --git a/packages/pwa-kit-extension-sdk/src/react/components/withApplicationExtensions.tsx b/packages/pwa-kit-extension-sdk/src/react/components/withApplicationExtensions.tsx index 8d081ac79a..83da59a7fd 100644 --- a/packages/pwa-kit-extension-sdk/src/react/components/withApplicationExtensions.tsx +++ b/packages/pwa-kit-extension-sdk/src/react/components/withApplicationExtensions.tsx @@ -11,7 +11,6 @@ import hoistNonReactStatics from 'hoist-non-react-statics' // Local import {applyHOCs} from '../utils' -import {useStore} from '../hooks/useApplicationExtensionsStore' // Types import {ApplicationExtension} from '../classes/ApplicationExtension' diff --git a/packages/pwa-kit-extension-sdk/src/react/hooks/useApplicationExtensionsStore.tsx b/packages/pwa-kit-extension-sdk/src/react/hooks/useApplicationExtensionsStore.tsx index 007075e7cd..019e59fd12 100644 --- a/packages/pwa-kit-extension-sdk/src/react/hooks/useApplicationExtensionsStore.tsx +++ b/packages/pwa-kit-extension-sdk/src/react/hooks/useApplicationExtensionsStore.tsx @@ -15,7 +15,7 @@ interface BaseStore { getSlice: (sliceName: string) => T | undefined } -export const useStore = create()( +export const useApplicationExtensionsStore = create()( devtools((set, get) => ({ state: {}, diff --git a/packages/template-typescript-minimal/app/pages/home.tsx b/packages/template-typescript-minimal/app/pages/home.tsx index f6ed0126ca..697748e6a9 100644 --- a/packages/template-typescript-minimal/app/pages/home.tsx +++ b/packages/template-typescript-minimal/app/pages/home.tsx @@ -5,16 +5,14 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ import React, {useEffect, useState} from 'react' +import {Link} from 'react-router-dom' import {useQuery} from '@tanstack/react-query' -import {useApplicationExtensions} from '@salesforce/pwa-kit-extension-sdk/react' +import {useApplicationExtensions, useApplicationExtensionsStore} from '@salesforce/pwa-kit-extension-sdk/react' import HelloTS from '../components/hello-typescript' import HelloJS from '../components/hello-javascript' -import {useStore as useExtensionsStore} from '@salesforce/pwa-kit-extension-sdk/react' -import {Link} from 'react-router-dom' - interface Props { value: number } @@ -88,7 +86,7 @@ h1 { const Home = ({value}: Props) => { const [counter, setCounter] = useState(0) const applicationExtensions = useApplicationExtensions() - const {counter: myCounter, incrementCounter, decrementCounter} = useExtensionsStore( + const {counter: myCounter, incrementCounter, decrementCounter} = useApplicationExtensionsStore( (state: Record) => state.state['@salesforce/extension-chakra-store-locator'] || {} ) From 75566cf859bc55dc6cbc31dca449404c185b4102 Mon Sep 17 00:00:00 2001 From: Ben Chypak Date: Tue, 21 Jan 2025 14:36:05 -0800 Subject: [PATCH 7/8] Clean clean clean --- .../src/setup-app.ts | 38 ++++++++++--------- .../withApplicationExtensionStore.tsx | 28 ++++++++++---- .../hooks/useApplicationExtensionsStore.tsx | 27 +++++++------ 3 files changed, 55 insertions(+), 38 deletions(-) diff --git a/packages/extension-chakra-store-locator/src/setup-app.ts b/packages/extension-chakra-store-locator/src/setup-app.ts index 81e08e6149..4fea615685 100644 --- a/packages/extension-chakra-store-locator/src/setup-app.ts +++ b/packages/extension-chakra-store-locator/src/setup-app.ts @@ -39,29 +39,31 @@ class StoreLocatorExtension extends ApplicationExtension { ) } + const withApplicationExtensionStoreOptions = { + id: extensionMeta.id, + sliceInitializer: (set: any, get: any) => ({ + // TODO: Kevin, this is where you are going to place your initial state and actions. E.g. "modalOpen: false" etc. + counter: 0, + incrementCounter: () => + { + console.log('incrementCounter', get()) + set((state: any) => ({ + counter: state.counter + 1 + })) + }, + decrementCounter: () => + set((state: any) => ({ + counter: state.counter - 1 + })) + }) + } + const HOCs = [ (component: React.ComponentType) => withStoreLocator(component, config), (component: React.ComponentType) => withOptionalCommerceSdkReactProvider(component, config), (component: React.ComponentType) => withOptionalChakra(component), - // TODO: Remove after cleaning up the API - (component: React.ComponentType) => withApplicationExtensionStore( - component, - { - id: '@salesforce/extension-chakra-store-locator', - sliceInitializer: (set: any, get: any) => ({ - counter: 0, - incrementCounter: () => - set((state: any) => ({ - counter: state.counter + 1 - })), - decrementCounter: () => - set((state: any) => ({ - counter: state.counter - 1 - })) - }) - } - ) + (component: React.ComponentType) => withApplicationExtensionStore(component, withApplicationExtensionStoreOptions) ] return applyHOCs(App, HOCs) diff --git a/packages/pwa-kit-extension-sdk/src/react/components/withApplicationExtensionStore.tsx b/packages/pwa-kit-extension-sdk/src/react/components/withApplicationExtensionStore.tsx index 464393185e..4f804c278e 100644 --- a/packages/pwa-kit-extension-sdk/src/react/components/withApplicationExtensionStore.tsx +++ b/packages/pwa-kit-extension-sdk/src/react/components/withApplicationExtensionStore.tsx @@ -11,21 +11,33 @@ import React from 'react' // Local import {useApplicationExtensionsStore} from '../hooks/useApplicationExtensionsStore' +// Local Types +import {SliceInitializer} from '../hooks/useApplicationExtensionsStore' + // Local Types type withStoreOptions = { - id?: string, - sliceInitializer?: any + id: string + sliceInitializer: SliceInitializer } -const withStore = (WrappedComponent: React.ComponentType, options: withStoreOptions) => { - const {id = '@salesforce/extension-chakra-store-locator', sliceInitializer} = options +/** + * This HOC is used to add a slice to the global store for an extension. + * + * @param WrappedComponent + * @param options + * @returns + */ +const withApplicationExtensionStore = ( + WrappedComponent: React.ComponentType, + options: withStoreOptions +) => { + const {id, sliceInitializer} = options // Because there extensions have unique slice names, we can safely add them to the global store. - useApplicationExtensionsStore - .getState() - .addSlice(id, sliceInitializer) + useApplicationExtensionsStore.getState().addSlice(id, sliceInitializer) + // Return the original component as we aren't modifying it in any way... yet. return WrappedComponent } -export default withStore +export default withApplicationExtensionStore diff --git a/packages/pwa-kit-extension-sdk/src/react/hooks/useApplicationExtensionsStore.tsx b/packages/pwa-kit-extension-sdk/src/react/hooks/useApplicationExtensionsStore.tsx index 019e59fd12..1481f36471 100644 --- a/packages/pwa-kit-extension-sdk/src/react/hooks/useApplicationExtensionsStore.tsx +++ b/packages/pwa-kit-extension-sdk/src/react/hooks/useApplicationExtensionsStore.tsx @@ -24,19 +24,22 @@ export const useApplicationExtensionsStore = create()( set((state) => ({ state: { ...state.state, - // [sliceName]: sliceInitializer(set, get) - // Here we have a modified version of the "set" that sets only the slice. - [sliceName]: sliceInitializer((action: any) => { - set((state: any) => ({ - state: { - ...state.state, - [sliceName]: { - ...state.state[sliceName], - ...action(state.state[sliceName]) + [sliceName]: sliceInitializer( + // Narrowed version of set. Which allows setting state of the current slice only. + (action: any) => { + set((state: any) => ({ + state: { + ...state.state, + [sliceName]: { + ...state.state[sliceName], + ...action(state.state[sliceName]) + } } - } - })) - }, get) + })) + }, + // Narrowed version of get. Which returns state of the current slice. + () => get().state[sliceName] + ) } })) }, From 76934f60752ff870bd7c88e71004e74c156eafa1 Mon Sep 17 00:00:00 2001 From: Ben Chypak Date: Tue, 21 Jan 2025 15:55:47 -0800 Subject: [PATCH 8/8] Clean up, docs, update extension starter --- .../src/hooks/use-extension-store.ts | 12 ++++++- .../src/setup-app.ts | 10 ++---- packages/extension-starter/README.md | 33 +++++++++++++++++++ .../src/hooks/use-extension-store.ts | 24 ++++++++++++++ packages/extension-starter/src/setup-app.ts | 29 ++++++++++++++-- .../hooks/useApplicationExtensionsStore.tsx | 2 +- .../app/pages/home.tsx | 19 ++++++----- 7 files changed, 110 insertions(+), 19 deletions(-) create mode 100644 packages/extension-starter/src/hooks/use-extension-store.ts diff --git a/packages/extension-chakra-store-locator/src/hooks/use-extension-store.ts b/packages/extension-chakra-store-locator/src/hooks/use-extension-store.ts index aba15fc639..44ddc18d83 100644 --- a/packages/extension-chakra-store-locator/src/hooks/use-extension-store.ts +++ b/packages/extension-chakra-store-locator/src/hooks/use-extension-store.ts @@ -11,4 +11,14 @@ import extensionMeta from '../../extension-meta.json' /** * This hook returns the store for the current application extension. */ -export const useExtensionStore = () => useApplicationExtensionsStore((state: Record) => state.state[extensionMeta.id] || {}) \ No newline at end of file +export const useExtensionStore = (defaultValue: any = {}) => { + const state = useApplicationExtensionsStore((state: Record) => state.state[extensionMeta.id] || defaultValue) + + if (state === undefined) { + throw new Error( + `'useExtensionStore' could not find your current application extension state! Make sure you have added the store to your extension.` + ) + } + + return state +} \ No newline at end of file diff --git a/packages/extension-chakra-store-locator/src/setup-app.ts b/packages/extension-chakra-store-locator/src/setup-app.ts index 4fea615685..92b025deac 100644 --- a/packages/extension-chakra-store-locator/src/setup-app.ts +++ b/packages/extension-chakra-store-locator/src/setup-app.ts @@ -7,7 +7,6 @@ // Third-Party import React from 'react' -import {unstable_batchedUpdates} from 'react-dom' import {RouteProps} from 'react-router-dom' // Platform Imports @@ -45,12 +44,9 @@ class StoreLocatorExtension extends ApplicationExtension { // TODO: Kevin, this is where you are going to place your initial state and actions. E.g. "modalOpen: false" etc. counter: 0, incrementCounter: () => - { - console.log('incrementCounter', get()) - set((state: any) => ({ - counter: state.counter + 1 - })) - }, + set((state: any) => ({ + counter: state.counter + 1 + })), decrementCounter: () => set((state: any) => ({ counter: state.counter - 1 diff --git a/packages/extension-starter/README.md b/packages/extension-starter/README.md index 1726cb5974..ddb1884d32 100644 --- a/packages/extension-starter/README.md +++ b/packages/extension-starter/README.md @@ -62,6 +62,39 @@ the extension as they like. > Congratulations! The Sample extension was successfully installed! Please visit https://www.npmjs.com/package/@salesforce/extension-starter for more information on how to use this extension. ``` +# State Management + +By default all extensions are enhanced with state management using the `withApplicationExtensionStore` higher-order component. Under the hood +the state is provided using [Zustand](https://www.npmjs.com/package/zustand) as a global store for the entire PWA-Kit application. +Each Application Extension inserts a "slice" into this global store following the +[slicing pattern](https://github.com/pmndrs/zustand/blob/37e1e3f193a5e5dec6fbd0f07514aec59a187e01/docs/guides/slices-pattern.md). +This allows you to have data separation from one extension to the other when it's important, but also allows you to access state its +associated actions of other extensions when needed. + +An examples of why you might want to access state and action from another extension would be opening a store-locator map modal provided via +the store-locator extension from other pages like the storefronts toolbar or the base project. + +This is how you would do something like this. + +``` +// /base-project/app/components/my-component.jsx +import {useExtensionsStore} from '@salesforce/pwa-kit-extension-sdk/react' + +export MyComponent = () => { + // Grab the slice of the extension state for "extension-a" + const {toggleMapsModal} = useExtensionsStore( + (state) => + state.state['@salesforce/extension-store-locator'] || {} + ) + + return ( +
+
+ ) +} +``` + # Advanced Usage In order to customize this Application Extension to your particular needs we suggest that you refer to the section titled diff --git a/packages/extension-starter/src/hooks/use-extension-store.ts b/packages/extension-starter/src/hooks/use-extension-store.ts new file mode 100644 index 0000000000..9b71c2272d --- /dev/null +++ b/packages/extension-starter/src/hooks/use-extension-store.ts @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2024, salesforce.com, inc. + * All rights reserved. + * SPDX-License-Identifier: BSD-3-Clause + * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +import {useApplicationExtensionsStore} from '@salesforce/pwa-kit-extension-sdk/react' +import extensionMeta from '../../extension-meta.json' + +/** + * This hook returns the store for the current application extension. + */ +export const useExtensionStore = () => { + const state = useApplicationExtensionsStore((state: Record) => state.state[extensionMeta.id]) + + if (state === undefined) { + throw new Error( + `'useExtensionStore' could not find your current application extension state! Make sure you have added the store to your extension.` + ) + } + + return state +} \ No newline at end of file diff --git a/packages/extension-starter/src/setup-app.ts b/packages/extension-starter/src/setup-app.ts index 31fadd111b..69815b839c 100644 --- a/packages/extension-starter/src/setup-app.ts +++ b/packages/extension-starter/src/setup-app.ts @@ -10,7 +10,8 @@ import React from 'react' import {RouteProps} from 'react-router-dom' // Platform Imports -import {ApplicationExtension} from '@salesforce/pwa-kit-extension-sdk/react' +import {ApplicationExtension, withApplicationExtensionStore} from '@salesforce/pwa-kit-extension-sdk/react' +import {applyHOCs} from '@salesforce/pwa-kit-extension-sdk/react/utils' // Local Imports import {Config} from './types' @@ -36,7 +37,31 @@ class Sample extends ApplicationExtension { extendApp>( App: React.ComponentType ): React.ComponentType { - return sampleHOC(App) + + const HOCs = [ + sampleHOC, + // NOTE: Add state management to the application extension. If your extension does not use state management, you can remove + // the use of `withApplicationExtensionStore`. + // Please refer to the "state management" section of the readme for more information on how to use this feature. + (component: React.ComponentType) => + withApplicationExtensionStore( + component, + { + id: extensionMeta.id, + sliceInitializer: (set: any, get: any) => ({ + sampleStateValue: 0, + sampleAction: () => + { + set((state: any) => ({ + counter: state.sampleStateValue + 1 + })) + } + }) + } + ) + ] + + return applyHOCs(App, HOCs) } /** diff --git a/packages/pwa-kit-extension-sdk/src/react/hooks/useApplicationExtensionsStore.tsx b/packages/pwa-kit-extension-sdk/src/react/hooks/useApplicationExtensionsStore.tsx index 1481f36471..6ab83a58de 100644 --- a/packages/pwa-kit-extension-sdk/src/react/hooks/useApplicationExtensionsStore.tsx +++ b/packages/pwa-kit-extension-sdk/src/react/hooks/useApplicationExtensionsStore.tsx @@ -36,7 +36,7 @@ export const useApplicationExtensionsStore = create()( } } })) - }, + }, // Narrowed version of get. Which returns state of the current slice. () => get().state[sliceName] ) diff --git a/packages/template-typescript-minimal/app/pages/home.tsx b/packages/template-typescript-minimal/app/pages/home.tsx index 697748e6a9..c9e3fd3d38 100644 --- a/packages/template-typescript-minimal/app/pages/home.tsx +++ b/packages/template-typescript-minimal/app/pages/home.tsx @@ -8,7 +8,10 @@ import React, {useEffect, useState} from 'react' import {Link} from 'react-router-dom' import {useQuery} from '@tanstack/react-query' -import {useApplicationExtensions, useApplicationExtensionsStore} from '@salesforce/pwa-kit-extension-sdk/react' +import { + useApplicationExtensions, + useApplicationExtensionsStore +} from '@salesforce/pwa-kit-extension-sdk/react' import HelloTS from '../components/hello-typescript' import HelloJS from '../components/hello-javascript' @@ -86,7 +89,11 @@ h1 { const Home = ({value}: Props) => { const [counter, setCounter] = useState(0) const applicationExtensions = useApplicationExtensions() - const {counter: myCounter, incrementCounter, decrementCounter} = useApplicationExtensionsStore( + const { + counter: myCounter, + incrementCounter, + decrementCounter + } = useApplicationExtensionsStore( (state: Record) => state.state['@salesforce/extension-chakra-store-locator'] || {} ) @@ -120,12 +127,8 @@ const Home = ({value}: Props) => { Support! Counter: {myCounter} - - + + /store-locator