Please feel free to leave comments / feedback in this issue! Thank you!
Summary
Here, we are proposing to introduce a new package in the pwa-kit monorepo - commerce-sdk-react
. This library contains a collection of easy-to-use React.js hooks that integrates Salesforce Commerce Cloud B2C Commerce API (known as SCAPI) in a PWA Kit project.
Background
Today, the retail react app template is the starting point for PWA Kit project. The SCAPI integration source code lives inside the template. When a PWA Kit project kicks off, the first thing developers do is to generate a new project, via npx pwa-kit-create-app
.
Problem
Once a project is generated, there is no way for a project to receive bug fixes / new updates.
Proposal
We want to extract re-usable code as much as possible from the template and move them into dedicated libraries and distribute and maintain the library via npm.
The new library commerce-sdk-react
aims to provide a SCAPI integration library in the form of React components and hooks. The library leverages the commerce-sdk-isomorphic package internally.
Features
- declarative promise resolution and data fetching
- SSR (yes, hooks that run on the server side!, see #664)
- Built-in cache and request deduplication
- Authentication & Token management
- TypeScript
- and more...
Public Interface
1. The <CommerceApiProvider />
Component
This is a react component that wraps your entire application. It is used to configure the library as well as providing access to the API clients and hooks throughout the entire react application. Typically in a PWA kit project, you will import the component in the AppConfig
special component.
The component takes the following props:
- proxy (the path for your SCAPI proxy)
- organizationId
- clientId
- siteId
- shortCode
- locale
- currency
Example: /app/components/_app-config/index.jsx
import React from 'react'
import {CommerceAPIProvider} from 'commerce-sdk-react'
const AppConfig = ({children}) => {
return (
<CommerceAPIProvider
proxy={`/mobify/proxy/api`}
organizationId="f_ecom_xxxx_xxx"
clientId="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
siteId="xxxx"
shortCode="xxxx"
locale="en-US"
currency="USD"
>
{children}
</CommerceAPIProvider>
)
}
export default AppConfig
2. The useCommerceApi
hook
This is a low level hook that gives developer access to the Commerce API clients. The return value is an object whose properties are initialized API clients, instantiated from individual commerce-sdk-isomorphic library classes.
Example: use the hook to get access to the api clients
import React, {useEffect} from 'react'
import {useCommerceApi} from 'commerce-sdk-react'
const MyComponent = () => {
const [data, setData] = useState({})
const api = useCommerceApi()
useEffect(() => {
const res = api.shopperProducts.getProduct()
if (res) {
setData(res)
}
}, [])
return ...
}
Note: you probably won't be using this low level hook directly most of the time. It is an escape hatch for when the other API specific hooks cannot meet your needs.
3. The Query Hooks
Query Hook is a concept, it describes a type of hooks that are used to fetch data. Typically these hooks make HTTP GET requests. e.g. fetch a single product.
The query hooks data will be cached locally and if you have multiple same query hooks that in used inside different react component, the requests will be de-duplicated.
Query hooks follow a similar signature that looks like:
Example:
import React, {useEffect} from 'react'
import {useProduct} from 'commerce-sdk-react/ShopperProducts'
const MyComponent = () => {
const {data, isLoading, error} = useProduct({parameters: {id: '37478392M'}})
if (isLoading) return "Loading..."
if (error) return `Something went wrong: ${error.message}`
return <div>{data.name}</div>
}
Query Hook examples:
- ShopperProduct API:
useProducts
, useProduct
, useCategories
, useCategory
- ShopperCustomer API:
useCustomer
, useCustomerAddress
, useCustomerBaskets
, useCustomerOrders
, useCustomerPaymentInstrument
, etc
- ShopperSearch API:
useProductSearch
, useSearchSuggestions
- etc...
4. The Action Hooks
Action Hook is a concept, it describes a type of hooks that are used to mutate data. Typically these hooks make HTTP POST/PUT/DELETE requests. e.g. add an item to the basket. These hooks are usually invoked by user interactivity such as clicking a button. Action hooks follow a similar signature that looks like:
In addition to the return values such as data
, isLoading
, it returns an execute
function, invoking this function will use the underlying isomorphic api client to send the request.
Unlike the query hooks, the action hook results are not cached globally. The results are local state. Calling the action hooks N times will send N network requests.
Example:
import React, {useEffect} from 'react'
import {useShopperBasketsAction} from 'commerce-sdk-react/ShopperBaskets'
const MyComponent = () => {
const createBasket = useShopperBasketsAction('createBasket')
if (createBasket.isLoading) return "Loading..."
if (createBasket.error) return `Something went wrong: ${error.message}`
return <button onClick={() => createBasket.execute({parameters: {items: [...]}})}>{data.name}</button>
}
Action Hook examples:
- ShopperBasket API:
useShopperBasketsAction
- etc...
list of actions:
export enum ShopperBasketActions {
addItemToBasket = 'addItemToBasket',
removeItemFromBasket = 'removeItemFromBasket',
updateItemInBasket = 'updateItemInBasket',
createBasket = 'createBasket',
mergeBasket = 'mergeBasket',
updateBasket = 'updateBasket',
updateBillingAddressForBasket = 'updateBillingAddressForBasket',
addCouponToBasket = 'addCouponToBasket',
removeCouponFromBasket = 'removeCouponFromBasket',
updateCustomerForBasket = 'updateCustomerForBasket',
addPaymentInstrumentToBasket = 'addPaymentInstrumentToBasket',
removePaymentInstrumentFromBasket = 'removePaymentInstrumentFromBasket',
updatePaymentInstrumentInBasket = 'updatePaymentInstrumentInBasket',
updateShippingAddressForShipment = 'updateShippingAddressForShipment',
updateShippingMethodForShipment = 'updateShippingMethodForShipment',
deleteBasket = 'deleteBasket',
transferBasket = 'transferBasket',
addGiftCertificateItemToBasket = 'addGiftCertificateItemToBasket',
removeGiftCertificateItemFromBasket = 'removeGiftCertificateItemFromBasket',
updateGiftCertificateItemInBasket = 'updateGiftCertificateItemInBasket',
addTaxesForBasketItem = 'addTaxesForBasketItem',
addPriceBooksToBasket = 'addPriceBooksToBasket',
createShipmentForBasket = 'createShipmentForBasket',
removeShipmentFromBasket = 'removeShipmentFromBasket',
updateShipmentForBasket = 'updateShipmentForBasket',
addTaxesForBasket = 'addTaxesForBasket'
}
4. The SLAS Helper Hooks
We also included a few special action hooks for SLAS. These helpers implements the common authentication flows: loginGuest
, loginRegisteredB2CUser
, logout
. Note that each helper could send multiple requests in order to get access token from the Shopper Login API.
These helpers manages the access token and refresh tokens internally, and persist the tokens in cookies/localstorage.
Example:
import React, {useEffect} from 'react'
import {useShopperLoginHelpers} from 'commerce-sdk-react/ShopperLogin'
const LoginForm = () => {
const login = useShopperLoginHelpers('login')
return <form>
<input type="text" name="username">
<input type="password" name="psw">
<button type="button" onClick={() => login.execute({credentials:{username, password}})}>Login</button>
</form>
}
Authentication / Token Management
It is our goal to hide the token management complexity from the developers and fully manage the tokens within the library.
First, when the provider component is mounted, the library will initialize the session by re-using existing token / grabing access token from SLAS API following the Public Client Shopper Login OAuth flows. The library stores the tokens in the browser (cookie/localstorage).
While the library tries to initialize the session, the other API hooks will be blocked until the SLAS calls are resolved and there is a valid JWT.